mirror of
https://github.com/sparrowwallet/sparrow.git
synced 2025-11-05 11:56:37 +00:00
Compare commits
77 commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3a5fa69fb6 | ||
|
|
4774830ce4 | ||
|
|
2f62a9e9c8 | ||
|
|
75bcfe2253 | ||
|
|
bedf1399ea | ||
|
|
58575793ea | ||
|
|
6c9b580d4f | ||
|
|
31909b7a15 | ||
|
|
092267339a | ||
|
|
0974918cff | ||
|
|
0f4c36b3c2 | ||
|
|
e1fe35fb74 | ||
|
|
d37fd00c4b | ||
|
|
5f54f86df7 | ||
|
|
e2fa3df08d | ||
|
|
6d6ede9abe | ||
|
|
cca9ab1056 | ||
|
|
9e33861110 | ||
|
|
c3d3fd1fda | ||
|
|
ca8553ecb8 | ||
|
|
d23ee8c086 | ||
|
|
e776a17ad4 | ||
|
|
480ce1e476 | ||
|
|
656cd90b08 | ||
|
|
8df0777959 | ||
|
|
84566b92e6 | ||
|
|
7802510e58 | ||
|
|
efb1eb1051 | ||
|
|
6240667478 | ||
|
|
2c27112dad | ||
|
|
6d53e1ed1d | ||
|
|
e8c5660897 | ||
|
|
bef6c750bd | ||
|
|
4ec3603789 | ||
|
|
90c9f9733f | ||
|
|
64efcf67d3 | ||
|
|
385d173948 | ||
|
|
d81b868049 | ||
|
|
2ff7a15d1e | ||
|
|
f48fa7e23c | ||
|
|
4632850e1e | ||
|
|
5f62523710 | ||
|
|
9dcf210762 | ||
|
|
c7e9a0a161 | ||
|
|
fa10714844 | ||
|
|
80105aee62 | ||
|
|
3c5fa58a16 | ||
|
|
2a2be2617c | ||
|
|
6c9a0d14cd | ||
|
|
f82fcb58bb | ||
|
|
5ec3bff6a4 | ||
|
|
134dc826ba | ||
|
|
cd2a6623a4 | ||
|
|
49ab9e40e3 | ||
|
|
cec7eac9ac | ||
|
|
33e043fd9a | ||
|
|
3aae26b196 | ||
|
|
73d4fd5049 | ||
|
|
a94380e882 | ||
|
|
e4dd4950bf | ||
|
|
26ce1b3469 | ||
|
|
ebce34f3d1 | ||
|
|
f28e00b97e | ||
|
|
25770c2426 | ||
|
|
799cac7b1f | ||
|
|
c265fd1969 | ||
|
|
890f0476b1 | ||
|
|
4d93381124 | ||
|
|
364909cfa3 | ||
|
|
38f0068411 | ||
|
|
8885e48ed9 | ||
|
|
31ce3ce68a | ||
|
|
b0d0514617 | ||
|
|
d7d23f9b58 | ||
|
|
3fdf093a26 | ||
|
|
74c298fd93 | ||
|
|
4298bfb053 |
110 changed files with 1949 additions and 1561 deletions
4
.github/workflows/package.yaml
vendored
4
.github/workflows/package.yaml
vendored
|
|
@ -12,11 +12,11 @@ jobs:
|
|||
matrix:
|
||||
os: [windows-2022, ubuntu-22.04, ubuntu-22.04-arm, macos-13, macos-14]
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
submodules: true
|
||||
- name: Set up JDK 22.0.2
|
||||
uses: actions/setup-java@v4
|
||||
uses: actions/setup-java@v5
|
||||
with:
|
||||
distribution: 'temurin'
|
||||
java-version: '22.0.2'
|
||||
|
|
|
|||
91
build.gradle
91
build.gradle
|
|
@ -1,8 +1,8 @@
|
|||
plugins {
|
||||
id 'application'
|
||||
id 'org-openjfx-javafxplugin'
|
||||
id 'org.beryx.jlink' version '3.1.1'
|
||||
id 'org.gradlex.extra-java-module-info' version '1.9'
|
||||
id 'org.beryx.jlink' version '3.1.3'
|
||||
id 'org.gradlex.extra-java-module-info' version '1.13'
|
||||
id 'io.matthewnelson.kmp.tor.resource-filterjar' version '408.16.3'
|
||||
}
|
||||
|
||||
|
|
@ -19,17 +19,16 @@ if(System.getProperty("os.arch") == "aarch64") {
|
|||
}
|
||||
def headless = "true".equals(System.getProperty("java.awt.headless"))
|
||||
|
||||
group 'com.sparrowwallet'
|
||||
version '2.2.2'
|
||||
group = 'com.sparrowwallet'
|
||||
version = '2.3.1'
|
||||
|
||||
repositories {
|
||||
mavenCentral()
|
||||
maven { url 'https://code.sparrowwallet.com/api/packages/sparrowwallet/maven' }
|
||||
maven { url = uri('https://code.sparrowwallet.com/api/packages/sparrowwallet/maven') }
|
||||
}
|
||||
|
||||
tasks.withType(AbstractArchiveTask) {
|
||||
preserveFileTimestamps = false
|
||||
reproducibleFileOrder = true
|
||||
tasks.withType(AbstractArchiveTask).configureEach {
|
||||
useFileSystemPermissions()
|
||||
}
|
||||
|
||||
javafx {
|
||||
|
|
@ -45,20 +44,20 @@ dependencies {
|
|||
//Any changes to the dependencies must be reflected in the module definitions below!
|
||||
implementation(project(':drongo'))
|
||||
implementation(project(':lark'))
|
||||
implementation('com.google.guava:guava:33.0.0-jre')
|
||||
implementation('com.google.guava:guava:33.5.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') {
|
||||
exclude group: 'org.slf4j'
|
||||
}
|
||||
implementation('org.jdbi:jdbi3-core:3.20.0') {
|
||||
implementation('org.jdbi:jdbi3-core:3.49.5') {
|
||||
exclude group: 'org.slf4j'
|
||||
}
|
||||
implementation('org.jdbi:jdbi3-sqlobject:3.20.0') {
|
||||
implementation('org.jdbi:jdbi3-sqlobject:3.49.5') {
|
||||
exclude group: 'org.slf4j'
|
||||
}
|
||||
implementation('org.flywaydb:flyway-core:9.22.3')
|
||||
implementation('org.fxmisc.richtext:richtextfx:0.10.4')
|
||||
implementation('org.fxmisc.richtext:richtextfx:0.11.6')
|
||||
implementation('no.tornado:tornadofx-controls:1.0.4')
|
||||
implementation('com.google.zxing:javase:3.4.0') {
|
||||
exclude group: 'com.beust', module: 'jcommander'
|
||||
|
|
@ -74,13 +73,15 @@ dependencies {
|
|||
implementation('com.fasterxml.jackson.core:jackson-databind:2.17.2')
|
||||
implementation('com.sparrowwallet:hummingbird:1.7.4')
|
||||
implementation('co.nstant.in:cbor:0.9')
|
||||
implementation('org.openpnp:openpnp-capture-java:0.0.28-5')
|
||||
implementation('org.openpnp:openpnp-capture-java:0.0.30-1')
|
||||
implementation("io.matthewnelson.kmp-tor:runtime:2.2.1")
|
||||
implementation("io.matthewnelson.kmp-tor:resource-exec-tor-gpl:408.16.3")
|
||||
implementation('org.jetbrains.kotlinx:kotlinx-coroutines-javafx:1.10.1') {
|
||||
exclude group: 'org.jetbrains.kotlin', module: 'kotlin-stdlib-common'
|
||||
}
|
||||
implementation('de.codecentric.centerdevice:centerdevice-nsmenufx:2.1.7')
|
||||
implementation('de.jangassen:nsmenufx:3.1.0') {
|
||||
exclude group: 'net.java.dev.jna', module: 'jna'
|
||||
}
|
||||
implementation('org.controlsfx:controlsfx:11.1.0' ) {
|
||||
exclude group: 'org.openjfx', module: 'javafx-base'
|
||||
exclude group: 'org.openjfx', module: 'javafx-graphics'
|
||||
|
|
@ -100,7 +101,7 @@ dependencies {
|
|||
implementation('com.sparrowwallet:tern:1.0.6')
|
||||
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-lang3:3.19.0')
|
||||
implementation('org.apache.commons:commons-compress:1.27.1')
|
||||
implementation('net.sourceforge.streamsupport:streamsupport:1.7.0')
|
||||
implementation('com.github.librepdf:openpdf:1.3.30')
|
||||
|
|
@ -109,6 +110,7 @@ dependencies {
|
|||
implementation('com.github.hervegirod:fxsvgimage:1.1')
|
||||
implementation('com.sparrowwallet:toucan:0.9.0')
|
||||
implementation('com.jcraft:jzlib:1.1.3')
|
||||
implementation('io.github.doblon8:jzbar:0.2.1')
|
||||
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')
|
||||
|
|
@ -141,6 +143,12 @@ application {
|
|||
mainClass = 'com.sparrowwallet.sparrow.SparrowWallet'
|
||||
|
||||
applicationDefaultJvmArgs = ["-XX:+HeapDumpOnOutOfMemoryError",
|
||||
"--enable-native-access=com.sparrowwallet.drongo",
|
||||
"--enable-native-access=com.sun.jna",
|
||||
"--enable-native-access=javafx.graphics",
|
||||
"--enable-native-access=com.fazecast.jSerialComm",
|
||||
"--enable-native-access=org.usb4java",
|
||||
"--enable-native-access=io.github.doblon8.jzbar",
|
||||
"--add-opens=javafx.graphics/com.sun.javafx.css=org.controlsfx.controls",
|
||||
"--add-opens=javafx.graphics/javafx.scene=org.controlsfx.controls",
|
||||
"--add-opens=javafx.controls/com.sun.javafx.scene.control.behavior=org.controlsfx.controls",
|
||||
|
|
@ -150,11 +158,6 @@ application {
|
|||
"--add-opens=javafx.controls/javafx.scene.control.cell=com.sparrowwallet.sparrow",
|
||||
"--add-opens=org.controlsfx.controls/impl.org.controlsfx.skin=com.sparrowwallet.sparrow",
|
||||
"--add-opens=org.controlsfx.controls/impl.org.controlsfx.skin=javafx.fxml",
|
||||
"--add-opens=javafx.graphics/com.sun.javafx.tk=centerdevice.nsmenufx",
|
||||
"--add-opens=javafx.graphics/com.sun.javafx.tk.quantum=centerdevice.nsmenufx",
|
||||
"--add-opens=javafx.graphics/com.sun.glass.ui=centerdevice.nsmenufx",
|
||||
"--add-opens=javafx.controls/com.sun.javafx.scene.control=centerdevice.nsmenufx",
|
||||
"--add-opens=javafx.graphics/com.sun.javafx.menu=centerdevice.nsmenufx",
|
||||
"--add-opens=javafx.graphics/com.sun.glass.ui=com.sparrowwallet.sparrow",
|
||||
"--add-opens=javafx.graphics/com.sun.javafx.application=com.sparrowwallet.sparrow",
|
||||
"--add-opens=javafx.graphics/javafx.scene.input=com.sparrowwallet.sparrow",
|
||||
|
|
@ -165,8 +168,7 @@ application {
|
|||
"--add-reads=org.flywaydb.core=java.desktop"]
|
||||
|
||||
if(os.macOsX) {
|
||||
applicationDefaultJvmArgs += ["-Dprism.lcdtext=false", "-Xdock:name=Sparrow",
|
||||
"--add-opens=javafx.graphics/com.sun.glass.ui.mac=centerdevice.nsmenufx"]
|
||||
applicationDefaultJvmArgs += ["-Dprism.lcdtext=false", "-Xdock:name=Sparrow"]
|
||||
}
|
||||
if(headless) {
|
||||
applicationDefaultJvmArgs += ["-Dglass.platform=Monocle", "-Dmonocle.platform=Headless", "-Dprism.order=sw"]
|
||||
|
|
@ -189,7 +191,14 @@ jlink {
|
|||
options = ['--strip-native-commands', '--strip-java-debug-attributes', '--compress', 'zip-6', '--no-header-files', '--no-man-pages', '--ignore-signing-information', '--exclude-files', '**.png', '--exclude-resources', 'glob:/com.sparrowwallet.merged.module/META-INF/*']
|
||||
launcher {
|
||||
name = 'sparrow'
|
||||
jvmArgs = ["--add-opens=javafx.graphics/com.sun.javafx.css=org.controlsfx.controls",
|
||||
jvmArgs = ["--enable-native-access=com.sparrowwallet.drongo",
|
||||
"--enable-native-access=com.sun.jna",
|
||||
"--enable-native-access=javafx.graphics",
|
||||
"--enable-native-access=com.sparrowwallet.merged.module",
|
||||
"--enable-native-access=com.fazecast.jSerialComm",
|
||||
"--enable-native-access=org.usb4java",
|
||||
"--enable-native-access=io.github.doblon8.jzbar",
|
||||
"--add-opens=javafx.graphics/com.sun.javafx.css=org.controlsfx.controls",
|
||||
"--add-opens=javafx.graphics/javafx.scene=org.controlsfx.controls",
|
||||
"--add-opens=javafx.controls/com.sun.javafx.scene.control.behavior=org.controlsfx.controls",
|
||||
"--add-opens=javafx.controls/com.sun.javafx.scene.control.inputmap=org.controlsfx.controls",
|
||||
|
|
@ -198,11 +207,6 @@ jlink {
|
|||
"--add-opens=javafx.controls/javafx.scene.control.cell=com.sparrowwallet.sparrow",
|
||||
"--add-opens=org.controlsfx.controls/impl.org.controlsfx.skin=com.sparrowwallet.sparrow",
|
||||
"--add-opens=org.controlsfx.controls/impl.org.controlsfx.skin=javafx.fxml",
|
||||
"--add-opens=javafx.graphics/com.sun.javafx.tk=centerdevice.nsmenufx",
|
||||
"--add-opens=javafx.graphics/com.sun.javafx.tk.quantum=centerdevice.nsmenufx",
|
||||
"--add-opens=javafx.graphics/com.sun.glass.ui=centerdevice.nsmenufx",
|
||||
"--add-opens=javafx.controls/com.sun.javafx.scene.control=centerdevice.nsmenufx",
|
||||
"--add-opens=javafx.graphics/com.sun.javafx.menu=centerdevice.nsmenufx",
|
||||
"--add-opens=javafx.graphics/com.sun.glass.ui=com.sparrowwallet.sparrow",
|
||||
"--add-opens=javafx.graphics/javafx.scene.input=com.sparrowwallet.sparrow",
|
||||
"--add-opens=javafx.graphics/com.sun.javafx.application=com.sparrowwallet.sparrow",
|
||||
|
|
@ -221,6 +225,7 @@ jlink {
|
|||
"--add-reads=com.sparrowwallet.merged.module=org.bouncycastle.pg",
|
||||
"--add-reads=com.sparrowwallet.merged.module=org.bouncycastle.provider",
|
||||
"--add-reads=com.sparrowwallet.merged.module=kotlin.stdlib",
|
||||
"--add-reads=com.sparrowwallet.merged.module=org.reactfx.reactfx",
|
||||
"--add-reads=kotlin.stdlib=kotlinx.coroutines.core",
|
||||
"--add-reads=org.flywaydb.core=java.desktop"]
|
||||
|
||||
|
|
@ -228,7 +233,7 @@ jlink {
|
|||
jvmArgs += ["-Djavax.accessibility.assistive_technologies", "-Djavax.accessibility.screen_magnifier_present=false"]
|
||||
}
|
||||
if(os.macOsX) {
|
||||
jvmArgs += ["-Dprism.lcdtext=false", "--add-opens=javafx.graphics/com.sun.glass.ui.mac=com.sparrowwallet.merged.module", "--add-opens=javafx.graphics/com.sun.glass.ui.mac=centerdevice.nsmenufx"]
|
||||
jvmArgs += ["-Dprism.lcdtext=false", "--add-opens=javafx.graphics/com.sun.glass.ui.mac=com.sparrowwallet.merged.module"]
|
||||
}
|
||||
if(headless) {
|
||||
jvmArgs += ["-Dglass.platform=Monocle", "-Dmonocle.platform=Headless", "-Dprism.order=sw"]
|
||||
|
|
@ -280,7 +285,8 @@ if(os.linux) {
|
|||
|
||||
tasks.register('addUserWritePermission', Exec) {
|
||||
if(os.windows) {
|
||||
commandLine 'icacls', "$buildDir\\image\\legal", '/grant', 'Users:(OI)(CI)F', '/T'
|
||||
def usersGroup = '*S-1-5-32-545' // Windows "Users" group SID (language-independent)
|
||||
commandLine 'icacls', "$buildDir\\image\\legal", '/grant', "${usersGroup}:(OI)(CI)F", '/T'
|
||||
} else {
|
||||
commandLine 'chmod', '-R', 'u+w', "$buildDir/image/legal"
|
||||
}
|
||||
|
|
@ -380,33 +386,12 @@ extraJavaModuleInfo {
|
|||
requires('java.desktop')
|
||||
requires('com.sun.jna')
|
||||
}
|
||||
module('de.codecentric.centerdevice:centerdevice-nsmenufx', 'centerdevice.nsmenufx') {
|
||||
exports('de.codecentric.centerdevice')
|
||||
requires('javafx.base')
|
||||
requires('javafx.controls')
|
||||
requires('javafx.graphics')
|
||||
}
|
||||
module('net.sourceforge.javacsv:javacsv', 'net.sourceforge.javacsv') {
|
||||
exports('com.csvreader')
|
||||
}
|
||||
module('com.google.guava:listenablefuture|empty-to-avoid-conflict-with-guava', 'com.google.guava.listenablefuture')
|
||||
module('com.google.code.findbugs:jsr305', 'com.google.code.findbugs.jsr305')
|
||||
module('j2objc-annotations-2.8.jar', 'com.google.j2objc.j2objc.annotations', '2.8')
|
||||
module('org.jdbi:jdbi3-core', 'org.jdbi.v3.core') {
|
||||
exports('org.jdbi.v3.core')
|
||||
exports('org.jdbi.v3.core.mapper')
|
||||
exports('org.jdbi.v3.core.statement')
|
||||
exports('org.jdbi.v3.core.result')
|
||||
exports('org.jdbi.v3.core.h2')
|
||||
exports('org.jdbi.v3.core.spi')
|
||||
requires('io.leangen.geantyref')
|
||||
requires('java.sql')
|
||||
requires('org.slf4j')
|
||||
requires('com.github.benmanes.caffeine')
|
||||
}
|
||||
module('io.leangen.geantyref:geantyref', 'io.leangen.geantyref') {
|
||||
exports('io.leangen.geantyref')
|
||||
}
|
||||
module('org.fxmisc.richtext:richtextfx', 'org.fxmisc.richtext') {
|
||||
exports('org.fxmisc.richtext')
|
||||
exports('org.fxmisc.richtext.event')
|
||||
|
|
@ -416,10 +401,10 @@ extraJavaModuleInfo {
|
|||
requires('javafx.graphics')
|
||||
requires('org.fxmisc.flowless')
|
||||
requires('org.reactfx.reactfx')
|
||||
requires('org.fxmisc.undo.undofx')
|
||||
requires('org.fxmisc.undo')
|
||||
requires('org.fxmisc.wellbehaved')
|
||||
}
|
||||
module('org.fxmisc.undo:undofx', 'org.fxmisc.undo.undofx') {
|
||||
module('org.fxmisc.undo:undofx', 'org.fxmisc.undo') {
|
||||
requires('javafx.base')
|
||||
requires('javafx.controls')
|
||||
requires('javafx.graphics')
|
||||
|
|
|
|||
|
|
@ -4,13 +4,12 @@ plugins {
|
|||
|
||||
dependencies {
|
||||
implementation 'com.google.gradle:osdetector-gradle-plugin:1.7.3'
|
||||
implementation 'org.javamodularity:moduleplugin:1.8.14'
|
||||
}
|
||||
|
||||
repositories {
|
||||
mavenCentral()
|
||||
maven {
|
||||
url "https://plugins.gradle.org/m2/"
|
||||
url = uri("https://plugins.gradle.org/m2/")
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -32,7 +32,6 @@ 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> {
|
||||
|
|
@ -40,10 +39,9 @@ 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);
|
||||
project.getTasks().register("configJavafxRun", ExecTask.class, project);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -33,27 +33,19 @@ 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;
|
||||
|
||||
|
|
@ -78,37 +70,11 @@ public class ExecTask extends DefaultTask {
|
|||
|
||||
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");
|
||||
|
|
|
|||
|
|
@ -83,7 +83,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="2.2.1"
|
||||
GIT_TAG="2.3.0"
|
||||
```
|
||||
|
||||
The project can then be initially cloned as follows:
|
||||
|
|
|
|||
2
drongo
2
drongo
|
|
@ -1 +1 @@
|
|||
Subproject commit abb598d3b041a9d0b3d0ba41b5fb9785e2100193
|
||||
Subproject commit e975cbe6f8d8574785124e6db5780d0541e20024
|
||||
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
Binary file not shown.
2
gradle/wrapper/gradle-wrapper.properties
vendored
2
gradle/wrapper/gradle-wrapper.properties
vendored
|
|
@ -1,6 +1,6 @@
|
|||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-bin.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-9.1.0-bin.zip
|
||||
networkTimeout=10000
|
||||
validateDistributionUrl=true
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
|
|
|
|||
15
gradlew
vendored
15
gradlew
vendored
|
|
@ -1,7 +1,7 @@
|
|||
#!/bin/sh
|
||||
|
||||
#
|
||||
# Copyright © 2015-2021 the original authors.
|
||||
# Copyright © 2015 the original authors.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
|
|
@ -15,6 +15,8 @@
|
|||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
#
|
||||
|
||||
##############################################################################
|
||||
#
|
||||
|
|
@ -55,7 +57,7 @@
|
|||
# Darwin, MinGW, and NonStop.
|
||||
#
|
||||
# (3) This script is generated from the Groovy template
|
||||
# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
|
||||
# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
|
||||
# within the Gradle project.
|
||||
#
|
||||
# You can find Gradle at https://github.com/gradle/gradle/.
|
||||
|
|
@ -84,7 +86,7 @@ done
|
|||
# shellcheck disable=SC2034
|
||||
APP_BASE_NAME=${0##*/}
|
||||
# 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
|
||||
APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit
|
||||
|
||||
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
||||
MAX_FD=maximum
|
||||
|
|
@ -112,7 +114,6 @@ case "$( uname )" in #(
|
|||
NONSTOP* ) nonstop=true ;;
|
||||
esac
|
||||
|
||||
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
|
||||
|
||||
|
||||
# Determine the Java command to use to start the JVM.
|
||||
|
|
@ -170,7 +171,6 @@ fi
|
|||
# For Cygwin or MSYS, switch paths to Windows format before running java
|
||||
if "$cygwin" || "$msys" ; then
|
||||
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
|
||||
CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
|
||||
|
||||
JAVACMD=$( cygpath --unix "$JAVACMD" )
|
||||
|
||||
|
|
@ -203,15 +203,14 @@ fi
|
|||
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,
|
||||
# * DEFAULT_JVM_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" \
|
||||
-classpath "$CLASSPATH" \
|
||||
org.gradle.wrapper.GradleWrapperMain \
|
||||
-jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \
|
||||
"$@"
|
||||
|
||||
# Stop when "xargs" is not available.
|
||||
|
|
|
|||
25
gradlew.bat
vendored
25
gradlew.bat
vendored
|
|
@ -13,6 +13,8 @@
|
|||
@rem See the License for the specific language governing permissions and
|
||||
@rem limitations under the License.
|
||||
@rem
|
||||
@rem SPDX-License-Identifier: Apache-2.0
|
||||
@rem
|
||||
|
||||
@if "%DEBUG%"=="" @echo off
|
||||
@rem ##########################################################################
|
||||
|
|
@ -43,11 +45,11 @@ set JAVA_EXE=java.exe
|
|||
%JAVA_EXE% -version >NUL 2>&1
|
||||
if %ERRORLEVEL% equ 0 goto execute
|
||||
|
||||
echo.
|
||||
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||
echo.
|
||||
echo Please set the JAVA_HOME variable in your environment to match the
|
||||
echo location of your Java installation.
|
||||
echo. 1>&2
|
||||
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
|
||||
echo. 1>&2
|
||||
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
|
||||
echo location of your Java installation. 1>&2
|
||||
|
||||
goto fail
|
||||
|
||||
|
|
@ -57,22 +59,21 @@ set JAVA_EXE=%JAVA_HOME%/bin/java.exe
|
|||
|
||||
if exist "%JAVA_EXE%" goto execute
|
||||
|
||||
echo.
|
||||
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
|
||||
echo.
|
||||
echo Please set the JAVA_HOME variable in your environment to match the
|
||||
echo location of your Java installation.
|
||||
echo. 1>&2
|
||||
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
|
||||
echo. 1>&2
|
||||
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
|
||||
echo location of your Java installation. 1>&2
|
||||
|
||||
goto fail
|
||||
|
||||
:execute
|
||||
@rem Setup the command line
|
||||
|
||||
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
|
||||
|
||||
|
||||
@rem Execute Gradle
|
||||
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
|
||||
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %*
|
||||
|
||||
:end
|
||||
@rem End local scope for the variables with windows NT shell
|
||||
|
|
|
|||
2
lark
2
lark
|
|
@ -1 +1 @@
|
|||
Subproject commit 5facb25ede49c30650a8460dc04982650edb397f
|
||||
Subproject commit 10e8d9cd4bbe9fde4dd93c059e2a9faeec6be3e0
|
||||
|
|
@ -21,7 +21,7 @@
|
|||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>2.2.2</string>
|
||||
<string>2.3.1</string>
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
<!-- See https://developer.apple.com/app-store/categories/ for list of AppStore categories -->
|
||||
|
|
@ -33,6 +33,8 @@
|
|||
<string>Copyright (C) 2021</string>
|
||||
<key>NSHighResolutionCapable</key>
|
||||
<string>true</string>
|
||||
<key>NSCameraUseContinuityCameraDeviceType</key>
|
||||
<true/>
|
||||
<key>NSCameraUsageDescription</key>
|
||||
<string>Sparrow requires access to the camera in order to scan QR codes</string>
|
||||
<key>NSLocalNetworkUsageDescription</key>
|
||||
|
|
|
|||
|
|
@ -3,13 +3,14 @@ package com.sparrowwallet.sparrow;
|
|||
import com.beust.jcommander.JCommander;
|
||||
import com.google.common.eventbus.Subscribe;
|
||||
import com.sparrowwallet.drongo.*;
|
||||
import com.sparrowwallet.drongo.address.Address;
|
||||
import com.sparrowwallet.drongo.crypto.*;
|
||||
import com.sparrowwallet.drongo.dns.DnsPayment;
|
||||
import com.sparrowwallet.drongo.dns.DnsPaymentCache;
|
||||
import com.sparrowwallet.drongo.policy.PolicyType;
|
||||
import com.sparrowwallet.drongo.protocol.*;
|
||||
import com.sparrowwallet.drongo.psbt.PSBT;
|
||||
import com.sparrowwallet.drongo.psbt.PSBTInput;
|
||||
import com.sparrowwallet.drongo.psbt.PSBTParseException;
|
||||
import com.sparrowwallet.drongo.psbt.PSBTSignatureException;
|
||||
import com.sparrowwallet.drongo.psbt.*;
|
||||
import com.sparrowwallet.drongo.silentpayments.SilentPaymentAddress;
|
||||
import com.sparrowwallet.drongo.wallet.*;
|
||||
import com.sparrowwallet.hummingbird.UR;
|
||||
import com.sparrowwallet.hummingbird.registry.CryptoPSBT;
|
||||
|
|
@ -30,7 +31,7 @@ import com.sparrowwallet.sparrow.transaction.TransactionView;
|
|||
import com.sparrowwallet.sparrow.wallet.Entry;
|
||||
import com.sparrowwallet.sparrow.wallet.WalletController;
|
||||
import com.sparrowwallet.sparrow.wallet.WalletForm;
|
||||
import de.codecentric.centerdevice.MenuToolkit;
|
||||
import de.jangassen.MenuToolkit;
|
||||
import javafx.animation.*;
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.binding.Bindings;
|
||||
|
|
@ -49,12 +50,14 @@ import javafx.geometry.Side;
|
|||
import javafx.scene.Node;
|
||||
import javafx.scene.Scene;
|
||||
import javafx.scene.control.*;
|
||||
import javafx.scene.image.Image;
|
||||
import javafx.scene.image.ImageView;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.control.Menu;
|
||||
import javafx.scene.control.MenuItem;
|
||||
import javafx.scene.input.*;
|
||||
import javafx.scene.layout.*;
|
||||
import javafx.scene.paint.Color;
|
||||
import javafx.stage.*;
|
||||
import javafx.stage.Window;
|
||||
import javafx.util.Duration;
|
||||
import org.controlsfx.control.Notifications;
|
||||
import org.controlsfx.control.StatusBar;
|
||||
|
|
@ -69,6 +72,7 @@ import java.nio.charset.StandardCharsets;
|
|||
import java.nio.file.Files;
|
||||
import java.text.ParseException;
|
||||
import java.util.*;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static com.sparrowwallet.sparrow.AppServices.*;
|
||||
|
|
@ -822,10 +826,10 @@ public class AppController implements Initializable {
|
|||
try(FileOutputStream outputStream = new FileOutputStream(file)) {
|
||||
if(asText) {
|
||||
PrintWriter writer = new PrintWriter(new OutputStreamWriter(outputStream, StandardCharsets.UTF_8));
|
||||
writer.print(transactionTabData.getPsbt().toBase64String(includeXpubs));
|
||||
writer.print(transactionTabData.getPsbt().getForExport().toBase64String(includeXpubs));
|
||||
writer.flush();
|
||||
} else {
|
||||
outputStream.write(transactionTabData.getPsbt().serialize(includeXpubs, true));
|
||||
outputStream.write(transactionTabData.getPsbt().getForExport().serialize(includeXpubs, true));
|
||||
}
|
||||
} catch(IOException e) {
|
||||
log.error("Error saving PSBT", e);
|
||||
|
|
@ -848,7 +852,7 @@ public class AppController implements Initializable {
|
|||
TabData tabData = (TabData)selectedTab.getUserData();
|
||||
if(tabData.getType() == TabData.TabType.TRANSACTION) {
|
||||
TransactionTabData transactionTabData = (TransactionTabData)tabData;
|
||||
String data = asBase64 ? transactionTabData.getPsbt().toBase64String() : transactionTabData.getPsbt().toString();
|
||||
String data = asBase64 ? transactionTabData.getPsbt().getForExport().toBase64String() : transactionTabData.getPsbt().getForExport().toString();
|
||||
|
||||
ClipboardContent content = new ClipboardContent();
|
||||
content.putString(data);
|
||||
|
|
@ -862,7 +866,7 @@ public class AppController implements Initializable {
|
|||
if(tabData.getType() == TabData.TabType.TRANSACTION) {
|
||||
TransactionTabData transactionTabData = (TransactionTabData)tabData;
|
||||
|
||||
byte[] psbtBytes = transactionTabData.getPsbt().serialize();
|
||||
byte[] psbtBytes = transactionTabData.getPsbt().getForExport().serialize();
|
||||
CryptoPSBT cryptoPSBT = new CryptoPSBT(psbtBytes);
|
||||
BBQR bbqr = new BBQR(BBQRType.PSBT, psbtBytes);
|
||||
QRDisplayDialog qrDisplayDialog = new QRDisplayDialog(cryptoPSBT.toUR(), bbqr, false, true, false);
|
||||
|
|
@ -1034,6 +1038,10 @@ public class AppController implements Initializable {
|
|||
cmd.add(System.getProperty(JPACKAGE_APP_PATH));
|
||||
cmd.addAll(args.toParams());
|
||||
final ProcessBuilder builder = new ProcessBuilder(cmd);
|
||||
if(OsType.getCurrent() == OsType.UNIX) {
|
||||
Map<String, String> env = builder.environment();
|
||||
env.remove("LD_LIBRARY_PATH");
|
||||
}
|
||||
builder.start();
|
||||
quit(event);
|
||||
} catch(Exception e) {
|
||||
|
|
@ -1422,6 +1430,10 @@ public class AppController implements Initializable {
|
|||
}
|
||||
|
||||
public void sendToMany(ActionEvent event) {
|
||||
sendToMany(Collections.emptyList());
|
||||
}
|
||||
|
||||
private void sendToMany(List<Payment> initialPayments) {
|
||||
if(sendToManyDialog != null) {
|
||||
Stage stage = (Stage)sendToManyDialog.getDialogPane().getScene().getWindow();
|
||||
stage.setAlwaysOnTop(true);
|
||||
|
|
@ -1437,7 +1449,7 @@ public class AppController implements Initializable {
|
|||
bitcoinUnit = wallet.getAutoUnit();
|
||||
}
|
||||
|
||||
sendToManyDialog = new SendToManyDialog(bitcoinUnit);
|
||||
sendToManyDialog = new SendToManyDialog(bitcoinUnit, initialPayments);
|
||||
sendToManyDialog.initModality(Modality.NONE);
|
||||
Optional<List<Payment>> optPayments = sendToManyDialog.showAndWait();
|
||||
sendToManyDialog = null;
|
||||
|
|
@ -1889,6 +1901,11 @@ public class AppController implements Initializable {
|
|||
}
|
||||
|
||||
private void addTransactionTab(String name, File file, PSBT psbt) {
|
||||
//Convert to PSBTv0 first
|
||||
if(psbt.getVersion() != null && psbt.getVersion() >= 2) {
|
||||
psbt.convertVersion(0);
|
||||
}
|
||||
|
||||
//Add any missing previous outputs if available in open wallets
|
||||
for(PSBTInput psbtInput : psbt.getPsbtInputs()) {
|
||||
if(psbtInput.getUtxo() == null) {
|
||||
|
|
@ -1908,6 +1925,39 @@ public class AppController implements Initializable {
|
|||
}
|
||||
}
|
||||
|
||||
//Add DNS payment information if not already cached
|
||||
for(PSBTOutput psbtOutput : psbt.getPsbtOutputs()) {
|
||||
if(psbtOutput.getDnssecProof() != null && !psbtOutput.getDnssecProof().isEmpty()) {
|
||||
Address address = psbtOutput.getScript() != null ? psbtOutput.getScript().getToAddress() : null;
|
||||
if(address != null && DnsPaymentCache.getDnsPayment(address) == null) {
|
||||
try {
|
||||
Optional<DnsPayment> optDnsPayment = psbtOutput.getDnsPayment();
|
||||
if(optDnsPayment.isPresent() && address.equals(optDnsPayment.get().bitcoinURI().getAddress())) {
|
||||
DnsPaymentCache.putDnsPayment(address, optDnsPayment.get());
|
||||
}
|
||||
} catch(Exception e) {
|
||||
log.debug("Error resolving DNS payment", e);
|
||||
}
|
||||
}
|
||||
|
||||
SilentPaymentAddress silentPaymentAddress = psbtOutput.getSilentPaymentAddress();
|
||||
if(address != null && silentPaymentAddress == null) {
|
||||
silentPaymentAddress = AppServices.get().getOpenWallets().keySet().stream()
|
||||
.map(wallet -> wallet.getSilentPaymentAddress(address)).filter(Objects::nonNull).findFirst().orElse(null);
|
||||
}
|
||||
if(silentPaymentAddress != null && DnsPaymentCache.getDnsPayment(silentPaymentAddress) == null) {
|
||||
try {
|
||||
Optional<DnsPayment> optDnsPayment = psbtOutput.getDnsPayment();
|
||||
if(optDnsPayment.isPresent() && silentPaymentAddress.equals(optDnsPayment.get().bitcoinURI().getSilentPaymentAddress())) {
|
||||
DnsPaymentCache.putDnsPayment(silentPaymentAddress, optDnsPayment.get());
|
||||
}
|
||||
} catch(Exception e) {
|
||||
log.debug("Error resolving DNS payment", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Window psbtWalletWindow = AppServices.get().getWindowForPSBT(psbt);
|
||||
if(psbtWalletWindow != null && !tabs.getScene().getWindow().equals(psbtWalletWindow)) {
|
||||
EventManager.get().post(new ViewPSBTEvent(psbtWalletWindow, name, file, psbt));
|
||||
|
|
@ -2046,23 +2096,33 @@ public class AppController implements Initializable {
|
|||
}
|
||||
|
||||
MenuItem moveRight = new MenuItem("Move Right");
|
||||
moveRight.setAccelerator(new KeyCodeCombination(KeyCode.RIGHT, KeyCombination.CONTROL_DOWN, KeyCombination.SHIFT_DOWN));
|
||||
moveRight.setOnAction(event -> {
|
||||
int index = tabs.getTabs().indexOf(tab);
|
||||
int currentIndex = tabs.getSelectionModel().getSelectedIndex();
|
||||
if(currentIndex + 1 >= tabs.getTabs().size()) {
|
||||
return;
|
||||
}
|
||||
Tab selectedTab = tabs.getSelectionModel().getSelectedItem();
|
||||
tabs.getTabs().removeListener(tabsChangeListener);
|
||||
tabs.getTabs().remove(tab);
|
||||
tabs.getTabs().add(index + 1, tab);
|
||||
tabs.getTabs().remove(selectedTab);
|
||||
tabs.getTabs().add(currentIndex + 1, selectedTab);
|
||||
tabs.getTabs().addListener(tabsChangeListener);
|
||||
tabs.getSelectionModel().select(tab);
|
||||
tabs.getSelectionModel().select(selectedTab);
|
||||
EventManager.get().post(new RequestOpenWalletsEvent()); //Rearrange recent files list
|
||||
});
|
||||
MenuItem moveLeft = new MenuItem("Move Left");
|
||||
moveLeft.setAccelerator(new KeyCodeCombination(KeyCode.LEFT, KeyCombination.CONTROL_DOWN, KeyCombination.SHIFT_DOWN));
|
||||
moveLeft.setOnAction(event -> {
|
||||
int index = tabs.getTabs().indexOf(tab);
|
||||
int currentIndex = tabs.getSelectionModel().getSelectedIndex();
|
||||
if(currentIndex == 0) {
|
||||
return;
|
||||
}
|
||||
Tab selectedTab = tabs.getSelectionModel().getSelectedItem();
|
||||
tabs.getTabs().removeListener(tabsChangeListener);
|
||||
tabs.getTabs().remove(tab);
|
||||
tabs.getTabs().add(index - 1, tab);
|
||||
tabs.getTabs().remove(selectedTab);
|
||||
tabs.getTabs().add(currentIndex - 1, selectedTab);
|
||||
tabs.getTabs().addListener(tabsChangeListener);
|
||||
tabs.getSelectionModel().select(tab);
|
||||
tabs.getSelectionModel().select(selectedTab);
|
||||
EventManager.get().post(new RequestOpenWalletsEvent()); //Rearrange recent files list
|
||||
});
|
||||
contextMenu.getItems().addAll(moveRight, moveLeft);
|
||||
|
|
@ -3107,6 +3167,11 @@ public class AppController implements Initializable {
|
|||
}
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void requestSendToMany(RequestSendToManyEvent event) {
|
||||
sendToMany(event.getPayments());
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void functionAction(FunctionActionEvent event) {
|
||||
selectTab(event.getWallet());
|
||||
|
|
|
|||
|
|
@ -91,8 +91,7 @@ public class AppServices {
|
|||
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> 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);
|
||||
private static final List<Double> LONG_FEE_RATES_RANGE = List.of(1d, 2d, 4d, 8d, 16d, 32d, 64d, 128d, 256d, 512d, 1024d, 2048d, 4096d, 8192d);
|
||||
public static final double FALLBACK_FEE_RATE = 20000d / 1000;
|
||||
public static final double TESTNET_FALLBACK_FEE_RATE = 1000d / 1000;
|
||||
|
||||
|
|
@ -142,6 +141,8 @@ public class AppServices {
|
|||
|
||||
private static Double minimumRelayFeeRate;
|
||||
|
||||
private static Double serverMinimumRelayFeeRate;
|
||||
|
||||
private static CurrencyRate fiatCurrencyExchangeRate;
|
||||
|
||||
private static List<Device> devices;
|
||||
|
|
@ -211,6 +212,7 @@ public class AppServices {
|
|||
preventSleepService = createPreventSleepService();
|
||||
|
||||
onlineProperty.addListener(onlineServicesListener);
|
||||
minimumRelayFeeRate = getConfiguredMinimumRelayFeeRate(config);
|
||||
|
||||
if(config.getMode() == Mode.ONLINE) {
|
||||
if(config.requiresInternalTor()) {
|
||||
|
|
@ -750,6 +752,26 @@ public class AppServices {
|
|||
return Math.max(minRate, Transaction.DUST_RELAY_TX_FEE);
|
||||
}
|
||||
|
||||
public static List<Double> getLongFeeRatesRange() {
|
||||
if(minimumRelayFeeRate == null || minimumRelayFeeRate >= Transaction.DEFAULT_MIN_RELAY_FEE) {
|
||||
return LONG_FEE_RATES_RANGE;
|
||||
} else {
|
||||
List<Double> longFeeRatesRange = new ArrayList<>();
|
||||
longFeeRatesRange.add(minimumRelayFeeRate);
|
||||
longFeeRatesRange.addAll(LONG_FEE_RATES_RANGE);
|
||||
return longFeeRatesRange;
|
||||
}
|
||||
}
|
||||
|
||||
public static List<Double> getFeeRatesRange() {
|
||||
if(minimumRelayFeeRate == null || minimumRelayFeeRate >= Transaction.DEFAULT_MIN_RELAY_FEE) {
|
||||
return LONG_FEE_RATES_RANGE.subList(0, LONG_FEE_RATES_RANGE.size() - 3);
|
||||
} else {
|
||||
List<Double> longFeeRatesRange = getLongFeeRatesRange();
|
||||
return longFeeRatesRange.subList(0, longFeeRatesRange.size() - 4);
|
||||
}
|
||||
}
|
||||
|
||||
public static Double getNextBlockMedianFeeRate() {
|
||||
return nextBlockMedianFeeRate == null ? getDefaultFeeRate() : nextBlockMedianFeeRate;
|
||||
}
|
||||
|
|
@ -788,10 +810,18 @@ public class AppServices {
|
|||
});
|
||||
}
|
||||
|
||||
public static Double getConfiguredMinimumRelayFeeRate(Config config) {
|
||||
return config.getMinRelayFeeRate() >= 0d && config.getMinRelayFeeRate() < Transaction.DEFAULT_MIN_RELAY_FEE ? config.getMinRelayFeeRate() : null;
|
||||
}
|
||||
|
||||
public static Double getMinimumRelayFeeRate() {
|
||||
return minimumRelayFeeRate == null ? Transaction.DEFAULT_MIN_RELAY_FEE : minimumRelayFeeRate;
|
||||
}
|
||||
|
||||
public static Double getServerMinimumRelayFeeRate() {
|
||||
return serverMinimumRelayFeeRate;
|
||||
}
|
||||
|
||||
public static CurrencyRate getFiatCurrencyExchangeRate() {
|
||||
return fiatCurrencyExchangeRate;
|
||||
}
|
||||
|
|
@ -805,8 +835,8 @@ public class AppServices {
|
|||
}
|
||||
|
||||
public static void addPayjoinURI(BitcoinURI bitcoinURI) {
|
||||
if(bitcoinURI.getPayjoinUrl() == null) {
|
||||
throw new IllegalArgumentException("Not a payjoin URI");
|
||||
if(bitcoinURI.getPayjoinUrl() == null || bitcoinURI.getAddress() == null) {
|
||||
throw new IllegalArgumentException("Not a valid payjoin URI");
|
||||
}
|
||||
payjoinURIs.put(bitcoinURI.getAddress(), bitcoinURI);
|
||||
}
|
||||
|
|
@ -1203,7 +1233,7 @@ public class AppServices {
|
|||
}
|
||||
|
||||
public static Font getMonospaceFont() {
|
||||
return Font.font("Roboto Mono", 13);
|
||||
return Font.font("Fragment Mono Regular", 13);
|
||||
}
|
||||
|
||||
public static boolean isOnWayland() {
|
||||
|
|
@ -1219,7 +1249,10 @@ public class AppServices {
|
|||
public void newConnection(ConnectionEvent event) {
|
||||
currentBlockHeight = event.getBlockHeight();
|
||||
System.setProperty(Network.BLOCK_HEIGHT_PROPERTY, Integer.toString(currentBlockHeight));
|
||||
minimumRelayFeeRate = Math.max(event.getMinimumRelayFeeRate(), Transaction.DEFAULT_MIN_RELAY_FEE);
|
||||
if(getConfiguredMinimumRelayFeeRate(Config.get()) == null) {
|
||||
minimumRelayFeeRate = event.getMinimumRelayFeeRate() == null ? Transaction.DEFAULT_MIN_RELAY_FEE : event.getMinimumRelayFeeRate();
|
||||
}
|
||||
serverMinimumRelayFeeRate = event.getMinimumRelayFeeRate();
|
||||
latestBlockHeader = event.getBlockHeader();
|
||||
Config.get().addRecentServer();
|
||||
|
||||
|
|
|
|||
|
|
@ -113,8 +113,8 @@ public class SparrowDesktop extends Application {
|
|||
private void initializeFonts() {
|
||||
GlyphFontRegistry.register(new FontAwesome5());
|
||||
GlyphFontRegistry.register(new FontAwesome5Brands());
|
||||
Font.loadFont(AppServices.class.getResourceAsStream("/font/RobotoMono-Regular.ttf"), 13);
|
||||
Font.loadFont(AppServices.class.getResourceAsStream("/font/RobotoMono-Italic.ttf"), 11);
|
||||
Font.loadFont(AppServices.class.getResourceAsStream("/font/FragmentMono-Regular.ttf"), 13);
|
||||
Font.loadFont(AppServices.class.getResourceAsStream("/font/FragmentMono-Italic.ttf"), 11);
|
||||
if(OsType.getCurrent() == OsType.MACOS) {
|
||||
Font.loadFont(AppServices.class.getResourceAsStream("/font/LiberationSans-Regular.ttf"), 13);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ import java.util.*;
|
|||
public class SparrowWallet {
|
||||
public static final String APP_ID = "sparrow";
|
||||
public static final String APP_NAME = "Sparrow";
|
||||
public static final String APP_VERSION = "2.2.2";
|
||||
public static final String APP_VERSION = "2.3.1";
|
||||
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";
|
||||
|
|
|
|||
|
|
@ -87,6 +87,8 @@ class CoinCell extends TreeTableCell<Entry, Number> implements ConfirmationsList
|
|||
} else if(entry instanceof UtxoEntry) {
|
||||
setGraphic(null);
|
||||
} else if(entry instanceof HashIndexEntry) {
|
||||
tooltip.hideConfirmations();
|
||||
|
||||
Region node = new Region();
|
||||
node.setPrefWidth(10);
|
||||
setGraphic(node);
|
||||
|
|
@ -148,6 +150,14 @@ class CoinCell extends TreeTableCell<Entry, Number> implements ConfirmationsList
|
|||
setTooltipText();
|
||||
}
|
||||
|
||||
public void hideConfirmations() {
|
||||
showConfirmations = false;
|
||||
isCoinbase = false;
|
||||
confirmationsProperty.unbind();
|
||||
|
||||
setTooltipText();
|
||||
}
|
||||
|
||||
private void setTooltipText() {
|
||||
setText(value + (showConfirmations ? " (" + getConfirmationsDescription() + ")" : ""));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,10 +6,16 @@ import javafx.beans.property.SimpleObjectProperty;
|
|||
import javafx.scene.Cursor;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.control.ComboBox;
|
||||
import javafx.scene.control.ContextMenu;
|
||||
import javafx.scene.control.MenuItem;
|
||||
import javafx.scene.control.SeparatorMenuItem;
|
||||
import javafx.scene.input.Clipboard;
|
||||
import javafx.scene.layout.Region;
|
||||
import javafx.scene.layout.StackPane;
|
||||
import org.controlsfx.control.textfield.CustomTextField;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class ComboBoxTextField extends CustomTextField {
|
||||
private final ObjectProperty<ComboBox<?>> comboProperty = new SimpleObjectProperty<>();
|
||||
|
||||
|
|
@ -68,4 +74,53 @@ public class ComboBoxTextField extends CustomTextField {
|
|||
public void setComboProperty(ComboBox<?> comboProperty) {
|
||||
this.comboProperty.set(comboProperty);
|
||||
}
|
||||
|
||||
public ContextMenu getCustomContextMenu(List<MenuItem> customItems) {
|
||||
return new CustomContextMenu(customItems);
|
||||
}
|
||||
|
||||
public class CustomContextMenu extends ContextMenu {
|
||||
public CustomContextMenu(List<MenuItem> customItems) {
|
||||
super();
|
||||
setFont(null);
|
||||
|
||||
MenuItem undo = new MenuItem("Undo");
|
||||
undo.setOnAction(_ -> undo());
|
||||
|
||||
MenuItem redo = new MenuItem("Redo");
|
||||
redo.setOnAction(_ -> redo());
|
||||
|
||||
MenuItem cut = new MenuItem("Cut");
|
||||
cut.setOnAction(_ -> cut());
|
||||
|
||||
MenuItem copy = new MenuItem("Copy");
|
||||
copy.setOnAction(_ -> copy());
|
||||
|
||||
MenuItem paste = new MenuItem("Paste");
|
||||
paste.setOnAction(_ -> paste());
|
||||
|
||||
MenuItem delete = new MenuItem("Delete");
|
||||
delete.setOnAction(_ -> deleteText(getSelection()));
|
||||
|
||||
MenuItem selectAll = new MenuItem("Select All");
|
||||
selectAll.setOnAction(_ -> selectAll());
|
||||
|
||||
getItems().addAll(undo, redo, new SeparatorMenuItem(), cut, copy, paste, delete, new SeparatorMenuItem(), selectAll);
|
||||
getItems().addAll(customItems);
|
||||
|
||||
setOnShowing(_ -> {
|
||||
boolean hasSelection = getSelection().getLength() > 0;
|
||||
boolean hasText = getText() != null && !getText().isEmpty();
|
||||
boolean clipboardHasContent = Clipboard.getSystemClipboard().hasString();
|
||||
|
||||
undo.setDisable(!isUndoable());
|
||||
redo.setDisable(!isRedoable());
|
||||
cut.setDisable(!isEditable() || !hasSelection);
|
||||
copy.setDisable(!hasSelection);
|
||||
paste.setDisable(!isEditable() || !clipboardHasContent);
|
||||
delete.setDisable(!hasSelection);
|
||||
selectAll.setDisable(!hasText);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,39 @@
|
|||
package com.sparrowwallet.sparrow.control;
|
||||
|
||||
import com.sparrowwallet.sparrow.AppServices;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.scene.control.Alert;
|
||||
import javafx.scene.control.ButtonType;
|
||||
import javafx.scene.control.CheckBox;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.layout.VBox;
|
||||
|
||||
import static com.sparrowwallet.sparrow.AppServices.getActiveWindow;
|
||||
import static com.sparrowwallet.sparrow.AppServices.setStageIcon;
|
||||
|
||||
public class ConfirmationAlert extends Alert {
|
||||
private final CheckBox dontAskAgain;
|
||||
|
||||
public ConfirmationAlert(String title, String contentText, ButtonType... buttons) {
|
||||
super(AlertType.CONFIRMATION, contentText, buttons);
|
||||
|
||||
initOwner(getActiveWindow());
|
||||
setStageIcon(getDialogPane().getScene().getWindow());
|
||||
getDialogPane().getScene().getStylesheets().add(AppServices.class.getResource("general.css").toExternalForm());
|
||||
setTitle(title);
|
||||
setHeaderText(title);
|
||||
|
||||
VBox contentBox = new VBox(20);
|
||||
contentBox.setPadding(new Insets(10, 20, 10, 20));
|
||||
Label contentLabel = new Label(contentText);
|
||||
contentLabel.setWrapText(true);
|
||||
dontAskAgain = new CheckBox("Don't ask again");
|
||||
contentBox.getChildren().addAll(contentLabel, dontAskAgain);
|
||||
|
||||
getDialogPane().setContent(contentBox);
|
||||
}
|
||||
|
||||
public boolean isDontAskAgain() {
|
||||
return dontAskAgain.isSelected();
|
||||
}
|
||||
}
|
||||
|
|
@ -11,6 +11,7 @@ import javafx.scene.control.Tooltip;
|
|||
import javafx.scene.input.Clipboard;
|
||||
import javafx.scene.input.ClipboardContent;
|
||||
import javafx.event.EventHandler;
|
||||
import javafx.scene.input.MouseButton;
|
||||
import javafx.scene.input.MouseEvent;
|
||||
|
||||
public class CopyableCoinLabel extends CopyableLabel {
|
||||
|
|
@ -29,6 +30,10 @@ public class CopyableCoinLabel extends CopyableLabel {
|
|||
valueProperty().addListener((observable, oldValue, newValue) -> setValueAsText((Long)newValue, Config.get().getUnitFormat(), Config.get().getBitcoinUnit()));
|
||||
|
||||
setOnMouseClicked(event -> {
|
||||
if(!event.getButton().equals(MouseButton.PRIMARY)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if(bitcoinUnit == null) {
|
||||
bitcoinUnit = Config.get().getBitcoinUnit();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -453,20 +453,26 @@ public class DevicePane extends TitledDescriptionPane {
|
|||
});
|
||||
vBox.getChildren().addAll(pinField, enterPinButton);
|
||||
|
||||
TilePane tilePane = new TilePane();
|
||||
tilePane.setPrefColumns(3);
|
||||
tilePane.setHgap(10);
|
||||
tilePane.setVgap(10);
|
||||
tilePane.setMaxWidth(150);
|
||||
tilePane.setMaxHeight(120);
|
||||
GridPane gridPane = new GridPane();
|
||||
gridPane.setHgap(10);
|
||||
gridPane.setVgap(10);
|
||||
gridPane.setMaxWidth(150);
|
||||
gridPane.setMaxHeight(device.getModel().hasZeroInPin() ? 160 : 120);
|
||||
|
||||
int[] digits = new int[] {7, 8, 9, 4, 5, 6, 1, 2, 3};
|
||||
int[] digits = device.getModel().hasZeroInPin() ? new int[] {7, 8, 9, 4, 5, 6, 1, 2, 3, 0} : new int[] {7, 8, 9, 4, 5, 6, 1, 2, 3};
|
||||
for(int i = 0; i < digits.length; i++) {
|
||||
Button pinButton = new Button();
|
||||
Glyph circle = new Glyph(FontAwesome5.FONT_NAME, "CIRCLE");
|
||||
pinButton.setGraphic(circle);
|
||||
pinButton.setUserData(digits[i]);
|
||||
tilePane.getChildren().add(pinButton);
|
||||
GridPane.setRowIndex(pinButton, i / 3);
|
||||
GridPane.setColumnIndex(pinButton, i % 3);
|
||||
if((i / 3) == 3) {
|
||||
GridPane.setHgrow(pinButton, Priority.ALWAYS);
|
||||
GridPane.setColumnSpan(pinButton, 3);
|
||||
pinButton.setMaxWidth(Double.MAX_VALUE);
|
||||
}
|
||||
gridPane.getChildren().add(pinButton);
|
||||
pinButton.setOnAction(event -> {
|
||||
pinField.setText(pinField.getText() + pinButton.getUserData());
|
||||
});
|
||||
|
|
@ -474,7 +480,7 @@ public class DevicePane extends TitledDescriptionPane {
|
|||
|
||||
HBox contentBox = new HBox();
|
||||
contentBox.setSpacing(50);
|
||||
contentBox.getChildren().add(tilePane);
|
||||
contentBox.getChildren().add(gridPane);
|
||||
contentBox.getChildren().add(vBox);
|
||||
contentBox.setPadding(new Insets(10, 0, 10, 0));
|
||||
contentBox.setAlignment(Pos.TOP_CENTER);
|
||||
|
|
|
|||
|
|
@ -5,6 +5,8 @@ import com.sparrowwallet.drongo.OsType;
|
|||
import com.sparrowwallet.drongo.Utils;
|
||||
import com.sparrowwallet.drongo.address.Address;
|
||||
import com.sparrowwallet.drongo.protocol.*;
|
||||
import com.sparrowwallet.drongo.silentpayments.SilentPayment;
|
||||
import com.sparrowwallet.drongo.silentpayments.SilentPaymentAddress;
|
||||
import com.sparrowwallet.drongo.wallet.*;
|
||||
import com.sparrowwallet.sparrow.AppServices;
|
||||
import com.sparrowwallet.sparrow.EventManager;
|
||||
|
|
@ -55,7 +57,7 @@ public class EntryCell extends TreeTableCell<Entry, Entry> implements Confirmati
|
|||
super.updateItem(entry, empty);
|
||||
|
||||
//Return immediately to avoid CPU usage when updating the same invisible cell to determine tableview size (see https://bugs.openjdk.org/browse/JDK-8280442)
|
||||
if(this == lastCell && !getTableRow().isVisible()) {
|
||||
if(this == lastCell && !getTableRow().isVisible() && isTableSizeRecalculation()) {
|
||||
return;
|
||||
}
|
||||
lastCell = this;
|
||||
|
|
@ -66,8 +68,7 @@ public class EntryCell extends TreeTableCell<Entry, Entry> implements Confirmati
|
|||
setText(null);
|
||||
setGraphic(null);
|
||||
} else {
|
||||
if(entry instanceof TransactionEntry) {
|
||||
TransactionEntry transactionEntry = (TransactionEntry)entry;
|
||||
if(entry instanceof TransactionEntry transactionEntry) {
|
||||
if(transactionEntry.getBlockTransaction().getHeight() == -1) {
|
||||
setText("Unconfirmed Parent");
|
||||
setContextMenu(new UnconfirmedTransactionContextMenu(transactionEntry));
|
||||
|
|
@ -101,7 +102,7 @@ public class EntryCell extends TreeTableCell<Entry, Entry> implements Confirmati
|
|||
actionBox.getChildren().add(viewTransactionButton);
|
||||
|
||||
BlockTransaction blockTransaction = transactionEntry.getBlockTransaction();
|
||||
if(blockTransaction.getHeight() <= 0 && canRBF(blockTransaction) &&
|
||||
if(blockTransaction.getHeight() <= 0 && canRBF(blockTransaction, transactionEntry.getWallet()) &&
|
||||
Config.get().isIncludeMempoolOutputs() && transactionEntry.getWallet().allInputsFromWallet(blockTransaction.getHash())) {
|
||||
Button increaseFeeButton = new Button("");
|
||||
increaseFeeButton.setGraphic(getIncreaseFeeRBFGlyph());
|
||||
|
|
@ -121,8 +122,7 @@ public class EntryCell extends TreeTableCell<Entry, Entry> implements Confirmati
|
|||
}
|
||||
|
||||
setGraphic(actionBox);
|
||||
} else if(entry instanceof NodeEntry) {
|
||||
NodeEntry nodeEntry = (NodeEntry)entry;
|
||||
} else if(entry instanceof NodeEntry nodeEntry) {
|
||||
Address address = nodeEntry.getAddress();
|
||||
setText(address.toString());
|
||||
setContextMenu(new AddressContextMenu(address, nodeEntry.getOutputDescriptor(), nodeEntry, true, getTreeTableView()));
|
||||
|
|
@ -163,8 +163,7 @@ public class EntryCell extends TreeTableCell<Entry, Entry> implements Confirmati
|
|||
setContextMenu(null);
|
||||
setGraphic(new HBox());
|
||||
}
|
||||
} else if(entry instanceof HashIndexEntry) {
|
||||
HashIndexEntry hashIndexEntry = (HashIndexEntry)entry;
|
||||
} else if(entry instanceof HashIndexEntry hashIndexEntry) {
|
||||
setText(hashIndexEntry.getDescription());
|
||||
setContextMenu(getTreeTableView().getStyleClass().contains("bip47") ? null : new HashIndexEntryContextMenu(getTreeTableView(), hashIndexEntry));
|
||||
Tooltip tooltip = new Tooltip();
|
||||
|
|
@ -212,13 +211,14 @@ public class EntryCell extends TreeTableCell<Entry, Entry> implements Confirmati
|
|||
|
||||
private static void increaseFee(TransactionEntry transactionEntry, boolean cancelTransaction) {
|
||||
BlockTransaction blockTransaction = transactionEntry.getBlockTransaction();
|
||||
boolean silentPaymentTransaction = transactionEntry.getWallet().isSilentPaymentsTransaction(blockTransaction);
|
||||
Map<BlockTransactionHashIndex, WalletNode> walletTxos = transactionEntry.getWallet().getWalletTxos();
|
||||
List<BlockTransactionHashIndex> utxos = transactionEntry.getChildren().stream()
|
||||
.filter(e -> e instanceof HashIndexEntry)
|
||||
.map(e -> (HashIndexEntry)e)
|
||||
.filter(e -> e.getType().equals(HashIndexEntry.Type.INPUT) && e.isSpendable())
|
||||
.map(e -> blockTransaction.getTransaction().getInputs().get((int)e.getHashIndex().getIndex()))
|
||||
.filter(i -> Config.get().isMempoolFullRbf() || i.isReplaceByFeeEnabled())
|
||||
.filter(i -> Config.get().isMempoolFullRbf() || i.isReplaceByFeeEnabled() || silentPaymentTransaction)
|
||||
.map(txInput -> walletTxos.keySet().stream().filter(txo -> txo.getHash().equals(txInput.getOutpoint().getHash()) && txo.getIndex() == txInput.getOutpoint().getIndex()).findFirst().get())
|
||||
.collect(Collectors.toList());
|
||||
|
||||
|
|
@ -243,6 +243,7 @@ public class EntryCell extends TreeTableCell<Entry, Entry> implements Confirmati
|
|||
.collect(Collectors.toList());
|
||||
|
||||
boolean consolidationTransaction = consolidationOutputs.size() == blockTransaction.getTransaction().getOutputs().size() && consolidationOutputs.size() == 1;
|
||||
boolean safeToAddInputsOrOutputs = transactionEntry.getWallet().isSafeToAddInputsOrOutputs(blockTransaction);
|
||||
long changeTotal = ourOutputs.stream().mapToLong(TransactionOutput::getValue).sum() - consolidationOutputs.stream().mapToLong(TransactionOutput::getValue).sum();
|
||||
Transaction tx = blockTransaction.getTransaction();
|
||||
double vSize = tx.getVirtualSize();
|
||||
|
|
@ -257,7 +258,7 @@ public class EntryCell extends TreeTableCell<Entry, Entry> implements Confirmati
|
|||
List<OutputGroup> outputGroups = transactionEntry.getWallet().getGroupedUtxos(txoFilters, feeRate, AppServices.getMinimumRelayFeeRate(), Config.get().isGroupByAddress())
|
||||
.stream().filter(outputGroup -> outputGroup.getEffectiveValue() >= 0).collect(Collectors.toList());
|
||||
Collections.shuffle(outputGroups);
|
||||
while((double)changeTotal / vSize < getMaxFeeRate() && !outputGroups.isEmpty() && !cancelTransaction && !consolidationTransaction) {
|
||||
while((double)changeTotal / vSize < getMaxFeeRate() && !outputGroups.isEmpty() && !cancelTransaction && !consolidationTransaction && safeToAddInputsOrOutputs) {
|
||||
//If there is insufficient change output, include another random output group so the fee can be increased
|
||||
OutputGroup outputGroup = outputGroups.remove(0);
|
||||
for(BlockTransactionHashIndex utxo : outputGroup.getUtxos()) {
|
||||
|
|
@ -298,9 +299,13 @@ public class EntryCell extends TreeTableCell<Entry, Entry> implements Confirmati
|
|||
label += " (Replaced By Fee)";
|
||||
}
|
||||
|
||||
if(txOutput.getScript().getToAddress() != null) {
|
||||
Address address = txOutput.getScript().getToAddress();
|
||||
if(address != null) {
|
||||
long value = txOutput.getValue();
|
||||
//Disable change creation by enabling max payment when there is only one output and no additional UTXOs included
|
||||
return new Payment(txOutput.getScript().getToAddress(), label, txOutput.getValue(), blockTransaction.getTransaction().getOutputs().size() == 1 && rbfChange == 0);
|
||||
boolean sendMax = blockTransaction.getTransaction().getOutputs().size() == 1 && rbfChange == 0;
|
||||
SilentPaymentAddress silentPaymentAddress = transactionEntry.getWallet().getSilentPaymentAddress(address);
|
||||
return silentPaymentAddress == null ? new Payment(address, label, value, sendMax) : new SilentPayment(silentPaymentAddress, label, value, sendMax);
|
||||
}
|
||||
|
||||
return null;
|
||||
|
|
@ -337,7 +342,7 @@ public class EntryCell extends TreeTableCell<Entry, Entry> implements Confirmati
|
|||
}
|
||||
|
||||
EventManager.get().post(new SendActionEvent(transactionEntry.getWallet(), utxos));
|
||||
Platform.runLater(() -> EventManager.get().post(new SpendUtxoEvent(transactionEntry.getWallet(), utxos, payments, opReturns.isEmpty() ? null : opReturns, rbfFee, true, blockTransaction)));
|
||||
Platform.runLater(() -> EventManager.get().post(new SpendUtxoEvent(transactionEntry.getWallet(), utxos, payments, opReturns.isEmpty() ? null : opReturns, rbfFee, true, blockTransaction, safeToAddInputsOrOutputs)));
|
||||
}
|
||||
|
||||
private static Double getMaxFeeRate() {
|
||||
|
|
@ -394,11 +399,11 @@ public class EntryCell extends TreeTableCell<Entry, Entry> implements Confirmati
|
|||
Payment payment = new Payment(freshAddress, label, inputTotal, true);
|
||||
|
||||
EventManager.get().post(new SendActionEvent(transactionEntry.getWallet(), utxos));
|
||||
Platform.runLater(() -> EventManager.get().post(new SpendUtxoEvent(transactionEntry.getWallet(), utxos, List.of(payment), null, blockTransaction.getFee(), true, null)));
|
||||
Platform.runLater(() -> EventManager.get().post(new SpendUtxoEvent(transactionEntry.getWallet(), utxos, List.of(payment), null, blockTransaction.getFee(), true, null, true)));
|
||||
}
|
||||
|
||||
private static boolean canRBF(BlockTransaction blockTransaction) {
|
||||
return Config.get().isMempoolFullRbf() || blockTransaction.getTransaction().isReplaceByFee();
|
||||
private static boolean canRBF(BlockTransaction blockTransaction, Wallet wallet) {
|
||||
return Config.get().isMempoolFullRbf() || blockTransaction.getTransaction().isReplaceByFee() || wallet.isSilentPaymentsTransaction(blockTransaction);
|
||||
}
|
||||
|
||||
private static boolean canSignMessage(WalletNode walletNode) {
|
||||
|
|
@ -476,7 +481,7 @@ public class EntryCell extends TreeTableCell<Entry, Entry> implements Confirmati
|
|||
tooltip += "\nFee rate: " + String.format("%.2f", feeRate) + " sats/vB";
|
||||
}
|
||||
|
||||
tooltip += "\nRBF: " + (canRBF(transactionEntry.getBlockTransaction()) ? "Enabled" : "Disabled");
|
||||
tooltip += "\nRBF: " + (canRBF(transactionEntry.getBlockTransaction(), transactionEntry.getWallet()) ? "Enabled" : "Disabled");
|
||||
}
|
||||
|
||||
return tooltip;
|
||||
|
|
@ -544,6 +549,7 @@ public class EntryCell extends TreeTableCell<Entry, Entry> implements Confirmati
|
|||
|
||||
private static class UnconfirmedTransactionContextMenu extends ContextMenu {
|
||||
public UnconfirmedTransactionContextMenu(TransactionEntry transactionEntry) {
|
||||
Wallet wallet = transactionEntry.getWallet();
|
||||
BlockTransaction blockTransaction = transactionEntry.getBlockTransaction();
|
||||
MenuItem viewTransaction = new MenuItem("View Transaction");
|
||||
viewTransaction.setGraphic(getViewTransactionGlyph());
|
||||
|
|
@ -553,7 +559,7 @@ public class EntryCell extends TreeTableCell<Entry, Entry> implements Confirmati
|
|||
});
|
||||
getItems().add(viewTransaction);
|
||||
|
||||
if(canRBF(blockTransaction) && Config.get().isIncludeMempoolOutputs() && transactionEntry.getWallet().allInputsFromWallet(blockTransaction.getHash())) {
|
||||
if(canRBF(blockTransaction, wallet) && Config.get().isIncludeMempoolOutputs() && transactionEntry.getWallet().allInputsFromWallet(blockTransaction.getHash())) {
|
||||
MenuItem increaseFee = new MenuItem("Increase Fee (RBF)");
|
||||
increaseFee.setGraphic(getIncreaseFeeRBFGlyph());
|
||||
increaseFee.setOnAction(AE -> {
|
||||
|
|
@ -564,7 +570,7 @@ public class EntryCell extends TreeTableCell<Entry, Entry> implements Confirmati
|
|||
getItems().add(increaseFee);
|
||||
}
|
||||
|
||||
if(canRBF(blockTransaction) && Config.get().isIncludeMempoolOutputs() && transactionEntry.getWallet().allInputsFromWallet(blockTransaction.getHash())) {
|
||||
if(canRBF(blockTransaction, wallet) && Config.get().isIncludeMempoolOutputs() && transactionEntry.getWallet().allInputsFromWallet(blockTransaction.getHash())) {
|
||||
MenuItem cancelTx = new MenuItem("Cancel Transaction (RBF)");
|
||||
cancelTx.setGraphic(getCancelTransactionRBFGlyph());
|
||||
cancelTx.setOnAction(AE -> {
|
||||
|
|
@ -850,4 +856,11 @@ public class EntryCell extends TreeTableCell<Entry, Entry> implements Confirmati
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isTableSizeRecalculation() {
|
||||
//As per https://bugs.openjdk.org/browse/JDK-8265669 we check for cell visibility to avoid unnecessary recalculation, but this can result in false positives
|
||||
//The method releaseCell in VirtualFlow is responsible for setting accumCell visibility to false after use, so check this method is calling updateItem
|
||||
return StackWalker.getInstance().walk(frames -> frames.anyMatch(frame -> frame.getClassName().equals("javafx.scene.control.skin.VirtualFlow")
|
||||
&& frame.getMethodName().equals("releaseCell")));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
package com.sparrowwallet.sparrow.control;
|
||||
|
||||
import com.sparrowwallet.drongo.protocol.Transaction;
|
||||
import com.sparrowwallet.sparrow.AppServices;
|
||||
import com.sparrowwallet.sparrow.net.FeeRatesSource;
|
||||
import javafx.application.Platform;
|
||||
|
|
@ -7,6 +8,7 @@ import javafx.scene.Node;
|
|||
import javafx.scene.control.Slider;
|
||||
import javafx.util.StringConverter;
|
||||
|
||||
import java.text.DecimalFormat;
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
|
|
@ -14,9 +16,11 @@ import static com.sparrowwallet.sparrow.AppServices.*;
|
|||
|
||||
public class FeeRangeSlider extends Slider {
|
||||
private static final double FEE_RATE_SCROLL_INCREMENT = 0.01;
|
||||
private static final DecimalFormat INTEGER_FEE_RATE_FORMAT = new DecimalFormat("0");
|
||||
private static final DecimalFormat FRACTIONAL_FEE_RATE_FORMAT = new DecimalFormat("0.###");
|
||||
|
||||
public FeeRangeSlider() {
|
||||
super(0, FEE_RATES_RANGE.size() - 1, 0);
|
||||
super(0, AppServices.getFeeRatesRange().size() - 1, 0);
|
||||
setMajorTickUnit(1);
|
||||
setMinorTickCount(0);
|
||||
setSnapToTicks(false);
|
||||
|
|
@ -27,11 +31,11 @@ public class FeeRangeSlider extends Slider {
|
|||
setLabelFormatter(new StringConverter<>() {
|
||||
@Override
|
||||
public String toString(Double object) {
|
||||
Long feeRate = LONG_FEE_RATES_RANGE.get(object.intValue());
|
||||
Double feeRate = AppServices.getLongFeeRatesRange().get(object.intValue());
|
||||
if(isLongFeeRange() && feeRate >= 1000) {
|
||||
return feeRate / 1000 + "k";
|
||||
return INTEGER_FEE_RATE_FORMAT.format(feeRate / 1000) + "k";
|
||||
}
|
||||
return Long.toString(feeRate);
|
||||
return feeRate > 0d && feeRate < Transaction.DEFAULT_MIN_RELAY_FEE ? FRACTIONAL_FEE_RATE_FORMAT.format(feeRate) : INTEGER_FEE_RATE_FORMAT.format(feeRate);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
@ -51,10 +55,10 @@ public class FeeRangeSlider extends Slider {
|
|||
setOnScroll(event -> {
|
||||
if(event.getDeltaY() != 0) {
|
||||
double newFeeRate = getFeeRate() + (event.getDeltaY() > 0 ? FEE_RATE_SCROLL_INCREMENT : -FEE_RATE_SCROLL_INCREMENT);
|
||||
if(newFeeRate < LONG_FEE_RATES_RANGE.get(0)) {
|
||||
newFeeRate = LONG_FEE_RATES_RANGE.get(0);
|
||||
} else if(newFeeRate > LONG_FEE_RATES_RANGE.get(LONG_FEE_RATES_RANGE.size() - 1)) {
|
||||
newFeeRate = LONG_FEE_RATES_RANGE.get(LONG_FEE_RATES_RANGE.size() - 1);
|
||||
if(newFeeRate < AppServices.getLongFeeRatesRange().getFirst()) {
|
||||
newFeeRate = AppServices.getLongFeeRatesRange().getFirst();
|
||||
} else if(newFeeRate > AppServices.getLongFeeRatesRange().getLast()) {
|
||||
newFeeRate = AppServices.getLongFeeRatesRange().getLast();
|
||||
}
|
||||
setFeeRate(newFeeRate);
|
||||
}
|
||||
|
|
@ -62,27 +66,79 @@ public class FeeRangeSlider extends Slider {
|
|||
}
|
||||
|
||||
public double getFeeRate() {
|
||||
return getFeeRate(AppServices.getMinimumRelayFeeRate());
|
||||
}
|
||||
|
||||
public double getFeeRate(Double minRelayFeeRate) {
|
||||
if(minRelayFeeRate >= Transaction.DEFAULT_MIN_RELAY_FEE) {
|
||||
return Math.pow(2.0, getValue());
|
||||
}
|
||||
|
||||
if(getValue() < 1.0d) {
|
||||
if(minRelayFeeRate == 0.0d) {
|
||||
return getValue();
|
||||
}
|
||||
return Math.pow(minRelayFeeRate, 1.0d - getValue());
|
||||
}
|
||||
|
||||
return Math.pow(2.0, getValue() - 1.0d);
|
||||
}
|
||||
|
||||
public void setFeeRate(double feeRate) {
|
||||
double value = Math.log(feeRate) / Math.log(2);
|
||||
setFeeRate(feeRate, AppServices.getMinimumRelayFeeRate());
|
||||
}
|
||||
|
||||
public void setFeeRate(double feeRate, Double minRelayFeeRate) {
|
||||
double value = getValue(feeRate, minRelayFeeRate);
|
||||
updateMaxFeeRange(value);
|
||||
setValue(value);
|
||||
}
|
||||
|
||||
private double getValue(double feeRate, Double minRelayFeeRate) {
|
||||
double value;
|
||||
|
||||
if(minRelayFeeRate >= Transaction.DEFAULT_MIN_RELAY_FEE) {
|
||||
value = Math.log(feeRate) / Math.log(2);
|
||||
} else {
|
||||
if(feeRate < Transaction.DEFAULT_MIN_RELAY_FEE) {
|
||||
if(minRelayFeeRate == 0.0d) {
|
||||
return feeRate;
|
||||
}
|
||||
value = 1.0d - (Math.log(feeRate) / Math.log(minRelayFeeRate));
|
||||
} else {
|
||||
value = (Math.log(feeRate) / Math.log(2.0)) + 1.0d;
|
||||
}
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
public void updateFeeRange(Double minRelayFeeRate, Double previousMinRelayFeeRate) {
|
||||
if(minRelayFeeRate != null && previousMinRelayFeeRate != null) {
|
||||
setFeeRate(getFeeRate(previousMinRelayFeeRate), minRelayFeeRate);
|
||||
}
|
||||
setMinorTickCount(1);
|
||||
setMinorTickCount(0);
|
||||
}
|
||||
|
||||
private void updateMaxFeeRange(double value) {
|
||||
if(value >= getMax() && !isLongFeeRange()) {
|
||||
setMax(LONG_FEE_RATES_RANGE.size() - 1);
|
||||
if(AppServices.getMinimumRelayFeeRate() < Transaction.DEFAULT_MIN_RELAY_FEE) {
|
||||
setMin(1.0d);
|
||||
}
|
||||
setMax(AppServices.getLongFeeRatesRange().size() - 1);
|
||||
updateTrackHighlight();
|
||||
} else if(value == getMin() && isLongFeeRange()) {
|
||||
setMax(FEE_RATES_RANGE.size() - 1);
|
||||
if(AppServices.getMinimumRelayFeeRate() < Transaction.DEFAULT_MIN_RELAY_FEE) {
|
||||
setMin(0.0d);
|
||||
}
|
||||
setMax(AppServices.getFeeRatesRange().size() - 1);
|
||||
updateTrackHighlight();
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isLongFeeRange() {
|
||||
return getMax() > FEE_RATES_RANGE.size() - 1;
|
||||
public boolean isLongFeeRange() {
|
||||
return getMax() > AppServices.getFeeRatesRange().size() - 1;
|
||||
}
|
||||
|
||||
public void updateTrackHighlight() {
|
||||
|
|
@ -137,9 +193,9 @@ public class FeeRangeSlider extends Slider {
|
|||
}
|
||||
|
||||
private int getPercentageOfFeeRange(Double feeRate) {
|
||||
double index = Math.log(feeRate) / Math.log(2);
|
||||
double index = getValue(feeRate, AppServices.getMinimumRelayFeeRate());
|
||||
if(isLongFeeRange()) {
|
||||
index *= ((double)FEE_RATES_RANGE.size() / (LONG_FEE_RATES_RANGE.size())) * 0.99;
|
||||
index *= ((double)AppServices.getFeeRatesRange().size() / (AppServices.getLongFeeRatesRange().size())) * 0.99;
|
||||
}
|
||||
return (int)Math.round(index * 10.0);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -240,6 +240,9 @@ public class MessageSignDialog extends Dialog<ButtonBar.ButtonData> {
|
|||
setFormatFromScriptType(address.getScriptType());
|
||||
if(wallet != null) {
|
||||
setWalletNodeFromAddress(wallet, address);
|
||||
if(walletNode != null) {
|
||||
setFormatFromScriptType(getSigningScriptType(walletNode));
|
||||
}
|
||||
}
|
||||
} catch(InvalidAddressException e) {
|
||||
//can't happen
|
||||
|
|
@ -273,7 +276,7 @@ public class MessageSignDialog extends Dialog<ButtonBar.ButtonData> {
|
|||
}
|
||||
|
||||
if(wallet != null && walletNode != null) {
|
||||
setFormatFromScriptType(wallet.getScriptType());
|
||||
setFormatFromScriptType(getSigningScriptType(walletNode));
|
||||
} else {
|
||||
formatGroup.selectToggle(formatElectrum);
|
||||
}
|
||||
|
|
@ -287,9 +290,13 @@ public class MessageSignDialog extends Dialog<ButtonBar.ButtonData> {
|
|||
}
|
||||
|
||||
private boolean canSign(Wallet wallet) {
|
||||
return wallet.getKeystores().get(0).hasPrivateKey()
|
||||
|| wallet.getKeystores().get(0).getSource() == KeystoreSource.HW_USB
|
||||
|| wallet.getKeystores().get(0).getWalletModel().isCard();
|
||||
return wallet.getKeystores().getFirst().hasPrivateKey()
|
||||
|| wallet.getKeystores().getFirst().getSource() == KeystoreSource.HW_USB
|
||||
|| wallet.getKeystores().getFirst().getWalletModel().isCard();
|
||||
}
|
||||
|
||||
private boolean canSignBip322(Wallet wallet) {
|
||||
return wallet.getKeystores().getFirst().hasPrivateKey();
|
||||
}
|
||||
|
||||
private Address getAddress()throws InvalidAddressException {
|
||||
|
|
@ -313,6 +320,11 @@ public class MessageSignDialog extends Dialog<ButtonBar.ButtonData> {
|
|||
walletNode = wallet.getWalletAddresses().get(address);
|
||||
}
|
||||
|
||||
private ScriptType getSigningScriptType(WalletNode walletNode) {
|
||||
ScriptType scriptType = walletNode.getWallet().getScriptType();
|
||||
return canSign(walletNode.getWallet()) && !canSignBip322(walletNode.getWallet()) ? ScriptType.P2PKH : scriptType;
|
||||
}
|
||||
|
||||
private void setFormatFromScriptType(ScriptType scriptType) {
|
||||
formatElectrum.setDisable(scriptType == ScriptType.P2TR);
|
||||
formatTrezor.setDisable(scriptType == ScriptType.P2TR || scriptType == ScriptType.P2PKH);
|
||||
|
|
@ -345,7 +357,7 @@ public class MessageSignDialog extends Dialog<ButtonBar.ButtonData> {
|
|||
|
||||
//Note we can expect a single keystore due to the check in the constructor
|
||||
Wallet signingWallet = walletNode.getWallet();
|
||||
if(signingWallet.getKeystores().get(0).hasPrivateKey()) {
|
||||
if(signingWallet.getKeystores().getFirst().hasPrivateKey()) {
|
||||
if(signingWallet.isEncrypted()) {
|
||||
EventManager.get().post(new RequestOpenWalletsEvent());
|
||||
} else {
|
||||
|
|
@ -358,7 +370,7 @@ public class MessageSignDialog extends Dialog<ButtonBar.ButtonData> {
|
|||
|
||||
private void signUnencryptedKeystore(Wallet decryptedWallet) {
|
||||
try {
|
||||
Keystore keystore = decryptedWallet.getKeystores().get(0);
|
||||
Keystore keystore = decryptedWallet.getKeystores().getFirst();
|
||||
ECKey privKey = keystore.getKey(walletNode);
|
||||
String signatureText;
|
||||
if(isBip322()) {
|
||||
|
|
@ -378,8 +390,8 @@ public class MessageSignDialog extends Dialog<ButtonBar.ButtonData> {
|
|||
}
|
||||
|
||||
private void signDeviceKeystore(Wallet deviceWallet) {
|
||||
List<String> fingerprints = List.of(deviceWallet.getKeystores().get(0).getKeyDerivation().getMasterFingerprint());
|
||||
KeyDerivation fullDerivation = deviceWallet.getKeystores().get(0).getKeyDerivation().extend(walletNode.getDerivation());
|
||||
List<String> fingerprints = List.of(deviceWallet.getKeystores().getFirst().getKeyDerivation().getMasterFingerprint());
|
||||
KeyDerivation fullDerivation = deviceWallet.getKeystores().getFirst().getKeyDerivation().extend(walletNode.getDerivation());
|
||||
DeviceSignMessageDialog deviceSignMessageDialog = new DeviceSignMessageDialog(fingerprints, deviceWallet, message.getText().trim(), fullDerivation);
|
||||
deviceSignMessageDialog.initOwner(getDialogPane().getScene().getWindow());
|
||||
Optional<String> optSignature = deviceSignMessageDialog.showAndWait();
|
||||
|
|
|
|||
|
|
@ -398,14 +398,14 @@ public class PrivateKeySweepDialog extends Dialog<Transaction> {
|
|||
|
||||
double feeRate = feeRange.getFeeRate();
|
||||
long fee = (long)Math.ceil(noFeeTransaction.getVirtualSize() * feeRate);
|
||||
if(feeRate == Transaction.DEFAULT_MIN_RELAY_FEE) {
|
||||
if(feeRate == AppServices.getMinimumRelayFeeRate() && feeRate > 0d) {
|
||||
fee++;
|
||||
}
|
||||
|
||||
long dustThreshold = destAddress.getScriptType().getDustThreshold(sweepOutput, Transaction.DUST_RELAY_TX_FEE);
|
||||
if(total - fee <= dustThreshold) {
|
||||
feeRate = Transaction.DEFAULT_MIN_RELAY_FEE;
|
||||
fee = (long)Math.ceil(noFeeTransaction.getVirtualSize() * feeRate) + 1;
|
||||
feeRate = AppServices.getMinimumRelayFeeRate();
|
||||
fee = (long)Math.ceil(noFeeTransaction.getVirtualSize() * feeRate) + (feeRate > 0d ? 1 : 0);
|
||||
|
||||
if(total - fee <= dustThreshold) {
|
||||
AppServices.showErrorDialog("Insufficient funds", "The unspent outputs for this private key contain insufficient funds to spend (" + total + " sats).");
|
||||
|
|
|
|||
|
|
@ -122,19 +122,21 @@ public class QRScanDialog extends Dialog<QRScanDialog.Result> {
|
|||
if(percentComplete.get() <= 0.0) {
|
||||
Platform.runLater(() -> percentComplete.set(opening ? 0.0 : -1.0));
|
||||
}
|
||||
});
|
||||
|
||||
if(opening) {
|
||||
webcamService.openedProperty().addListener((_, _, opened) -> {
|
||||
if(opened) {
|
||||
Platform.runLater(() -> {
|
||||
try {
|
||||
postOpenUpdate = true;
|
||||
List<CaptureDevice> newDevices = new ArrayList<>(webcamService.getDevices());
|
||||
List<CaptureDevice> newDevices = new ArrayList<>(webcamService.getAvailableDevices());
|
||||
newDevices.removeAll(foundDevices);
|
||||
foundDevices.addAll(newDevices);
|
||||
foundDevices.removeIf(device -> !webcamService.getDevices().contains(device));
|
||||
|
||||
if(Config.get().getWebcamDevice() != null && webcamDeviceProperty.get() == null) {
|
||||
if(webcamService.getDevice() != null) {
|
||||
for(CaptureDevice device : foundDevices) {
|
||||
if(device.getName().equals(Config.get().getWebcamDevice())) {
|
||||
if(device.equals(webcamService.getDevice())) {
|
||||
webcamDeviceProperty.set(device);
|
||||
}
|
||||
}
|
||||
|
|
@ -146,10 +148,7 @@ public class QRScanDialog extends Dialog<QRScanDialog.Result> {
|
|||
postOpenUpdate = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
webcamService.closedProperty().addListener((_, _, closed) -> {
|
||||
if(closed && webcamResolutionProperty.get() != null) {
|
||||
} else if(webcamResolutionProperty.get() != null) {
|
||||
webcamService.setResolution(webcamResolutionProperty.get());
|
||||
webcamService.setDevice(webcamDeviceProperty.get());
|
||||
Platform.runLater(() -> {
|
||||
|
|
@ -190,6 +189,7 @@ public class QRScanDialog extends Dialog<QRScanDialog.Result> {
|
|||
});
|
||||
webcamDeviceProperty.addListener((_, _, newValue) -> {
|
||||
Config.get().setWebcamDevice(newValue.getName());
|
||||
Config.get().setWebcamDeviceId(newValue.getUniqueId());
|
||||
if(!Objects.equals(webcamService.getDevice(), newValue)) {
|
||||
webcamService.cancel();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -57,7 +57,7 @@ public class RecentBlocksView extends Pane {
|
|||
}
|
||||
|
||||
public void updateFeeRatesSource(FeeRatesSource feeRatesSource) {
|
||||
tooltip.setText("Fee rates from " + feeRatesSource.getDescription());
|
||||
tooltip.setText("Fee rate estimate from " + feeRatesSource.getDescription());
|
||||
if(getCubes() != null && !getCubes().isEmpty()) {
|
||||
getCubes().getFirst().setFeeRatesSource(feeRatesSource);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ package com.sparrowwallet.sparrow.control;
|
|||
|
||||
import com.sparrowwallet.drongo.protocol.Script;
|
||||
import com.sparrowwallet.drongo.protocol.ScriptChunk;
|
||||
import com.sparrowwallet.drongo.protocol.ScriptOpCodes;
|
||||
import javafx.geometry.Pos;
|
||||
import org.controlsfx.control.decoration.Decorator;
|
||||
import org.controlsfx.control.decoration.GraphicDecoration;
|
||||
|
|
@ -53,7 +54,11 @@ public class ScriptArea extends CodeArea {
|
|||
for (int i = 0; i < script.getChunks().size(); i++) {
|
||||
ScriptChunk chunk = script.getChunks().get(i);
|
||||
if(chunk.isOpCode()) {
|
||||
if(chunk.getOpcode() == ScriptOpCodes.OP_0 && witnessScript != null) {
|
||||
append("<empty>", "script-other");
|
||||
} else {
|
||||
append(chunk.toString(), "script-opcode");
|
||||
}
|
||||
} else if(chunk.isPubKey()) {
|
||||
append("<pubkey" + pubKeyCount++ + ">", "script-pubkey");
|
||||
} else if(chunk.isSignature()) {
|
||||
|
|
|
|||
|
|
@ -5,36 +5,49 @@ import com.sparrowwallet.drongo.BitcoinUnit;
|
|||
import com.sparrowwallet.drongo.OsType;
|
||||
import com.sparrowwallet.drongo.address.Address;
|
||||
import com.sparrowwallet.drongo.address.InvalidAddressException;
|
||||
import com.sparrowwallet.drongo.dns.DnsPayment;
|
||||
import com.sparrowwallet.drongo.dns.DnsPaymentCache;
|
||||
import com.sparrowwallet.drongo.dns.DnsPaymentResolver;
|
||||
import com.sparrowwallet.drongo.dns.DnsPaymentValidationException;
|
||||
import com.sparrowwallet.drongo.protocol.Transaction;
|
||||
import com.sparrowwallet.drongo.silentpayments.SilentPayment;
|
||||
import com.sparrowwallet.drongo.silentpayments.SilentPaymentAddress;
|
||||
import com.sparrowwallet.drongo.uri.BitcoinURIParseException;
|
||||
import com.sparrowwallet.drongo.wallet.Payment;
|
||||
import com.sparrowwallet.sparrow.AppServices;
|
||||
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
|
||||
import com.sparrowwallet.sparrow.EventManager;
|
||||
import com.sparrowwallet.sparrow.event.RequestConnectEvent;
|
||||
import com.sparrowwallet.sparrow.glyphfont.GlyphUtils;
|
||||
import com.sparrowwallet.sparrow.io.Config;
|
||||
import javafx.application.Platform;
|
||||
import javafx.collections.FXCollections;
|
||||
import javafx.collections.ObservableList;
|
||||
import javafx.concurrent.Service;
|
||||
import javafx.concurrent.Task;
|
||||
import javafx.event.ActionEvent;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.control.*;
|
||||
import javafx.scene.image.Image;
|
||||
import javafx.scene.image.ImageView;
|
||||
import javafx.scene.input.Clipboard;
|
||||
import javafx.scene.layout.StackPane;
|
||||
import javafx.stage.FileChooser;
|
||||
import javafx.util.StringConverter;
|
||||
import org.controlsfx.control.spreadsheet.*;
|
||||
import org.controlsfx.glyphfont.Glyph;
|
||||
|
||||
import java.io.*;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.IntStream;
|
||||
|
||||
public class SendToManyDialog extends Dialog<List<Payment>> {
|
||||
private final BitcoinUnit bitcoinUnit;
|
||||
private final SpreadsheetView spreadsheetView;
|
||||
public static final AddressCellType ADDRESS = new AddressCellType();
|
||||
public static final SendToAddressCellType SEND_TO_ADDRESS = new SendToAddressCellType();
|
||||
|
||||
public SendToManyDialog(BitcoinUnit bitcoinUnit) {
|
||||
public SendToManyDialog(BitcoinUnit bitcoinUnit, List<Payment> payments) {
|
||||
this.bitcoinUnit = bitcoinUnit;
|
||||
|
||||
final DialogPane dialogPane = new SendToManyDialogPane();
|
||||
|
|
@ -44,7 +57,8 @@ public class SendToManyDialog extends Dialog<List<Payment>> {
|
|||
dialogPane.setHeaderText("Send to many recipients by specifying addresses and amounts.\nOnly the first row's label is necessary.");
|
||||
dialogPane.setGraphic(new DialogImage(DialogImage.Type.SPARROW));
|
||||
|
||||
List<Payment> initialPayments = IntStream.range(0, 100).mapToObj(i -> new Payment(null, null, -1, false)).collect(Collectors.toList());
|
||||
List<Payment> initialPayments = IntStream.range(0, 100)
|
||||
.mapToObj(i -> i < payments.size() ? payments.get(i) : new Payment(null, null, -1, false)).collect(Collectors.toList());
|
||||
Grid grid = getGrid(initialPayments);
|
||||
|
||||
spreadsheetView = new SpreadsheetView(grid) {
|
||||
|
|
@ -69,14 +83,16 @@ public class SendToManyDialog extends Dialog<List<Payment>> {
|
|||
dialogPane.setContent(stackPane);
|
||||
|
||||
dialogPane.getButtonTypes().addAll(ButtonType.OK, ButtonType.CANCEL);
|
||||
Button okButton = (Button) dialogPane.lookupButton(ButtonType.OK);
|
||||
okButton.addEventFilter(ActionEvent.ACTION, event -> {
|
||||
getPayments();
|
||||
event.consume();
|
||||
});
|
||||
|
||||
final ButtonType loadCsvButtonType = new javafx.scene.control.ButtonType("Load CSV", ButtonBar.ButtonData.LEFT);
|
||||
dialogPane.getButtonTypes().add(loadCsvButtonType);
|
||||
|
||||
setResultConverter((dialogButton) -> {
|
||||
ButtonBar.ButtonData data = dialogButton == null ? null : dialogButton.getButtonData();
|
||||
return data == ButtonBar.ButtonData.OK_DONE ? getPayments() : null;
|
||||
});
|
||||
setResultConverter((_) -> null);
|
||||
|
||||
dialogPane.setPrefWidth(850);
|
||||
dialogPane.setPrefHeight(500);
|
||||
|
|
@ -86,18 +102,24 @@ public class SendToManyDialog extends Dialog<List<Payment>> {
|
|||
}
|
||||
|
||||
private Grid getGrid(List<Payment> payments) {
|
||||
int rowCount = payments.size();
|
||||
return createGrid(payments.stream().map(payment -> new SendToPayment(payment, SendToAddress.fromPayment(payment))).collect(Collectors.toList()));
|
||||
}
|
||||
|
||||
private Grid createGrid(List<SendToPayment> sendToPayments) {
|
||||
int rowCount = sendToPayments.size();
|
||||
int columnCount = 3;
|
||||
GridBase grid = new GridBase(rowCount, columnCount);
|
||||
ObservableList<ObservableList<SpreadsheetCell>> rows = FXCollections.observableArrayList();
|
||||
for(int row = 0; row < grid.getRowCount(); ++row) {
|
||||
SendToPayment sendToPayment = sendToPayments.get(row);
|
||||
final ObservableList<SpreadsheetCell> list = FXCollections.observableArrayList();
|
||||
|
||||
SpreadsheetCell addressCell = ADDRESS.createCell(row, 0, 1, 1, payments.get(row).getAddress());
|
||||
SendToAddress sendToAddress = sendToPayment.sendToAddress();
|
||||
SpreadsheetCell addressCell = SEND_TO_ADDRESS.createCell(row, 0, 1, 1, sendToAddress);
|
||||
addressCell.getStyleClass().add("fixed-width");
|
||||
list.add(addressCell);
|
||||
|
||||
double amount = (double)payments.get(row).getAmount();
|
||||
double amount = (double)sendToPayment.payment().getAmount();
|
||||
if(bitcoinUnit == BitcoinUnit.BTC) {
|
||||
amount = amount / Transaction.SATOSHIS_PER_BITCOIN;
|
||||
}
|
||||
|
|
@ -109,7 +131,7 @@ public class SendToManyDialog extends Dialog<List<Payment>> {
|
|||
}
|
||||
list.add(amountCell);
|
||||
|
||||
list.add(SpreadsheetCellType.STRING.createCell(row, 2, 1, 1, payments.get(row).getLabel()));
|
||||
list.add(SpreadsheetCellType.STRING.createCell(row, 2, 1, 1, sendToPayment.payment().getLabel()));
|
||||
rows.add(list);
|
||||
}
|
||||
grid.setRows(rows);
|
||||
|
|
@ -118,32 +140,49 @@ public class SendToManyDialog extends Dialog<List<Payment>> {
|
|||
return grid;
|
||||
}
|
||||
|
||||
private List<Payment> getPayments() {
|
||||
List<Payment> payments = new ArrayList<>();
|
||||
Grid grid = spreadsheetView.getGrid();
|
||||
String firstLabel = null;
|
||||
for(int row = 0; row < grid.getRowCount(); row++) {
|
||||
private void getPayments() {
|
||||
if(needsResolution() && Config.get().hasServer() && !AppServices.isConnected() && !AppServices.isConnecting()) {
|
||||
if(Config.get().getConnectToResolve() == null || Config.get().getConnectToResolve() == Boolean.FALSE) {
|
||||
Platform.runLater(() -> {
|
||||
ConfirmationAlert confirmationAlert = new ConfirmationAlert("Connect to resolve?", "You are currently offline. Connect to resolve the addresses?", ButtonType.NO, ButtonType.YES);
|
||||
Optional<ButtonType> optType = confirmationAlert.showAndWait();
|
||||
if(confirmationAlert.isDontAskAgain() && optType.isPresent()) {
|
||||
Config.get().setConnectToResolve(optType.get() == ButtonType.YES);
|
||||
}
|
||||
if(optType.isPresent() && optType.get() == ButtonType.YES) {
|
||||
EventManager.get().post(new RequestConnectEvent());
|
||||
}
|
||||
});
|
||||
} else {
|
||||
Platform.runLater(() -> EventManager.get().post(new RequestConnectEvent()));
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
CreatePaymentsService createPaymentsService = new CreatePaymentsService();
|
||||
createPaymentsService.setOnSucceeded(_ -> {
|
||||
List<Payment> payments = createPaymentsService.getValue();
|
||||
if(payments != null) {
|
||||
setResult(payments);
|
||||
}
|
||||
});
|
||||
createPaymentsService.setOnFailed(event -> {
|
||||
Throwable ex = event.getSource().getException();
|
||||
AppServices.showErrorDialog("Error creating payments", ex.getMessage());
|
||||
});
|
||||
createPaymentsService.start();
|
||||
}
|
||||
|
||||
private boolean needsResolution() {
|
||||
for(int row = 0; row < spreadsheetView.getGrid().getRowCount(); row++) {
|
||||
ObservableList<SpreadsheetCell> rowCells = spreadsheetView.getItems().get(row);
|
||||
Address address = (Address)rowCells.get(0).getItem();
|
||||
Double value = (Double)rowCells.get(1).getItem();
|
||||
String label = (String)rowCells.get(2).getItem();
|
||||
if(firstLabel == null) {
|
||||
firstLabel = label;
|
||||
}
|
||||
if(label == null || label.isEmpty()) {
|
||||
label = firstLabel;
|
||||
}
|
||||
|
||||
if(address != null && value != null) {
|
||||
if(bitcoinUnit == BitcoinUnit.BTC) {
|
||||
value = value * Transaction.SATOSHIS_PER_BITCOIN;
|
||||
}
|
||||
|
||||
payments.add(new Payment(address, label, value.longValue(), false));
|
||||
SendToAddress sendToAddress = (SendToAddress)rowCells.getFirst().getItem();
|
||||
if(sendToAddress.hrn != null && DnsPaymentCache.getDnsPayment(sendToAddress.hrn) == null) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return payments;
|
||||
return false;
|
||||
}
|
||||
|
||||
private class SendToManyDialogPane extends DialogPane {
|
||||
|
|
@ -153,7 +192,7 @@ public class SendToManyDialog extends Dialog<List<Payment>> {
|
|||
if(buttonType.getButtonData() == ButtonBar.ButtonData.LEFT) {
|
||||
Button loadButton = new Button(buttonType.getText());
|
||||
loadButton.setGraphicTextGap(5);
|
||||
loadButton.setGraphic(getGlyph(FontAwesome5.Glyph.ARROW_UP));
|
||||
loadButton.setGraphic(GlyphUtils.getUpArrowGlyph());
|
||||
final ButtonBar.ButtonData buttonData = buttonType.getButtonData();
|
||||
ButtonBar.setButtonData(loadButton, buttonData);
|
||||
loadButton.setOnAction(event -> {
|
||||
|
|
@ -168,7 +207,7 @@ public class SendToManyDialog extends Dialog<List<Payment>> {
|
|||
File file = fileChooser.showOpenDialog(this.getScene().getWindow());
|
||||
if(file != null) {
|
||||
try {
|
||||
List<Payment> csvPayments = new ArrayList<>();
|
||||
List<SendToPayment> csvPayments = new ArrayList<>();
|
||||
try(Reader reader = new FileReader(file, StandardCharsets.UTF_8)) {
|
||||
CsvReader csvReader = new CsvReader(reader);
|
||||
while(csvReader.readRecord()) {
|
||||
|
|
@ -184,9 +223,22 @@ public class SendToManyDialog extends Dialog<List<Payment>> {
|
|||
} else {
|
||||
amount = Long.parseLong(csvReader.get(1).replace(",", ""));
|
||||
}
|
||||
Address address = Address.fromString(csvReader.get(0));
|
||||
String label = csvReader.get(2);
|
||||
csvPayments.add(new Payment(address, label, amount, false));
|
||||
Optional<String> optDnsPaymentHrn = DnsPayment.getHrn(csvReader.get(0));
|
||||
if(optDnsPaymentHrn.isPresent()) {
|
||||
Payment payment = new Payment(null, label, amount, false);
|
||||
csvPayments.add(new SendToPayment(payment, new SendToAddress(optDnsPaymentHrn.get())));
|
||||
} else {
|
||||
try {
|
||||
SilentPaymentAddress silentPaymentAddress = SilentPaymentAddress.from(csvReader.get(0));
|
||||
Payment payment = new SilentPayment(silentPaymentAddress, label, amount, false);
|
||||
csvPayments.add(new SendToPayment(payment, SendToAddress.fromPayment(payment)));
|
||||
} catch(Exception e) {
|
||||
Address address = Address.fromString(csvReader.get(0));
|
||||
Payment payment = new Payment(address, label, amount, false);
|
||||
csvPayments.add(new SendToPayment(payment, SendToAddress.fromPayment(payment)));
|
||||
}
|
||||
}
|
||||
} catch(NumberFormatException e) {
|
||||
//ignore and continue - probably a header line
|
||||
} catch(InvalidAddressException e) {
|
||||
|
|
@ -199,7 +251,7 @@ public class SendToManyDialog extends Dialog<List<Payment>> {
|
|||
return;
|
||||
}
|
||||
|
||||
spreadsheetView.setGrid(getGrid(csvPayments));
|
||||
spreadsheetView.setGrid(createGrid(csvPayments));
|
||||
}
|
||||
} catch(IOException e) {
|
||||
AppServices.showErrorDialog("Cannot load CSV", e.getMessage());
|
||||
|
|
@ -214,24 +266,18 @@ public class SendToManyDialog extends Dialog<List<Payment>> {
|
|||
|
||||
return button;
|
||||
}
|
||||
|
||||
private Glyph getGlyph(FontAwesome5.Glyph glyphName) {
|
||||
Glyph glyph = new Glyph(FontAwesome5.FONT_NAME, glyphName);
|
||||
glyph.setFontSize(11);
|
||||
return glyph;
|
||||
}
|
||||
}
|
||||
|
||||
public static class AddressCellType extends SpreadsheetCellType<Address> {
|
||||
public AddressCellType() {
|
||||
this(new StringConverterWithFormat<>(new AddressStringConverter()) {
|
||||
public static class SendToAddressCellType extends SpreadsheetCellType<SendToAddress> {
|
||||
public SendToAddressCellType() {
|
||||
this(new StringConverterWithFormat<>(new SendToAddressStringConverter()) {
|
||||
@Override
|
||||
public String toString(Address item) {
|
||||
public String toString(SendToAddress item) {
|
||||
return toStringFormat(item, ""); //$NON-NLS-1$
|
||||
}
|
||||
|
||||
@Override
|
||||
public Address fromString(String str) {
|
||||
public SendToAddress fromString(String str) {
|
||||
if(str == null || str.isEmpty()) { //$NON-NLS-1$
|
||||
return null;
|
||||
} else {
|
||||
|
|
@ -240,7 +286,7 @@ public class SendToManyDialog extends Dialog<List<Payment>> {
|
|||
}
|
||||
|
||||
@Override
|
||||
public String toStringFormat(Address item, String format) {
|
||||
public String toStringFormat(SendToAddress item, String format) {
|
||||
try {
|
||||
if(item == null) {
|
||||
return ""; //$NON-NLS-1$
|
||||
|
|
@ -254,7 +300,7 @@ public class SendToManyDialog extends Dialog<List<Payment>> {
|
|||
});
|
||||
}
|
||||
|
||||
public AddressCellType(StringConverter<Address> converter) {
|
||||
public SendToAddressCellType(StringConverter<SendToAddress> converter) {
|
||||
super(converter);
|
||||
}
|
||||
|
||||
|
|
@ -264,7 +310,7 @@ public class SendToManyDialog extends Dialog<List<Payment>> {
|
|||
}
|
||||
|
||||
public SpreadsheetCell createCell(final int row, final int column, final int rowSpan, final int columnSpan,
|
||||
final Address value) {
|
||||
final SendToAddress value) {
|
||||
SpreadsheetCell cell = new SpreadsheetCellBase(row, column, rowSpan, columnSpan, this);
|
||||
cell.setItem(value);
|
||||
return cell;
|
||||
|
|
@ -277,7 +323,7 @@ public class SendToManyDialog extends Dialog<List<Payment>> {
|
|||
|
||||
@Override
|
||||
public boolean match(Object value, Object... options) {
|
||||
if(value instanceof Address)
|
||||
if(value instanceof SendToAddress)
|
||||
return true;
|
||||
else {
|
||||
try {
|
||||
|
|
@ -290,9 +336,9 @@ public class SendToManyDialog extends Dialog<List<Payment>> {
|
|||
}
|
||||
|
||||
@Override
|
||||
public Address convertValue(Object value) {
|
||||
if(value instanceof Address)
|
||||
return (Address)value;
|
||||
public SendToAddress convertValue(Object value) {
|
||||
if(value instanceof SendToAddress)
|
||||
return (SendToAddress)value;
|
||||
else {
|
||||
try {
|
||||
return converter.fromString(value == null ? null : value.toString());
|
||||
|
|
@ -303,13 +349,155 @@ public class SendToManyDialog extends Dialog<List<Payment>> {
|
|||
}
|
||||
|
||||
@Override
|
||||
public String toString(Address item) {
|
||||
public String toString(SendToAddress item) {
|
||||
return converter.toString(item);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString(Address item, String format) {
|
||||
return ((StringConverterWithFormat<Address>)converter).toStringFormat(item, format);
|
||||
public String toString(SendToAddress item, String format) {
|
||||
return ((StringConverterWithFormat<SendToAddress>)converter).toStringFormat(item, format);
|
||||
}
|
||||
};
|
||||
|
||||
public static class SendToAddress {
|
||||
private final String hrn;
|
||||
private final Address address;
|
||||
private final SilentPaymentAddress silentPaymentAddress;
|
||||
|
||||
public SendToAddress(String hrn) {
|
||||
this.hrn = hrn;
|
||||
this.address = null;
|
||||
this.silentPaymentAddress = null;
|
||||
}
|
||||
|
||||
public SendToAddress(Address address) {
|
||||
this.hrn = null;
|
||||
this.address = address;
|
||||
this.silentPaymentAddress = null;
|
||||
}
|
||||
|
||||
public SendToAddress(SilentPaymentAddress silentPaymentAddress) {
|
||||
this.hrn = null;
|
||||
this.address = null;
|
||||
this.silentPaymentAddress = silentPaymentAddress;
|
||||
}
|
||||
|
||||
public String toString() {
|
||||
return hrn == null ? silentPaymentAddress == null ? (address == null ? null : address.toString()) : silentPaymentAddress.toString() : hrn;
|
||||
}
|
||||
|
||||
public static SendToAddress fromPayment(Payment payment) {
|
||||
DnsPayment dnsPayment = DnsPaymentCache.getDnsPayment(payment);
|
||||
if(dnsPayment != null) {
|
||||
return new SendToAddress(dnsPayment.hrn());
|
||||
}
|
||||
return payment instanceof SilentPayment ? new SendToAddress(((SilentPayment)payment).getSilentPaymentAddress()) : new SendToAddress(payment.getAddress());
|
||||
}
|
||||
|
||||
public Payment toPayment(String label, long value, boolean sendMax) throws DnsPaymentValidationException, IOException, ExecutionException, InterruptedException, BitcoinURIParseException {
|
||||
if(hrn != null) {
|
||||
DnsPayment dnsPayment = DnsPaymentCache.getDnsPayment(hrn);
|
||||
if(dnsPayment == null) {
|
||||
DnsPaymentResolver resolver = new DnsPaymentResolver(hrn);
|
||||
Optional<DnsPayment> optDnsPayment = resolver.resolve();
|
||||
if(optDnsPayment.isPresent()) {
|
||||
dnsPayment = optDnsPayment.get();
|
||||
if(dnsPayment.hasAddress()) {
|
||||
DnsPaymentCache.putDnsPayment(dnsPayment.bitcoinURI().getAddress(), dnsPayment);
|
||||
} else if(dnsPayment.hasSilentPaymentAddress()) {
|
||||
DnsPaymentCache.putDnsPayment(dnsPayment.bitcoinURI().getSilentPaymentAddress(), dnsPayment);
|
||||
}
|
||||
return getPayment(optDnsPayment.get(), label, value, sendMax);
|
||||
} else {
|
||||
throw new IllegalArgumentException("Payment to " + hrn + " could not be resolved.");
|
||||
}
|
||||
} else {
|
||||
return getPayment(dnsPayment, label, value, sendMax);
|
||||
}
|
||||
}
|
||||
|
||||
if(silentPaymentAddress != null) {
|
||||
return new SilentPayment(silentPaymentAddress, label, value, sendMax);
|
||||
} else {
|
||||
return new Payment(address, label, value, sendMax);
|
||||
}
|
||||
}
|
||||
|
||||
private static Payment getPayment(DnsPayment dnsPayment, String label, long value, boolean sendMax) {
|
||||
if(dnsPayment.hasAddress()) {
|
||||
return new Payment(dnsPayment.bitcoinURI().getAddress(), label, value, sendMax);
|
||||
} else if(dnsPayment.hasSilentPaymentAddress()) {
|
||||
return new SilentPayment(dnsPayment.bitcoinURI().getSilentPaymentAddress(), label, value, sendMax);
|
||||
} else {
|
||||
throw new IllegalArgumentException("Payment to " + dnsPayment + " has no associated address.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static class SendToAddressStringConverter extends StringConverter<SendToAddress> {
|
||||
private final AddressStringConverter addressStringConverter = new AddressStringConverter();
|
||||
|
||||
@Override
|
||||
public SendToAddress fromString(String value) {
|
||||
Optional<String> optDnsPaymentHrn = DnsPayment.getHrn(value);
|
||||
if(optDnsPaymentHrn.isPresent()) {
|
||||
return new SendToAddress(optDnsPaymentHrn.get());
|
||||
}
|
||||
|
||||
try {
|
||||
SilentPaymentAddress silentPaymentAddress = SilentPaymentAddress.from(value);
|
||||
return new SendToAddress(silentPaymentAddress);
|
||||
} catch(Exception e) {
|
||||
Address address = addressStringConverter.fromString(value);
|
||||
return address == null ? null : new SendToAddress(address);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString(SendToAddress value) {
|
||||
return value.toString();
|
||||
}
|
||||
}
|
||||
|
||||
private class CreatePaymentsService extends Service<List<Payment>> {
|
||||
@Override
|
||||
protected Task<List<Payment>> createTask() {
|
||||
return new Task<>() {
|
||||
@Override
|
||||
protected List<Payment> call() throws Exception {
|
||||
return getPayments();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private List<Payment> getPayments() throws DnsPaymentValidationException, IOException, ExecutionException, InterruptedException, BitcoinURIParseException {
|
||||
List<Payment> payments = new ArrayList<>();
|
||||
Grid grid = spreadsheetView.getGrid();
|
||||
String firstLabel = null;
|
||||
for(int row = 0; row < grid.getRowCount(); row++) {
|
||||
ObservableList<SpreadsheetCell> rowCells = spreadsheetView.getItems().get(row);
|
||||
SendToAddress sendToAddress = (SendToAddress)rowCells.get(0).getItem();
|
||||
Double value = (Double)rowCells.get(1).getItem();
|
||||
String label = (String)rowCells.get(2).getItem();
|
||||
if(firstLabel == null) {
|
||||
firstLabel = label;
|
||||
}
|
||||
if(label == null || label.isEmpty()) {
|
||||
label = firstLabel;
|
||||
}
|
||||
|
||||
if(sendToAddress != null && value != null) {
|
||||
if(bitcoinUnit == BitcoinUnit.BTC) {
|
||||
value = value * Transaction.SATOSHIS_PER_BITCOIN;
|
||||
}
|
||||
|
||||
payments.add(sendToAddress.toPayment(label, value.longValue(), false));
|
||||
}
|
||||
}
|
||||
|
||||
return payments;
|
||||
}
|
||||
}
|
||||
|
||||
private record SendToPayment(Payment payment, SendToAddress sendToAddress) {}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,8 +4,12 @@ import com.sparrowwallet.drongo.KeyPurpose;
|
|||
import com.sparrowwallet.drongo.OsType;
|
||||
import com.sparrowwallet.drongo.address.Address;
|
||||
import com.sparrowwallet.drongo.bip47.PaymentCode;
|
||||
import com.sparrowwallet.drongo.dns.DnsPayment;
|
||||
import com.sparrowwallet.drongo.dns.DnsPaymentCache;
|
||||
import com.sparrowwallet.drongo.protocol.Sha256Hash;
|
||||
import com.sparrowwallet.drongo.protocol.TransactionOutput;
|
||||
import com.sparrowwallet.drongo.silentpayments.SilentPayment;
|
||||
import com.sparrowwallet.drongo.silentpayments.SilentPaymentAddress;
|
||||
import com.sparrowwallet.drongo.uri.BitcoinURI;
|
||||
import com.sparrowwallet.drongo.wallet.*;
|
||||
import com.sparrowwallet.sparrow.*;
|
||||
|
|
@ -22,6 +26,7 @@ import javafx.beans.property.SimpleBooleanProperty;
|
|||
import javafx.beans.property.SimpleObjectProperty;
|
||||
import javafx.embed.swing.SwingFXUtils;
|
||||
import javafx.event.EventHandler;
|
||||
import javafx.geometry.HPos;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.Group;
|
||||
|
|
@ -103,6 +108,7 @@ public class TransactionDiagram extends GridPane {
|
|||
expandedDiagram.setId("transactionDiagram");
|
||||
expandedDiagram.setExpanded(true);
|
||||
expandedDiagram.setFinal(isFinal());
|
||||
expandedDiagram.setMaxWidth(AppServices.getActiveWindow().getWidth() - 200);
|
||||
updateDerivedDiagram(expandedDiagram);
|
||||
|
||||
HBox buttonBox = new HBox();
|
||||
|
|
@ -120,7 +126,7 @@ public class TransactionDiagram extends GridPane {
|
|||
AppServices.setStageIcon(stage);
|
||||
stage.setScene(scene);
|
||||
stage.setOnShowing(e -> {
|
||||
AppServices.moveToActiveWindowScreen(stage, 600, 460);
|
||||
AppServices.moveToActiveWindowScreen(stage, expandedDiagram.getMaxWidth(), 460);
|
||||
});
|
||||
stage.setOnHidden(e -> {
|
||||
expandedDiagram = null;
|
||||
|
|
@ -137,6 +143,39 @@ public class TransactionDiagram extends GridPane {
|
|||
}
|
||||
};
|
||||
|
||||
public TransactionDiagram() {
|
||||
ColumnConstraints col1 = new ColumnConstraints();
|
||||
col1.setPrefWidth(22);
|
||||
col1.setHgrow(Priority.NEVER);
|
||||
|
||||
ColumnConstraints col2 = new ColumnConstraints();
|
||||
col2.setHgrow(Priority.ALWAYS);
|
||||
col2.setPercentWidth(25);
|
||||
col2.setFillWidth(true);
|
||||
|
||||
ColumnConstraints col3 = new ColumnConstraints();
|
||||
col3.setPrefWidth(140);
|
||||
col3.setHgrow(Priority.NEVER);
|
||||
|
||||
ColumnConstraints col4 = new ColumnConstraints();
|
||||
Label label = new Label();
|
||||
col4.setMinWidth(TextUtils.computeTextWidth(label.getFont(), "Transaction", 0) + 20);
|
||||
col4.setHgrow(Priority.NEVER);
|
||||
col4.setHalignment(HPos.CENTER);
|
||||
|
||||
ColumnConstraints col5 = new ColumnConstraints();
|
||||
col5.setPrefWidth(140);
|
||||
col5.setHgrow(Priority.NEVER);
|
||||
|
||||
ColumnConstraints col6 = new ColumnConstraints();
|
||||
col6.setHgrow(Priority.ALWAYS);
|
||||
col6.setPercentWidth(25);
|
||||
col6.setFillWidth(true);
|
||||
|
||||
getColumnConstraints().addAll(col1, col2, col3, col4, col5, col6);
|
||||
setPadding(new Insets(0, 0, 0, 40));
|
||||
}
|
||||
|
||||
public void update(WalletTransaction walletTx) {
|
||||
setMinHeight(getDiagramHeight());
|
||||
setMaxHeight(getDiagramHeight());
|
||||
|
|
@ -165,7 +204,7 @@ public class TransactionDiagram extends GridPane {
|
|||
|
||||
VBox messagePane = new VBox();
|
||||
messagePane.setPrefHeight(getDiagramHeight());
|
||||
messagePane.setPadding(new Insets(0, 10, 0, 280));
|
||||
messagePane.setPadding(new Insets(0, 10, 0, 10));
|
||||
messagePane.setAlignment(Pos.CENTER);
|
||||
messagePane.getChildren().add(createSpacer());
|
||||
|
||||
|
|
@ -225,7 +264,6 @@ public class TransactionDiagram extends GridPane {
|
|||
GridPane.setConstraints(outputsPane, 5, 0);
|
||||
|
||||
getChildren().clear();
|
||||
getChildren().addAll(inputsTypePane, inputsPane, inputsLinesPane, txPane, outputsLinesPane, outputsPane);
|
||||
|
||||
List<Payment> userPayments = getUserPayments();
|
||||
if(!isFinal() && userPayments.size() > 1) {
|
||||
|
|
@ -234,6 +272,8 @@ public class TransactionDiagram extends GridPane {
|
|||
getChildren().add(totalsPane);
|
||||
}
|
||||
|
||||
getChildren().addAll(inputsTypePane, inputsPane, inputsLinesPane, txPane, outputsLinesPane, outputsPane);
|
||||
|
||||
if(contextMenu == null) {
|
||||
contextMenu = new ContextMenu();
|
||||
MenuItem menuItem = new MenuItem("Save as Image...");
|
||||
|
|
@ -407,8 +447,6 @@ public class TransactionDiagram extends GridPane {
|
|||
|
||||
private Pane getInputsLabels(List<Map<BlockTransactionHashIndex, WalletNode>> displayedUtxoSets) {
|
||||
VBox inputsBox = new VBox();
|
||||
inputsBox.setMaxWidth(isExpanded() ? 300 : 150);
|
||||
inputsBox.setPrefWidth(isExpanded() ? 230 : 150);
|
||||
inputsBox.setPadding(new Insets(0, 10, 0, 10));
|
||||
inputsBox.minHeightProperty().bind(minHeightProperty());
|
||||
inputsBox.setAlignment(Pos.BASELINE_RIGHT);
|
||||
|
|
@ -640,7 +678,8 @@ public class TransactionDiagram extends GridPane {
|
|||
|
||||
double width = 140.0;
|
||||
long sum = walletTx.getTotal();
|
||||
List<Long> values = walletTx.getTransaction().getOutputs().stream().filter(txo -> txo.getScript().getToAddress() != null).map(TransactionOutput::getValue).collect(Collectors.toList());
|
||||
List<Long> values = walletTx.getOutputs().stream().filter(output -> !(output instanceof WalletTransaction.NonAddressOutput))
|
||||
.map(output -> output.getTransactionOutput().getValue()).collect(Collectors.toList());
|
||||
values.add(walletTx.getFee());
|
||||
int numOutputs = displayedPayments.size() + walletTx.getChangeMap().size() + 1;
|
||||
for(int i = 1; i <= numOutputs; i++) {
|
||||
|
|
@ -676,8 +715,6 @@ public class TransactionDiagram extends GridPane {
|
|||
|
||||
private Pane getOutputsLabels(List<Payment> displayedPayments) {
|
||||
VBox outputsBox = new VBox();
|
||||
outputsBox.setMaxWidth(isExpanded() ? 350 : 150);
|
||||
outputsBox.setPrefWidth(isExpanded() ? 230 : 150);
|
||||
outputsBox.setPadding(new Insets(0, 20, 0, 10));
|
||||
outputsBox.setAlignment(Pos.BASELINE_LEFT);
|
||||
outputsBox.getChildren().add(createSpacer());
|
||||
|
|
@ -686,15 +723,16 @@ public class TransactionDiagram extends GridPane {
|
|||
for(Payment payment : displayedPayments) {
|
||||
Glyph outputGlyph = GlyphUtils.getOutputGlyph(walletTx, payment);
|
||||
boolean labelledPayment = outputGlyph.getStyleClass().stream().anyMatch(style -> List.of("premix-icon", "badbank-icon", "whirlpoolfee-icon", "anchor-icon").contains(style)) || payment instanceof AdditionalPayment || payment.getLabel() != null;
|
||||
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);
|
||||
Label recipientLabel = new Label(payment.getLabel() == null || payment.getType() == Payment.Type.FAKE_MIX || payment.getType() == Payment.Type.MIX ? payment.toString().substring(0, 8) + "..." : payment.getLabel(), outputGlyph);
|
||||
recipientLabel.getStyleClass().add("output-label");
|
||||
recipientLabel.getStyleClass().add(labelledPayment ? "payment-label" : "recipient-label");
|
||||
Wallet toWallet = walletTx.getToWallet(AppServices.get().getOpenWallets().keySet(), payment);
|
||||
WalletNode toNode = walletTx.getWallet() != null && !walletTx.getWallet().isBip47() ? walletTx.getAddressNodeMap().get(payment.getAddress()) : null;
|
||||
WalletNode toNode = payment instanceof WalletNodePayment walletNodePayment ? walletNodePayment.getWalletNode() : null;
|
||||
Wallet toBip47Wallet = getBip47SendWallet(payment);
|
||||
DnsPayment dnsPayment = DnsPaymentCache.getDnsPayment(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())
|
||||
+ (payment instanceof AdditionalPayment ? (isExpanded() ? "\n" : "(click to expand)\n") + payment : (toWallet == null ? (dnsPayment == null ? (payment.getLabel() == null ? (toNode != null ? toNode : (toBip47Wallet == null ? "external address" : toBip47Wallet.getDisplayName())) : payment.getLabel()) : dnsPayment.toString()) : toWallet.getFullDisplayName()) + "\n" + payment.getDisplayAddress())
|
||||
+ (walletTx.isDuplicateAddress(payment) ? " (Duplicate)" : ""));
|
||||
recipientTooltip.getStyleClass().add("recipient-label");
|
||||
recipientTooltip.setShowDelay(new Duration(TOOLTIP_SHOW_DELAY));
|
||||
|
|
@ -719,9 +757,13 @@ public class TransactionDiagram extends GridPane {
|
|||
paymentBox.getChildren().addAll(region, amountLabel);
|
||||
}
|
||||
|
||||
if(payment instanceof SilentPayment silentPayment) {
|
||||
outputNodes.add(new OutputNode(paymentBox, silentPayment.isAddressComputed() ? silentPayment.getAddress() : null, payment.getAmount(), null, silentPayment.getSilentPaymentAddress()));
|
||||
} else {
|
||||
Wallet bip47Wallet = toWallet != null && toWallet.isBip47() ? toWallet : (toBip47Wallet != null && toBip47Wallet.isBip47() ? toBip47Wallet : null);
|
||||
PaymentCode paymentCode = bip47Wallet == null ? null : bip47Wallet.getKeystores().getFirst().getExternalPaymentCode();
|
||||
outputNodes.add(new OutputNode(paymentBox, payment.getAddress(), payment.getAmount(), paymentCode));
|
||||
outputNodes.add(new OutputNode(paymentBox, payment.getAddress(), payment.getAmount(), paymentCode, null));
|
||||
}
|
||||
}
|
||||
|
||||
Set<Integer> seenIndexes = new HashSet<>();
|
||||
|
|
@ -785,7 +827,7 @@ public class TransactionDiagram extends GridPane {
|
|||
outputsBox.getChildren().add(outputNode.outputLabel);
|
||||
outputsBox.getChildren().add(createSpacer());
|
||||
|
||||
ContextMenu contextMenu = new LabelContextMenu(outputNode.address, outputNode.amount, outputNode.paymentCode);
|
||||
ContextMenu contextMenu = new LabelContextMenu(outputNode.address, outputNode.amount, outputNode.paymentCode, outputNode.silentPaymentAddress);
|
||||
if(!outputNode.outputLabel.getChildren().isEmpty() && outputNode.outputLabel.getChildren().get(0) instanceof Label outputLabelControl) {
|
||||
outputLabelControl.setContextMenu(contextMenu);
|
||||
}
|
||||
|
|
@ -960,8 +1002,11 @@ public class TransactionDiagram extends GridPane {
|
|||
}
|
||||
|
||||
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();
|
||||
List<TransactionOutput> addressOutputs = walletTx.getOutputs().stream().filter(output -> !(output instanceof WalletTransaction.NonAddressOutput))
|
||||
.map(WalletTransaction.Output::getTransactionOutput).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);
|
||||
}
|
||||
|
||||
|
|
@ -1111,7 +1156,7 @@ public class TransactionDiagram extends GridPane {
|
|||
}
|
||||
|
||||
public String toString() {
|
||||
return additionalPayments.stream().map(payment -> payment.getAddress().toString()).collect(Collectors.joining("\n"));
|
||||
return additionalPayments.stream().map(Payment::toString).collect(Collectors.joining("\n"));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1120,25 +1165,27 @@ public class TransactionDiagram extends GridPane {
|
|||
public Address address;
|
||||
public long amount;
|
||||
public PaymentCode paymentCode;
|
||||
public SilentPaymentAddress silentPaymentAddress;
|
||||
|
||||
public OutputNode(Pane outputLabel, Address address, long amount) {
|
||||
this(outputLabel, address, amount, null);
|
||||
this(outputLabel, address, amount, null, null);
|
||||
}
|
||||
|
||||
public OutputNode(Pane outputLabel, Address address, long amount, PaymentCode paymentCode) {
|
||||
public OutputNode(Pane outputLabel, Address address, long amount, PaymentCode paymentCode, SilentPaymentAddress silentPaymentAddress) {
|
||||
this.outputLabel = outputLabel;
|
||||
this.address = address;
|
||||
this.amount = amount;
|
||||
this.paymentCode = paymentCode;
|
||||
this.silentPaymentAddress = silentPaymentAddress;
|
||||
}
|
||||
}
|
||||
|
||||
private class LabelContextMenu extends ContextMenu {
|
||||
public LabelContextMenu(Address address, long value) {
|
||||
this(address, value, null);
|
||||
this(address, value, null, null);
|
||||
}
|
||||
|
||||
public LabelContextMenu(Address address, long value, PaymentCode paymentCode) {
|
||||
public LabelContextMenu(Address address, long value, PaymentCode paymentCode, SilentPaymentAddress silentPaymentAddress) {
|
||||
if(address != null) {
|
||||
MenuItem copyAddress = new MenuItem("Copy Address");
|
||||
copyAddress.setOnAction(event -> {
|
||||
|
|
@ -1186,6 +1233,17 @@ public class TransactionDiagram extends GridPane {
|
|||
});
|
||||
getItems().add(copyPaymentCode);
|
||||
}
|
||||
|
||||
if(silentPaymentAddress != null) {
|
||||
MenuItem copySilentPaymentAddress = new MenuItem("Copy Silent Payment Address");
|
||||
copySilentPaymentAddress.setOnAction(AE -> {
|
||||
hide();
|
||||
ClipboardContent content = new ClipboardContent();
|
||||
content.putString(silentPaymentAddress.toString());
|
||||
Clipboard.getSystemClipboard().setContent(content);
|
||||
});
|
||||
getItems().add(copySilentPaymentAddress);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -90,20 +90,20 @@ public class TransactionDiagramLabel extends HBox {
|
|||
outputLabels.add(mixOutputLabel);
|
||||
}
|
||||
} 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)) {
|
||||
&& walletTx.getWallet().getStandardAccountType() == StandardAccount.WHIRLPOOL_POSTMIX && !walletTx.getWalletNodePayments().isEmpty()) {
|
||||
OutputLabel remixOutputLabel = getRemixOutputLabel(transactionDiagram, walletTx.getPayments());
|
||||
if(remixOutputLabel != null) {
|
||||
outputLabels.add(remixOutputLabel);
|
||||
}
|
||||
} else {
|
||||
List<Payment> payments = walletTx.getPayments().stream().filter(payment -> payment.getType() == Payment.Type.DEFAULT && !walletTx.isConsolidationSend(payment)).collect(Collectors.toList());
|
||||
List<Payment> payments = walletTx.getExternalPayments().stream().filter(payment -> payment.getType() == Payment.Type.DEFAULT).collect(Collectors.toList());
|
||||
List<OutputLabel> paymentLabels = payments.stream().map(payment -> getOutputLabel(transactionDiagram, payment)).collect(Collectors.toList());
|
||||
if(walletTx.getSelectedUtxos().values().stream().allMatch(Objects::isNull)) {
|
||||
paymentLabels.sort(Comparator.comparingInt(paymentLabel -> (paymentLabel.text.startsWith("Receive") ? 0 : 1)));
|
||||
}
|
||||
outputLabels.addAll(paymentLabels);
|
||||
|
||||
List<Payment> consolidations = walletTx.getPayments().stream().filter(payment -> payment.getType() == Payment.Type.DEFAULT && walletTx.isConsolidationSend(payment)).collect(Collectors.toList());
|
||||
List<Payment> consolidations = walletTx.getWalletNodePayments().stream().filter(payment -> payment.getType() == Payment.Type.DEFAULT).collect(Collectors.toList());
|
||||
outputLabels.addAll(consolidations.stream().map(consolidation -> getOutputLabel(transactionDiagram, consolidation)).collect(Collectors.toList()));
|
||||
|
||||
List<Payment> mixes = walletTx.getPayments().stream().filter(payment -> payment.getType() == Payment.Type.MIX || payment.getType() == Payment.Type.FAKE_MIX).collect(Collectors.toList());
|
||||
|
|
@ -203,10 +203,10 @@ public class TransactionDiagramLabel extends HBox {
|
|||
private OutputLabel getOutputLabel(TransactionDiagram transactionDiagram, Payment payment) {
|
||||
WalletTransaction walletTx = transactionDiagram.getWalletTransaction();
|
||||
Wallet toWallet = walletTx.getToWallet(AppServices.get().getOpenWallets().keySet(), payment);
|
||||
WalletNode toNode = walletTx.getWallet() != null && !walletTx.getWallet().isBip47() ? walletTx.getAddressNodeMap().get(payment.getAddress()) : null;
|
||||
WalletNode toNode = payment instanceof WalletNodePayment walletNodePayment ? walletNodePayment.getWalletNode() : null;
|
||||
|
||||
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();
|
||||
String text = (toWallet == null ? (toNode != null ? "Consolidate " : "Pay ") : "Receive ") + transactionDiagram.getSatsValue(payment.getAmount()) + " sats to " + payment;
|
||||
|
||||
return getOutputLabel(glyph, text);
|
||||
}
|
||||
|
|
@ -240,7 +240,7 @@ public class TransactionDiagramLabel extends HBox {
|
|||
icon.setGraphic(glyph);
|
||||
|
||||
CopyableLabel label = new CopyableLabel();
|
||||
label.setFont(Font.font("Roboto Mono Italic", 13));
|
||||
label.setFont(Font.font("Fragment Mono Italic", 13));
|
||||
label.setText(text);
|
||||
|
||||
HBox output = new HBox(5);
|
||||
|
|
|
|||
|
|
@ -6,8 +6,9 @@ public enum WebcamPixelFormat {
|
|||
//Only V4L2 formats defined in linux/videodev2.h are required here, declared in order of priority for supported formats
|
||||
PIX_FMT_RGB24("RGB3", true),
|
||||
PIX_FMT_YUYV("YUYV", true),
|
||||
PIX_FMT_MJPG("MJPG", true),
|
||||
PIX_FMT_NV12("NV12", false);
|
||||
PIX_FMT_NV12("NV12", true),
|
||||
PIX_FMT_YU12("YU12", true),
|
||||
PIX_FMT_MJPG("MJPG", true);
|
||||
|
||||
private final String name;
|
||||
private final boolean supported;
|
||||
|
|
@ -25,6 +26,14 @@ public enum WebcamPixelFormat {
|
|||
return supported;
|
||||
}
|
||||
|
||||
public int getFourCC() {
|
||||
char a = name.charAt(0);
|
||||
char b = name.charAt(1);
|
||||
char c = name.charAt(2);
|
||||
char d = name.charAt(3);
|
||||
return ((int) a) | ((int) b << 8) | ((int) c << 16) | ((int) d << 24);
|
||||
}
|
||||
|
||||
public String toString() {
|
||||
return name;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import com.google.zxing.qrcode.QRCodeReader;
|
|||
import com.sparrowwallet.bokmakierie.Bokmakierie;
|
||||
import com.sparrowwallet.drongo.OsType;
|
||||
import com.sparrowwallet.sparrow.io.Config;
|
||||
import com.sparrowwallet.sparrow.io.ZBar;
|
||||
import javafx.beans.property.BooleanProperty;
|
||||
import javafx.beans.property.ObjectProperty;
|
||||
import javafx.beans.property.SimpleBooleanProperty;
|
||||
|
|
@ -15,7 +16,6 @@ import javafx.concurrent.ScheduledService;
|
|||
import javafx.concurrent.Task;
|
||||
import javafx.embed.swing.SwingFXUtils;
|
||||
import javafx.scene.image.Image;
|
||||
import net.sourceforge.zbar.ZBar;
|
||||
import org.openpnp.capture.*;
|
||||
import org.openpnp.capture.library.OpenpnpCaptureLibrary;
|
||||
import org.slf4j.Logger;
|
||||
|
|
@ -27,6 +27,9 @@ import java.awt.image.BufferedImage;
|
|||
import java.awt.image.WritableRaster;
|
||||
import java.util.*;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.Semaphore;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.function.Function;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
|
@ -34,13 +37,18 @@ import java.util.stream.Stream;
|
|||
public class WebcamService extends ScheduledService<Image> {
|
||||
private static final Logger log = LoggerFactory.getLogger(WebcamService.class);
|
||||
|
||||
private final Semaphore taskSemaphore = new Semaphore(1);
|
||||
private final AtomicBoolean cancelRequested = new AtomicBoolean(false);
|
||||
private final AtomicBoolean captureClosed = new AtomicBoolean(false);
|
||||
|
||||
private List<CaptureDevice> devices;
|
||||
private List<CaptureDevice> availableDevices;
|
||||
private Set<WebcamResolution> resolutions;
|
||||
|
||||
private WebcamResolution resolution;
|
||||
private CaptureDevice device;
|
||||
private final BooleanProperty opening = new SimpleBooleanProperty(false);
|
||||
private final BooleanProperty closed = new SimpleBooleanProperty(false);
|
||||
private final BooleanProperty opened = new SimpleBooleanProperty(false);
|
||||
|
||||
private final ObjectProperty<Result> resultProperty = new SimpleObjectProperty<>(null);
|
||||
|
||||
|
|
@ -105,26 +113,44 @@ public class WebcamService extends ScheduledService<Image> {
|
|||
return new Task<>() {
|
||||
@Override
|
||||
protected Image call() throws Exception {
|
||||
if(cancelRequested.get() || isCancelled() || captureClosed.get()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if(!taskSemaphore.tryAcquire()) {
|
||||
log.warn("Skipped execution of webcam capture task, another task is running");
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
if(stream == null) {
|
||||
if(devices == null) {
|
||||
devices = capture.getDevices();
|
||||
availableDevices = new ArrayList<>(devices);
|
||||
|
||||
if(devices.isEmpty()) {
|
||||
throw new UnsupportedOperationException("No cameras available");
|
||||
}
|
||||
}
|
||||
|
||||
CaptureDevice selectedDevice = devices.stream().filter(d -> !d.getFormats().isEmpty()).findFirst().orElse(devices.getFirst());
|
||||
while(stream == null && !availableDevices.isEmpty()) {
|
||||
CaptureDevice selectedDevice = availableDevices.stream().filter(d -> !d.getFormats().isEmpty()).findFirst().orElse(availableDevices.getFirst());
|
||||
|
||||
if(device != null) {
|
||||
for(CaptureDevice webcam : devices) {
|
||||
if(webcam.getName().equals(device.getName())) {
|
||||
for(CaptureDevice webcam : availableDevices) {
|
||||
if(webcam.equals(device)) {
|
||||
selectedDevice = webcam;
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else if(Config.get().getWebcamDevice() != null) {
|
||||
for(CaptureDevice webcam : devices) {
|
||||
for(CaptureDevice webcam : availableDevices) {
|
||||
if(webcam.getUniqueId().equals(Config.get().getWebcamDeviceId())) {
|
||||
selectedDevice = webcam;
|
||||
break;
|
||||
}
|
||||
if(webcam.getName().equals(Config.get().getWebcamDevice())) {
|
||||
selectedDevice = webcam;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -163,6 +189,11 @@ public class WebcamService extends ScheduledService<Image> {
|
|||
}
|
||||
}
|
||||
|
||||
//On Linux, formats not defined in WebcamPixelFormat are unsupported
|
||||
if(OsType.getCurrent() == OsType.UNIX && WebcamPixelFormat.fromFourCC(format.getFormatInfo().fourcc) == null) {
|
||||
log.warn("Unsupported camera pixel format " + WebcamPixelFormat.fourCCToString(format.getFormatInfo().fourcc));
|
||||
}
|
||||
|
||||
if(log.isDebugEnabled()) {
|
||||
log.debug("Opening capture stream on " + device + " with format " + format.getFormatInfo().width + "x" + format.getFormatInfo().height + " (" + WebcamPixelFormat.fourCCToString(format.getFormatInfo().fourcc) + ")");
|
||||
}
|
||||
|
|
@ -170,15 +201,23 @@ public class WebcamService extends ScheduledService<Image> {
|
|||
opening.set(true);
|
||||
stream = device.openStream(format);
|
||||
opening.set(false);
|
||||
closed.set(false);
|
||||
|
||||
try {
|
||||
zoomLimits = stream.getPropertyLimits(CaptureProperty.Zoom);
|
||||
} catch(Throwable e) {
|
||||
log.debug("Error getting zoom limits on " + device + ", assuming no zoom function");
|
||||
}
|
||||
|
||||
if(stream == null) {
|
||||
availableDevices.remove(device);
|
||||
}
|
||||
}
|
||||
|
||||
if(stream == null) {
|
||||
throw new UnsupportedOperationException("No usable cameras available, tried " + devices);
|
||||
}
|
||||
|
||||
opened.set(true);
|
||||
BufferedImage originalImage = stream.capture();
|
||||
CroppedDimension cropped = getCroppedDimension(originalImage);
|
||||
BufferedImage croppedImage = originalImage.getSubimage(cropped.x, cropped.y, cropped.length, cropped.length);
|
||||
|
|
@ -195,6 +234,7 @@ public class WebcamService extends ScheduledService<Image> {
|
|||
return image;
|
||||
} finally {
|
||||
opening.set(false);
|
||||
taskSemaphore.release();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
@ -204,22 +244,39 @@ public class WebcamService extends ScheduledService<Image> {
|
|||
public void reset() {
|
||||
stream = null;
|
||||
zoomLimits = null;
|
||||
cancelRequested.set(false);
|
||||
super.reset();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean cancel() {
|
||||
cancelRequested.set(true);
|
||||
boolean cancelled = super.cancel();
|
||||
|
||||
try {
|
||||
if(taskSemaphore.tryAcquire(1, TimeUnit.SECONDS)) {
|
||||
taskSemaphore.release();
|
||||
} else {
|
||||
log.error("Timed out waiting for task semaphore to be available to cancel, cancelling anyway");
|
||||
}
|
||||
} catch(InterruptedException e) {
|
||||
log.error("Interrupted while waiting for task semaphore to be available to cancel, cancelling anyway");
|
||||
}
|
||||
|
||||
if(stream != null) {
|
||||
stream.close();
|
||||
closed.set(true);
|
||||
opened.set(false);
|
||||
}
|
||||
|
||||
return super.cancel();
|
||||
return cancelled;
|
||||
}
|
||||
|
||||
public void close() {
|
||||
public synchronized void close() {
|
||||
if(!captureClosed.get()) {
|
||||
captureClosed.set(true);
|
||||
capture.close();
|
||||
}
|
||||
}
|
||||
|
||||
public PropertyLimits getZoomLimits() {
|
||||
return zoomLimits;
|
||||
|
|
@ -262,9 +319,6 @@ public class WebcamService extends ScheduledService<Image> {
|
|||
}
|
||||
|
||||
private Result readQR(BufferedImage bufferedImage) {
|
||||
LuminanceSource source = new BufferedImageLuminanceSource(bufferedImage);
|
||||
BinaryBitmap bitmap = new BinaryBitmap(new HybridBinarizer(source));
|
||||
|
||||
try {
|
||||
com.sparrowwallet.bokmakierie.Result result = bokmakierie.scan(bufferedImage);
|
||||
if(result != null) {
|
||||
|
|
@ -282,6 +336,8 @@ public class WebcamService extends ScheduledService<Image> {
|
|||
}
|
||||
|
||||
try {
|
||||
LuminanceSource source = new BufferedImageLuminanceSource(bufferedImage);
|
||||
BinaryBitmap bitmap = new BinaryBitmap(new HybridBinarizer(source));
|
||||
return qrReader.decode(bitmap, Map.of(DecodeHintType.TRY_HARDER, Boolean.TRUE));
|
||||
} catch(ReaderException e) {
|
||||
// fall thru, it means there is no QR code in image
|
||||
|
|
@ -336,6 +392,10 @@ public class WebcamService extends ScheduledService<Image> {
|
|||
return devices;
|
||||
}
|
||||
|
||||
public List<CaptureDevice> getAvailableDevices() {
|
||||
return availableDevices;
|
||||
}
|
||||
|
||||
public Set<WebcamResolution> getResolutions() {
|
||||
return resolutions;
|
||||
}
|
||||
|
|
@ -376,8 +436,12 @@ public class WebcamService extends ScheduledService<Image> {
|
|||
return opening;
|
||||
}
|
||||
|
||||
public BooleanProperty closedProperty() {
|
||||
return closed;
|
||||
public BooleanProperty openedProperty() {
|
||||
return opened;
|
||||
}
|
||||
|
||||
public boolean getCancelRequested() {
|
||||
return cancelRequested.get();
|
||||
}
|
||||
|
||||
public static <T extends Enum<T>> T getNearestEnum(T target) {
|
||||
|
|
@ -385,10 +449,27 @@ public class WebcamService extends ScheduledService<Image> {
|
|||
}
|
||||
|
||||
public static <T extends Enum<T>> T getNearestEnum(T target, T[] values) {
|
||||
int ordinal = target.ordinal();
|
||||
return Stream.concat(ordinal > 0 ? Stream.of(values[ordinal - 1]) : Stream.empty(), ordinal < values.length - 1 ? Stream.of(values[ordinal + 1]) : Stream.empty())
|
||||
.findFirst()
|
||||
.orElse(null);
|
||||
if(values == null || values.length == 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
int targetOrdinal = target.ordinal();
|
||||
if(values.length == 1) {
|
||||
return values[0];
|
||||
}
|
||||
|
||||
for(int i = 0; i < values.length; i++) {
|
||||
if(targetOrdinal < values[i].ordinal()) {
|
||||
if(i == 0) {
|
||||
return values[0];
|
||||
}
|
||||
int diffToPrev = Math.abs(targetOrdinal - values[i - 1].ordinal());
|
||||
int diffToNext = Math.abs(targetOrdinal - values[i].ordinal());
|
||||
return diffToPrev <= diffToNext ? values[i - 1] : values[i];
|
||||
}
|
||||
}
|
||||
|
||||
return values[values.length - 1];
|
||||
}
|
||||
|
||||
private static class CroppedDimension {
|
||||
|
|
|
|||
|
|
@ -61,7 +61,7 @@ public class WebcamView {
|
|||
});
|
||||
|
||||
service.valueProperty().addListener((observable, oldValue, newValue) -> {
|
||||
if(newValue != null) {
|
||||
if(newValue != null && !service.getCancelRequested()) {
|
||||
imageProperty.set(newValue);
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
package com.sparrowwallet.sparrow.event;
|
||||
|
||||
import com.sparrowwallet.drongo.protocol.BlockHeader;
|
||||
import com.sparrowwallet.sparrow.AppServices;
|
||||
import com.sparrowwallet.sparrow.net.MempoolRateSize;
|
||||
|
||||
import java.util.List;
|
||||
|
|
@ -13,6 +14,7 @@ public class ConnectionEvent extends FeeRatesUpdatedEvent {
|
|||
private final int blockHeight;
|
||||
private final BlockHeader blockHeader;
|
||||
private final Double minimumRelayFeeRate;
|
||||
private final Double previousMinimumRelayFeeRate;
|
||||
|
||||
public ConnectionEvent(List<String> serverVersion, String serverBanner, int blockHeight, BlockHeader blockHeader, Map<Integer, Double> targetBlockFeeRates, Set<MempoolRateSize> mempoolRateSizes, Double minimumRelayFeeRate) {
|
||||
super(targetBlockFeeRates, mempoolRateSizes);
|
||||
|
|
@ -21,6 +23,7 @@ public class ConnectionEvent extends FeeRatesUpdatedEvent {
|
|||
this.blockHeight = blockHeight;
|
||||
this.blockHeader = blockHeader;
|
||||
this.minimumRelayFeeRate = minimumRelayFeeRate;
|
||||
this.previousMinimumRelayFeeRate = AppServices.getMinimumRelayFeeRate();
|
||||
}
|
||||
|
||||
public List<String> getServerVersion() {
|
||||
|
|
@ -42,4 +45,8 @@ public class ConnectionEvent extends FeeRatesUpdatedEvent {
|
|||
public Double getMinimumRelayFeeRate() {
|
||||
return minimumRelayFeeRate;
|
||||
}
|
||||
|
||||
public Double getPreviousMinimumRelayFeeRate() {
|
||||
return previousMinimumRelayFeeRate;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,17 @@
|
|||
package com.sparrowwallet.sparrow.event;
|
||||
|
||||
import com.sparrowwallet.drongo.wallet.Payment;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class RequestSendToManyEvent {
|
||||
private final List<Payment> payments;
|
||||
|
||||
public RequestSendToManyEvent(List<Payment> payments) {
|
||||
this.payments = payments;
|
||||
}
|
||||
|
||||
public List<Payment> getPayments() {
|
||||
return payments;
|
||||
}
|
||||
}
|
||||
|
|
@ -17,12 +17,13 @@ public class SpendUtxoEvent {
|
|||
private final boolean requireAllUtxos;
|
||||
private final BlockTransaction replacedTransaction;
|
||||
private final PaymentCode paymentCode;
|
||||
private final boolean allowPaymentChanges;
|
||||
|
||||
public SpendUtxoEvent(Wallet wallet, List<BlockTransactionHashIndex> utxos) {
|
||||
this(wallet, utxos, null, null, null, false, null);
|
||||
this(wallet, utxos, null, null, null, false, null, true);
|
||||
}
|
||||
|
||||
public SpendUtxoEvent(Wallet wallet, List<BlockTransactionHashIndex> utxos, List<Payment> payments, List<byte[]> opReturns, Long fee, boolean requireAllUtxos, BlockTransaction replacedTransaction) {
|
||||
public SpendUtxoEvent(Wallet wallet, List<BlockTransactionHashIndex> utxos, List<Payment> payments, List<byte[]> opReturns, Long fee, boolean requireAllUtxos, BlockTransaction replacedTransaction, boolean allowPaymentChanges) {
|
||||
this.wallet = wallet;
|
||||
this.utxos = utxos;
|
||||
this.payments = payments;
|
||||
|
|
@ -31,6 +32,7 @@ public class SpendUtxoEvent {
|
|||
this.requireAllUtxos = requireAllUtxos;
|
||||
this.replacedTransaction = replacedTransaction;
|
||||
this.paymentCode = null;
|
||||
this.allowPaymentChanges = allowPaymentChanges;
|
||||
}
|
||||
|
||||
public SpendUtxoEvent(Wallet wallet, List<Payment> payments, List<byte[]> opReturns, PaymentCode paymentCode) {
|
||||
|
|
@ -42,6 +44,7 @@ public class SpendUtxoEvent {
|
|||
this.requireAllUtxos = false;
|
||||
this.replacedTransaction = null;
|
||||
this.paymentCode = paymentCode;
|
||||
this.allowPaymentChanges = false;
|
||||
}
|
||||
|
||||
public Wallet getWallet() {
|
||||
|
|
@ -75,4 +78,8 @@ public class SpendUtxoEvent {
|
|||
public PaymentCode getPaymentCode() {
|
||||
return paymentCode;
|
||||
}
|
||||
|
||||
public boolean allowPaymentChanges() {
|
||||
return allowPaymentChanges;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,9 @@
|
|||
package com.sparrowwallet.sparrow.event;
|
||||
|
||||
import com.sparrowwallet.drongo.protocol.Transaction;
|
||||
|
||||
public class TransactionOutputsChangedEvent extends TransactionChangedEvent {
|
||||
public TransactionOutputsChangedEvent(Transaction transaction) {
|
||||
super(transaction);
|
||||
}
|
||||
}
|
||||
|
|
@ -14,9 +14,16 @@ import java.util.List;
|
|||
*/
|
||||
public class WalletNodeHistoryChangedEvent {
|
||||
private final String scriptHash;
|
||||
private final String status;
|
||||
|
||||
public WalletNodeHistoryChangedEvent(String scriptHash) {
|
||||
this.scriptHash = scriptHash;
|
||||
this.status = null;
|
||||
}
|
||||
|
||||
public WalletNodeHistoryChangedEvent(String scriptHash, String status) {
|
||||
this.scriptHash = scriptHash;
|
||||
this.status = status;
|
||||
}
|
||||
|
||||
public WalletNode getWalletNode(Wallet wallet) {
|
||||
|
|
@ -70,4 +77,8 @@ public class WalletNodeHistoryChangedEvent {
|
|||
public String getScriptHash() {
|
||||
return scriptHash;
|
||||
}
|
||||
|
||||
public String getStatus() {
|
||||
return status;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
package com.sparrowwallet.sparrow.glyphfont;
|
||||
|
||||
import com.sparrowwallet.drongo.wallet.Payment;
|
||||
import com.sparrowwallet.drongo.wallet.WalletNodePayment;
|
||||
import com.sparrowwallet.drongo.wallet.WalletTransaction;
|
||||
import com.sparrowwallet.sparrow.AppServices;
|
||||
import com.sparrowwallet.sparrow.control.TransactionDiagram;
|
||||
|
|
@ -15,7 +16,7 @@ public class GlyphUtils {
|
|||
return getFakeMixGlyph();
|
||||
} else if(payment.getType().equals(Payment.Type.ANCHOR)) {
|
||||
return getAnchorGlyph();
|
||||
} else if(walletTx.isConsolidationSend(payment)) {
|
||||
} else if(payment instanceof WalletNodePayment) {
|
||||
return getConsolidationGlyph();
|
||||
} else if(walletTx.isPremixSend(payment)) {
|
||||
return getPremixGlyph();
|
||||
|
|
@ -213,6 +214,13 @@ public class GlyphUtils {
|
|||
return busyGlyph;
|
||||
}
|
||||
|
||||
public static Glyph getUpArrowGlyph() {
|
||||
Glyph upGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.ARROW_UP);
|
||||
upGlyph.getStyleClass().add("arrow-up");
|
||||
upGlyph.setFontSize(12);
|
||||
return upGlyph;
|
||||
}
|
||||
|
||||
public static Glyph getDownArrowGlyph() {
|
||||
Glyph downGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.ARROW_DOWN);
|
||||
downGlyph.getStyleClass().add("arrow-down");
|
||||
|
|
|
|||
|
|
@ -192,7 +192,7 @@ public class Bip129 implements KeystoreFileExport, KeystoreFileImport, WalletExp
|
|||
public void exportWallet(Wallet wallet, OutputStream outputStream, String password) throws ExportException {
|
||||
try {
|
||||
String record = "BSMS 1.0\n" +
|
||||
OutputDescriptor.getOutputDescriptor(wallet) +
|
||||
OutputDescriptor.getOutputDescriptor(wallet, KeyPurpose.DEFAULT_PURPOSES, null) +
|
||||
"\n/0/*,/1/*\n" +
|
||||
wallet.getNode(KeyPurpose.RECEIVE).getChildren().iterator().next().getAddress();
|
||||
outputStream.write(record.getBytes(StandardCharsets.UTF_8));
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ package com.sparrowwallet.sparrow.io;
|
|||
|
||||
import com.google.gson.*;
|
||||
import com.sparrowwallet.drongo.BitcoinUnit;
|
||||
import com.sparrowwallet.drongo.protocol.Transaction;
|
||||
import com.sparrowwallet.sparrow.UnitFormat;
|
||||
import com.sparrowwallet.sparrow.Mode;
|
||||
import com.sparrowwallet.sparrow.Theme;
|
||||
|
|
@ -52,6 +53,9 @@ public class Config {
|
|||
private boolean showDeprecatedImportExport = false;
|
||||
private boolean signBsmsExports = false;
|
||||
private boolean preventSleep = false;
|
||||
private Boolean connectToBroadcast;
|
||||
private Boolean connectToResolve;
|
||||
private Boolean suggestSendToMany;
|
||||
private List<File> recentWalletFiles;
|
||||
private Integer keyDerivationPeriod;
|
||||
private long dustAttackThreshold = DUST_ATTACK_THRESHOLD_SATS;
|
||||
|
|
@ -61,6 +65,7 @@ public class Config {
|
|||
private boolean mirrorCapture = true;
|
||||
private boolean useZbar = true;
|
||||
private String webcamDevice;
|
||||
private String webcamDeviceId;
|
||||
private ServerType serverType;
|
||||
private Server publicElectrumServer;
|
||||
private Server coreServer;
|
||||
|
|
@ -69,6 +74,7 @@ public class Config {
|
|||
private File coreDataDir;
|
||||
private String coreAuth;
|
||||
private boolean useLegacyCoreWallet;
|
||||
private boolean legacyServer;
|
||||
private Server electrumServer;
|
||||
private List<Server> recentElectrumServers;
|
||||
private File electrumServerCert;
|
||||
|
|
@ -79,6 +85,7 @@ public class Config {
|
|||
private int maxPageSize = DEFAULT_PAGE_SIZE;
|
||||
private boolean usePayNym;
|
||||
private boolean mempoolFullRbf;
|
||||
private double minRelayFeeRate = Transaction.DEFAULT_MIN_RELAY_FEE;
|
||||
private Double appWidth;
|
||||
private Double appHeight;
|
||||
|
||||
|
|
@ -347,6 +354,34 @@ public class Config {
|
|||
|
||||
public void setPreventSleep(boolean preventSleep) {
|
||||
this.preventSleep = preventSleep;
|
||||
flush();
|
||||
}
|
||||
|
||||
public Boolean getConnectToBroadcast() {
|
||||
return connectToBroadcast;
|
||||
}
|
||||
|
||||
public void setConnectToBroadcast(Boolean connectToBroadcast) {
|
||||
this.connectToBroadcast = connectToBroadcast;
|
||||
flush();
|
||||
}
|
||||
|
||||
public Boolean getConnectToResolve() {
|
||||
return connectToResolve;
|
||||
}
|
||||
|
||||
public void setConnectToResolve(Boolean connectToResolve) {
|
||||
this.connectToResolve = connectToResolve;
|
||||
flush();
|
||||
}
|
||||
|
||||
public Boolean getSuggestSendToMany() {
|
||||
return suggestSendToMany;
|
||||
}
|
||||
|
||||
public void setSuggestSendToMany(Boolean suggestSendToMany) {
|
||||
this.suggestSendToMany = suggestSendToMany;
|
||||
flush();
|
||||
}
|
||||
|
||||
public List<File> getRecentWalletFiles() {
|
||||
|
|
@ -415,6 +450,15 @@ public class Config {
|
|||
flush();
|
||||
}
|
||||
|
||||
public String getWebcamDeviceId() {
|
||||
return webcamDeviceId;
|
||||
}
|
||||
|
||||
public void setWebcamDeviceId(String webcamDeviceId) {
|
||||
this.webcamDeviceId = webcamDeviceId;
|
||||
flush();
|
||||
}
|
||||
|
||||
public ServerType getServerType() {
|
||||
return serverType;
|
||||
}
|
||||
|
|
@ -549,6 +593,15 @@ public class Config {
|
|||
flush();
|
||||
}
|
||||
|
||||
public boolean isLegacyServer() {
|
||||
return legacyServer;
|
||||
}
|
||||
|
||||
public void setLegacyServer(boolean legacyServer) {
|
||||
this.legacyServer = legacyServer;
|
||||
flush();
|
||||
}
|
||||
|
||||
public Server getElectrumServer() {
|
||||
return electrumServer;
|
||||
}
|
||||
|
|
@ -667,6 +720,14 @@ public class Config {
|
|||
flush();
|
||||
}
|
||||
|
||||
public double getMinRelayFeeRate() {
|
||||
return minRelayFeeRate;
|
||||
}
|
||||
|
||||
public void setMinRelayFeeRate(double minRelayFeeRate) {
|
||||
this.minRelayFeeRate = minRelayFeeRate;
|
||||
}
|
||||
|
||||
public Double getAppWidth() {
|
||||
return appWidth;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -126,7 +126,7 @@ public class Descriptor implements WalletImport, WalletExport {
|
|||
} else if(line.startsWith("#")) {
|
||||
continue;
|
||||
} else {
|
||||
paragraph.append(line);
|
||||
paragraph.append(line.replaceFirst("^.+:", "").trim());
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -37,7 +37,8 @@ public class ElectrumPersonalServer implements WalletExport {
|
|||
try {
|
||||
BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(outputStream, StandardCharsets.UTF_8));
|
||||
writer.write("# Electrum Personal Server configuration file fragments\n");
|
||||
writer.write("# Copy the lines below into the relevant sections in your EPS config.ini file\n\n");
|
||||
writer.write("# First close Sparrow and edit your config file in Sparrow home to set \"legacyServer\": true\n");
|
||||
writer.write("# Then copy the lines below into the relevant sections in your EPS config.ini file\n\n");
|
||||
writer.write("# Copy into [master-public-keys] section\n");
|
||||
Wallet masterWallet = wallet.isMasterWallet() ? wallet : wallet.getMasterWallet();
|
||||
writeWalletXpub(masterWallet, writer);
|
||||
|
|
|
|||
|
|
@ -684,7 +684,7 @@ public class Storage {
|
|||
|
||||
public static Executor getSingleThreadedExecutor() {
|
||||
if(singleThreadedExecutor == null) {
|
||||
BasicThreadFactory factory = new BasicThreadFactory.Builder().namingPattern("LoadWalletService-single").daemon(true).priority(Thread.MIN_PRIORITY).build();
|
||||
BasicThreadFactory factory = BasicThreadFactory.builder().namingPattern("LoadWalletService-single").daemon(true).priority(Thread.MIN_PRIORITY).build();
|
||||
singleThreadedExecutor = Executors.newSingleThreadScheduledExecutor(factory);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,29 +1,21 @@
|
|||
package net.sourceforge.zbar;
|
||||
package com.sparrowwallet.sparrow.io;
|
||||
|
||||
import com.sparrowwallet.sparrow.net.NativeUtils;
|
||||
import io.github.doblon8.jzbar.Config;
|
||||
import io.github.doblon8.jzbar.Image;
|
||||
import io.github.doblon8.jzbar.ImageScanner;
|
||||
import io.github.doblon8.jzbar.SymbolType;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.awt.*;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.awt.image.DataBufferByte;
|
||||
import java.util.Iterator;
|
||||
|
||||
public class ZBar {
|
||||
private static final Logger log = LoggerFactory.getLogger(ZBar.class);
|
||||
|
||||
private final static boolean enabled;
|
||||
|
||||
static { // static initializer
|
||||
if(com.sparrowwallet.sparrow.io.Config.get().isUseZbar()) {
|
||||
enabled = loadLibrary();
|
||||
} else {
|
||||
enabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
public static boolean isEnabled() {
|
||||
return enabled;
|
||||
return com.sparrowwallet.sparrow.io.Config.get().isUseZbar();
|
||||
}
|
||||
|
||||
public static Scan scan(BufferedImage bufferedImage) {
|
||||
|
|
@ -41,19 +33,12 @@ public class ZBar {
|
|||
image.setData(data);
|
||||
|
||||
try(ImageScanner scanner = new ImageScanner()) {
|
||||
scanner.setConfig(Symbol.NONE, Config.ENABLE, 0);
|
||||
scanner.setConfig(Symbol.QRCODE, Config.ENABLE, 1);
|
||||
scanner.setConfig(SymbolType.NONE, Config.ENABLE, 0);
|
||||
scanner.setConfig(SymbolType.QRCODE, Config.ENABLE, 1);
|
||||
int result = scanner.scanImage(image);
|
||||
if(result != 0) {
|
||||
try(SymbolSet results = scanner.getResults()) {
|
||||
Scan scan = null;
|
||||
for(Iterator<Symbol> iter = results.iterator(); iter.hasNext(); ) {
|
||||
try(Symbol symbol = iter.next()) {
|
||||
scan = new Scan(getRawBytes(symbol.getData()), symbol.getData());
|
||||
}
|
||||
}
|
||||
return scan;
|
||||
}
|
||||
String symbolData = image.getFirstSymbol().getData();
|
||||
return new Scan(getRawBytes(symbolData), symbolData);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -97,31 +82,6 @@ public class ZBar {
|
|||
return outputData;
|
||||
}
|
||||
|
||||
private static boolean loadLibrary() {
|
||||
try {
|
||||
String osName = System.getProperty("os.name");
|
||||
String osArch = System.getProperty("os.arch");
|
||||
if(osName.startsWith("Mac") && osArch.equals("aarch64")) {
|
||||
NativeUtils.loadLibraryFromJar("/native/osx/aarch64/libzbar.dylib");
|
||||
} else if(osName.startsWith("Mac")) {
|
||||
NativeUtils.loadLibraryFromJar("/native/osx/x64/libzbar.dylib");
|
||||
} else if(osName.startsWith("Windows")) {
|
||||
NativeUtils.loadLibraryFromJar("/native/windows/x64/iconv-2.dll");
|
||||
NativeUtils.loadLibraryFromJar("/native/windows/x64/zbar.dll");
|
||||
} else if(osArch.equals("aarch64")) {
|
||||
NativeUtils.loadLibraryFromJar("/native/linux/aarch64/libzbar.so");
|
||||
} else {
|
||||
NativeUtils.loadLibraryFromJar("/native/linux/x64/libzbar.so");
|
||||
}
|
||||
|
||||
return true;
|
||||
} catch(Exception e) {
|
||||
log.warn("Could not load ZBar native libraries, disabling. " + e.getMessage());
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static byte[] getRawBytes(String str) {
|
||||
char[] chars = str.toCharArray();
|
||||
byte[] bytes = new byte[chars.length];
|
||||
|
|
@ -171,7 +171,7 @@ public class DbPersistence implements Persistence {
|
|||
|
||||
private synchronized void createUpdateExecutor(Wallet masterWallet) {
|
||||
if(updateExecutor == null) {
|
||||
BasicThreadFactory factory = new BasicThreadFactory.Builder().namingPattern(masterWallet.getFullName() + "-dbupdater").daemon(true).priority(Thread.NORM_PRIORITY).build();
|
||||
BasicThreadFactory factory = BasicThreadFactory.builder().namingPattern(masterWallet.getFullName() + "-dbupdater").daemon(true).priority(Thread.NORM_PRIORITY).build();
|
||||
updateExecutor = Executors.newSingleThreadExecutor(factory);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -76,6 +76,10 @@ public class ElectrumServer {
|
|||
|
||||
private static final Set<String> sameHeightTxioScriptHashes = ConcurrentHashMap.newKeySet();
|
||||
|
||||
private final static Map<String, Integer> subscribedRecent = new ConcurrentHashMap<>();
|
||||
|
||||
private final static Map<String, String> broadcastRecent = new ConcurrentHashMap<>();
|
||||
|
||||
private static ElectrumServerRpc electrumServerRpc = new SimpleElectrumServerRpc();
|
||||
|
||||
private static Cormorant cormorant;
|
||||
|
|
@ -1062,9 +1066,10 @@ public class ElectrumServer {
|
|||
List<BlockTransactionHash> recentTransactions = feeRatesSource.getRecentMempoolTransactions();
|
||||
Map<BlockTransactionHash, Transaction> setReferences = new HashMap<>();
|
||||
setReferences.put(recentTransactions.getFirst(), null);
|
||||
if(recentTransactions.size() > 1) {
|
||||
Random random = new Random();
|
||||
if(random.nextBoolean()) {
|
||||
setReferences.put(recentTransactions.get(random.nextInt(recentTransactions.size())), null);
|
||||
int halfSize = recentTransactions.size() / 2;
|
||||
setReferences.put(recentTransactions.get(halfSize == 1 ? 1 : random.nextInt(halfSize) + 1), null);
|
||||
}
|
||||
Map<Sha256Hash, BlockTransaction> transactions = getTransactions(null, setReferences, Collections.emptyMap());
|
||||
return transactions.values().stream().filter(blxTx -> blxTx.getTransaction() != null).toList();
|
||||
|
|
@ -1250,11 +1255,11 @@ public class ElectrumServer {
|
|||
if(!serverVersion.isEmpty()) {
|
||||
String server = serverVersion.getFirst().toLowerCase(Locale.ROOT);
|
||||
if(server.contains("electrumx")) {
|
||||
return new ServerCapability(true);
|
||||
return new ServerCapability(true, true);
|
||||
}
|
||||
|
||||
if(server.startsWith("cormorant")) {
|
||||
return new ServerCapability(true, false, true);
|
||||
return new ServerCapability(true, false, true, false);
|
||||
}
|
||||
|
||||
if(server.startsWith("electrs/")) {
|
||||
|
|
@ -1266,7 +1271,7 @@ public class ElectrumServer {
|
|||
try {
|
||||
Version version = new Version(electrsVersion);
|
||||
if(version.compareTo(ELECTRS_MIN_BATCHING_VERSION) >= 0) {
|
||||
return new ServerCapability(true);
|
||||
return new ServerCapability(true, true);
|
||||
}
|
||||
} catch(Exception e) {
|
||||
//ignore
|
||||
|
|
@ -1282,7 +1287,7 @@ public class ElectrumServer {
|
|||
try {
|
||||
Version version = new Version(fulcrumVersion);
|
||||
if(version.compareTo(FULCRUM_MIN_BATCHING_VERSION) >= 0) {
|
||||
return new ServerCapability(true);
|
||||
return new ServerCapability(true, true);
|
||||
}
|
||||
} catch(Exception e) {
|
||||
//ignore
|
||||
|
|
@ -1301,15 +1306,19 @@ public class ElectrumServer {
|
|||
Version version = new Version(mempoolElectrsVersion);
|
||||
if(version.compareTo(MEMPOOL_ELECTRS_MIN_BATCHING_VERSION) > 0 ||
|
||||
(version.compareTo(MEMPOOL_ELECTRS_MIN_BATCHING_VERSION) == 0 && (!mempoolElectrsSuffix.contains("dev") || mempoolElectrsSuffix.contains("dev-249848d")))) {
|
||||
return new ServerCapability(true, 25);
|
||||
return new ServerCapability(true, 25, false);
|
||||
}
|
||||
} catch(Exception e) {
|
||||
//ignore
|
||||
}
|
||||
}
|
||||
|
||||
if(server.startsWith("electrumpersonalserver")) {
|
||||
return new ServerCapability(false, false);
|
||||
}
|
||||
}
|
||||
|
||||
return new ServerCapability(false);
|
||||
return new ServerCapability(false, true);
|
||||
}
|
||||
|
||||
public static class ServerVersionService extends Service<List<String>> {
|
||||
|
|
@ -1602,6 +1611,31 @@ public class ElectrumServer {
|
|||
Set<MempoolRateSize> mempoolRateSizes = electrumServer.getMempoolRateSizes();
|
||||
EventManager.get().post(new MempoolRateSizesUpdatedEvent(mempoolRateSizes));
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void walletNodeHistoryChanged(WalletNodeHistoryChangedEvent event) {
|
||||
String status = broadcastRecent.remove(event.getScriptHash());
|
||||
if(status != null && status.equals(event.getStatus())) {
|
||||
Map<String, String> subscribeScriptHashes = new HashMap<>();
|
||||
Random random = new Random();
|
||||
int subscriptions = random.nextInt(2) + 1;
|
||||
for(int i = 0; i < subscriptions; i++) {
|
||||
byte[] randomScriptHashBytes = new byte[32];
|
||||
random.nextBytes(randomScriptHashBytes);
|
||||
String randomScriptHash = Utils.bytesToHex(randomScriptHashBytes);
|
||||
if(!subscribedScriptHashes.containsKey(randomScriptHash)) {
|
||||
subscribeScriptHashes.put("m/" + subscribeScriptHashes.size(), randomScriptHash);
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
electrumServerRpc.subscribeScriptHashes(transport, null, subscribeScriptHashes);
|
||||
subscribeScriptHashes.values().forEach(scriptHash -> subscribedRecent.put(scriptHash, AppServices.getCurrentBlockHeight()));
|
||||
} catch(ElectrumServerRpcException e) {
|
||||
log.debug("Error subscribing to recent mempool transaction outputs", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static class ReadRunnable implements Runnable {
|
||||
|
|
@ -2002,7 +2036,7 @@ public class ElectrumServer {
|
|||
Config config = Config.get();
|
||||
if(!isBlockstorm(totalBlocks) && !AppServices.isUsingProxy() && config.getServer().getProtocol().equals(Protocol.SSL)
|
||||
&& (config.getServerType() == ServerType.PUBLIC_ELECTRUM_SERVER || config.getServerType() == ServerType.ELECTRUM_SERVER)) {
|
||||
subscribeRecent(electrumServer);
|
||||
subscribeRecent(electrumServer, AppServices.getCurrentBlockHeight() == null ? endHeight : AppServices.getCurrentBlockHeight());
|
||||
}
|
||||
|
||||
Double nextBlockMedianFeeRate = null;
|
||||
|
|
@ -2018,13 +2052,15 @@ public class ElectrumServer {
|
|||
return Network.get() != Network.MAINNET && totalBlocks > 2;
|
||||
}
|
||||
|
||||
private final static Set<String> subscribedRecent = Collections.newSetFromMap(new ConcurrentHashMap<>());
|
||||
|
||||
private void subscribeRecent(ElectrumServer electrumServer) {
|
||||
Set<String> unsubscribeScriptHashes = new HashSet<>(subscribedRecent);
|
||||
private void subscribeRecent(ElectrumServer electrumServer, int currentHeight) {
|
||||
Set<String> unsubscribeScriptHashes = subscribedRecent.entrySet().stream().filter(entry -> entry.getValue() == null || entry.getValue() <= currentHeight - 3)
|
||||
.map(Map.Entry::getKey).collect(Collectors.toSet());
|
||||
unsubscribeScriptHashes.removeIf(subscribedScriptHashes::containsKey);
|
||||
if(!unsubscribeScriptHashes.isEmpty() && serverCapability.supportsUnsubscribe()) {
|
||||
electrumServerRpc.unsubscribeScriptHashes(transport, unsubscribeScriptHashes);
|
||||
subscribedRecent.removeAll(unsubscribeScriptHashes);
|
||||
}
|
||||
subscribedRecent.keySet().removeAll(unsubscribeScriptHashes);
|
||||
broadcastRecent.keySet().removeAll(unsubscribeScriptHashes);
|
||||
|
||||
Map<String, String> subscribeScriptHashes = new HashMap<>();
|
||||
List<BlockTransaction> recentTransactions = electrumServer.getRecentMempoolTransactions();
|
||||
|
|
@ -2033,7 +2069,7 @@ public class ElectrumServer {
|
|||
TransactionOutput txOutput = blkTx.getTransaction().getOutputs().get(i);
|
||||
String scriptHash = getScriptHash(txOutput);
|
||||
if(!subscribedScriptHashes.containsKey(scriptHash)) {
|
||||
subscribeScriptHashes.put("m/" + i, getScriptHash(txOutput));
|
||||
subscribeScriptHashes.put("m/" + subscribeScriptHashes.size(), scriptHash);
|
||||
}
|
||||
if(Math.random() < 0.1d) {
|
||||
break;
|
||||
|
|
@ -2042,9 +2078,20 @@ public class ElectrumServer {
|
|||
}
|
||||
|
||||
if(!subscribeScriptHashes.isEmpty()) {
|
||||
Random random = new Random();
|
||||
int additionalRandomScriptHashes = random.nextInt(8);
|
||||
for(int i = 0; i < additionalRandomScriptHashes; i++) {
|
||||
byte[] randomScriptHashBytes = new byte[32];
|
||||
random.nextBytes(randomScriptHashBytes);
|
||||
String randomScriptHash = Utils.bytesToHex(randomScriptHashBytes);
|
||||
if(!subscribedScriptHashes.containsKey(randomScriptHash)) {
|
||||
subscribeScriptHashes.put("m/" + subscribeScriptHashes.size(), randomScriptHash);
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
electrumServerRpc.subscribeScriptHashes(transport, null, subscribeScriptHashes);
|
||||
subscribedRecent.addAll(subscribeScriptHashes.values());
|
||||
subscribeScriptHashes.values().forEach(scriptHash -> subscribedRecent.put(scriptHash, currentHeight));
|
||||
} catch(ElectrumServerRpcException e) {
|
||||
log.debug("Error subscribing to recent mempool transactions", e);
|
||||
}
|
||||
|
|
@ -2066,6 +2113,9 @@ public class ElectrumServer {
|
|||
Random random = new Random();
|
||||
if(random.nextBoolean()) {
|
||||
BlockTransaction blkTx = recentTransactions.get(random.nextInt(recentTransactions.size()));
|
||||
String scriptHash = getScriptHash(blkTx.getTransaction().getOutputs().getFirst());
|
||||
String status = getScriptHashStatus(List.of(new ScriptHashTx(0, blkTx.getHashAsString(), blkTx.getFee())));
|
||||
broadcastRecent.put(scriptHash, status);
|
||||
electrumServer.broadcastTransaction(blkTx.getTransaction());
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -124,7 +124,7 @@ public enum ExchangeSource {
|
|||
return historicalRates;
|
||||
}
|
||||
},
|
||||
COINGECKO("Coingecko", "No historical rates") {
|
||||
COINGECKO("Coingecko", "Historical rates for the last 365 days") {
|
||||
@Override
|
||||
public List<Currency> getSupportedCurrencies() {
|
||||
return getRates().rates.entrySet().stream().filter(rate -> "fiat".equals(rate.getValue().type) && isValidISO4217Code(rate.getKey().toUpperCase(Locale.ROOT)))
|
||||
|
|
@ -167,6 +167,11 @@ public enum ExchangeSource {
|
|||
long startDate = start.getTime() / 1000;
|
||||
long endDate = end.getTime() / 1000;
|
||||
|
||||
Calendar cal = Calendar.getInstance();
|
||||
cal.add(Calendar.YEAR, -1);
|
||||
startDate = Math.max(cal.getTimeInMillis() / 1000, startDate);
|
||||
endDate = Math.max(cal.getTimeInMillis() / 1000, endDate);
|
||||
|
||||
String url = "https://api.coingecko.com/api/v3/coins/bitcoin/market_chart/range?vs_currency=" + currency.getCurrencyCode() + "&from=" + startDate + "&to=" + endDate;
|
||||
|
||||
if(log.isInfoEnabled()) {
|
||||
|
|
|
|||
|
|
@ -1,11 +1,17 @@
|
|||
package com.sparrowwallet.sparrow.net;
|
||||
|
||||
import com.sparrowwallet.drongo.OsType;
|
||||
|
||||
import java.io.*;
|
||||
import java.nio.file.FileSystemNotFoundException;
|
||||
import java.nio.file.FileSystems;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.ProviderNotFoundException;
|
||||
import java.nio.file.StandardCopyOption;
|
||||
import java.nio.file.attribute.PosixFilePermission;
|
||||
import java.nio.file.attribute.PosixFilePermissions;
|
||||
import java.util.EnumSet;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* A simple library class which helps with loading dynamic libraries stored in the
|
||||
|
|
@ -111,9 +117,33 @@ public class NativeUtils {
|
|||
String tempDir = System.getProperty("java.io.tmpdir");
|
||||
File generatedDir = new File(tempDir, prefix + System.nanoTime());
|
||||
|
||||
if (!generatedDir.mkdir())
|
||||
if(!createOwnerOnlyDirectory(generatedDir)) {
|
||||
throw new IOException("Failed to create temp directory " + generatedDir.getName());
|
||||
}
|
||||
|
||||
return generatedDir;
|
||||
}
|
||||
|
||||
public static boolean createOwnerOnlyDirectory(File directory) throws IOException {
|
||||
try {
|
||||
if(OsType.getCurrent() == OsType.WINDOWS) {
|
||||
Files.createDirectories(directory.toPath());
|
||||
return true;
|
||||
}
|
||||
|
||||
Files.createDirectories(directory.toPath(), PosixFilePermissions.asFileAttribute(getDirectoryOwnerOnlyPosixFilePermissions()));
|
||||
return true;
|
||||
} catch(UnsupportedOperationException e) {
|
||||
return directory.mkdirs();
|
||||
}
|
||||
}
|
||||
|
||||
private static Set<PosixFilePermission> getDirectoryOwnerOnlyPosixFilePermissions() {
|
||||
Set<PosixFilePermission> ownerOnly = EnumSet.noneOf(PosixFilePermission.class);
|
||||
ownerOnly.add(PosixFilePermission.OWNER_READ);
|
||||
ownerOnly.add(PosixFilePermission.OWNER_WRITE);
|
||||
ownerOnly.add(PosixFilePermission.OWNER_EXECUTE);
|
||||
|
||||
return ownerOnly;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,27 +7,30 @@ public class ServerCapability {
|
|||
private final int maxTargetBlocks;
|
||||
private final boolean supportsRecentMempool;
|
||||
private final boolean supportsBlockStats;
|
||||
private final boolean supportsUnsubscribe;
|
||||
|
||||
public ServerCapability(boolean supportsBatching) {
|
||||
this(supportsBatching, AppServices.TARGET_BLOCKS_RANGE.getLast());
|
||||
public ServerCapability(boolean supportsBatching, boolean supportsUnsubscribe) {
|
||||
this(supportsBatching, AppServices.TARGET_BLOCKS_RANGE.getLast(), supportsUnsubscribe);
|
||||
}
|
||||
|
||||
public ServerCapability(boolean supportsBatching, int maxTargetBlocks) {
|
||||
public ServerCapability(boolean supportsBatching, int maxTargetBlocks, boolean supportsUnsubscribe) {
|
||||
this.supportsBatching = supportsBatching;
|
||||
this.maxTargetBlocks = maxTargetBlocks;
|
||||
this.supportsRecentMempool = false;
|
||||
this.supportsBlockStats = false;
|
||||
this.supportsUnsubscribe = supportsUnsubscribe;
|
||||
}
|
||||
|
||||
public ServerCapability(boolean supportsBatching, boolean supportsRecentMempool, boolean supportsBlockStats) {
|
||||
this(supportsBatching, AppServices.TARGET_BLOCKS_RANGE.getLast(), supportsRecentMempool, supportsBlockStats);
|
||||
public ServerCapability(boolean supportsBatching, boolean supportsRecentMempool, boolean supportsBlockStats, boolean supportsUnsubscribe) {
|
||||
this(supportsBatching, AppServices.TARGET_BLOCKS_RANGE.getLast(), supportsRecentMempool, supportsBlockStats, supportsUnsubscribe);
|
||||
}
|
||||
|
||||
public ServerCapability(boolean supportsBatching, int maxTargetBlocks, boolean supportsRecentMempool, boolean supportsBlockStats) {
|
||||
public ServerCapability(boolean supportsBatching, int maxTargetBlocks, boolean supportsRecentMempool, boolean supportsBlockStats, boolean supportsUnsubscribe) {
|
||||
this.supportsBatching = supportsBatching;
|
||||
this.maxTargetBlocks = maxTargetBlocks;
|
||||
this.supportsRecentMempool = supportsRecentMempool;
|
||||
this.supportsBlockStats = supportsBlockStats;
|
||||
this.supportsUnsubscribe = supportsUnsubscribe;
|
||||
}
|
||||
|
||||
public boolean supportsBatching() {
|
||||
|
|
@ -45,4 +48,8 @@ public class ServerCapability {
|
|||
public boolean supportsBlockStats() {
|
||||
return supportsBlockStats;
|
||||
}
|
||||
|
||||
public boolean supportsUnsubscribe() {
|
||||
return supportsUnsubscribe;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import com.sparrowwallet.drongo.wallet.Wallet;
|
|||
import com.sparrowwallet.sparrow.AppServices;
|
||||
import com.sparrowwallet.sparrow.EventManager;
|
||||
import com.sparrowwallet.sparrow.event.WalletHistoryStatusEvent;
|
||||
import com.sparrowwallet.sparrow.io.Config;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
|
|
@ -38,16 +39,32 @@ public class SimpleElectrumServerRpc implements ElectrumServerRpc {
|
|||
|
||||
@Override
|
||||
public List<String> getServerVersion(Transport transport, String clientName, String[] supportedVersions) {
|
||||
if(Config.get().getServerType() == ServerType.ELECTRUM_SERVER && Config.get().isLegacyServer()) {
|
||||
return getLegacyServerVersion(transport, clientName);
|
||||
}
|
||||
|
||||
try {
|
||||
JsonRpcClient client = new JsonRpcClient(transport);
|
||||
//Using 1.4 as the version number as EPS tries to parse this number to a float :(
|
||||
return new RetryLogic<List<String>>(MAX_RETRIES, RETRY_DELAY, IllegalStateException.class).getResult(() ->
|
||||
client.createRequest().returnAsList(String.class).method("server.version").id(idCounter.incrementAndGet()).params(clientName, "1.4").execute());
|
||||
client.createRequest().returnAsList(String.class).method("server.version").id(idCounter.incrementAndGet()).params(clientName, supportedVersions).execute());
|
||||
} catch(JsonRpcException e) {
|
||||
return getLegacyServerVersion(transport, clientName);
|
||||
} catch(Exception e) {
|
||||
throw new ElectrumServerRpcException("Error getting server version", e);
|
||||
}
|
||||
}
|
||||
|
||||
private List<String> getLegacyServerVersion(Transport transport, String clientName) {
|
||||
try {
|
||||
//Fallback to using 1.4 as the version number as EPS tries to parse this number to a float :(
|
||||
JsonRpcClient client = new JsonRpcClient(transport);
|
||||
return new RetryLogic<List<String>>(MAX_RETRIES, RETRY_DELAY, IllegalStateException.class).getResult(() ->
|
||||
client.createRequest().returnAsList(String.class).method("server.version").id(idCounter.incrementAndGet()).params(clientName, "1.4").execute());
|
||||
} catch(Exception ex) {
|
||||
throw new ElectrumServerRpcException("Error getting legacy server version", ex);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getServerBanner(Transport transport) {
|
||||
try {
|
||||
|
|
|
|||
|
|
@ -38,6 +38,6 @@ public class SubscriptionService {
|
|||
existingStatuses.add(status);
|
||||
}
|
||||
|
||||
Platform.runLater(() -> EventManager.get().post(new WalletNodeHistoryChangedEvent(scriptHash)));
|
||||
Platform.runLater(() -> EventManager.get().post(new WalletNodeHistoryChangedEvent(scriptHash, status)));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ import java.security.cert.Certificate;
|
|||
|
||||
public class TcpOverTlsTransport extends TcpTransport {
|
||||
private static final Logger log = LoggerFactory.getLogger(TcpOverTlsTransport.class);
|
||||
public static final int PAD_TO_MULTIPLE_OF_BYTES = 96;
|
||||
|
||||
protected final SSLSocketFactory sslSocketFactory;
|
||||
|
||||
|
|
@ -41,6 +42,24 @@ public class TcpOverTlsTransport extends TcpTransport {
|
|||
sslSocketFactory = sslContext.getSocketFactory();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void writeRequest(String request) throws IOException {
|
||||
int currentLength = request.length();
|
||||
int targetLength;
|
||||
if(currentLength % PAD_TO_MULTIPLE_OF_BYTES == 0) {
|
||||
targetLength = currentLength;
|
||||
} else {
|
||||
targetLength = ((currentLength / PAD_TO_MULTIPLE_OF_BYTES) + 1) * PAD_TO_MULTIPLE_OF_BYTES;
|
||||
}
|
||||
|
||||
int paddingNeeded = targetLength - currentLength;
|
||||
if(paddingNeeded > 0) {
|
||||
super.writeRequest(request + " ".repeat(paddingNeeded));
|
||||
} else {
|
||||
super.writeRequest(request);
|
||||
}
|
||||
}
|
||||
|
||||
private TrustManager[] getTrustManagers(File crtFile) throws IOException, CertificateException, NoSuchAlgorithmException, KeyStoreException {
|
||||
if(crtFile == null) {
|
||||
return new TrustManager[] {
|
||||
|
|
|
|||
|
|
@ -97,7 +97,7 @@ public class TcpTransport implements CloseableTransport, TimeoutCounter {
|
|||
}
|
||||
}
|
||||
|
||||
private void writeRequest(String request) throws IOException {
|
||||
protected void writeRequest(String request) throws IOException {
|
||||
if(log.isTraceEnabled()) {
|
||||
log.trace("Sending to electrum server at " + server + ": " + request);
|
||||
}
|
||||
|
|
@ -106,7 +106,7 @@ public class TcpTransport implements CloseableTransport, TimeoutCounter {
|
|||
throw new IllegalStateException("Socket connection has not been established.");
|
||||
}
|
||||
|
||||
PrintWriter out = new PrintWriter(new BufferedWriter(new OutputStreamWriter(socket.getOutputStream())));
|
||||
PrintWriter out = new PrintWriter(new BufferedWriter(new OutputStreamWriter(socket.getOutputStream(), StandardCharsets.UTF_8)));
|
||||
out.println(request);
|
||||
out.flush();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -149,6 +149,9 @@ public class BitcoindClient {
|
|||
List<String> loadedWallets;
|
||||
try {
|
||||
loadedWallets = getBitcoindService().listWallets();
|
||||
if(loadedWallets == null) {
|
||||
throw new BitcoinRPCException("Wallet support must be enabled in Bitcoin Core");
|
||||
}
|
||||
legacyWalletExists = loadedWallets.contains(Bwt.DEFAULT_CORE_WALLET);
|
||||
} catch(JsonRpcException e) {
|
||||
if(e.getErrorMessage().getCode() == RPC_METHOD_NOT_FOUND) {
|
||||
|
|
|
|||
|
|
@ -35,10 +35,11 @@ public class ElectrumServerService {
|
|||
}
|
||||
|
||||
@JsonRpcMethod("server.version")
|
||||
public List<String> getServerVersion(@JsonRpcParam("client_name") String clientName, @JsonRpcParam("protocol_version") String protocolVersion) throws UnsupportedVersionException {
|
||||
Version clientVersion = new Version(protocolVersion);
|
||||
public List<String> getServerVersion(@JsonRpcParam("client_name") String clientName, @JsonRpcParam("protocol_version") String[] protocolVersion) throws UnsupportedVersionException {
|
||||
String version = protocolVersion.length > 1 ? protocolVersion[1] : protocolVersion[0];
|
||||
Version clientVersion = new Version(version);
|
||||
if(clientVersion.compareTo(VERSION) < 0) {
|
||||
throw new UnsupportedVersionException(protocolVersion);
|
||||
throw new UnsupportedVersionException(version);
|
||||
}
|
||||
|
||||
return List.of(Cormorant.SERVER_NAME + " " + SparrowWallet.APP_VERSION, VERSION.get());
|
||||
|
|
|
|||
|
|
@ -40,6 +40,10 @@ public class Payjoin {
|
|||
this.wallet = wallet;
|
||||
this.psbt = psbt;
|
||||
|
||||
if(payjoinURI.getAddress() == null) {
|
||||
throw new IllegalArgumentException("Payjoin URI must have an address");
|
||||
}
|
||||
|
||||
for(PSBTInput psbtInput : psbt.getPsbtInputs()) {
|
||||
if(psbtInput.getUtxo() == null) {
|
||||
throw new IllegalArgumentException("Original PSBT for payjoin transaction must have non_witness_utxo or witness_utxo fields for all inputs");
|
||||
|
|
@ -104,6 +108,9 @@ public class Payjoin {
|
|||
} catch(PSBTParseException e) {
|
||||
log.error("Error parsing received PSBT", e);
|
||||
throw new PayjoinReceiverException("Payjoin receiver returned invalid PSBT", e);
|
||||
} catch(PayjoinReceiverException e) {
|
||||
log.error("Payjoin receiver error", e);
|
||||
throw e;
|
||||
} catch(Exception e) {
|
||||
log.error("Payjoin error", e);
|
||||
throw new PayjoinReceiverException("Payjoin error", e);
|
||||
|
|
|
|||
|
|
@ -616,6 +616,7 @@ public class PayNymController {
|
|||
List<byte[]> opReturns = List.of(blindedPaymentCode);
|
||||
Double feeRate = AppServices.getDefaultFeeRate();
|
||||
Double minimumFeeRate = AppServices.getMinimumFeeRate();
|
||||
Double minRelayFeeRate = AppServices.getMinimumRelayFeeRate();
|
||||
boolean groupByAddress = Config.get().isGroupByAddress();
|
||||
boolean includeMempoolOutputs = Config.get().isIncludeMempoolOutputs();
|
||||
|
||||
|
|
@ -623,7 +624,9 @@ public class PayNymController {
|
|||
List<UtxoSelector> utxoSelectors = List.of(utxos == null ? new KnapsackUtxoSelector(noInputsFee) : new PresetUtxoSelector(utxos, true, false));
|
||||
List<TxoFilter> txoFilters = List.of(new SpentTxoFilter(), new FrozenTxoFilter(), new CoinbaseTxoFilter(wallet));
|
||||
|
||||
return wallet.createWalletTransaction(utxoSelectors, txoFilters, payments, opReturns, Collections.emptySet(), feeRate, minimumFeeRate, null, AppServices.getCurrentBlockHeight(), groupByAddress, includeMempoolOutputs);
|
||||
TransactionParameters params = new TransactionParameters(utxoSelectors, txoFilters, payments, opReturns, Collections.emptySet(),
|
||||
feeRate, minimumFeeRate, minRelayFeeRate, null, AppServices.getCurrentBlockHeight(), groupByAddress, includeMempoolOutputs, true);
|
||||
return wallet.createWalletTransaction(params);
|
||||
}
|
||||
|
||||
private Map<BlockTransaction, WalletNode> getNotificationTransaction(PaymentCode externalPaymentCode) {
|
||||
|
|
|
|||
|
|
@ -4,9 +4,12 @@ import com.sparrowwallet.drongo.KeyPurpose;
|
|||
import com.sparrowwallet.drongo.SecureString;
|
||||
import com.sparrowwallet.drongo.Utils;
|
||||
import com.sparrowwallet.drongo.address.Address;
|
||||
import com.sparrowwallet.drongo.policy.PolicyType;
|
||||
import com.sparrowwallet.drongo.protocol.*;
|
||||
import com.sparrowwallet.drongo.psbt.PSBT;
|
||||
import com.sparrowwallet.drongo.psbt.PSBTInput;
|
||||
import com.sparrowwallet.drongo.silentpayments.SilentPayment;
|
||||
import com.sparrowwallet.drongo.silentpayments.SilentPaymentAddress;
|
||||
import com.sparrowwallet.drongo.uri.BitcoinURI;
|
||||
import com.sparrowwallet.drongo.wallet.*;
|
||||
import com.sparrowwallet.hummingbird.UR;
|
||||
|
|
@ -57,7 +60,6 @@ import tornadofx.control.Fieldset;
|
|||
import com.google.common.eventbus.Subscribe;
|
||||
import tornadofx.control.Form;
|
||||
|
||||
import javax.swing.text.html.Option;
|
||||
import java.io.*;
|
||||
import java.net.*;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
|
@ -180,6 +182,15 @@ public class HeadersController extends TransactionFormController implements Init
|
|||
@FXML
|
||||
private CopyableLabel blockTimestamp;
|
||||
|
||||
@FXML
|
||||
private Field signedByField;
|
||||
|
||||
@FXML
|
||||
private CopyableLabel signedBy;
|
||||
|
||||
@FXML
|
||||
private Form blockchainSpacerForm;
|
||||
|
||||
@FXML
|
||||
private Form signingWalletForm;
|
||||
|
||||
|
|
@ -451,6 +462,7 @@ public class HeadersController extends TransactionFormController implements Init
|
|||
headersForm.setWalletTransaction(getWalletTransaction(headersForm.getInputTransactions()));
|
||||
|
||||
blockchainForm.managedProperty().bind(blockchainForm.visibleProperty());
|
||||
blockchainSpacerForm.managedProperty().bind(blockchainForm.managedProperty());
|
||||
|
||||
signingWalletForm.managedProperty().bind(signingWalletForm.visibleProperty());
|
||||
sigHashForm.managedProperty().bind(sigHashForm.visibleProperty());
|
||||
|
|
@ -636,24 +648,27 @@ public class HeadersController extends TransactionFormController implements Init
|
|||
}
|
||||
|
||||
List<Payment> payments = new ArrayList<>();
|
||||
List<WalletTransaction.Output> outputs = new ArrayList<>();
|
||||
Map<WalletNode, Long> changeMap = new LinkedHashMap<>();
|
||||
Map<Script, WalletNode> receiveOutputScripts = wallet.getWalletOutputScripts(KeyPurpose.RECEIVE);
|
||||
Map<Script, WalletNode> changeOutputScripts = wallet.getWalletOutputScripts(wallet.getChangeKeyPurpose());
|
||||
for(TransactionOutput txOutput : headersForm.getTransaction().getOutputs()) {
|
||||
WalletNode changeNode = changeOutputScripts.get(txOutput.getScript());
|
||||
if(changeNode != null) {
|
||||
if(headersForm.getTransaction().getOutputs().size() == 4 && headersForm.getTransaction().getOutputs().stream().anyMatch(txo -> txo != txOutput && txo.getValue() == txOutput.getValue())) {
|
||||
if(selectedTxos.values().stream().allMatch(Objects::nonNull)) {
|
||||
payments.add(new Payment(txOutput.getScript().getToAddress(), ".." + changeNode + " (Fake Mix)", txOutput.getValue(), false, Payment.Type.FAKE_MIX));
|
||||
payments.add(new WalletNodePayment(changeNode, ".." + changeNode + " (Fake Mix)", txOutput.getValue(), false, Payment.Type.FAKE_MIX));
|
||||
} else {
|
||||
payments.add(new Payment(txOutput.getScript().getToAddress(), ".." + changeNode + " (Mix)", txOutput.getValue(), false, Payment.Type.MIX));
|
||||
payments.add(new WalletNodePayment(changeNode, ".." + changeNode + " (Mix)", txOutput.getValue(), false, Payment.Type.MIX));
|
||||
}
|
||||
} else {
|
||||
if(changeMap.containsKey(changeNode)) {
|
||||
payments.add(new Payment(txOutput.getScript().getToAddress(), headersForm.getName(), txOutput.getValue(), false, Payment.Type.DEFAULT));
|
||||
payments.add(new WalletNodePayment(changeNode, headersForm.getName(), txOutput.getValue(), false, Payment.Type.DEFAULT));
|
||||
} else {
|
||||
changeMap.put(changeNode, txOutput.getValue());
|
||||
}
|
||||
}
|
||||
outputs.add(new WalletTransaction.ChangeOutput(txOutput, changeNode, txOutput.getValue()));
|
||||
} else {
|
||||
Payment.Type paymentType = Payment.Type.DEFAULT;
|
||||
Wallet masterWallet = wallet.isMasterWallet() ? wallet : wallet.getMasterWallet();
|
||||
|
|
@ -664,24 +679,44 @@ public class HeadersController extends TransactionFormController implements Init
|
|||
|
||||
BlockTransactionHashIndex receivedTxo = walletTxos.keySet().stream().filter(txo -> txo.getHash().equals(txOutput.getHash()) && txo.getIndex() == txOutput.getIndex()).findFirst().orElse(null);
|
||||
String label = headersForm.getName() == null || (headersForm.getName().startsWith("[") && headersForm.getName().endsWith("]") && headersForm.getName().length() == 8) ? null : headersForm.getName();
|
||||
try {
|
||||
Payment payment = new Payment(txOutput.getScript().getToAddresses()[0], receivedTxo != null ? receivedTxo.getLabel() : label, txOutput.getValue(), false, paymentType);
|
||||
Address address = txOutput.getScript().getToAddress();
|
||||
WalletNode receiveNode = receiveOutputScripts.get(txOutput.getScript());
|
||||
SilentPaymentAddress silentPaymentAddress = headersForm.getSilentPaymentAddress(txOutput);
|
||||
label = receivedTxo != null ? receivedTxo.getLabel() : label;
|
||||
if(address != null || silentPaymentAddress != null) {
|
||||
Payment payment;
|
||||
if(silentPaymentAddress != null) {
|
||||
payment = new SilentPayment(silentPaymentAddress, address, label, txOutput.getValue(), false);
|
||||
} else if(receiveNode != null) {
|
||||
payment = new WalletNodePayment(receiveNode, label, txOutput.getValue(), false, paymentType);
|
||||
} else {
|
||||
payment = new Payment(address, label, txOutput.getValue(), false, paymentType);
|
||||
}
|
||||
WalletTransaction createdTx = AppServices.get().getCreatedTransaction(selectedTxos.keySet());
|
||||
if(createdTx != null) {
|
||||
Optional<String> optLabel = createdTx.getPayments().stream().filter(pymt -> pymt.getAddress().equals(payment.getAddress()) && pymt.getAmount() == payment.getAmount()).map(Payment::getLabel).findFirst();
|
||||
Optional<String> optLabel = createdTx.getPayments().stream()
|
||||
.filter(pymt -> (pymt instanceof SilentPayment silentPayment ? silentPayment.getSilentPaymentAddress().equals(silentPaymentAddress) :
|
||||
pymt.getAddress().equals(payment.getAddress())) && pymt.getAmount() == payment.getAmount()).map(Payment::getLabel).findFirst();
|
||||
if(optLabel.isPresent()) {
|
||||
payment.setLabel(optLabel.get());
|
||||
outputIndexLabels.put(txOutput.getIndex(), optLabel.get());
|
||||
}
|
||||
}
|
||||
payments.add(payment);
|
||||
} catch(Exception e) {
|
||||
//ignore
|
||||
if(payment instanceof SilentPayment silentPayment) {
|
||||
outputs.add(new WalletTransaction.SilentPaymentOutput(txOutput, silentPayment));
|
||||
} else if(payment instanceof WalletNodePayment walletNodePayment) {
|
||||
outputs.add(new WalletTransaction.ConsolidationOutput(txOutput, walletNodePayment, walletNodePayment.getAmount()));
|
||||
} else {
|
||||
outputs.add(new WalletTransaction.PaymentOutput(txOutput, payment));
|
||||
}
|
||||
} else {
|
||||
outputs.add(new WalletTransaction.NonAddressOutput(txOutput));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return new WalletTransaction(wallet, headersForm.getTransaction(), Collections.emptyList(), List.of(selectedTxos), payments, changeMap, fee.getValue(), walletInputTransactions);
|
||||
return new WalletTransaction(wallet, headersForm.getTransaction(), Collections.emptyList(), List.of(selectedTxos), payments, outputs, changeMap, fee.getValue(), walletInputTransactions);
|
||||
} else {
|
||||
Map<BlockTransactionHashIndex, WalletNode> selectedTxos = headersForm.getTransaction().getInputs().stream()
|
||||
.collect(Collectors.toMap(txInput -> getBlockTransactionInput(inputTransactions, txInput),
|
||||
|
|
@ -691,16 +726,25 @@ public class HeadersController extends TransactionFormController implements Init
|
|||
selectedTxos.entrySet().forEach(entry -> entry.setValue(null));
|
||||
|
||||
List<Payment> payments = new ArrayList<>();
|
||||
List<WalletTransaction.Output> outputs = new ArrayList<>();
|
||||
for(TransactionOutput txOutput : headersForm.getTransaction().getOutputs()) {
|
||||
try {
|
||||
Address address = txOutput.getScript().getToAddress();
|
||||
SilentPaymentAddress silentPaymentAddress = headersForm.getSilentPaymentAddress(txOutput);
|
||||
BlockTransactionHashIndex receivedTxo = getBlockTransactionOutput(txOutput);
|
||||
payments.add(new Payment(txOutput.getScript().getToAddresses()[0], receivedTxo != null ? receivedTxo.getLabel() : null, txOutput.getValue(), false));
|
||||
} catch(Exception e) {
|
||||
//ignore
|
||||
String label = receivedTxo != null ? receivedTxo.getLabel() : null;
|
||||
if(address != null || silentPaymentAddress != null) {
|
||||
Payment payment = (silentPaymentAddress == null ?
|
||||
new Payment(address, label, txOutput.getValue(), false) :
|
||||
new SilentPayment(silentPaymentAddress, address, label, txOutput.getValue(), false));
|
||||
payments.add(payment);
|
||||
outputs.add(payment instanceof SilentPayment silentPayment ? new WalletTransaction.SilentPaymentOutput(txOutput, silentPayment) :
|
||||
new WalletTransaction.PaymentOutput(txOutput, payment));
|
||||
} else {
|
||||
outputs.add(new WalletTransaction.NonAddressOutput(txOutput));
|
||||
}
|
||||
}
|
||||
|
||||
return new WalletTransaction(null, headersForm.getTransaction(), Collections.emptyList(), List.of(selectedTxos), payments, Collections.emptyMap(), fee.getValue(), inputTransactions);
|
||||
return new WalletTransaction(null, headersForm.getTransaction(), Collections.emptyList(), List.of(selectedTxos), payments, outputs, Collections.emptyMap(), fee.getValue(), inputTransactions);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -774,6 +818,7 @@ public class HeadersController extends TransactionFormController implements Init
|
|||
|
||||
blockHeightField.managedProperty().bind(blockHeightField.visibleProperty());
|
||||
blockTimestampField.managedProperty().bind(blockTimestampField.visibleProperty());
|
||||
signedByField.managedProperty().bind(signedByField.visibleProperty());
|
||||
|
||||
if(blockTransaction.getHeight() > 0) {
|
||||
blockHeightField.setVisible(true);
|
||||
|
|
@ -791,6 +836,19 @@ public class HeadersController extends TransactionFormController implements Init
|
|||
} else {
|
||||
blockTimestampField.setVisible(false);
|
||||
}
|
||||
|
||||
if(headersForm.getWalletTransaction() != null && headersForm.getWalletTransaction().getWallet() != null
|
||||
&& headersForm.getWalletTransaction().getWallet().getPolicyType() == PolicyType.MULTI
|
||||
&& headersForm.getWalletTransaction().getWallet().getDefaultPolicy().getNumSignaturesRequired() < headersForm.getWalletTransaction().getWallet().getKeystores().size()) {
|
||||
signedByField.setVisible(true);
|
||||
Wallet wallet = headersForm.getWalletTransaction().getWallet();
|
||||
Map<TransactionInput, Map<TransactionSignature, Keystore>> signedKeystores = wallet.getSignedKeystores(blockTransaction.getTransaction());
|
||||
StringJoiner joiner = new StringJoiner(", ");
|
||||
signedKeystores.values().stream().flatMap(map -> map.values().stream()).distinct().forEach(keystore -> joiner.add(keystore.getLabel()));
|
||||
signedBy.setText(joiner.toString());
|
||||
} else {
|
||||
signedByField.setVisible(false);
|
||||
}
|
||||
}
|
||||
|
||||
private void initializeSignButton(Wallet signingWallet) {
|
||||
|
|
@ -927,7 +985,7 @@ public class HeadersController extends TransactionFormController implements Init
|
|||
|
||||
//Don't include non witness utxo fields for segwit wallets when displaying the PSBT as a QR - it can add greatly to the time required for scanning
|
||||
boolean includeNonWitnessUtxos = !Arrays.asList(ScriptType.WITNESS_TYPES).contains(headersForm.getSigningWallet().getScriptType());
|
||||
byte[] psbtBytes = headersForm.getPsbt().serialize(true, includeNonWitnessUtxos);
|
||||
byte[] psbtBytes = headersForm.getPsbt().getForExport().serialize(true, includeNonWitnessUtxos);
|
||||
|
||||
CryptoPSBT cryptoPSBT = new CryptoPSBT(psbtBytes);
|
||||
BBQR bbqr = addBbqrOption ? new BBQR(BBQRType.PSBT, psbtBytes) : null;
|
||||
|
|
@ -1010,7 +1068,7 @@ public class HeadersController extends TransactionFormController implements Init
|
|||
}
|
||||
|
||||
try(FileOutputStream outputStream = new FileOutputStream(file)) {
|
||||
outputStream.write(headersForm.getPsbt().serialize());
|
||||
outputStream.write(headersForm.getPsbt().getForExport().serialize());
|
||||
} catch(IOException e) {
|
||||
log.error("Error saving PSBT", e);
|
||||
AppServices.showErrorDialog("Error saving PSBT", "Cannot write to " + file.getAbsolutePath());
|
||||
|
|
@ -1067,7 +1125,12 @@ public class HeadersController extends TransactionFormController implements Init
|
|||
|
||||
private void signUnencryptedKeystores(Wallet unencryptedWallet) {
|
||||
try {
|
||||
unencryptedWallet.sign(headersForm.getPsbt());
|
||||
Map<PSBTInput, WalletNode> signingNodes = unencryptedWallet.getSigningNodes(headersForm.getPsbt());
|
||||
List<SilentPayment> silentPayments = unencryptedWallet.computeSilentPaymentOutputs(headersForm.getPsbt(), signingNodes);
|
||||
if(!silentPayments.isEmpty()) {
|
||||
EventManager.get().post(new TransactionOutputsChangedEvent(headersForm.getTransaction()));
|
||||
}
|
||||
unencryptedWallet.sign(signingNodes);
|
||||
updateSignedKeystores(headersForm.getSigningWallet());
|
||||
} catch(Exception e) {
|
||||
log.warn("Failed to Sign", e);
|
||||
|
|
@ -1139,7 +1202,7 @@ public class HeadersController extends TransactionFormController implements Init
|
|||
|
||||
if(fee.getValue() > 0) {
|
||||
double feeRateAmt = fee.getValue() / headersForm.getTransaction().getVirtualSize();
|
||||
if(feeRateAmt > AppServices.LONG_FEE_RATES_RANGE.get(AppServices.LONG_FEE_RATES_RANGE.size() - 1)) {
|
||||
if(feeRateAmt > AppServices.getLongFeeRatesRange().getLast()) {
|
||||
Optional<ButtonType> optType = AppServices.showWarningDialog("Very high fee rate!",
|
||||
"This transaction pays a very high fee rate of " + String.format("%.0f", feeRateAmt) + " sats/vB.\n\nBroadcast this transaction?", ButtonType.YES, ButtonType.NO);
|
||||
if(optType.isPresent() && optType.get() == ButtonType.NO) {
|
||||
|
|
@ -1225,9 +1288,17 @@ public class HeadersController extends TransactionFormController implements Init
|
|||
|
||||
UnitFormat format = Config.get().getUnitFormat() == null ? UnitFormat.DOT : Config.get().getUnitFormat();
|
||||
if(failMessage.startsWith("min relay fee not met")) {
|
||||
AppServices.showErrorDialog("Error broadcasting transaction", "The fee rate for the signed transaction is below the minimum " + format.getCurrencyFormat().format(AppServices.getMinimumRelayFeeRate()) + " sats/vB. " +
|
||||
if(AppServices.getServerMinimumRelayFeeRate() != null && !AppServices.getServerMinimumRelayFeeRate().equals(AppServices.getMinimumRelayFeeRate())) {
|
||||
AppServices.showErrorDialog("Error broadcasting transaction", "The fee rate for the signed transaction is below the minimum configured relay fee rate for the server of " +
|
||||
format.getCurrencyFormat().format(AppServices.getServerMinimumRelayFeeRate()) + " sats/vB.");
|
||||
} else {
|
||||
Double minRelayFeeRate = AppServices.getServerMinimumRelayFeeRate() != null ? AppServices.getServerMinimumRelayFeeRate() : AppServices.getMinimumRelayFeeRate();
|
||||
AppServices.showErrorDialog("Error broadcasting transaction", "The fee rate for the signed transaction is below the minimum " + format.getCurrencyFormat().format(minRelayFeeRate) + " sats/vB. " +
|
||||
"This usually happens because a keystore has created a signature that is larger than necessary.\n\n" +
|
||||
"You can solve this by recreating the transaction with a slightly increased fee rate.");
|
||||
}
|
||||
} else if(failMessage.startsWith("dust")) {
|
||||
AppServices.showErrorDialog("Error broadcasting transaction", "The server will not accept this transaction for broadcast due to its configured dust limit policy.");
|
||||
} else if(failMessage.startsWith("bad-txns-inputs-missingorspent")) {
|
||||
AppServices.showErrorDialog("Error broadcasting transaction", "The server returned an error indicating some or all of the UTXOs this transaction is spending are missing or have already been spent.");
|
||||
} else if(failMessage.contains("mempool min fee not met")) {
|
||||
|
|
@ -1428,6 +1499,7 @@ public class HeadersController extends TransactionFormController implements Init
|
|||
errorGlyph.getStyleClass().add("failure");
|
||||
blockHeightField.setVisible(false);
|
||||
blockTimestampField.setVisible(false);
|
||||
signedByField.setVisible(false);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1559,6 +1631,23 @@ public class HeadersController extends TransactionFormController implements Init
|
|||
|
||||
signButtonBox.setVisible(false);
|
||||
broadcastButtonBox.setVisible(true);
|
||||
|
||||
if(Config.get().hasServer() && !AppServices.isConnected() && !AppServices.isConnecting()) {
|
||||
if(Config.get().getConnectToBroadcast() == null) {
|
||||
Platform.runLater(() -> {
|
||||
ConfirmationAlert confirmationAlert = new ConfirmationAlert("Connect to broadcast?", "Connect to the configured server to broadcast the transaction?", ButtonType.NO, ButtonType.YES);
|
||||
Optional<ButtonType> optType = confirmationAlert.showAndWait();
|
||||
if(confirmationAlert.isDontAskAgain() && optType.isPresent()) {
|
||||
Config.get().setConnectToBroadcast(optType.get() == ButtonType.YES);
|
||||
}
|
||||
if(optType.isPresent() && optType.get() == ButtonType.YES) {
|
||||
EventManager.get().post(new RequestConnectEvent());
|
||||
}
|
||||
});
|
||||
} else if(Config.get().getConnectToBroadcast()) {
|
||||
Platform.runLater(() -> EventManager.get().post(new RequestConnectEvent()));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1570,6 +1659,13 @@ public class HeadersController extends TransactionFormController implements Init
|
|||
}
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void transactionOutputsChanged(TransactionOutputsChangedEvent event) {
|
||||
if(event.getTransaction().equals(headersForm.getTransaction())) {
|
||||
headersForm.setWalletTransaction(getWalletTransaction(headersForm.getInputTransactions()));
|
||||
}
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void transactionExtracted(TransactionExtractedEvent event) {
|
||||
if(event.getPsbt().equals(headersForm.getPsbt())) {
|
||||
|
|
|
|||
|
|
@ -337,7 +337,7 @@ public class InputController extends TransactionFormController implements Initia
|
|||
}
|
||||
} else {
|
||||
if(txInput.isAbsoluteTimeLocked()) {
|
||||
txInput.setSequenceNumber(TransactionInput.SEQUENCE_LOCKTIME_DISABLED - 1);
|
||||
txInput.setSequenceNumber(TransactionInput.SEQUENCE_RBF_DISABLED);
|
||||
if(oldValue != null) {
|
||||
EventManager.get().post(new TransactionChangedEvent(transaction));
|
||||
}
|
||||
|
|
@ -389,7 +389,7 @@ public class InputController extends TransactionFormController implements Initia
|
|||
if(rbf.selectedProperty().getValue()) {
|
||||
txInput.setSequenceNumber(TransactionInput.SEQUENCE_RBF_ENABLED);
|
||||
} else {
|
||||
txInput.setSequenceNumber(TransactionInput.SEQUENCE_LOCKTIME_DISABLED - 1);
|
||||
txInput.setSequenceNumber(TransactionInput.SEQUENCE_RBF_DISABLED);
|
||||
}
|
||||
if(old_toggle != null) {
|
||||
EventManager.get().post(new TransactionChangedEvent(transaction));
|
||||
|
|
|
|||
|
|
@ -5,14 +5,12 @@ import com.sparrowwallet.drongo.address.Address;
|
|||
import com.sparrowwallet.drongo.protocol.NonStandardScriptException;
|
||||
import com.sparrowwallet.drongo.protocol.TransactionInput;
|
||||
import com.sparrowwallet.drongo.protocol.TransactionOutput;
|
||||
import com.sparrowwallet.drongo.silentpayments.SilentPaymentAddress;
|
||||
import com.sparrowwallet.drongo.wallet.*;
|
||||
import com.sparrowwallet.sparrow.AppServices;
|
||||
import com.sparrowwallet.sparrow.EventManager;
|
||||
import com.sparrowwallet.sparrow.control.*;
|
||||
import com.sparrowwallet.sparrow.event.PSBTReorderedEvent;
|
||||
import com.sparrowwallet.sparrow.event.UnitFormatChangedEvent;
|
||||
import com.sparrowwallet.sparrow.event.BlockTransactionOutputsFetchedEvent;
|
||||
import com.sparrowwallet.sparrow.event.ViewTransactionEvent;
|
||||
import com.sparrowwallet.sparrow.event.*;
|
||||
import com.sparrowwallet.sparrow.net.ElectrumServer;
|
||||
import javafx.fxml.FXML;
|
||||
import javafx.fxml.Initializable;
|
||||
|
|
@ -70,20 +68,7 @@ public class OutputController extends TransactionFormController implements Initi
|
|||
updateOutputLegendFromWallet(txOutput, walletTransaction != null ? walletTransaction.getWallet() : null);
|
||||
});
|
||||
updateOutputLegendFromWallet(txOutput, outputForm.getWallet());
|
||||
|
||||
value.setValue(txOutput.getValue());
|
||||
to.setVisible(false);
|
||||
try {
|
||||
Address[] addresses = txOutput.getScript().getToAddresses();
|
||||
to.setVisible(true);
|
||||
if(addresses.length == 1) {
|
||||
address.setAddress(addresses[0]);
|
||||
} else {
|
||||
address.setText("multiple addresses");
|
||||
}
|
||||
} catch(NonStandardScriptException e) {
|
||||
//ignore
|
||||
}
|
||||
updateSends(txOutput);
|
||||
|
||||
spentField.managedProperty().bind(spentField.visibleProperty());
|
||||
spentByField.managedProperty().bind(spentByField.visibleProperty());
|
||||
|
|
@ -98,6 +83,32 @@ public class OutputController extends TransactionFormController implements Initi
|
|||
}
|
||||
|
||||
initializeScriptField(scriptPubKeyArea);
|
||||
updateScriptPubKey(txOutput);
|
||||
}
|
||||
|
||||
private void updateSends(TransactionOutput txOutput) {
|
||||
value.setValue(txOutput.getValue());
|
||||
to.setVisible(false);
|
||||
Address toAddress = txOutput.getScript().getToAddress();
|
||||
SilentPaymentAddress silentPaymentAddress = outputForm.getSilentPaymentAddress(txOutput);
|
||||
if(toAddress != null) {
|
||||
to.setVisible(true);
|
||||
address.setAddress(toAddress);
|
||||
} else if(silentPaymentAddress != null) {
|
||||
to.setVisible(true);
|
||||
address.setText(silentPaymentAddress.toAbbreviatedString());
|
||||
} else {
|
||||
try {
|
||||
txOutput.getScript().getToAddresses();
|
||||
to.setVisible(true);
|
||||
address.setText("multiple addresses");
|
||||
} catch(NonStandardScriptException e) {
|
||||
//ignore
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void updateScriptPubKey(TransactionOutput txOutput) {
|
||||
scriptPubKeyArea.clear();
|
||||
scriptPubKeyArea.appendScript(txOutput.getScript(), null, null);
|
||||
}
|
||||
|
|
@ -115,11 +126,14 @@ public class OutputController extends TransactionFormController implements Initi
|
|||
WalletTransaction.Output output = outputs.get(outputForm.getIndex());
|
||||
if(output instanceof WalletTransaction.NonAddressOutput) {
|
||||
outputFieldset.setText(baseText);
|
||||
} else if(output instanceof WalletTransaction.SilentPaymentOutput) {
|
||||
outputFieldset.setText(baseText + " - Silent Payment");
|
||||
} else if(output instanceof WalletTransaction.ConsolidationOutput) {
|
||||
outputFieldset.setText(baseText + " - Consolidation");
|
||||
} else if(output instanceof WalletTransaction.PaymentOutput paymentOutput) {
|
||||
Payment payment = paymentOutput.getPayment();
|
||||
Wallet toWallet = walletTx.getToWallet(AppServices.get().getOpenWallets().keySet(), payment);
|
||||
WalletNode toNode = walletTx.getWallet() != null && !walletTx.getWallet().isBip47() ? walletTx.getAddressNodeMap().get(payment.getAddress()) : null;
|
||||
outputFieldset.setText(baseText + (toWallet == null ? (toNode != null ? " - Consolidation" : " - Payment") : " - Received to " + toWallet.getFullDisplayName()));
|
||||
outputFieldset.setText(baseText + (toWallet == null ? " - Payment" : " - Received to " + toWallet.getFullDisplayName()));
|
||||
} else if(output instanceof WalletTransaction.ChangeOutput changeOutput) {
|
||||
outputFieldset.setText(baseText + " - Change to " + changeOutput.getWalletNode().toString());
|
||||
} else {
|
||||
|
|
@ -206,4 +220,12 @@ public class OutputController extends TransactionFormController implements Initi
|
|||
updateOutputLegendFromWallet(outputForm.getTransactionOutput(), null);
|
||||
}
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void transactionOutputsChanged(TransactionOutputsChangedEvent event) {
|
||||
if(event.getTransaction().equals(outputForm.getTransaction())) {
|
||||
updateSends(outputForm.getTransactionOutput());
|
||||
updateScriptPubKey(outputForm.getTransactionOutput());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -89,7 +89,11 @@ public class OutputForm extends IndexedTransactionForm {
|
|||
}
|
||||
} else if(output instanceof WalletTransaction.PaymentOutput paymentOutput) {
|
||||
Payment payment = paymentOutput.getPayment();
|
||||
return new Label(payment.getLabel() != null && payment.getType() != Payment.Type.FAKE_MIX && payment.getType() != Payment.Type.MIX ? payment.getLabel() : payment.getAddress().toString(),
|
||||
return new Label(payment.getLabel() != null && payment.getType() != Payment.Type.FAKE_MIX && payment.getType() != Payment.Type.MIX ? payment.getLabel() : payment.toString(),
|
||||
GlyphUtils.getOutputGlyph(getWalletTransaction(), payment));
|
||||
} else if(output instanceof WalletTransaction.ConsolidationOutput consolidationOutput) {
|
||||
Payment payment = consolidationOutput.getWalletNodePayment();
|
||||
return new Label(payment.getLabel() != null && payment.getType() != Payment.Type.FAKE_MIX && payment.getType() != Payment.Type.MIX ? payment.getLabel() : payment.toString(),
|
||||
GlyphUtils.getOutputGlyph(getWalletTransaction(), payment));
|
||||
} else if(output instanceof WalletTransaction.ChangeOutput changeOutput) {
|
||||
return new Label("Change", GlyphUtils.getChangeGlyph());
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import com.sparrowwallet.drongo.protocol.TransactionOutput;
|
|||
import com.sparrowwallet.sparrow.EventManager;
|
||||
import com.sparrowwallet.sparrow.control.CopyableCoinLabel;
|
||||
import com.sparrowwallet.sparrow.control.CopyableLabel;
|
||||
import com.sparrowwallet.sparrow.event.TransactionOutputsChangedEvent;
|
||||
import com.sparrowwallet.sparrow.event.UnitFormatChangedEvent;
|
||||
import javafx.fxml.FXML;
|
||||
import javafx.fxml.Initializable;
|
||||
|
|
@ -60,4 +61,11 @@ public class OutputsController extends TransactionFormController implements Init
|
|||
public void unitFormatChanged(UnitFormatChangedEvent event) {
|
||||
total.refresh(event.getUnitFormat(), event.getBitcoinUnit());
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void transactionOutputsChanged(TransactionOutputsChangedEvent event) {
|
||||
if(event.getTransaction().equals(outputsForm.getTransaction())) {
|
||||
updatePieData(outputsPie, outputsForm.getTransaction().getOutputs());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,8 @@ package com.sparrowwallet.sparrow.transaction;
|
|||
import com.sparrowwallet.drongo.KeyPurpose;
|
||||
import com.sparrowwallet.drongo.protocol.*;
|
||||
import com.sparrowwallet.drongo.psbt.PSBT;
|
||||
import com.sparrowwallet.drongo.psbt.PSBTOutput;
|
||||
import com.sparrowwallet.drongo.silentpayments.SilentPaymentAddress;
|
||||
import com.sparrowwallet.drongo.wallet.*;
|
||||
import com.sparrowwallet.sparrow.io.Storage;
|
||||
import javafx.beans.property.SimpleObjectProperty;
|
||||
|
|
@ -193,4 +195,16 @@ public class TransactionData {
|
|||
public Wallet getWallet() {
|
||||
return getSigningWallet() != null ? getSigningWallet() : (getWalletTransaction() != null ? getWalletTransaction().getWallet() : null);
|
||||
}
|
||||
|
||||
protected SilentPaymentAddress getSilentPaymentAddress(TransactionOutput txOutput) {
|
||||
if(getPsbt() != null && txOutput.getParent() != null) {
|
||||
for(PSBTOutput psbtOutput : getPsbt().getPsbtOutputs()) {
|
||||
if(psbtOutput.getOutput().getIndex() == txOutput.getIndex() && psbtOutput.getSilentPaymentAddress() != null) {
|
||||
return psbtOutput.getSilentPaymentAddress();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,8 +2,10 @@ package com.sparrowwallet.sparrow.transaction;
|
|||
|
||||
import com.sparrowwallet.drongo.protocol.Sha256Hash;
|
||||
import com.sparrowwallet.drongo.protocol.Transaction;
|
||||
import com.sparrowwallet.drongo.protocol.TransactionOutput;
|
||||
import com.sparrowwallet.drongo.protocol.TransactionSignature;
|
||||
import com.sparrowwallet.drongo.psbt.PSBT;
|
||||
import com.sparrowwallet.drongo.silentpayments.SilentPaymentAddress;
|
||||
import com.sparrowwallet.drongo.wallet.*;
|
||||
import com.sparrowwallet.sparrow.io.Storage;
|
||||
import javafx.beans.property.SimpleObjectProperty;
|
||||
|
|
@ -112,6 +114,10 @@ public abstract class TransactionForm {
|
|||
return txdata.getWallet();
|
||||
}
|
||||
|
||||
public SilentPaymentAddress getSilentPaymentAddress(TransactionOutput output) {
|
||||
return txdata.getSilentPaymentAddress(output);
|
||||
}
|
||||
|
||||
public boolean isEditable() {
|
||||
if(getBlockTransaction() != null) {
|
||||
return false;
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import com.sparrowwallet.drongo.BitcoinUnit;
|
|||
import com.sparrowwallet.drongo.address.Address;
|
||||
import com.sparrowwallet.drongo.protocol.NonStandardScriptException;
|
||||
import com.sparrowwallet.drongo.protocol.TransactionOutput;
|
||||
import com.sparrowwallet.drongo.silentpayments.SilentPaymentAddress;
|
||||
import com.sparrowwallet.sparrow.UnitFormat;
|
||||
import com.sparrowwallet.sparrow.BaseController;
|
||||
import com.sparrowwallet.sparrow.EventManager;
|
||||
|
|
@ -33,17 +34,7 @@ public abstract class TransactionFormController extends BaseController {
|
|||
long totalAmt = 0;
|
||||
for(int i = 0; i < outputs.size(); i++) {
|
||||
TransactionOutput output = outputs.get(i);
|
||||
String name = "#" + i;
|
||||
try {
|
||||
Address[] addresses = output.getScript().getToAddresses();
|
||||
if(addresses.length == 1) {
|
||||
name = name + " " + addresses[0].getAddress();
|
||||
} else {
|
||||
name = name + " [" + addresses[0].getAddress() + ",...]";
|
||||
}
|
||||
} catch(NonStandardScriptException e) {
|
||||
//ignore
|
||||
}
|
||||
String name = getPieDataName(i, output);
|
||||
|
||||
totalAmt += output.getValue();
|
||||
outputsPieData.add(new PieChart.Data(name, output.getValue()));
|
||||
|
|
@ -52,6 +43,34 @@ public abstract class TransactionFormController extends BaseController {
|
|||
addPieData(pie, outputsPieData);
|
||||
}
|
||||
|
||||
protected void updatePieData(PieChart pie, List<TransactionOutput> outputs) {
|
||||
for(int i = 0; i < outputs.size(); i++) {
|
||||
TransactionOutput output = outputs.get(i);
|
||||
String name = getPieDataName(i, output);
|
||||
pie.getData().get(i).setName(name);
|
||||
}
|
||||
}
|
||||
|
||||
private String getPieDataName(int i, TransactionOutput output) {
|
||||
String name = "#" + i;
|
||||
Address address = output.getScript().getToAddress();
|
||||
SilentPaymentAddress silentPaymentAddress = getTransactionForm().getSilentPaymentAddress(output);
|
||||
if(address != null) {
|
||||
name = name + " " + address.getAddress();
|
||||
} else if(silentPaymentAddress != null) {
|
||||
name = name + " " + silentPaymentAddress.toAbbreviatedString();
|
||||
} else {
|
||||
try {
|
||||
Address[] addresses = output.getScript().getToAddresses();
|
||||
name = name + " [" + addresses[0].getAddress() + ",...]";
|
||||
} catch(NonStandardScriptException e) {
|
||||
//ignore
|
||||
}
|
||||
}
|
||||
|
||||
return name;
|
||||
}
|
||||
|
||||
protected void addCoinbasePieData(PieChart pie, long value) {
|
||||
ObservableList<PieChart.Data> outputsPieData = FXCollections.observableList(List.of(new PieChart.Data("Coinbase", value)));
|
||||
addPieData(pie, outputsPieData);
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
package com.sparrowwallet.sparrow.wallet;
|
||||
|
||||
import com.google.common.base.Throwables;
|
||||
import com.google.common.eventbus.Subscribe;
|
||||
import com.sparrowwallet.drongo.BitcoinUnit;
|
||||
import com.sparrowwallet.drongo.KeyPurpose;
|
||||
|
|
@ -9,15 +10,15 @@ import com.sparrowwallet.drongo.address.P2PKHAddress;
|
|||
import com.sparrowwallet.drongo.bip47.InvalidPaymentCodeException;
|
||||
import com.sparrowwallet.drongo.bip47.PaymentCode;
|
||||
import com.sparrowwallet.drongo.crypto.ECKey;
|
||||
import com.sparrowwallet.drongo.dns.DnsPaymentCache;
|
||||
import com.sparrowwallet.drongo.protocol.ScriptType;
|
||||
import com.sparrowwallet.drongo.protocol.Transaction;
|
||||
import com.sparrowwallet.drongo.protocol.TransactionOutput;
|
||||
import com.sparrowwallet.drongo.silentpayments.SilentPayment;
|
||||
import com.sparrowwallet.drongo.silentpayments.SilentPaymentAddress;
|
||||
import com.sparrowwallet.drongo.uri.BitcoinURI;
|
||||
import com.sparrowwallet.drongo.wallet.*;
|
||||
import com.sparrowwallet.sparrow.UnitFormat;
|
||||
import com.sparrowwallet.sparrow.AppServices;
|
||||
import com.sparrowwallet.sparrow.CurrencyRate;
|
||||
import com.sparrowwallet.sparrow.EventManager;
|
||||
import com.sparrowwallet.sparrow.*;
|
||||
import com.sparrowwallet.sparrow.control.*;
|
||||
import com.sparrowwallet.sparrow.event.*;
|
||||
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
|
||||
|
|
@ -25,6 +26,8 @@ import com.sparrowwallet.sparrow.io.CardApi;
|
|||
import com.sparrowwallet.sparrow.io.Config;
|
||||
import com.sparrowwallet.sparrow.io.Storage;
|
||||
import com.sparrowwallet.sparrow.net.ExchangeSource;
|
||||
import com.sparrowwallet.drongo.dns.DnsPayment;
|
||||
import com.sparrowwallet.drongo.dns.DnsPaymentResolver;
|
||||
import com.sparrowwallet.sparrow.paynym.PayNym;
|
||||
import com.sparrowwallet.sparrow.paynym.PayNymDialog;
|
||||
import javafx.application.Platform;
|
||||
|
|
@ -36,21 +39,32 @@ import javafx.beans.value.ChangeListener;
|
|||
import javafx.beans.value.ObservableValue;
|
||||
import javafx.collections.FXCollections;
|
||||
import javafx.collections.ListChangeListener;
|
||||
import javafx.concurrent.Service;
|
||||
import javafx.concurrent.Task;
|
||||
import javafx.event.ActionEvent;
|
||||
import javafx.fxml.FXML;
|
||||
import javafx.fxml.Initializable;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.control.*;
|
||||
import javafx.scene.input.Clipboard;
|
||||
import javafx.scene.input.ClipboardContent;
|
||||
import javafx.scene.layout.HBox;
|
||||
import org.controlsfx.glyphfont.Glyph;
|
||||
import org.controlsfx.validation.ValidationResult;
|
||||
import org.controlsfx.validation.ValidationSupport;
|
||||
import org.controlsfx.validation.Validator;
|
||||
import org.girod.javafx.svgimage.SVGImage;
|
||||
import org.girod.javafx.svgimage.SVGLoader;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.net.URL;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.text.DecimalFormat;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.TimeoutException;
|
||||
import java.util.regex.Pattern;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
|
|
@ -129,8 +143,14 @@ public class PaymentController extends WalletFormController implements Initializ
|
|||
}
|
||||
};
|
||||
|
||||
private final ObjectProperty<WalletNode> consolidationNodeProperty = new SimpleObjectProperty<>();
|
||||
|
||||
private final ObjectProperty<PayNym> payNymProperty = new SimpleObjectProperty<>();
|
||||
|
||||
private final ObjectProperty<SilentPaymentAddress> silentPaymentAddressProperty = new SimpleObjectProperty<>();
|
||||
|
||||
private final ObjectProperty<DnsPayment> dnsPaymentProperty = new SimpleObjectProperty<>();
|
||||
|
||||
private static final Wallet payNymWallet = new Wallet() {
|
||||
@Override
|
||||
public String getFullDisplayName() {
|
||||
|
|
@ -145,6 +165,127 @@ public class PaymentController extends WalletFormController implements Initializ
|
|||
}
|
||||
};
|
||||
|
||||
private final ChangeListener<String> addressListener = new ChangeListener<>() {
|
||||
@Override
|
||||
public void changed(ObservableValue<? extends String> observable, String oldValue, String newValue) {
|
||||
address.leftProperty().set(null);
|
||||
|
||||
if(consolidationNodeProperty.get() != null && !newValue.equals(consolidationNodeProperty.get().getAddress().toString())) {
|
||||
consolidationNodeProperty.set(null);
|
||||
}
|
||||
|
||||
if(payNymProperty.get() != null && !newValue.equals(payNymProperty.get().nymName())) {
|
||||
payNymProperty.set(null);
|
||||
}
|
||||
|
||||
if(dnsPaymentProperty.get() != null && !newValue.equals(dnsPaymentProperty.get().hrn())) {
|
||||
dnsPaymentProperty.set(null);
|
||||
}
|
||||
|
||||
if(silentPaymentAddressProperty.get() != null && !newValue.equals(silentPaymentAddressProperty.get().getAddress())) {
|
||||
silentPaymentAddressProperty.set(null);
|
||||
}
|
||||
|
||||
try {
|
||||
BitcoinURI bitcoinURI = new BitcoinURI(newValue);
|
||||
Platform.runLater(() -> updateFromURI(bitcoinURI));
|
||||
return;
|
||||
} catch(Exception e) {
|
||||
//ignore, not a URI
|
||||
}
|
||||
|
||||
Optional<String> optDnsPaymentHrn = DnsPayment.getHrn(newValue);
|
||||
if(optDnsPaymentHrn.isPresent()) {
|
||||
String dnsPaymentHrn = optDnsPaymentHrn.get();
|
||||
DnsPayment cachedDnsPayment = DnsPaymentCache.getDnsPayment(dnsPaymentHrn);
|
||||
if(cachedDnsPayment != null) {
|
||||
setDnsPayment(cachedDnsPayment);
|
||||
return;
|
||||
}
|
||||
|
||||
if(Config.get().hasServer() && !AppServices.isConnected() && !AppServices.isConnecting()) {
|
||||
if(Config.get().getConnectToResolve() == null || Config.get().getConnectToResolve() == Boolean.FALSE) {
|
||||
Platform.runLater(() -> {
|
||||
ConfirmationAlert confirmationAlert = new ConfirmationAlert("Connect to resolve?", "You are currently offline. Connect to resolve the address?", ButtonType.NO, ButtonType.YES);
|
||||
Optional<ButtonType> optType = confirmationAlert.showAndWait();
|
||||
if(confirmationAlert.isDontAskAgain() && optType.isPresent()) {
|
||||
Config.get().setConnectToResolve(optType.get() == ButtonType.YES);
|
||||
}
|
||||
if(optType.isPresent() && optType.get() == ButtonType.YES) {
|
||||
EventManager.get().post(new RequestConnectEvent());
|
||||
}
|
||||
});
|
||||
} else {
|
||||
Platform.runLater(() -> EventManager.get().post(new RequestConnectEvent()));
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
DnsPaymentService dnsPaymentService = new DnsPaymentService(dnsPaymentHrn);
|
||||
dnsPaymentService.setOnSucceeded(_ -> dnsPaymentService.getValue().ifPresent(dnsPayment -> setDnsPayment(dnsPayment)));
|
||||
dnsPaymentService.setOnFailed(failEvent -> {
|
||||
if(failEvent.getSource().getException() != null && !(failEvent.getSource().getException().getCause() instanceof TimeoutException)) {
|
||||
AppServices.showErrorDialog("Validation failed for " + dnsPaymentHrn, Throwables.getRootCause(failEvent.getSource().getException()).getMessage());
|
||||
}
|
||||
});
|
||||
dnsPaymentService.start();
|
||||
return;
|
||||
}
|
||||
|
||||
if(sendController.getWalletForm().getWallet().hasPaymentCode()) {
|
||||
try {
|
||||
PaymentCode paymentCode = new PaymentCode(newValue);
|
||||
Wallet recipientBip47Wallet = sendController.getWalletForm().getWallet().getChildWallet(paymentCode, sendController.getWalletForm().getWallet().getScriptType());
|
||||
if(recipientBip47Wallet == null && sendController.getWalletForm().getWallet().getScriptType() != ScriptType.P2PKH) {
|
||||
recipientBip47Wallet = sendController.getWalletForm().getWallet().getChildWallet(paymentCode, ScriptType.P2PKH);
|
||||
}
|
||||
|
||||
if(recipientBip47Wallet != null) {
|
||||
PayNym payNym = PayNym.fromWallet(recipientBip47Wallet);
|
||||
Platform.runLater(() -> setPayNym(payNym));
|
||||
} else if(!paymentCode.equals(sendController.getWalletForm().getWallet().getPaymentCode())) {
|
||||
ButtonType previewType = new ButtonType("Preview Transaction", ButtonBar.ButtonData.YES);
|
||||
Optional<ButtonType> optButton = AppServices.showAlertDialog("Send notification transaction?", "This payment code is not yet linked with a notification transaction. Send a notification transaction?", Alert.AlertType.CONFIRMATION, ButtonType.CANCEL, previewType);
|
||||
if(optButton.isPresent() && optButton.get() == previewType) {
|
||||
Payment payment = new Payment(paymentCode.getNotificationAddress(), "Link " + paymentCode.toAbbreviatedString(), MINIMUM_P2PKH_OUTPUT_SATS, false);
|
||||
Platform.runLater(() -> EventManager.get().post(new SpendUtxoEvent(sendController.getWalletForm().getWallet(), List.of(payment), List.of(new byte[80]), paymentCode)));
|
||||
} else {
|
||||
Platform.runLater(() -> address.setText(""));
|
||||
}
|
||||
}
|
||||
} catch(Exception e) {
|
||||
//ignore, not a payment code
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
SilentPaymentAddress silentPaymentAddress = SilentPaymentAddress.from(newValue);
|
||||
setSilentPaymentAddress(silentPaymentAddress);
|
||||
} catch(Exception e) {
|
||||
//ignore, not a silent payment address
|
||||
}
|
||||
|
||||
try {
|
||||
Address toAddress = Address.fromString(newValue);
|
||||
WalletNode walletNode = sendController.getWalletNode(toAddress);
|
||||
if(walletNode != null) {
|
||||
consolidationNodeProperty.set(walletNode);
|
||||
}
|
||||
label.requestFocus();
|
||||
} catch(Exception e) {
|
||||
//ignore, not an address
|
||||
}
|
||||
|
||||
revalidateAmount();
|
||||
maxButton.setDisable(!isMaxButtonEnabled());
|
||||
sendController.updateTransaction();
|
||||
|
||||
if(validationSupport != null) {
|
||||
validationSupport.setErrorDecorationEnabled(true);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@Override
|
||||
public void initialize(URL location, ResourceBundle resources) {
|
||||
EventManager.get().register(this);
|
||||
|
|
@ -210,6 +351,28 @@ public class PaymentController extends WalletFormController implements Initializ
|
|||
revalidateAmount();
|
||||
});
|
||||
|
||||
silentPaymentAddressProperty.addListener((observable, oldValue, silentPaymentAddress) -> {
|
||||
revalidateAmount();
|
||||
});
|
||||
|
||||
dnsPaymentProperty.addListener((observable, oldValue, dnsPayment) -> {
|
||||
if(dnsPayment != null) {
|
||||
MenuItem copyMenuItem = new MenuItem("Copy URI");
|
||||
copyMenuItem.setOnAction(e -> {
|
||||
ClipboardContent content = new ClipboardContent();
|
||||
content.putString(dnsPayment.bitcoinURI().toURIString());
|
||||
Clipboard.getSystemClipboard().setContent(content);
|
||||
});
|
||||
address.setContextMenu(address.getCustomContextMenu(List.of(copyMenuItem)));
|
||||
} else {
|
||||
address.setContextMenu(address.getCustomContextMenu(Collections.emptyList()));
|
||||
}
|
||||
|
||||
revalidateAmount();
|
||||
maxButton.setDisable(!isMaxButtonEnabled());
|
||||
sendController.updateTransaction();
|
||||
});
|
||||
|
||||
address.setTextFormatter(new TextFormatter<>(change -> {
|
||||
String controlNewText = change.getControlNewText();
|
||||
if(!controlNewText.equals(controlNewText.trim())) {
|
||||
|
|
@ -222,55 +385,8 @@ public class PaymentController extends WalletFormController implements Initializ
|
|||
return change;
|
||||
}));
|
||||
|
||||
address.textProperty().addListener((observable, oldValue, newValue) -> {
|
||||
address.leftProperty().set(null);
|
||||
|
||||
if(payNymProperty.get() != null && !newValue.equals(payNymProperty.get().nymName())) {
|
||||
payNymProperty.set(null);
|
||||
}
|
||||
|
||||
try {
|
||||
BitcoinURI bitcoinURI = new BitcoinURI(newValue);
|
||||
Platform.runLater(() -> updateFromURI(bitcoinURI));
|
||||
return;
|
||||
} catch(Exception e) {
|
||||
//ignore, not a URI
|
||||
}
|
||||
|
||||
if(sendController.getWalletForm().getWallet().hasPaymentCode()) {
|
||||
try {
|
||||
PaymentCode paymentCode = new PaymentCode(newValue);
|
||||
Wallet recipientBip47Wallet = sendController.getWalletForm().getWallet().getChildWallet(paymentCode, sendController.getWalletForm().getWallet().getScriptType());
|
||||
if(recipientBip47Wallet == null && sendController.getWalletForm().getWallet().getScriptType() != ScriptType.P2PKH) {
|
||||
recipientBip47Wallet = sendController.getWalletForm().getWallet().getChildWallet(paymentCode, ScriptType.P2PKH);
|
||||
}
|
||||
|
||||
if(recipientBip47Wallet != null) {
|
||||
PayNym payNym = PayNym.fromWallet(recipientBip47Wallet);
|
||||
Platform.runLater(() -> setPayNym(payNym));
|
||||
} else if(!paymentCode.equals(sendController.getWalletForm().getWallet().getPaymentCode())) {
|
||||
ButtonType previewType = new ButtonType("Preview Transaction", ButtonBar.ButtonData.YES);
|
||||
Optional<ButtonType> optButton = AppServices.showAlertDialog("Send notification transaction?", "This payment code is not yet linked with a notification transaction. Send a notification transaction?", Alert.AlertType.CONFIRMATION, ButtonType.CANCEL, previewType);
|
||||
if(optButton.isPresent() && optButton.get() == previewType) {
|
||||
Payment payment = new Payment(paymentCode.getNotificationAddress(), "Link " + paymentCode.toAbbreviatedString(), MINIMUM_P2PKH_OUTPUT_SATS, false);
|
||||
Platform.runLater(() -> EventManager.get().post(new SpendUtxoEvent(sendController.getWalletForm().getWallet(), List.of(payment), List.of(new byte[80]), paymentCode)));
|
||||
} else {
|
||||
Platform.runLater(() -> address.setText(""));
|
||||
}
|
||||
}
|
||||
} catch(Exception e) {
|
||||
//ignore, not a payment code
|
||||
}
|
||||
}
|
||||
|
||||
revalidateAmount();
|
||||
maxButton.setDisable(!isMaxButtonEnabled());
|
||||
sendController.updateTransaction();
|
||||
|
||||
if(validationSupport != null) {
|
||||
validationSupport.setErrorDecorationEnabled(true);
|
||||
}
|
||||
});
|
||||
address.textProperty().addListener(addressListener);
|
||||
address.setContextMenu(address.getCustomContextMenu(Collections.emptyList()));
|
||||
|
||||
label.textProperty().addListener((observable, oldValue, newValue) -> {
|
||||
maxButton.setDisable(!isMaxButtonEnabled());
|
||||
|
|
@ -328,6 +444,37 @@ public class PaymentController extends WalletFormController implements Initializ
|
|||
}
|
||||
}
|
||||
|
||||
public void setDnsPayment(DnsPayment dnsPayment) {
|
||||
if(dnsPayment.hasAddress()) {
|
||||
DnsPaymentCache.putDnsPayment(dnsPayment.bitcoinURI().getAddress(), dnsPayment);
|
||||
} else if(dnsPayment.hasSilentPaymentAddress()) {
|
||||
DnsPaymentCache.putDnsPayment(dnsPayment.bitcoinURI().getSilentPaymentAddress(), dnsPayment);
|
||||
setSilentPaymentAddress(dnsPayment.bitcoinURI().getSilentPaymentAddress());
|
||||
} else {
|
||||
AppServices.showWarningDialog("No Address Provided", "The DNS payment instruction for " + dnsPayment.hrn() + " resolved correctly but did not contain a bitcoin address.");
|
||||
return;
|
||||
}
|
||||
|
||||
dnsPaymentProperty.set(dnsPayment);
|
||||
address.setText(dnsPayment.hrn());
|
||||
revalidate(address, addressListener);
|
||||
address.leftProperty().set(getBitcoinCharacter());
|
||||
if(label.getText().isEmpty() || (label.getText().startsWith("₿") && !label.getText().contains(" "))) {
|
||||
label.setText(dnsPayment.toString());
|
||||
}
|
||||
label.requestFocus();
|
||||
}
|
||||
|
||||
private void setSilentPaymentAddress(SilentPaymentAddress silentPaymentAddress) {
|
||||
if(!sendController.getWalletForm().getWallet().canSendSilentPayments()) {
|
||||
Platform.runLater(() -> AppServices.showErrorDialog("Silent Payments Unsupported", "This wallet does not support sending silent payments. Use a single signature software wallet."));
|
||||
return;
|
||||
}
|
||||
|
||||
silentPaymentAddressProperty.set(silentPaymentAddress);
|
||||
label.requestFocus();
|
||||
}
|
||||
|
||||
private void updateOpenWallets() {
|
||||
updateOpenWallets(AppServices.get().getOpenWallets().keySet());
|
||||
}
|
||||
|
|
@ -399,6 +546,16 @@ public class PaymentController extends WalletFormController implements Initializ
|
|||
}
|
||||
|
||||
private Address getRecipientAddress() throws InvalidAddressException {
|
||||
SilentPaymentAddress silentPaymentAddress = silentPaymentAddressProperty.get();
|
||||
if(silentPaymentAddress != null) {
|
||||
return SilentPayment.getDummyAddress();
|
||||
}
|
||||
|
||||
DnsPayment dnsPayment = dnsPaymentProperty.get();
|
||||
if(dnsPayment != null && dnsPayment.hasAddress()) {
|
||||
return dnsPayment.bitcoinURI().getAddress();
|
||||
}
|
||||
|
||||
PayNym payNym = payNymProperty.get();
|
||||
if(payNym == null) {
|
||||
return Address.fromString(address.getText());
|
||||
|
|
@ -516,7 +673,17 @@ public class PaymentController extends WalletFormController implements Initializ
|
|||
Long value = sendAll ? Long.valueOf(getRecipientDustThreshold() + 1) : getRecipientValueSats();
|
||||
|
||||
if(!label.getText().isEmpty() && value != null && value >= getRecipientDustThreshold()) {
|
||||
Payment payment = new Payment(recipientAddress, label.getText(), value, sendAll);
|
||||
Payment payment;
|
||||
SilentPaymentAddress silentPaymentAddress = silentPaymentAddressProperty.get();
|
||||
WalletNode consolidationNode = consolidationNodeProperty.get();
|
||||
if(silentPaymentAddress != null) {
|
||||
payment = new SilentPayment(silentPaymentAddress, label.getText(), value, sendAll);
|
||||
} else if(consolidationNode != null) {
|
||||
payment = new WalletNodePayment(consolidationNode, label.getText(), value, sendAll);
|
||||
} else {
|
||||
payment = new Payment(recipientAddress, label.getText(), value, sendAll);
|
||||
}
|
||||
|
||||
if(address.getUserData() != null) {
|
||||
payment.setType((Payment.Type)address.getUserData());
|
||||
}
|
||||
|
|
@ -533,7 +700,14 @@ public class PaymentController extends WalletFormController implements Initializ
|
|||
public void setPayment(Payment payment) {
|
||||
if(getRecipientValueSats() == null || payment.getAmount() != getRecipientValueSats()) {
|
||||
if(payment.getAddress() != null) {
|
||||
DnsPayment dnsPayment = DnsPaymentCache.getDnsPayment(payment);
|
||||
if(dnsPayment != null) {
|
||||
address.setText(dnsPayment.hrn());
|
||||
} else if(payment instanceof SilentPayment silentPayment) {
|
||||
address.setText(silentPayment.getSilentPaymentAddress().getAddress());
|
||||
} else {
|
||||
address.setText(payment.getAddress().toString());
|
||||
}
|
||||
address.setUserData(payment.getType());
|
||||
}
|
||||
if(payment.getLabel() != null && !label.getText().equals(payment.getLabel())) {
|
||||
|
|
@ -564,7 +738,10 @@ public class PaymentController extends WalletFormController implements Initializ
|
|||
setSendMax(false);
|
||||
|
||||
dustAmountProperty.set(false);
|
||||
consolidationNodeProperty.set(null);
|
||||
payNymProperty.set(null);
|
||||
dnsPaymentProperty.set(null);
|
||||
silentPaymentAddressProperty.set(null);
|
||||
}
|
||||
|
||||
public void setMaxInput(ActionEvent event) {
|
||||
|
|
@ -572,8 +749,7 @@ public class PaymentController extends WalletFormController implements Initializ
|
|||
if(utxoSelector == null) {
|
||||
MaxUtxoSelector maxUtxoSelector = new MaxUtxoSelector();
|
||||
sendController.utxoSelectorProperty().set(maxUtxoSelector);
|
||||
} else if(utxoSelector instanceof PresetUtxoSelector && !isValidAddressAndLabel() && sendController.getPaymentTabs().getTabs().size() == 1) {
|
||||
PresetUtxoSelector presetUtxoSelector = (PresetUtxoSelector)utxoSelector;
|
||||
} else if(utxoSelector instanceof PresetUtxoSelector presetUtxoSelector && !isValidAddressAndLabel() && sendController.getPaymentTabs().getTabs().size() == 1) {
|
||||
Payment payment = new Payment(null, null, presetUtxoSelector.getPresetUtxos().stream().mapToLong(BlockTransactionHashIndex::getValue).sum(), true);
|
||||
setPayment(payment);
|
||||
return;
|
||||
|
|
@ -625,7 +801,7 @@ public class PaymentController extends WalletFormController implements Initializ
|
|||
setRecipientValueSats(bitcoinURI.getAmount());
|
||||
setFiatAmount(AppServices.getFiatCurrencyExchangeRate(), bitcoinURI.getAmount());
|
||||
}
|
||||
if(bitcoinURI.getPayjoinUrl() != null) {
|
||||
if(bitcoinURI.getAddress() != null && bitcoinURI.getPayjoinUrl() != null) {
|
||||
AppServices.addPayjoinURI(bitcoinURI);
|
||||
}
|
||||
sendController.updateTransaction();
|
||||
|
|
@ -676,10 +852,33 @@ public class PaymentController extends WalletFormController implements Initializ
|
|||
public static Glyph getPayNymGlyph() {
|
||||
Glyph payNymGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.ROBOT);
|
||||
payNymGlyph.getStyleClass().add("paynym-icon");
|
||||
payNymGlyph.setFontSize(12);
|
||||
payNymGlyph.setFontSize(10);
|
||||
return payNymGlyph;
|
||||
}
|
||||
|
||||
public static Node getBitcoinCharacter() {
|
||||
try {
|
||||
URL url;
|
||||
if(Config.get().getTheme() == Theme.DARK) {
|
||||
url = AppServices.class.getResource("/image/bitcoin-character-invert.svg");
|
||||
} else {
|
||||
url = AppServices.class.getResource("/image/bitcoin-character.svg");
|
||||
}
|
||||
if(url != null) {
|
||||
SVGImage svgImage = SVGLoader.load(url);
|
||||
HBox hBox = new HBox();
|
||||
hBox.setAlignment(Pos.CENTER);
|
||||
hBox.getChildren().add(svgImage);
|
||||
hBox.setPadding(new Insets(0, 2, 0, 4));
|
||||
return hBox;
|
||||
}
|
||||
} catch(Exception e) {
|
||||
log.error("Could not load bitcoin character");
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public static Glyph getNfcCardGlyph() {
|
||||
Glyph nfcCardGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.WIFI);
|
||||
nfcCardGlyph.getStyleClass().add("nfccard-icon");
|
||||
|
|
@ -723,4 +922,23 @@ public class PaymentController extends WalletFormController implements Initializ
|
|||
public void openWallets(OpenWalletsEvent event) {
|
||||
updateOpenWallets(event.getWallets());
|
||||
}
|
||||
|
||||
private static class DnsPaymentService extends Service<Optional<DnsPayment>> {
|
||||
private final String hrn;
|
||||
|
||||
public DnsPaymentService(String hrn) {
|
||||
this.hrn = hrn;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Task<Optional<DnsPayment>> createTask() {
|
||||
return new Task<>() {
|
||||
@Override
|
||||
protected Optional<DnsPayment> call() throws Exception {
|
||||
DnsPaymentResolver resolver = new DnsPaymentResolver(hrn);
|
||||
return resolver.resolve();
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,12 +6,12 @@ import com.sparrowwallet.drongo.KeyPurpose;
|
|||
import com.sparrowwallet.drongo.Network;
|
||||
import com.sparrowwallet.drongo.SecureString;
|
||||
import com.sparrowwallet.drongo.address.Address;
|
||||
import com.sparrowwallet.drongo.address.InvalidAddressException;
|
||||
import com.sparrowwallet.drongo.bip47.PaymentCode;
|
||||
import com.sparrowwallet.drongo.bip47.SecretPoint;
|
||||
import com.sparrowwallet.drongo.crypto.ECKey;
|
||||
import com.sparrowwallet.drongo.protocol.*;
|
||||
import com.sparrowwallet.drongo.psbt.PSBT;
|
||||
import com.sparrowwallet.drongo.silentpayments.SilentPayment;
|
||||
import com.sparrowwallet.drongo.wallet.*;
|
||||
import com.sparrowwallet.sparrow.*;
|
||||
import com.sparrowwallet.sparrow.control.*;
|
||||
|
|
@ -172,7 +172,7 @@ public class SendController extends WalletFormController implements Initializabl
|
|||
|
||||
private final Set<WalletNode> excludedChangeNodes = new HashSet<>();
|
||||
|
||||
private final Map<Wallet, Map<Address, WalletNode>> addressNodeMap = new HashMap<>();
|
||||
private final Map<Address, WalletNode> walletAddresses = new HashMap<>();
|
||||
|
||||
private final ChangeListener<String> feeListener = new ChangeListener<>() {
|
||||
@Override
|
||||
|
|
@ -484,18 +484,41 @@ public class SendController extends WalletFormController implements Initializabl
|
|||
validationSupport.setValidationDecorator(new StyleClassValidationDecoration());
|
||||
validationSupport.registerValidator(fee, Validator.combine(
|
||||
(Control c, String newValue) -> ValidationResult.fromErrorIf( c, "Insufficient Inputs", userFeeSet.get() && insufficientInputsProperty.get()),
|
||||
(Control c, String newValue) -> ValidationResult.fromErrorIf( c, "Insufficient Fee", getFeeValueSats() != null && getFeeValueSats() == 0),
|
||||
(Control c, String newValue) -> ValidationResult.fromErrorIf( c, "Insufficient Fee Rate", isInsufficientFeeRate())
|
||||
));
|
||||
|
||||
validationSupport.setErrorDecorationEnabled(false);
|
||||
}
|
||||
|
||||
public Tab addPaymentTab() {
|
||||
public void addPaymentTab() {
|
||||
if(Config.get().getSuggestSendToMany() == null && openSendToMany()) {
|
||||
return;
|
||||
}
|
||||
|
||||
Tab tab = getPaymentTab();
|
||||
paymentTabs.getTabs().add(tab);
|
||||
paymentTabs.getSelectionModel().select(tab);
|
||||
return tab;
|
||||
}
|
||||
|
||||
private boolean openSendToMany() {
|
||||
try {
|
||||
List<Payment> payments = getPayments();
|
||||
if(payments.size() == 3) {
|
||||
ConfirmationAlert confirmationAlert = new ConfirmationAlert("Open Send To Many?", "Open the Tools > Send To Many dialog to add multiple payments?", ButtonType.NO, ButtonType.YES);
|
||||
Optional<ButtonType> optType = confirmationAlert.showAndWait();
|
||||
if(confirmationAlert.isDontAskAgain() && optType.isPresent()) {
|
||||
Config.get().setSuggestSendToMany(optType.get() == ButtonType.YES);
|
||||
}
|
||||
if(optType.isPresent() && optType.get() == ButtonType.YES) {
|
||||
Platform.runLater(() -> EventManager.get().post(new RequestSendToManyEvent(payments)));
|
||||
return true;
|
||||
}
|
||||
}
|
||||
} catch(Exception e) {
|
||||
//ignore
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public Tab getPaymentTab() {
|
||||
|
|
@ -582,18 +605,25 @@ public class SendController extends WalletFormController implements Initializabl
|
|||
try {
|
||||
List<Payment> payments = transactionPayments != null ? transactionPayments : getPayments();
|
||||
updateOptimizationButtons(payments);
|
||||
if(!userFeeSet.get() || (getFeeValueSats() != null && getFeeValueSats() > 0)) {
|
||||
if(!userFeeSet.get() || getFeeValueSats() != null) {
|
||||
Wallet wallet = getWalletForm().getWallet();
|
||||
Long userFee = userFeeSet.get() ? getFeeValueSats() : null;
|
||||
double feeRate = getUserFeeRate();
|
||||
double minRelayFeeRate = AppServices.getMinimumRelayFeeRate();
|
||||
Integer currentBlockHeight = AppServices.getCurrentBlockHeight();
|
||||
boolean groupByAddress = Config.get().isGroupByAddress();
|
||||
boolean includeMempoolOutputs = Config.get().isIncludeMempoolOutputs();
|
||||
BlockTransaction replacedTransaction = replacedTransactionProperty.get();
|
||||
|
||||
walletTransactionService = new WalletTransactionService(addressNodeMap, wallet, getUtxoSelectors(payments), getTxoFilters(),
|
||||
//Disable RBF for silent payments, as we can't guarantee RBF won't be attempted on another device without knowledge to recompute the address if necessary
|
||||
boolean allowRbf = (replacedTransaction == null || replacedTransaction.getTransaction().isReplaceByFee())
|
||||
&& payments.stream().noneMatch(payment -> payment instanceof SilentPayment);
|
||||
|
||||
TransactionParameters params = new TransactionParameters(getUtxoSelectors(payments), getTxoFilters(),
|
||||
payments, opReturnsList, excludedChangeNodes,
|
||||
feeRate, getMinimumFeeRate(), userFee, currentBlockHeight, groupByAddress, includeMempoolOutputs, replacedTransaction);
|
||||
feeRate, getMinimumFeeRate(), minRelayFeeRate, userFee,
|
||||
currentBlockHeight, groupByAddress, includeMempoolOutputs, allowRbf);
|
||||
walletTransactionService = new WalletTransactionService(wallet, params, replacedTransaction);
|
||||
walletTransactionService.setOnSucceeded(event -> {
|
||||
if(!walletTransactionService.isIgnoreResult()) {
|
||||
walletTransactionProperty.setValue(walletTransactionService.getValue());
|
||||
|
|
@ -628,12 +658,12 @@ public class SendController extends WalletFormController implements Initializabl
|
|||
|
||||
walletTransactionService.start();
|
||||
}
|
||||
} catch(InvalidAddressException | IllegalStateException e) {
|
||||
} catch(IllegalStateException e) {
|
||||
walletTransactionProperty.setValue(null);
|
||||
}
|
||||
}
|
||||
|
||||
private List<UtxoSelector> getUtxoSelectors(List<Payment> payments) throws InvalidAddressException {
|
||||
private List<UtxoSelector> getUtxoSelectors(List<Payment> payments) {
|
||||
if(utxoSelectorProperty.get() != null) {
|
||||
return List.of(utxoSelectorProperty.get());
|
||||
}
|
||||
|
|
@ -655,39 +685,14 @@ public class SendController extends WalletFormController implements Initializabl
|
|||
}
|
||||
|
||||
private static class WalletTransactionService extends Service<WalletTransaction> {
|
||||
private final Map<Wallet, Map<Address, WalletNode>> addressNodeMap;
|
||||
private final Wallet wallet;
|
||||
private final List<UtxoSelector> utxoSelectors;
|
||||
private final List<TxoFilter> txoFilters;
|
||||
private final List<Payment> payments;
|
||||
private final List<byte[]> opReturns;
|
||||
private final Set<WalletNode> excludedChangeNodes;
|
||||
private final double feeRate;
|
||||
private final double longTermFeeRate;
|
||||
private final Long fee;
|
||||
private final Integer currentBlockHeight;
|
||||
private final boolean groupByAddress;
|
||||
private final boolean includeMempoolOutputs;
|
||||
private final TransactionParameters params;
|
||||
private final BlockTransaction replacedTransaction;
|
||||
private boolean ignoreResult;
|
||||
|
||||
public WalletTransactionService(Map<Wallet, Map<Address, WalletNode>> addressNodeMap,
|
||||
Wallet wallet, List<UtxoSelector> utxoSelectors, List<TxoFilter> txoFilters,
|
||||
List<Payment> payments, List<byte[]> opReturns, Set<WalletNode> excludedChangeNodes,
|
||||
double feeRate, double longTermFeeRate, Long fee, Integer currentBlockHeight, boolean groupByAddress, boolean includeMempoolOutputs, BlockTransaction replacedTransaction) {
|
||||
this.addressNodeMap = addressNodeMap;
|
||||
public WalletTransactionService(Wallet wallet, TransactionParameters params, BlockTransaction replacedTransaction) {
|
||||
this.wallet = wallet;
|
||||
this.utxoSelectors = utxoSelectors;
|
||||
this.txoFilters = txoFilters;
|
||||
this.payments = payments;
|
||||
this.opReturns = opReturns;
|
||||
this.excludedChangeNodes = excludedChangeNodes;
|
||||
this.feeRate = feeRate;
|
||||
this.longTermFeeRate = longTermFeeRate;
|
||||
this.fee = fee;
|
||||
this.currentBlockHeight = currentBlockHeight;
|
||||
this.groupByAddress = groupByAddress;
|
||||
this.includeMempoolOutputs = includeMempoolOutputs;
|
||||
this.params = params;
|
||||
this.replacedTransaction = replacedTransaction;
|
||||
}
|
||||
|
||||
|
|
@ -698,16 +703,17 @@ public class SendController extends WalletFormController implements Initializabl
|
|||
try {
|
||||
return getWalletTransaction();
|
||||
} catch(InsufficientFundsException e) {
|
||||
if(e.getTargetValue() != null && replacedTransaction != null && utxoSelectors.size() == 1 && utxoSelectors.get(0) instanceof PresetUtxoSelector presetUtxoSelector) {
|
||||
if(e.getTargetValue() != null && replacedTransaction != null && wallet.isSafeToAddInputsOrOutputs(replacedTransaction)
|
||||
&& params.utxoSelectors().size() == 1 && params.utxoSelectors().getFirst() instanceof PresetUtxoSelector presetUtxoSelector) {
|
||||
//Creating RBF transaction - include additional UTXOs if available to pay desired fee
|
||||
List<TxoFilter> filters = new ArrayList<>(txoFilters);
|
||||
List<TxoFilter> filters = new ArrayList<>(params.txoFilters());
|
||||
filters.add(presetUtxoSelector.asExcludeTxoFilter());
|
||||
List<OutputGroup> outputGroups = wallet.getGroupedUtxos(filters, feeRate, AppServices.getMinimumRelayFeeRate(), Config.get().isGroupByAddress())
|
||||
List<OutputGroup> outputGroups = wallet.getGroupedUtxos(filters, params.feeRate(), AppServices.getMinimumRelayFeeRate(), Config.get().isGroupByAddress())
|
||||
.stream().filter(outputGroup -> outputGroup.getEffectiveValue() >= 0).collect(Collectors.toList());
|
||||
Collections.shuffle(outputGroups);
|
||||
|
||||
while(!outputGroups.isEmpty() && presetUtxoSelector.getPresetUtxos().stream().mapToLong(BlockTransactionHashIndex::getValue).sum() < e.getTargetValue()) {
|
||||
OutputGroup outputGroup = outputGroups.remove(0);
|
||||
OutputGroup outputGroup = outputGroups.removeFirst();
|
||||
for(BlockTransactionHashIndex utxo : outputGroup.getUtxos()) {
|
||||
presetUtxoSelector.getPresetUtxos().add(utxo);
|
||||
}
|
||||
|
|
@ -721,12 +727,12 @@ public class SendController extends WalletFormController implements Initializabl
|
|||
}
|
||||
|
||||
private WalletTransaction getWalletTransaction() throws InsufficientFundsException {
|
||||
try {
|
||||
updateMessage("Selecting UTXOs...");
|
||||
WalletTransaction walletTransaction = wallet.createWalletTransaction(utxoSelectors, txoFilters, payments, opReturns, excludedChangeNodes,
|
||||
feeRate, longTermFeeRate, fee, currentBlockHeight, groupByAddress, includeMempoolOutputs);
|
||||
updateMessage("Deriving keys...");
|
||||
walletTransaction.updateAddressNodeMap(addressNodeMap, walletTransaction.getWallet());
|
||||
return walletTransaction;
|
||||
return wallet.createWalletTransaction(params);
|
||||
} finally {
|
||||
updateMessage("");
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
@ -854,7 +860,7 @@ public class SendController extends WalletFormController implements Initializabl
|
|||
* @return the fee rate to use when constructing a transaction
|
||||
*/
|
||||
public Double getUserFeeRate() {
|
||||
return (userFeeSet.get() ? Transaction.DEFAULT_MIN_RELAY_FEE : getFeeRate());
|
||||
return (userFeeSet.get() ? AppServices.getMinimumRelayFeeRate() : getFeeRate());
|
||||
}
|
||||
|
||||
public Double getFeeRate() {
|
||||
|
|
@ -918,7 +924,6 @@ public class SendController extends WalletFormController implements Initializabl
|
|||
|
||||
private void setFeeRatePriority(Double feeRateAmt) {
|
||||
Map<Integer, Double> targetBlocksFeeRates = getTargetBlocksFeeRates();
|
||||
Integer targetBlocks = getTargetBlocks(feeRateAmt);
|
||||
if(targetBlocksFeeRates.get(Integer.MAX_VALUE) != null) {
|
||||
Double minFeeRate = targetBlocksFeeRates.get(Integer.MAX_VALUE);
|
||||
if(minFeeRate > 1.0 && feeRateAmt < minFeeRate) {
|
||||
|
|
@ -939,9 +944,10 @@ public class SendController extends WalletFormController implements Initializabl
|
|||
}
|
||||
}
|
||||
|
||||
Integer targetBlocks = getTargetBlocks(feeRateAmt);
|
||||
if(targetBlocks != null) {
|
||||
if(targetBlocks < FeeRatesSource.BLOCKS_IN_HALF_HOUR) {
|
||||
Double maxFeeRate = FEE_RATES_RANGE.get(FEE_RATES_RANGE.size() - 1).doubleValue();
|
||||
Double maxFeeRate = AppServices.getFeeRatesRange().getLast();
|
||||
Double highestBlocksRate = targetBlocksFeeRates.get(TARGET_BLOCKS_RANGE.get(0));
|
||||
if(highestBlocksRate < maxFeeRate && feeRateAmt > (highestBlocksRate + ((maxFeeRate - highestBlocksRate) / 10))) {
|
||||
feeRatePriority.setText("Overpaid");
|
||||
|
|
@ -1091,7 +1097,7 @@ public class SendController extends WalletFormController implements Initializabl
|
|||
|
||||
paymentCodeProperty.set(null);
|
||||
|
||||
addressNodeMap.clear();
|
||||
walletAddresses.clear();
|
||||
}
|
||||
|
||||
public UtxoSelector getUtxoSelector() {
|
||||
|
|
@ -1169,13 +1175,20 @@ public class SendController extends WalletFormController implements Initializabl
|
|||
WalletTransaction walletTransaction = walletTransactionProperty.get();
|
||||
Set<WalletNode> nodes = new LinkedHashSet<>(walletTransaction.getSelectedUtxos().values());
|
||||
nodes.addAll(walletTransaction.getChangeMap().keySet());
|
||||
Map<Address, WalletNode> addressNodeMap = walletTransaction.getAddressNodeMap();
|
||||
nodes.addAll(addressNodeMap.values().stream().filter(Objects::nonNull).collect(Collectors.toList()));
|
||||
nodes.addAll(walletTransaction.getWalletNodePayments().stream().map(WalletNodePayment::getWalletNode).collect(Collectors.toList()));
|
||||
|
||||
//All wallet nodes applicable to this transaction are stored so when the subscription status for one is updated, the history for all can be fetched in one atomic update
|
||||
walletForm.addWalletTransactionNodes(nodes);
|
||||
}
|
||||
|
||||
public WalletNode getWalletNode(Address address) {
|
||||
if(walletAddresses.isEmpty()) {
|
||||
walletAddresses.putAll(getWalletForm().getWallet().getWalletAddresses());
|
||||
}
|
||||
|
||||
return walletAddresses.get(address);
|
||||
}
|
||||
|
||||
public void broadcastNotification(ActionEvent event) {
|
||||
Wallet wallet = getWalletForm().getWallet();
|
||||
Storage storage = AppServices.get().getOpenWallets().get(wallet);
|
||||
|
|
@ -1205,7 +1218,7 @@ public class SendController extends WalletFormController implements Initializabl
|
|||
|
||||
public void broadcastNotification(Wallet decryptedWallet) {
|
||||
try {
|
||||
PaymentCode paymentCode = decryptedWallet.getPaymentCode();
|
||||
PaymentCode paymentCode = decryptedWallet.isMasterWallet() ? decryptedWallet.getPaymentCode() : decryptedWallet.getMasterWallet().getPaymentCode();
|
||||
PaymentCode externalPaymentCode = paymentCodeProperty.get();
|
||||
WalletTransaction walletTransaction = walletTransactionProperty.get();
|
||||
WalletNode input0Node = walletTransaction.getSelectedUtxos().entrySet().iterator().next().getValue();
|
||||
|
|
@ -1219,11 +1232,14 @@ public class SendController extends WalletFormController implements Initializabl
|
|||
List<UtxoSelector> utxoSelectors = List.of(new PresetUtxoSelector(walletTransaction.getSelectedUtxos().keySet(), true, false));
|
||||
Long userFee = userFeeSet.get() ? getFeeValueSats() : null;
|
||||
double feeRate = getUserFeeRate();
|
||||
Double minRelayFeeRate = AppServices.getMinimumRelayFeeRate();
|
||||
Integer currentBlockHeight = AppServices.getCurrentBlockHeight();
|
||||
boolean groupByAddress = Config.get().isGroupByAddress();
|
||||
boolean includeMempoolOutputs = Config.get().isIncludeMempoolOutputs();
|
||||
|
||||
WalletTransaction finalWalletTx = decryptedWallet.createWalletTransaction(utxoSelectors, getTxoFilters(), walletTransaction.getPayments(), List.of(blindedPaymentCode), excludedChangeNodes, feeRate, getMinimumFeeRate(), userFee, currentBlockHeight, groupByAddress, includeMempoolOutputs);
|
||||
TransactionParameters params = new TransactionParameters(utxoSelectors, getTxoFilters(), walletTransaction.getPayments(), List.of(blindedPaymentCode),
|
||||
excludedChangeNodes, feeRate, getMinimumFeeRate(), minRelayFeeRate, userFee, currentBlockHeight, groupByAddress, includeMempoolOutputs, true);
|
||||
WalletTransaction finalWalletTx = decryptedWallet.createWalletTransaction(params);
|
||||
PSBT psbt = finalWalletTx.createPSBT();
|
||||
decryptedWallet.sign(psbt);
|
||||
decryptedWallet.finalise(psbt);
|
||||
|
|
@ -1482,7 +1498,7 @@ public class SendController extends WalletFormController implements Initializabl
|
|||
notificationButton.setVisible(isNotificationTransaction);
|
||||
notificationButton.setDefaultButton(isNotificationTransaction);
|
||||
|
||||
setInputFieldsDisabled(isNotificationTransaction, false);
|
||||
setInputFieldsDisabled(!event.allowPaymentChanges(), false);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1611,18 +1627,26 @@ public class SendController extends WalletFormController implements Initializabl
|
|||
recentBlocksView.updateFeeRatesSource(event.getFeeRateSource());
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void connection(ConnectionEvent event) {
|
||||
if(!Objects.equals(event.getMinimumRelayFeeRate(), event.getPreviousMinimumRelayFeeRate())) {
|
||||
feeRange.updateFeeRange(event.getMinimumRelayFeeRate(), event.getPreviousMinimumRelayFeeRate());
|
||||
updateTransaction();
|
||||
}
|
||||
}
|
||||
|
||||
private class PrivacyAnalysisTooltip extends VBox {
|
||||
private final List<Label> analysisLabels = new ArrayList<>();
|
||||
|
||||
public PrivacyAnalysisTooltip(WalletTransaction walletTransaction) {
|
||||
List<Payment> payments = walletTransaction.getPayments();
|
||||
List<Payment> userPayments = payments.stream().filter(payment -> payment.getType() != Payment.Type.FAKE_MIX).collect(Collectors.toList());
|
||||
Map<Address, WalletNode> walletAddresses = walletTransaction.getAddressNodeMap();
|
||||
List<WalletNodePayment> walletNodePayments = walletTransaction.getWalletNodePayments();
|
||||
OptimizationStrategy optimizationStrategy = getPreferredOptimizationStrategy();
|
||||
boolean fakeMixPresent = payments.stream().anyMatch(payment -> payment.getType() == Payment.Type.FAKE_MIX);
|
||||
boolean roundPaymentAmounts = userPayments.stream().anyMatch(payment -> payment.getAmount() % 100 == 0);
|
||||
boolean mixedAddressTypes = userPayments.stream().anyMatch(payment -> payment.getAddress().getScriptType() != getWalletForm().getWallet().getFreshNode(KeyPurpose.RECEIVE).getAddress().getScriptType());
|
||||
boolean addressReuse = userPayments.stream().anyMatch(payment -> walletAddresses.get(payment.getAddress()) != null && !walletAddresses.get(payment.getAddress()).getTransactionOutputs().isEmpty());
|
||||
boolean addressReuse = walletNodePayments.stream().anyMatch(walletNodePayment -> !walletNodePayment.getWalletNode().getTransactionOutputs().isEmpty());
|
||||
boolean payjoinPresent = userPayments.stream().anyMatch(payment -> AppServices.getPayjoinURI(payment.getAddress()) != null);
|
||||
|
||||
if(optimizationStrategy == OptimizationStrategy.PRIVACY) {
|
||||
|
|
|
|||
|
|
@ -41,8 +41,11 @@ import java.io.IOException;
|
|||
import java.net.URL;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.*;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static com.sparrowwallet.drongo.OutputDescriptor.KEY_ORIGIN_PATTERN;
|
||||
import static com.sparrowwallet.drongo.OutputDescriptor.XPUB_PATTERN;
|
||||
import static com.sparrowwallet.sparrow.AppServices.showErrorDialog;
|
||||
import static com.sparrowwallet.sparrow.AppServices.showWarningDialog;
|
||||
|
||||
|
|
@ -455,6 +458,26 @@ public class SettingsController extends WalletFormController implements Initiali
|
|||
AppServices.showWarningDialog("Legacy multisig wallet detected", "Sparrow supports BIP67 compatible multisig wallets only.\n\nThe public keys will be lexicographically sorted, and the output descriptor represented with sortedmulti.");
|
||||
}
|
||||
|
||||
Matcher matcher = XPUB_PATTERN.matcher(text.get());
|
||||
while(matcher.find()) {
|
||||
String keyDerivationPath = null;
|
||||
if(matcher.group(1) != null) {
|
||||
Matcher keyOriginMatcher = KEY_ORIGIN_PATTERN.matcher(matcher.group(1));
|
||||
if(keyOriginMatcher.matches()) {
|
||||
keyDerivationPath = keyOriginMatcher.group(2);
|
||||
}
|
||||
}
|
||||
String extKey = matcher.group(2);
|
||||
String childDerivationPath = matcher.group(3);
|
||||
|
||||
if(ExtendedKey.Header.getHeaders(Network.get()).stream().anyMatch(header -> header.isPrivateKey() && extKey.startsWith(header.name())) &&
|
||||
(keyDerivationPath != null || (childDerivationPath != null && !(childDerivationPath.equals("/0/*") || childDerivationPath.equals("/1/*") || childDerivationPath.equals("/<0;1>/*"))))) {
|
||||
AppServices.showWarningDialog("Private extended key detected", "Sparrow will convert the provided private key to a public key for use in a watch only wallet.\n\nTo import a private key, use the Master Private Key option when creating a Software Wallet.");
|
||||
} else if(childDerivationPath != null && !(childDerivationPath.endsWith("/0/*") || childDerivationPath.endsWith("/1/*") || childDerivationPath.endsWith("/<0;1>/*"))) {
|
||||
AppServices.showWarningDialog("Non standard child derivation detected", "Sparrow does not support non-BIP32 wallets without standard receive and change chains.\n\nThe provided descriptor will be amended if necessary.");
|
||||
}
|
||||
}
|
||||
|
||||
setDescriptorText(text.get().replace("\n", ""));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -27,13 +27,14 @@ open module com.sparrowwallet.sparrow {
|
|||
requires com.google.gson;
|
||||
requires org.jdbi.v3.core;
|
||||
requires org.jdbi.v3.sqlobject;
|
||||
requires io.leangen.geantyref;
|
||||
requires org.flywaydb.core;
|
||||
requires com.zaxxer.hikari;
|
||||
requires com.h2database;
|
||||
requires com.sparrowwallet.hummingbird;
|
||||
requires org.fxmisc.flowless;
|
||||
requires openpnp.capture.java;
|
||||
requires centerdevice.nsmenufx;
|
||||
requires nsmenufx;
|
||||
requires org.jcommander;
|
||||
requires jul.to.slf4j;
|
||||
requires net.sourceforge.javacsv;
|
||||
|
|
@ -56,4 +57,5 @@ open module com.sparrowwallet.sparrow {
|
|||
requires com.sparrowwallet.tern;
|
||||
requires com.sparrowwallet.lark;
|
||||
requires com.sun.jna;
|
||||
requires io.github.doblon8.jzbar;
|
||||
}
|
||||
|
|
@ -1,76 +0,0 @@
|
|||
/*------------------------------------------------------------------------
|
||||
* Config
|
||||
*
|
||||
* Copyright 2010 (c) Jeff Brown <spadix@users.sourceforge.net>
|
||||
*
|
||||
* This file is part of the ZBar Bar Code Reader.
|
||||
*
|
||||
* The ZBar Bar Code Reader is free software; you can redistribute it
|
||||
* and/or modify it under the terms of the GNU Lesser Public License as
|
||||
* published by the Free Software Foundation; either version 2.1 of
|
||||
* the License, or (at your option) any later version.
|
||||
*
|
||||
* The ZBar Bar Code Reader is distributed in the hope that it will be
|
||||
* useful, but WITHOUT ANY WARRANTY; without even the implied warranty
|
||||
* of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Lesser Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Lesser Public License
|
||||
* along with the ZBar Bar Code Reader; if not, write to the Free
|
||||
* Software Foundation, Inc., 51 Franklin St, Fifth Floor,
|
||||
* Boston, MA 02110-1301 USA
|
||||
*
|
||||
* http://sourceforge.net/projects/zbar
|
||||
*------------------------------------------------------------------------*/
|
||||
|
||||
package net.sourceforge.zbar;
|
||||
|
||||
/**
|
||||
* Decoder configuration options.
|
||||
*/
|
||||
public class Config {
|
||||
/**
|
||||
* Enable symbology/feature.
|
||||
*/
|
||||
public static final int ENABLE = 0;
|
||||
/**
|
||||
* Enable check digit when optional.
|
||||
*/
|
||||
public static final int ADD_CHECK = 1;
|
||||
/**
|
||||
* Return check digit when present.
|
||||
*/
|
||||
public static final int EMIT_CHECK = 2;
|
||||
/**
|
||||
* Enable full ASCII character set.
|
||||
*/
|
||||
public static final int ASCII = 3;
|
||||
|
||||
/**
|
||||
* Minimum data length for valid decode.
|
||||
*/
|
||||
public static final int MIN_LEN = 0x20;
|
||||
/**
|
||||
* Maximum data length for valid decode.
|
||||
*/
|
||||
public static final int MAX_LEN = 0x21;
|
||||
|
||||
/**
|
||||
* Required video consistency frames.
|
||||
*/
|
||||
public static final int UNCERTAINTY = 0x40;
|
||||
|
||||
/**
|
||||
* Enable scanner to collect position data.
|
||||
*/
|
||||
public static final int POSITION = 0x80;
|
||||
|
||||
/**
|
||||
* Image scanner vertical scan density.
|
||||
*/
|
||||
public static final int X_DENSITY = 0x100;
|
||||
/**
|
||||
* Image scanner horizontal scan density.
|
||||
*/
|
||||
public static final int Y_DENSITY = 0x101;
|
||||
}
|
||||
|
|
@ -1,197 +0,0 @@
|
|||
/*------------------------------------------------------------------------
|
||||
* Image
|
||||
*
|
||||
* Copyright 2007-2010 (c) Jeff Brown <spadix@users.sourceforge.net>
|
||||
*
|
||||
* This file is part of the ZBar Bar Code Reader.
|
||||
*
|
||||
* The ZBar Bar Code Reader is free software; you can redistribute it
|
||||
* and/or modify it under the terms of the GNU Lesser Public License as
|
||||
* published by the Free Software Foundation; either version 2.1 of
|
||||
* the License, or (at your option) any later version.
|
||||
*
|
||||
* The ZBar Bar Code Reader is distributed in the hope that it will be
|
||||
* useful, but WITHOUT ANY WARRANTY; without even the implied warranty
|
||||
* of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Lesser Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Lesser Public License
|
||||
* along with the ZBar Bar Code Reader; if not, write to the Free
|
||||
* Software Foundation, Inc., 51 Franklin St, Fifth Floor,
|
||||
* Boston, MA 02110-1301 USA
|
||||
*
|
||||
* http://sourceforge.net/projects/zbar
|
||||
*------------------------------------------------------------------------*/
|
||||
|
||||
package net.sourceforge.zbar;
|
||||
|
||||
import java.io.Closeable;
|
||||
|
||||
/**
|
||||
* stores image data samples along with associated format and size
|
||||
* metadata.
|
||||
*/
|
||||
public class Image implements Closeable {
|
||||
static {
|
||||
init();
|
||||
}
|
||||
|
||||
/**
|
||||
* C pointer to a zbar_symbol_t.
|
||||
*/
|
||||
private long peer;
|
||||
private Object data;
|
||||
|
||||
public Image() {
|
||||
peer = create();
|
||||
}
|
||||
|
||||
public Image(int width, int height) {
|
||||
this();
|
||||
setSize(width, height);
|
||||
}
|
||||
|
||||
public Image(int width, int height, String format) {
|
||||
this();
|
||||
setSize(width, height);
|
||||
setFormat(format);
|
||||
}
|
||||
|
||||
public Image(String format) {
|
||||
this();
|
||||
setFormat(format);
|
||||
}
|
||||
|
||||
Image(long peer) {
|
||||
this.peer = peer;
|
||||
}
|
||||
|
||||
private static native void init();
|
||||
|
||||
/**
|
||||
* Create an associated peer instance.
|
||||
*/
|
||||
private native long create();
|
||||
|
||||
public void close() {
|
||||
destroy();
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean up native data associated with an instance.
|
||||
*/
|
||||
public synchronized void destroy() {
|
||||
if(peer != 0) {
|
||||
destroy(peer);
|
||||
peer = 0;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Destroy the associated peer instance.
|
||||
*/
|
||||
private native void destroy(long peer);
|
||||
|
||||
/**
|
||||
* Image format conversion.
|
||||
*
|
||||
* @returns a @em new image with the sample data from the original
|
||||
* image converted to the requested format fourcc. the original
|
||||
* image is unaffected.
|
||||
*/
|
||||
public Image convert(String format) {
|
||||
long newpeer = convert(peer, format);
|
||||
if(newpeer == 0) {
|
||||
return (null);
|
||||
}
|
||||
return (new Image(newpeer));
|
||||
}
|
||||
|
||||
private native long convert(long peer, String format);
|
||||
|
||||
/**
|
||||
* Retrieve the image format fourcc.
|
||||
*/
|
||||
public native String getFormat();
|
||||
|
||||
/**
|
||||
* Specify the fourcc image format code for image sample data.
|
||||
*/
|
||||
public native void setFormat(String format);
|
||||
|
||||
/**
|
||||
* Retrieve a "sequence" (page/frame) number associated with this
|
||||
* image.
|
||||
*/
|
||||
public native int getSequence();
|
||||
|
||||
/**
|
||||
* Associate a "sequence" (page/frame) number with this image.
|
||||
*/
|
||||
public native void setSequence(int seq);
|
||||
|
||||
/**
|
||||
* Retrieve the width of the image.
|
||||
*/
|
||||
public native int getWidth();
|
||||
|
||||
/**
|
||||
* Retrieve the height of the image.
|
||||
*/
|
||||
public native int getHeight();
|
||||
|
||||
/**
|
||||
* Retrieve the size of the image.
|
||||
*/
|
||||
public native int[] getSize();
|
||||
|
||||
/**
|
||||
* Specify the pixel size of the image.
|
||||
*/
|
||||
public native void setSize(int[] size);
|
||||
|
||||
/**
|
||||
* Specify the pixel size of the image.
|
||||
*/
|
||||
public native void setSize(int width, int height);
|
||||
|
||||
/**
|
||||
* Retrieve the crop region of the image.
|
||||
*/
|
||||
public native int[] getCrop();
|
||||
|
||||
/**
|
||||
* Specify the crop region of the image.
|
||||
*/
|
||||
public native void setCrop(int[] crop);
|
||||
|
||||
/**
|
||||
* Specify the crop region of the image.
|
||||
*/
|
||||
public native void setCrop(int x, int y, int width, int height);
|
||||
|
||||
/**
|
||||
* Retrieve the image sample data.
|
||||
*/
|
||||
public native byte[] getData();
|
||||
|
||||
/**
|
||||
* Specify image sample data.
|
||||
*/
|
||||
public native void setData(byte[] data);
|
||||
|
||||
/**
|
||||
* Specify image sample data.
|
||||
*/
|
||||
public native void setData(int[] data);
|
||||
|
||||
/**
|
||||
* Retrieve the decoded results associated with this image.
|
||||
*/
|
||||
public SymbolSet getSymbols() {
|
||||
return (new SymbolSet(getSymbols(peer)));
|
||||
}
|
||||
|
||||
private native long getSymbols(long peer);
|
||||
|
||||
}
|
||||
|
|
@ -1,110 +0,0 @@
|
|||
/*------------------------------------------------------------------------
|
||||
* ImageScanner
|
||||
*
|
||||
* Copyright 2007-2010 (c) Jeff Brown <spadix@users.sourceforge.net>
|
||||
*
|
||||
* This file is part of the ZBar Bar Code Reader.
|
||||
*
|
||||
* The ZBar Bar Code Reader is free software; you can redistribute it
|
||||
* and/or modify it under the terms of the GNU Lesser Public License as
|
||||
* published by the Free Software Foundation; either version 2.1 of
|
||||
* the License, or (at your option) any later version.
|
||||
*
|
||||
* The ZBar Bar Code Reader is distributed in the hope that it will be
|
||||
* useful, but WITHOUT ANY WARRANTY; without even the implied warranty
|
||||
* of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Lesser Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Lesser Public License
|
||||
* along with the ZBar Bar Code Reader; if not, write to the Free
|
||||
* Software Foundation, Inc., 51 Franklin St, Fifth Floor,
|
||||
* Boston, MA 02110-1301 USA
|
||||
*
|
||||
* http://sourceforge.net/projects/zbar
|
||||
*------------------------------------------------------------------------*/
|
||||
|
||||
package net.sourceforge.zbar;
|
||||
|
||||
import java.io.Closeable;
|
||||
|
||||
/**
|
||||
* Read barcodes from 2-D images.
|
||||
*/
|
||||
public class ImageScanner implements Closeable {
|
||||
static {
|
||||
init();
|
||||
}
|
||||
|
||||
/**
|
||||
* C pointer to a zbar_image_scanner_t.
|
||||
*/
|
||||
private long peer;
|
||||
|
||||
public ImageScanner() {
|
||||
peer = create();
|
||||
}
|
||||
|
||||
private static native void init();
|
||||
|
||||
/**
|
||||
* Create an associated peer instance.
|
||||
*/
|
||||
private native long create();
|
||||
|
||||
public void close() {
|
||||
destroy();
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean up native data associated with an instance.
|
||||
*/
|
||||
public synchronized void destroy() {
|
||||
if(peer != 0) {
|
||||
destroy(peer);
|
||||
peer = 0;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Destroy the associated peer instance.
|
||||
*/
|
||||
private native void destroy(long peer);
|
||||
|
||||
/**
|
||||
* Set config for indicated symbology (0 for all) to specified value.
|
||||
*/
|
||||
public native void setConfig(int symbology, int config, int value) throws IllegalArgumentException;
|
||||
|
||||
/**
|
||||
* Parse configuration string and apply to image scanner.
|
||||
*/
|
||||
public native void parseConfig(String config);
|
||||
|
||||
/**
|
||||
* Enable or disable the inter-image result cache (default disabled).
|
||||
* Mostly useful for scanning video frames, the cache filters duplicate
|
||||
* results from consecutive images, while adding some consistency
|
||||
* checking and hysteresis to the results. Invoking this method also
|
||||
* clears the cache.
|
||||
*/
|
||||
public native void enableCache(boolean enable);
|
||||
|
||||
/**
|
||||
* Retrieve decode results for last scanned image.
|
||||
*
|
||||
* @returns the SymbolSet result container
|
||||
*/
|
||||
public SymbolSet getResults() {
|
||||
return (new SymbolSet(getResults(peer)));
|
||||
}
|
||||
|
||||
private native long getResults(long peer);
|
||||
|
||||
/**
|
||||
* Scan for symbols in provided Image.
|
||||
* The image format must currently be "Y800" or "GRAY".
|
||||
*
|
||||
* @returns the number of symbols successfully decoded from the image.
|
||||
*/
|
||||
public native int scanImage(Image image);
|
||||
}
|
||||
|
|
@ -1,44 +0,0 @@
|
|||
/*------------------------------------------------------------------------
|
||||
* Modifier
|
||||
*
|
||||
* Copyright 2010 (c) Jeff Brown <spadix@users.sourceforge.net>
|
||||
*
|
||||
* This file is part of the ZBar Bar Code Reader.
|
||||
*
|
||||
* The ZBar Bar Code Reader is free software; you can redistribute it
|
||||
* and/or modify it under the terms of the GNU Lesser Public License as
|
||||
* published by the Free Software Foundation; either version 2.1 of
|
||||
* the License, or (at your option) any later version.
|
||||
*
|
||||
* The ZBar Bar Code Reader is distributed in the hope that it will be
|
||||
* useful, but WITHOUT ANY WARRANTY; without even the implied warranty
|
||||
* of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Lesser Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Lesser Public License
|
||||
* along with the ZBar Bar Code Reader; if not, write to the Free
|
||||
* Software Foundation, Inc., 51 Franklin St, Fifth Floor,
|
||||
* Boston, MA 02110-1301 USA
|
||||
*
|
||||
* http://sourceforge.net/projects/zbar
|
||||
*------------------------------------------------------------------------*/
|
||||
|
||||
package net.sourceforge.zbar;
|
||||
|
||||
/**
|
||||
* Decoder symbology modifiers.
|
||||
*/
|
||||
public class Modifier {
|
||||
/**
|
||||
* barcode tagged as GS1 (EAN.UCC) reserved
|
||||
* (eg, FNC1 before first data character).
|
||||
* data may be parsed as a sequence of GS1 AIs
|
||||
*/
|
||||
public static final int GS1 = 0;
|
||||
|
||||
/**
|
||||
* barcode tagged as AIM reserved
|
||||
* (eg, FNC1 after first character or digit pair)
|
||||
*/
|
||||
public static final int AIM = 1;
|
||||
}
|
||||
|
|
@ -1,52 +0,0 @@
|
|||
/*------------------------------------------------------------------------
|
||||
* Orientation
|
||||
*
|
||||
* Copyright 2010 (c) Jeff Brown <spadix@users.sourceforge.net>
|
||||
*
|
||||
* This file is part of the ZBar Bar Code Reader.
|
||||
*
|
||||
* The ZBar Bar Code Reader is free software; you can redistribute it
|
||||
* and/or modify it under the terms of the GNU Lesser Public License as
|
||||
* published by the Free Software Foundation; either version 2.1 of
|
||||
* the License, or (at your option) any later version.
|
||||
*
|
||||
* The ZBar Bar Code Reader is distributed in the hope that it will be
|
||||
* useful, but WITHOUT ANY WARRANTY; without even the implied warranty
|
||||
* of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Lesser Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Lesser Public License
|
||||
* along with the ZBar Bar Code Reader; if not, write to the Free
|
||||
* Software Foundation, Inc., 51 Franklin St, Fifth Floor,
|
||||
* Boston, MA 02110-1301 USA
|
||||
*
|
||||
* http://sourceforge.net/projects/zbar
|
||||
*------------------------------------------------------------------------*/
|
||||
|
||||
package net.sourceforge.zbar;
|
||||
|
||||
/**
|
||||
* Decoded symbol coarse orientation.
|
||||
*/
|
||||
public class Orientation {
|
||||
/**
|
||||
* Unable to determine orientation.
|
||||
*/
|
||||
public static final int UNKNOWN = -1;
|
||||
/**
|
||||
* Upright, read left to right.
|
||||
*/
|
||||
public static final int UP = 0;
|
||||
/**
|
||||
* sideways, read top to bottom
|
||||
*/
|
||||
public static final int RIGHT = 1;
|
||||
/**
|
||||
* upside-down, read right to left
|
||||
*/
|
||||
public static final int DOWN = 2;
|
||||
/**
|
||||
* sideways, read bottom to top
|
||||
*/
|
||||
public static final int LEFT = 3;
|
||||
}
|
||||
|
|
@ -1,265 +0,0 @@
|
|||
/*------------------------------------------------------------------------
|
||||
* Symbol
|
||||
*
|
||||
* Copyright 2007-2010 (c) Jeff Brown <spadix@users.sourceforge.net>
|
||||
*
|
||||
* This file is part of the ZBar Bar Code Reader.
|
||||
*
|
||||
* The ZBar Bar Code Reader is free software; you can redistribute it
|
||||
* and/or modify it under the terms of the GNU Lesser Public License as
|
||||
* published by the Free Software Foundation; either version 2.1 of
|
||||
* the License, or (at your option) any later version.
|
||||
*
|
||||
* The ZBar Bar Code Reader is distributed in the hope that it will be
|
||||
* useful, but WITHOUT ANY WARRANTY; without even the implied warranty
|
||||
* of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Lesser Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Lesser Public License
|
||||
* along with the ZBar Bar Code Reader; if not, write to the Free
|
||||
* Software Foundation, Inc., 51 Franklin St, Fifth Floor,
|
||||
* Boston, MA 02110-1301 USA
|
||||
*
|
||||
* http://sourceforge.net/projects/zbar
|
||||
*------------------------------------------------------------------------*/
|
||||
|
||||
package net.sourceforge.zbar;
|
||||
|
||||
import java.io.Closeable;
|
||||
|
||||
/**
|
||||
* Immutable container for decoded result symbols associated with an image
|
||||
* or a composite symbol.
|
||||
*/
|
||||
public class Symbol implements Closeable {
|
||||
/**
|
||||
* No symbol decoded.
|
||||
*/
|
||||
public static final int NONE = 0;
|
||||
/**
|
||||
* Symbol detected but not decoded.
|
||||
*/
|
||||
public static final int PARTIAL = 1;
|
||||
|
||||
/**
|
||||
* EAN-8.
|
||||
*/
|
||||
public static final int EAN8 = 8;
|
||||
/**
|
||||
* UPC-E.
|
||||
*/
|
||||
public static final int UPCE = 9;
|
||||
/**
|
||||
* ISBN-10 (from EAN-13).
|
||||
*/
|
||||
public static final int ISBN10 = 10;
|
||||
/**
|
||||
* UPC-A.
|
||||
*/
|
||||
public static final int UPCA = 12;
|
||||
/**
|
||||
* EAN-13.
|
||||
*/
|
||||
public static final int EAN13 = 13;
|
||||
/**
|
||||
* ISBN-13 (from EAN-13).
|
||||
*/
|
||||
public static final int ISBN13 = 14;
|
||||
/**
|
||||
* Interleaved 2 of 5.
|
||||
*/
|
||||
public static final int I25 = 25;
|
||||
/**
|
||||
* DataBar (RSS-14).
|
||||
*/
|
||||
public static final int DATABAR = 34;
|
||||
/**
|
||||
* DataBar Expanded.
|
||||
*/
|
||||
public static final int DATABAR_EXP = 35;
|
||||
/**
|
||||
* Codabar.
|
||||
*/
|
||||
public static final int CODABAR = 38;
|
||||
/**
|
||||
* Code 39.
|
||||
*/
|
||||
public static final int CODE39 = 39;
|
||||
/**
|
||||
* PDF417.
|
||||
*/
|
||||
public static final int PDF417 = 57;
|
||||
/**
|
||||
* QR Code.
|
||||
*/
|
||||
public static final int QRCODE = 64;
|
||||
/**
|
||||
* Code 93.
|
||||
*/
|
||||
public static final int CODE93 = 93;
|
||||
/**
|
||||
* Code 128.
|
||||
*/
|
||||
public static final int CODE128 = 128;
|
||||
|
||||
static {
|
||||
init();
|
||||
}
|
||||
|
||||
/**
|
||||
* C pointer to a zbar_symbol_t.
|
||||
*/
|
||||
private long peer;
|
||||
/**
|
||||
* Cached attributes.
|
||||
*/
|
||||
private int type;
|
||||
|
||||
/**
|
||||
* Symbols are only created by other package methods.
|
||||
*/
|
||||
Symbol(long peer) {
|
||||
this.peer = peer;
|
||||
}
|
||||
|
||||
private static native void init();
|
||||
|
||||
public void close() {
|
||||
destroy();
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean up native data associated with an instance.
|
||||
*/
|
||||
public synchronized void destroy() {
|
||||
if(peer != 0) {
|
||||
destroy(peer);
|
||||
peer = 0;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Release the associated peer instance.
|
||||
*/
|
||||
private native void destroy(long peer);
|
||||
|
||||
/**
|
||||
* Retrieve type of decoded symbol.
|
||||
*/
|
||||
public int getType() {
|
||||
if(type == 0) {
|
||||
type = getType(peer);
|
||||
}
|
||||
return (type);
|
||||
}
|
||||
|
||||
private native int getType(long peer);
|
||||
|
||||
/**
|
||||
* Retrieve symbology boolean configs settings used during decode.
|
||||
*/
|
||||
public native int getConfigMask();
|
||||
|
||||
/**
|
||||
* Retrieve symbology characteristics detected during decode.
|
||||
*/
|
||||
public native int getModifierMask();
|
||||
|
||||
/**
|
||||
* Retrieve data decoded from symbol as a String.
|
||||
*/
|
||||
public native String getData();
|
||||
|
||||
/**
|
||||
* Retrieve raw data bytes decoded from symbol.
|
||||
*/
|
||||
public native byte[] getDataBytes();
|
||||
|
||||
/**
|
||||
* Retrieve a symbol confidence metric. Quality is an unscaled,
|
||||
* relative quantity: larger values are better than smaller
|
||||
* values, where "large" and "small" are application dependent.
|
||||
*/
|
||||
public native int getQuality();
|
||||
|
||||
/**
|
||||
* Retrieve current cache count. When the cache is enabled for
|
||||
* the image_scanner this provides inter-frame reliability and
|
||||
* redundancy information for video streams.
|
||||
*
|
||||
* @returns < 0 if symbol is still uncertain
|
||||
* @returns 0 if symbol is newly verified
|
||||
* @returns > 0 for duplicate symbols
|
||||
*/
|
||||
public native int getCount();
|
||||
|
||||
/**
|
||||
* Retrieve an approximate, axis-aligned bounding box for the
|
||||
* symbol.
|
||||
*/
|
||||
public int[] getBounds() {
|
||||
int n = getLocationSize(peer);
|
||||
if(n <= 0) {
|
||||
return (null);
|
||||
}
|
||||
|
||||
int[] bounds = new int[4];
|
||||
int xmin = Integer.MAX_VALUE;
|
||||
int xmax = Integer.MIN_VALUE;
|
||||
int ymin = Integer.MAX_VALUE;
|
||||
int ymax = Integer.MIN_VALUE;
|
||||
|
||||
for(int i = 0; i < n; i++) {
|
||||
int x = getLocationX(peer, i);
|
||||
if(xmin > x) {
|
||||
xmin = x;
|
||||
}
|
||||
if(xmax < x) {
|
||||
xmax = x;
|
||||
}
|
||||
|
||||
int y = getLocationY(peer, i);
|
||||
if(ymin > y) {
|
||||
ymin = y;
|
||||
}
|
||||
if(ymax < y) {
|
||||
ymax = y;
|
||||
}
|
||||
}
|
||||
bounds[0] = xmin;
|
||||
bounds[1] = ymin;
|
||||
bounds[2] = xmax - xmin;
|
||||
bounds[3] = ymax - ymin;
|
||||
return (bounds);
|
||||
}
|
||||
|
||||
private native int getLocationSize(long peer);
|
||||
|
||||
private native int getLocationX(long peer, int idx);
|
||||
|
||||
private native int getLocationY(long peer, int idx);
|
||||
|
||||
public int[] getLocationPoint(int idx) {
|
||||
int[] p = new int[2];
|
||||
p[0] = getLocationX(peer, idx);
|
||||
p[1] = getLocationY(peer, idx);
|
||||
return (p);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve general axis-aligned, orientation of decoded
|
||||
* symbol.
|
||||
*/
|
||||
public native int getOrientation();
|
||||
|
||||
/**
|
||||
* Retrieve components of a composite result.
|
||||
*/
|
||||
public SymbolSet getComponents() {
|
||||
return (new SymbolSet(getComponents(peer)));
|
||||
}
|
||||
|
||||
private native long getComponents(long peer);
|
||||
|
||||
native long next();
|
||||
}
|
||||
|
|
@ -1,75 +0,0 @@
|
|||
/*------------------------------------------------------------------------
|
||||
* SymbolIterator
|
||||
*
|
||||
* Copyright 2007-2010 (c) Jeff Brown <spadix@users.sourceforge.net>
|
||||
*
|
||||
* This file is part of the ZBar Bar Code Reader.
|
||||
*
|
||||
* The ZBar Bar Code Reader is free software; you can redistribute it
|
||||
* and/or modify it under the terms of the GNU Lesser Public License as
|
||||
* published by the Free Software Foundation; either version 2.1 of
|
||||
* the License, or (at your option) any later version.
|
||||
*
|
||||
* The ZBar Bar Code Reader is distributed in the hope that it will be
|
||||
* useful, but WITHOUT ANY WARRANTY; without even the implied warranty
|
||||
* of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Lesser Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Lesser Public License
|
||||
* along with the ZBar Bar Code Reader; if not, write to the Free
|
||||
* Software Foundation, Inc., 51 Franklin St, Fifth Floor,
|
||||
* Boston, MA 02110-1301 USA
|
||||
*
|
||||
* http://sourceforge.net/projects/zbar
|
||||
*------------------------------------------------------------------------*/
|
||||
|
||||
package net.sourceforge.zbar;
|
||||
|
||||
/**
|
||||
* Iterator over a SymbolSet.
|
||||
*/
|
||||
public class SymbolIterator implements java.util.Iterator<Symbol> {
|
||||
/**
|
||||
* Next symbol to be returned by the iterator.
|
||||
*/
|
||||
private Symbol current;
|
||||
|
||||
/**
|
||||
* SymbolIterators are only created by internal interface methods.
|
||||
*/
|
||||
SymbolIterator(Symbol first) {
|
||||
current = first;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the iteration has more elements.
|
||||
*/
|
||||
public boolean hasNext() {
|
||||
return (current != null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the next element in the iteration.
|
||||
*/
|
||||
public Symbol next() {
|
||||
if(current == null) {
|
||||
throw (new java.util.NoSuchElementException("access past end of SymbolIterator"));
|
||||
}
|
||||
|
||||
Symbol result = current;
|
||||
long sym = current.next();
|
||||
if(sym != 0) {
|
||||
current = new Symbol(sym);
|
||||
} else {
|
||||
current = null;
|
||||
}
|
||||
return (result);
|
||||
}
|
||||
|
||||
/**
|
||||
* Raises UnsupportedOperationException.
|
||||
*/
|
||||
public void remove() {
|
||||
throw (new UnsupportedOperationException("SymbolIterator is immutable"));
|
||||
}
|
||||
}
|
||||
|
|
@ -1,93 +0,0 @@
|
|||
/*------------------------------------------------------------------------
|
||||
* SymbolSet
|
||||
*
|
||||
* Copyright 2007-2010 (c) Jeff Brown <spadix@users.sourceforge.net>
|
||||
*
|
||||
* This file is part of the ZBar Bar Code Reader.
|
||||
*
|
||||
* The ZBar Bar Code Reader is free software; you can redistribute it
|
||||
* and/or modify it under the terms of the GNU Lesser Public License as
|
||||
* published by the Free Software Foundation; either version 2.1 of
|
||||
* the License, or (at your option) any later version.
|
||||
*
|
||||
* The ZBar Bar Code Reader is distributed in the hope that it will be
|
||||
* useful, but WITHOUT ANY WARRANTY; without even the implied warranty
|
||||
* of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Lesser Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Lesser Public License
|
||||
* along with the ZBar Bar Code Reader; if not, write to the Free
|
||||
* Software Foundation, Inc., 51 Franklin St, Fifth Floor,
|
||||
* Boston, MA 02110-1301 USA
|
||||
*
|
||||
* http://sourceforge.net/projects/zbar
|
||||
*------------------------------------------------------------------------*/
|
||||
|
||||
package net.sourceforge.zbar;
|
||||
|
||||
import java.io.Closeable;
|
||||
|
||||
/**
|
||||
* Immutable container for decoded result symbols associated with an image
|
||||
* or a composite symbol.
|
||||
*/
|
||||
public class SymbolSet extends java.util.AbstractCollection<Symbol> implements Closeable {
|
||||
static {
|
||||
init();
|
||||
}
|
||||
|
||||
/**
|
||||
* C pointer to a zbar_symbol_set_t.
|
||||
*/
|
||||
private long peer;
|
||||
|
||||
/**
|
||||
* SymbolSets are only created by other package methods.
|
||||
*/
|
||||
SymbolSet(long peer) {
|
||||
this.peer = peer;
|
||||
}
|
||||
|
||||
private static native void init();
|
||||
|
||||
public void close() {
|
||||
destroy();
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean up native data associated with an instance.
|
||||
*/
|
||||
public synchronized void destroy() {
|
||||
if(peer != 0) {
|
||||
destroy(peer);
|
||||
peer = 0;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Release the associated peer instance.
|
||||
*/
|
||||
private native void destroy(long peer);
|
||||
|
||||
/**
|
||||
* Retrieve an iterator over the Symbol elements in this collection.
|
||||
*/
|
||||
public java.util.Iterator<Symbol> iterator() {
|
||||
long sym = firstSymbol(peer);
|
||||
if(sym == 0) {
|
||||
return (new SymbolIterator(null));
|
||||
}
|
||||
|
||||
return (new SymbolIterator(new Symbol(sym)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the number of elements in the collection.
|
||||
*/
|
||||
public native int size();
|
||||
|
||||
/**
|
||||
* Retrieve C pointer to first symbol in the set.
|
||||
*/
|
||||
private native long firstSymbol(long peer);
|
||||
}
|
||||
|
|
@ -329,6 +329,10 @@ HorizontalHeaderColumn > TableColumnHeader.column-header.table-column{
|
|||
-fx-stroke: #696c77;
|
||||
}
|
||||
|
||||
#blockchainForm #blockStatus {
|
||||
-fx-text-fill: white;
|
||||
}
|
||||
|
||||
.root .progress-indicator.progress-timer.warn > .determinate-indicator > .indicator {
|
||||
-fx-background-color: -fx-box-border, radial-gradient(center 50% 50%, radius 50%, #e06c75 70%, derive(-fx-control-inner-background, -9%) 100%);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -46,7 +46,7 @@
|
|||
|
||||
.id, .fixed-width {
|
||||
-fx-font-size: 13px;
|
||||
-fx-font-family: 'Roboto Mono';
|
||||
-fx-font-family: 'Fragment Mono Regular';
|
||||
}
|
||||
|
||||
.form-separator {
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@
|
|||
|
||||
.virtualized-scroll-pane .code-area, .uneditable-codearea {
|
||||
-fx-font-size: 13px;
|
||||
-fx-font-family: 'Roboto Mono';
|
||||
-fx-font-family: 'Fragment Mono Regular';
|
||||
-fx-padding: 4;
|
||||
-fx-fill: -fx-text-inner-color;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -44,7 +44,7 @@
|
|||
|
||||
#transactionDiagram .input-label, #transactionDiagram .recipient-label, #transactionDiagram .change-label, #transactionDiagram .fee-tooltip, #transactionDiagram .transaction-tooltip {
|
||||
-fx-font-size: 13px;
|
||||
-fx-font-family: 'Roboto Mono';
|
||||
-fx-font-family: 'Fragment Mono Regular';
|
||||
}
|
||||
|
||||
#transactionDiagram .fee-warning-icon {
|
||||
|
|
|
|||
|
|
@ -41,6 +41,8 @@
|
|||
</columnConstraints>
|
||||
<rowConstraints>
|
||||
<RowConstraints />
|
||||
<RowConstraints />
|
||||
<RowConstraints vgrow="SOMETIMES" />
|
||||
</rowConstraints>
|
||||
<Form GridPane.columnIndex="0" GridPane.rowIndex="0" GridPane.columnSpan="2">
|
||||
<Fieldset text="Transaction" inputGrow="SOMETIMES" wrapWidth="620">
|
||||
|
|
@ -74,9 +76,11 @@
|
|||
|
||||
<TabPane side="RIGHT" GridPane.columnIndex="0" GridPane.rowIndex="2" GridPane.columnSpan="2" styleClass="headers-tabs">
|
||||
<Tab text="Overview" closable="false">
|
||||
<VBox spacing="8">
|
||||
<TransactionDiagram fx:id="transactionDiagram" maxWidth="700" final="true"/>
|
||||
<TransactionDiagramLabel fx:id="transactionDiagramLabel" maxWidth="640" prefWidth="640" />
|
||||
<VBox spacing="8" alignment="CENTER">
|
||||
<Region VBox.vgrow="SOMETIMES" />
|
||||
<TransactionDiagram fx:id="transactionDiagram" final="true"/>
|
||||
<TransactionDiagramLabel fx:id="transactionDiagramLabel" />
|
||||
<Region VBox.vgrow="SOMETIMES" />
|
||||
</VBox>
|
||||
</Tab>
|
||||
<Tab text="Detail" closable="false">
|
||||
|
|
@ -176,7 +180,12 @@
|
|||
|
||||
<Separator GridPane.columnIndex="0" GridPane.rowIndex="5" GridPane.columnSpan="2" styleClass="form-separator"/>
|
||||
|
||||
<DynamicForm fx:id="blockchainForm" GridPane.columnIndex="0" GridPane.columnSpan="2" GridPane.rowIndex="6">
|
||||
<GridPane GridPane.columnIndex="0" GridPane.columnSpan="2" GridPane.rowIndex="6">
|
||||
<columnConstraints>
|
||||
<ColumnConstraints percentWidth="80" />
|
||||
<ColumnConstraints percentWidth="20" />
|
||||
</columnConstraints>
|
||||
<DynamicForm fx:id="blockchainForm" GridPane.columnIndex="0" GridPane.rowIndex="0">
|
||||
<Fieldset text="Blockchain" inputGrow="SOMETIMES">
|
||||
<Field text="Status:">
|
||||
<Label fx:id="blockStatus" contentDisplay="RIGHT" graphicTextGap="5" />
|
||||
|
|
@ -187,8 +196,28 @@
|
|||
<Field fx:id="blockTimestampField" text="Timestamp:">
|
||||
<CopyableLabel fx:id="blockTimestamp" />
|
||||
</Field>
|
||||
<Field fx:id="signedByField" text="Signed by:">
|
||||
<CopyableLabel fx:id="signedBy" />
|
||||
</Field>
|
||||
</Fieldset>
|
||||
</DynamicForm>
|
||||
<Form fx:id="blockchainSpacerForm" GridPane.columnIndex="1" GridPane.rowIndex="0" visible="false">
|
||||
<Fieldset text="Spacer" inputGrow="SOMETIMES">
|
||||
<VBox>
|
||||
<ProgressBar styleClass="signatures-progress-bar" maxWidth="Infinity" minHeight="50" prefHeight="50" progress="0" />
|
||||
</VBox>
|
||||
<VBox>
|
||||
<HBox styleClass="signatures-buttons" spacing="20">
|
||||
<Button HBox.hgrow="ALWAYS" textAlignment="CENTER" text="Spacer" contentDisplay="TOP" wrapText="true">
|
||||
<graphic>
|
||||
<Glyph fontFamily="Font Awesome 5 Free Solid" fontSize="20" icon="SEARCH" />
|
||||
</graphic>
|
||||
</Button>
|
||||
</HBox>
|
||||
</VBox>
|
||||
</Fieldset>
|
||||
</Form>
|
||||
</GridPane>
|
||||
|
||||
<Form fx:id="signingWalletForm" GridPane.columnIndex="0" GridPane.rowIndex="6">
|
||||
<Fieldset text="Signatures" inputGrow="SOMETIMES" styleClass="relaxedLabelFieldSet">
|
||||
|
|
@ -236,7 +265,7 @@
|
|||
<Fieldset text="Signatures" inputGrow="SOMETIMES">
|
||||
<VBox>
|
||||
<SignaturesProgressBar fx:id="signaturesProgressBar" />
|
||||
<ProgressBar fx:id="broadcastProgressBar" maxWidth="Infinity" prefHeight="50" />
|
||||
<ProgressBar fx:id="broadcastProgressBar" maxWidth="Infinity" minHeight="50" prefHeight="50" />
|
||||
</VBox>
|
||||
<VBox>
|
||||
<HBox fx:id="signButtonBox" styleClass="signatures-buttons" spacing="20">
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@
|
|||
|
||||
.chart-legend-item {
|
||||
-fx-font-size: 13px;
|
||||
-fx-font-family: 'Roboto Mono';
|
||||
-fx-font-family: 'Fragment Mono Regular';
|
||||
}
|
||||
|
||||
.default-color0.chart-pie { -fx-pie-color: #ca1243 }
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@
|
|||
|
||||
.chart-legend-item {
|
||||
-fx-font-size: 13;
|
||||
-fx-font-family: 'Roboto Mono';
|
||||
-fx-font-family: 'Fragment Mono Regular';
|
||||
}
|
||||
|
||||
.default-color7.chart-pie { -fx-pie-color: #0184bc }
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
#txhex {
|
||||
-fx-background-color: -fx-control-inner-background;
|
||||
-fx-font-size: 13px;
|
||||
-fx-font-family: 'Roboto Mono';
|
||||
-fx-font-family: 'Fragment Mono Regular';
|
||||
-fx-padding: 2;
|
||||
color-0: #ca1243;
|
||||
color-1: #d75f00;
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@
|
|||
|
||||
#fingerprint, #derivation, #xpub {
|
||||
-fx-font-size: 13px;
|
||||
-fx-font-family: 'Roboto Mono';
|
||||
-fx-font-family: 'Fragment Mono Regular';
|
||||
}
|
||||
|
||||
#type {
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@
|
|||
<Insets top="10.0" bottom="10.0" />
|
||||
</padding>
|
||||
<columnConstraints>
|
||||
<ColumnConstraints prefWidth="410" />
|
||||
<ColumnConstraints prefWidth="410" hgrow="SOMETIMES" />
|
||||
<ColumnConstraints prefWidth="200" />
|
||||
<ColumnConstraints prefWidth="105" />
|
||||
</columnConstraints>
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@
|
|||
<Insets left="25.0" right="25.0" top="25.0" />
|
||||
</padding>
|
||||
<columnConstraints>
|
||||
<ColumnConstraints prefWidth="620" />
|
||||
<ColumnConstraints prefWidth="620" hgrow="SOMETIMES" />
|
||||
<ColumnConstraints prefWidth="140" />
|
||||
</columnConstraints>
|
||||
<rowConstraints>
|
||||
|
|
|
|||
|
|
@ -71,7 +71,7 @@
|
|||
|
||||
#transactionDiagram .input-label, #transactionDiagram .recipient-label, #transactionDiagram .change-label, #transactionDiagram .fee-tooltip, #transactionDiagram .transaction-tooltip {
|
||||
-fx-font-size: 13px;
|
||||
-fx-font-family: 'Roboto Mono';
|
||||
-fx-font-family: 'Fragment Mono Regular';
|
||||
}
|
||||
|
||||
#transactionDiagram .fee-warning-icon {
|
||||
|
|
|
|||
|
|
@ -36,7 +36,7 @@
|
|||
<Insets left="25.0" right="25.0" top="25.0" />
|
||||
</padding>
|
||||
<columnConstraints>
|
||||
<ColumnConstraints prefWidth="410" />
|
||||
<ColumnConstraints prefWidth="410" hgrow="SOMETIMES" />
|
||||
<ColumnConstraints prefWidth="200" />
|
||||
<ColumnConstraints prefWidth="140" />
|
||||
</columnConstraints>
|
||||
|
|
@ -152,9 +152,9 @@
|
|||
<RecentBlocksView fx:id="recentBlocksView" styleClass="feeRatesChart" AnchorPane.topAnchor="10" AnchorPane.leftAnchor="74" translateY="30" minHeight="135"/>
|
||||
</AnchorPane>
|
||||
</GridPane>
|
||||
<AnchorPane>
|
||||
<TransactionDiagram fx:id="transactionDiagram" maxWidth="700" AnchorPane.leftAnchor="100" />
|
||||
</AnchorPane>
|
||||
<StackPane VBox.vgrow="SOMETIMES">
|
||||
<TransactionDiagram fx:id="transactionDiagram" />
|
||||
</StackPane>
|
||||
</VBox>
|
||||
</center>
|
||||
<bottom>
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@
|
|||
|
||||
.address-cell, .utxo-row.entry-cell {
|
||||
-fx-font-size: 13px;
|
||||
-fx-font-family: 'Roboto Mono';
|
||||
-fx-font-family: 'Fragment Mono Regular';
|
||||
}
|
||||
|
||||
.cell > .hyperlink {
|
||||
|
|
@ -149,7 +149,7 @@
|
|||
|
||||
.address-text-field {
|
||||
-fx-font-size: 13px;
|
||||
-fx-font-family: 'Roboto Mono';
|
||||
-fx-font-family: 'Fragment Mono Regular';
|
||||
}
|
||||
|
||||
.unconfirmed-row {
|
||||
|
|
|
|||
BIN
src/main/resources/font/FragmentMono-Italic.ttf
Normal file
BIN
src/main/resources/font/FragmentMono-Italic.ttf
Normal file
Binary file not shown.
BIN
src/main/resources/font/FragmentMono-Regular.ttf
Normal file
BIN
src/main/resources/font/FragmentMono-Regular.ttf
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
11
src/main/resources/image/bitcoin-character-invert.svg
Normal file
11
src/main/resources/image/bitcoin-character-invert.svg
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="7px" height="13px" viewBox="0 0 7 13" version="1.1">
|
||||
<g id="surface1">
|
||||
<path style=" stroke:none;fill-rule:nonzero;fill:rgb(255, 255, 255);fill-opacity:1;" d="M 3.84375 1.777344 L 3.867188 1.371094 C 3.382812 1.339844 3.144531 1.339844 2.78125 1.339844 L 0.410156 1.339844 C 0.300781 1.339844 0.195312 1.386719 0.121094 1.460938 C 0.0429688 1.539062 0 1.644531 0 1.75 L 0 11.121094 C 0 11.226562 0.0429688 11.332031 0.121094 11.410156 C 0.195312 11.484375 0.300781 11.527344 0.410156 11.527344 L 3.023438 11.527344 C 3.351562 11.527344 3.625 11.527344 3.984375 11.5 L 3.949219 11.09375 L 3.976562 11.5 C 4.753906 11.457031 5.496094 11.1875 6.054688 10.699219 C 6.335938 10.457031 6.566406 10.15625 6.726562 9.808594 C 6.886719 9.460938 6.976562 9.0625 6.976562 8.632812 C 6.976562 7.921875 6.777344 7.257812 6.34375 6.746094 C 6.125 6.488281 5.855469 6.273438 5.535156 6.109375 C 5.21875 5.945312 4.851562 5.832031 4.445312 5.773438 L 4.386719 6.175781 L 4.449219 6.582031 C 5.097656 6.480469 5.65625 6.183594 6.039062 5.722656 C 6.421875 5.265625 6.625 4.660156 6.621094 3.992188 C 6.621094 3.617188 6.554688 3.261719 6.417969 2.949219 C 6.21875 2.476562 5.867188 2.089844 5.425781 1.824219 C 4.984375 1.558594 4.453125 1.410156 3.871094 1.371094 L 3.867188 1.371094 L 3.84375 1.777344 L 3.8125 2.1875 C 4.453125 2.230469 4.949219 2.433594 5.28125 2.734375 C 5.445312 2.886719 5.574219 3.0625 5.660156 3.269531 C 5.75 3.476562 5.800781 3.714844 5.800781 3.992188 C 5.800781 4.503906 5.652344 4.902344 5.40625 5.199219 C 5.160156 5.492188 4.804688 5.695312 4.324219 5.773438 C 4.121094 5.804688 3.972656 5.976562 3.976562 6.179688 C 3.976562 6.382812 4.125 6.554688 4.324219 6.582031 C 4.65625 6.632812 4.929688 6.71875 5.15625 6.835938 C 5.5 7.015625 5.738281 7.253906 5.902344 7.550781 C 6.066406 7.847656 6.152344 8.210938 6.152344 8.636719 C 6.152344 8.953125 6.089844 9.226562 5.980469 9.464844 C 5.8125 9.824219 5.539062 10.109375 5.183594 10.320312 C 4.828125 10.527344 4.394531 10.65625 3.925781 10.683594 L 3.917969 10.683594 C 3.59375 10.710938 3.351562 10.710938 3.023438 10.710938 L 0.820312 10.710938 L 0.820312 2.160156 L 2.777344 2.160156 C 3.148438 2.160156 3.347656 2.160156 3.816406 2.1875 L 3.84375 1.777344 L 3.8125 2.1875 Z M 3.84375 1.777344 "/>
|
||||
<path style=" stroke:none;fill-rule:nonzero;fill:rgb(255, 255, 255);fill-opacity:1;" d="M 4.460938 5.808594 L 0.5 5.808594 C 0.273438 5.808594 0.0898438 5.988281 0.0898438 6.214844 C 0.0898438 6.441406 0.273438 6.625 0.5 6.625 L 4.460938 6.625 C 4.691406 6.625 4.875 6.441406 4.875 6.214844 C 4.875 5.988281 4.691406 5.808594 4.460938 5.808594 "/>
|
||||
<path style=" stroke:none;fill-rule:nonzero;fill:rgb(255, 255, 255);fill-opacity:1;" d="M 0.96875 0.410156 L 0.96875 1.6875 C 0.96875 1.914062 1.152344 2.097656 1.378906 2.097656 C 1.605469 2.097656 1.789062 1.914062 1.789062 1.6875 L 1.789062 0.410156 C 1.789062 0.183594 1.605469 0 1.378906 0 C 1.152344 0 0.96875 0.183594 0.96875 0.410156 "/>
|
||||
<path style=" stroke:none;fill-rule:nonzero;fill:rgb(255, 255, 255);fill-opacity:1;" d="M 2.820312 0.410156 L 2.820312 1.6875 C 2.820312 1.914062 3.003906 2.097656 3.230469 2.097656 C 3.457031 2.097656 3.640625 1.914062 3.640625 1.6875 L 3.640625 0.410156 C 3.640625 0.183594 3.457031 0 3.230469 0 C 3.003906 0 2.820312 0.183594 2.820312 0.410156 "/>
|
||||
<path style=" stroke:none;fill-rule:nonzero;fill:rgb(255, 255, 255);fill-opacity:1;" d="M 0.851562 11.300781 L 0.851562 12.578125 C 0.851562 12.804688 1.035156 12.988281 1.261719 12.988281 C 1.488281 12.988281 1.671875 12.804688 1.671875 12.578125 L 1.671875 11.300781 C 1.671875 11.074219 1.488281 10.890625 1.261719 10.890625 C 1.035156 10.890625 0.851562 11.074219 0.851562 11.300781 "/>
|
||||
<path style=" stroke:none;fill-rule:nonzero;fill:rgb(255, 255, 255);fill-opacity:1;" d="M 2.699219 11.300781 L 2.699219 12.578125 C 2.699219 12.804688 2.882812 12.988281 3.113281 12.988281 C 3.339844 12.988281 3.523438 12.804688 3.523438 12.578125 L 3.523438 11.300781 C 3.523438 11.074219 3.339844 10.890625 3.113281 10.890625 C 2.886719 10.890625 2.699219 11.074219 2.699219 11.300781 "/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 4.1 KiB |
11
src/main/resources/image/bitcoin-character.svg
Normal file
11
src/main/resources/image/bitcoin-character.svg
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="7px" height="13px" viewBox="0 0 7 13" version="1.1">
|
||||
<g id="surface1">
|
||||
<path style=" stroke:none;fill-rule:nonzero;fill:rgb(14.117648%,14.117648%,14.117648%);fill-opacity:1;" d="M 3.84375 1.777344 L 3.867188 1.371094 C 3.382812 1.339844 3.144531 1.339844 2.78125 1.339844 L 0.410156 1.339844 C 0.300781 1.339844 0.195312 1.386719 0.121094 1.460938 C 0.0429688 1.539062 0 1.644531 0 1.75 L 0 11.121094 C 0 11.226562 0.0429688 11.332031 0.121094 11.410156 C 0.195312 11.484375 0.300781 11.527344 0.410156 11.527344 L 3.023438 11.527344 C 3.351562 11.527344 3.625 11.527344 3.984375 11.5 L 3.949219 11.09375 L 3.976562 11.5 C 4.753906 11.457031 5.496094 11.1875 6.054688 10.699219 C 6.335938 10.457031 6.566406 10.15625 6.726562 9.808594 C 6.886719 9.460938 6.976562 9.0625 6.976562 8.632812 C 6.976562 7.921875 6.777344 7.257812 6.34375 6.746094 C 6.125 6.488281 5.855469 6.273438 5.535156 6.109375 C 5.21875 5.945312 4.851562 5.832031 4.445312 5.773438 L 4.386719 6.175781 L 4.449219 6.582031 C 5.097656 6.480469 5.65625 6.183594 6.039062 5.722656 C 6.421875 5.265625 6.625 4.660156 6.621094 3.992188 C 6.621094 3.617188 6.554688 3.261719 6.417969 2.949219 C 6.21875 2.476562 5.867188 2.089844 5.425781 1.824219 C 4.984375 1.558594 4.453125 1.410156 3.871094 1.371094 L 3.867188 1.371094 L 3.84375 1.777344 L 3.8125 2.1875 C 4.453125 2.230469 4.949219 2.433594 5.28125 2.734375 C 5.445312 2.886719 5.574219 3.0625 5.660156 3.269531 C 5.75 3.476562 5.800781 3.714844 5.800781 3.992188 C 5.800781 4.503906 5.652344 4.902344 5.40625 5.199219 C 5.160156 5.492188 4.804688 5.695312 4.324219 5.773438 C 4.121094 5.804688 3.972656 5.976562 3.976562 6.179688 C 3.976562 6.382812 4.125 6.554688 4.324219 6.582031 C 4.65625 6.632812 4.929688 6.71875 5.15625 6.835938 C 5.5 7.015625 5.738281 7.253906 5.902344 7.550781 C 6.066406 7.847656 6.152344 8.210938 6.152344 8.636719 C 6.152344 8.953125 6.089844 9.226562 5.980469 9.464844 C 5.8125 9.824219 5.539062 10.109375 5.183594 10.320312 C 4.828125 10.527344 4.394531 10.65625 3.925781 10.683594 L 3.917969 10.683594 C 3.59375 10.710938 3.351562 10.710938 3.023438 10.710938 L 0.820312 10.710938 L 0.820312 2.160156 L 2.777344 2.160156 C 3.148438 2.160156 3.347656 2.160156 3.816406 2.1875 L 3.84375 1.777344 L 3.8125 2.1875 Z M 3.84375 1.777344 "/>
|
||||
<path style=" stroke:none;fill-rule:nonzero;fill:rgb(14.117648%,14.117648%,14.117648%);fill-opacity:1;" d="M 4.460938 5.808594 L 0.5 5.808594 C 0.273438 5.808594 0.0898438 5.988281 0.0898438 6.214844 C 0.0898438 6.441406 0.273438 6.625 0.5 6.625 L 4.460938 6.625 C 4.691406 6.625 4.875 6.441406 4.875 6.214844 C 4.875 5.988281 4.691406 5.808594 4.460938 5.808594 "/>
|
||||
<path style=" stroke:none;fill-rule:nonzero;fill:rgb(14.117648%,14.117648%,14.117648%);fill-opacity:1;" d="M 0.96875 0.410156 L 0.96875 1.6875 C 0.96875 1.914062 1.152344 2.097656 1.378906 2.097656 C 1.605469 2.097656 1.789062 1.914062 1.789062 1.6875 L 1.789062 0.410156 C 1.789062 0.183594 1.605469 0 1.378906 0 C 1.152344 0 0.96875 0.183594 0.96875 0.410156 "/>
|
||||
<path style=" stroke:none;fill-rule:nonzero;fill:rgb(14.117648%,14.117648%,14.117648%);fill-opacity:1;" d="M 2.820312 0.410156 L 2.820312 1.6875 C 2.820312 1.914062 3.003906 2.097656 3.230469 2.097656 C 3.457031 2.097656 3.640625 1.914062 3.640625 1.6875 L 3.640625 0.410156 C 3.640625 0.183594 3.457031 0 3.230469 0 C 3.003906 0 2.820312 0.183594 2.820312 0.410156 "/>
|
||||
<path style=" stroke:none;fill-rule:nonzero;fill:rgb(14.117648%,14.117648%,14.117648%);fill-opacity:1;" d="M 0.851562 11.300781 L 0.851562 12.578125 C 0.851562 12.804688 1.035156 12.988281 1.261719 12.988281 C 1.488281 12.988281 1.671875 12.804688 1.671875 12.578125 L 1.671875 11.300781 C 1.671875 11.074219 1.488281 10.890625 1.261719 10.890625 C 1.035156 10.890625 0.851562 11.074219 0.851562 11.300781 "/>
|
||||
<path style=" stroke:none;fill-rule:nonzero;fill:rgb(14.117648%,14.117648%,14.117648%);fill-opacity:1;" d="M 2.699219 11.300781 L 2.699219 12.578125 C 2.699219 12.804688 2.882812 12.988281 3.113281 12.988281 C 3.339844 12.988281 3.523438 12.804688 3.523438 12.578125 L 3.523438 11.300781 C 3.523438 11.074219 3.339844 10.890625 3.113281 10.890625 C 2.886719 10.890625 2.699219 11.074219 2.699219 11.300781 "/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 4.2 KiB |
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue