Compare commits

..

No commits in common. "master" and "2.2.3" have entirely different histories.

95 changed files with 1476 additions and 1563 deletions

View file

@ -12,11 +12,11 @@ jobs:
matrix: matrix:
os: [windows-2022, ubuntu-22.04, ubuntu-22.04-arm, macos-13, macos-14] os: [windows-2022, ubuntu-22.04, ubuntu-22.04-arm, macos-13, macos-14]
steps: steps:
- uses: actions/checkout@v5 - uses: actions/checkout@v4
with: with:
submodules: true submodules: true
- name: Set up JDK 22.0.2 - name: Set up JDK 22.0.2
uses: actions/setup-java@v5 uses: actions/setup-java@v4
with: with:
distribution: 'temurin' distribution: 'temurin'
java-version: '22.0.2' java-version: '22.0.2'

View file

@ -1,8 +1,8 @@
plugins { plugins {
id 'application' id 'application'
id 'org-openjfx-javafxplugin' id 'org-openjfx-javafxplugin'
id 'org.beryx.jlink' version '3.1.3' id 'org.beryx.jlink' version '3.1.1'
id 'org.gradlex.extra-java-module-info' version '1.13' id 'org.gradlex.extra-java-module-info' version '1.9'
id 'io.matthewnelson.kmp.tor.resource-filterjar' version '408.16.3' id 'io.matthewnelson.kmp.tor.resource-filterjar' version '408.16.3'
} }
@ -19,16 +19,17 @@ if(System.getProperty("os.arch") == "aarch64") {
} }
def headless = "true".equals(System.getProperty("java.awt.headless")) def headless = "true".equals(System.getProperty("java.awt.headless"))
group = 'com.sparrowwallet' group 'com.sparrowwallet'
version = '2.3.1' version '2.2.3'
repositories { repositories {
mavenCentral() mavenCentral()
maven { url = uri('https://code.sparrowwallet.com/api/packages/sparrowwallet/maven') } maven { url 'https://code.sparrowwallet.com/api/packages/sparrowwallet/maven' }
} }
tasks.withType(AbstractArchiveTask).configureEach { tasks.withType(AbstractArchiveTask) {
useFileSystemPermissions() preserveFileTimestamps = false
reproducibleFileOrder = true
} }
javafx { javafx {
@ -44,20 +45,20 @@ dependencies {
//Any changes to the dependencies must be reflected in the module definitions below! //Any changes to the dependencies must be reflected in the module definitions below!
implementation(project(':drongo')) implementation(project(':drongo'))
implementation(project(':lark')) implementation(project(':lark'))
implementation('com.google.guava:guava:33.5.0-jre') implementation('com.google.guava:guava:33.0.0-jre')
implementation('com.google.code.gson:gson:2.9.1') implementation('com.google.code.gson:gson:2.9.1')
implementation('com.h2database:h2:2.1.214') implementation('com.h2database:h2:2.1.214')
implementation('com.zaxxer:HikariCP:4.0.3') { implementation('com.zaxxer:HikariCP:4.0.3') {
exclude group: 'org.slf4j' exclude group: 'org.slf4j'
} }
implementation('org.jdbi:jdbi3-core:3.49.5') { implementation('org.jdbi:jdbi3-core:3.20.0') {
exclude group: 'org.slf4j' exclude group: 'org.slf4j'
} }
implementation('org.jdbi:jdbi3-sqlobject:3.49.5') { implementation('org.jdbi:jdbi3-sqlobject:3.20.0') {
exclude group: 'org.slf4j' exclude group: 'org.slf4j'
} }
implementation('org.flywaydb:flyway-core:9.22.3') implementation('org.flywaydb:flyway-core:9.22.3')
implementation('org.fxmisc.richtext:richtextfx:0.11.6') implementation('org.fxmisc.richtext:richtextfx:0.10.4')
implementation('no.tornado:tornadofx-controls:1.0.4') implementation('no.tornado:tornadofx-controls:1.0.4')
implementation('com.google.zxing:javase:3.4.0') { implementation('com.google.zxing:javase:3.4.0') {
exclude group: 'com.beust', module: 'jcommander' exclude group: 'com.beust', module: 'jcommander'
@ -73,15 +74,13 @@ dependencies {
implementation('com.fasterxml.jackson.core:jackson-databind:2.17.2') implementation('com.fasterxml.jackson.core:jackson-databind:2.17.2')
implementation('com.sparrowwallet:hummingbird:1.7.4') implementation('com.sparrowwallet:hummingbird:1.7.4')
implementation('co.nstant.in:cbor:0.9') implementation('co.nstant.in:cbor:0.9')
implementation('org.openpnp:openpnp-capture-java:0.0.30-1') implementation('org.openpnp:openpnp-capture-java:0.0.28-6')
implementation("io.matthewnelson.kmp-tor:runtime:2.2.1") implementation("io.matthewnelson.kmp-tor:runtime:2.2.1")
implementation("io.matthewnelson.kmp-tor:resource-exec-tor-gpl:408.16.3") implementation("io.matthewnelson.kmp-tor:resource-exec-tor-gpl:408.16.3")
implementation('org.jetbrains.kotlinx:kotlinx-coroutines-javafx:1.10.1') { implementation('org.jetbrains.kotlinx:kotlinx-coroutines-javafx:1.10.1') {
exclude group: 'org.jetbrains.kotlin', module: 'kotlin-stdlib-common' exclude group: 'org.jetbrains.kotlin', module: 'kotlin-stdlib-common'
} }
implementation('de.jangassen:nsmenufx:3.1.0') { implementation('de.codecentric.centerdevice:centerdevice-nsmenufx:2.1.7')
exclude group: 'net.java.dev.jna', module: 'jna'
}
implementation('org.controlsfx:controlsfx:11.1.0' ) { implementation('org.controlsfx:controlsfx:11.1.0' ) {
exclude group: 'org.openjfx', module: 'javafx-base' exclude group: 'org.openjfx', module: 'javafx-base'
exclude group: 'org.openjfx', module: 'javafx-graphics' exclude group: 'org.openjfx', module: 'javafx-graphics'
@ -101,7 +100,7 @@ dependencies {
implementation('com.sparrowwallet:tern:1.0.6') implementation('com.sparrowwallet:tern:1.0.6')
implementation('io.reactivex.rxjava2:rxjava:2.2.15') implementation('io.reactivex.rxjava2:rxjava:2.2.15')
implementation('io.reactivex.rxjava2:rxjavafx:2.2.2') implementation('io.reactivex.rxjava2:rxjavafx:2.2.2')
implementation('org.apache.commons:commons-lang3:3.19.0') implementation('org.apache.commons:commons-lang3:3.7')
implementation('org.apache.commons:commons-compress:1.27.1') implementation('org.apache.commons:commons-compress:1.27.1')
implementation('net.sourceforge.streamsupport:streamsupport:1.7.0') implementation('net.sourceforge.streamsupport:streamsupport:1.7.0')
implementation('com.github.librepdf:openpdf:1.3.30') implementation('com.github.librepdf:openpdf:1.3.30')
@ -110,7 +109,6 @@ dependencies {
implementation('com.github.hervegirod:fxsvgimage:1.1') implementation('com.github.hervegirod:fxsvgimage:1.1')
implementation('com.sparrowwallet:toucan:0.9.0') implementation('com.sparrowwallet:toucan:0.9.0')
implementation('com.jcraft:jzlib:1.1.3') 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') testImplementation('org.junit.jupiter:junit-jupiter-api:5.10.0')
testRuntimeOnly('org.junit.jupiter:junit-jupiter-engine:5.10.0') testRuntimeOnly('org.junit.jupiter:junit-jupiter-engine:5.10.0')
testRuntimeOnly('org.junit.platform:junit-platform-launcher') testRuntimeOnly('org.junit.platform:junit-platform-launcher')
@ -143,12 +141,6 @@ application {
mainClass = 'com.sparrowwallet.sparrow.SparrowWallet' mainClass = 'com.sparrowwallet.sparrow.SparrowWallet'
applicationDefaultJvmArgs = ["-XX:+HeapDumpOnOutOfMemoryError", 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/com.sun.javafx.css=org.controlsfx.controls",
"--add-opens=javafx.graphics/javafx.scene=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.behavior=org.controlsfx.controls",
@ -158,6 +150,11 @@ application {
"--add-opens=javafx.controls/javafx.scene.control.cell=com.sparrowwallet.sparrow", "--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=com.sparrowwallet.sparrow",
"--add-opens=org.controlsfx.controls/impl.org.controlsfx.skin=javafx.fxml", "--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.glass.ui=com.sparrowwallet.sparrow",
"--add-opens=javafx.graphics/com.sun.javafx.application=com.sparrowwallet.sparrow", "--add-opens=javafx.graphics/com.sun.javafx.application=com.sparrowwallet.sparrow",
"--add-opens=javafx.graphics/javafx.scene.input=com.sparrowwallet.sparrow", "--add-opens=javafx.graphics/javafx.scene.input=com.sparrowwallet.sparrow",
@ -168,7 +165,8 @@ application {
"--add-reads=org.flywaydb.core=java.desktop"] "--add-reads=org.flywaydb.core=java.desktop"]
if(os.macOsX) { if(os.macOsX) {
applicationDefaultJvmArgs += ["-Dprism.lcdtext=false", "-Xdock:name=Sparrow"] applicationDefaultJvmArgs += ["-Dprism.lcdtext=false", "-Xdock:name=Sparrow",
"--add-opens=javafx.graphics/com.sun.glass.ui.mac=centerdevice.nsmenufx"]
} }
if(headless) { if(headless) {
applicationDefaultJvmArgs += ["-Dglass.platform=Monocle", "-Dmonocle.platform=Headless", "-Dprism.order=sw"] applicationDefaultJvmArgs += ["-Dglass.platform=Monocle", "-Dmonocle.platform=Headless", "-Dprism.order=sw"]
@ -191,14 +189,7 @@ 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/*'] 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 { launcher {
name = 'sparrow' name = 'sparrow'
jvmArgs = ["--enable-native-access=com.sparrowwallet.drongo", jvmArgs = ["--add-opens=javafx.graphics/com.sun.javafx.css=org.controlsfx.controls",
"--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.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.behavior=org.controlsfx.controls",
"--add-opens=javafx.controls/com.sun.javafx.scene.control.inputmap=org.controlsfx.controls", "--add-opens=javafx.controls/com.sun.javafx.scene.control.inputmap=org.controlsfx.controls",
@ -207,6 +198,11 @@ jlink {
"--add-opens=javafx.controls/javafx.scene.control.cell=com.sparrowwallet.sparrow", "--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=com.sparrowwallet.sparrow",
"--add-opens=org.controlsfx.controls/impl.org.controlsfx.skin=javafx.fxml", "--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.glass.ui=com.sparrowwallet.sparrow",
"--add-opens=javafx.graphics/javafx.scene.input=com.sparrowwallet.sparrow", "--add-opens=javafx.graphics/javafx.scene.input=com.sparrowwallet.sparrow",
"--add-opens=javafx.graphics/com.sun.javafx.application=com.sparrowwallet.sparrow", "--add-opens=javafx.graphics/com.sun.javafx.application=com.sparrowwallet.sparrow",
@ -225,7 +221,6 @@ jlink {
"--add-reads=com.sparrowwallet.merged.module=org.bouncycastle.pg", "--add-reads=com.sparrowwallet.merged.module=org.bouncycastle.pg",
"--add-reads=com.sparrowwallet.merged.module=org.bouncycastle.provider", "--add-reads=com.sparrowwallet.merged.module=org.bouncycastle.provider",
"--add-reads=com.sparrowwallet.merged.module=kotlin.stdlib", "--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=kotlin.stdlib=kotlinx.coroutines.core",
"--add-reads=org.flywaydb.core=java.desktop"] "--add-reads=org.flywaydb.core=java.desktop"]
@ -233,7 +228,7 @@ jlink {
jvmArgs += ["-Djavax.accessibility.assistive_technologies", "-Djavax.accessibility.screen_magnifier_present=false"] jvmArgs += ["-Djavax.accessibility.assistive_technologies", "-Djavax.accessibility.screen_magnifier_present=false"]
} }
if(os.macOsX) { if(os.macOsX) {
jvmArgs += ["-Dprism.lcdtext=false", "--add-opens=javafx.graphics/com.sun.glass.ui.mac=com.sparrowwallet.merged.module"] 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"]
} }
if(headless) { if(headless) {
jvmArgs += ["-Dglass.platform=Monocle", "-Dmonocle.platform=Headless", "-Dprism.order=sw"] jvmArgs += ["-Dglass.platform=Monocle", "-Dmonocle.platform=Headless", "-Dprism.order=sw"]
@ -285,8 +280,7 @@ if(os.linux) {
tasks.register('addUserWritePermission', Exec) { tasks.register('addUserWritePermission', Exec) {
if(os.windows) { if(os.windows) {
def usersGroup = '*S-1-5-32-545' // Windows "Users" group SID (language-independent) commandLine 'icacls', "$buildDir\\image\\legal", '/grant', 'Users:(OI)(CI)F', '/T'
commandLine 'icacls', "$buildDir\\image\\legal", '/grant', "${usersGroup}:(OI)(CI)F", '/T'
} else { } else {
commandLine 'chmod', '-R', 'u+w', "$buildDir/image/legal" commandLine 'chmod', '-R', 'u+w', "$buildDir/image/legal"
} }
@ -386,12 +380,33 @@ extraJavaModuleInfo {
requires('java.desktop') requires('java.desktop')
requires('com.sun.jna') 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') { module('net.sourceforge.javacsv:javacsv', 'net.sourceforge.javacsv') {
exports('com.csvreader') exports('com.csvreader')
} }
module('com.google.guava:listenablefuture|empty-to-avoid-conflict-with-guava', 'com.google.guava.listenablefuture') 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('com.google.code.findbugs:jsr305', 'com.google.code.findbugs.jsr305')
module('j2objc-annotations-2.8.jar', 'com.google.j2objc.j2objc.annotations', '2.8') 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') { module('org.fxmisc.richtext:richtextfx', 'org.fxmisc.richtext') {
exports('org.fxmisc.richtext') exports('org.fxmisc.richtext')
exports('org.fxmisc.richtext.event') exports('org.fxmisc.richtext.event')
@ -401,10 +416,10 @@ extraJavaModuleInfo {
requires('javafx.graphics') requires('javafx.graphics')
requires('org.fxmisc.flowless') requires('org.fxmisc.flowless')
requires('org.reactfx.reactfx') requires('org.reactfx.reactfx')
requires('org.fxmisc.undo') requires('org.fxmisc.undo.undofx')
requires('org.fxmisc.wellbehaved') requires('org.fxmisc.wellbehaved')
} }
module('org.fxmisc.undo:undofx', 'org.fxmisc.undo') { module('org.fxmisc.undo:undofx', 'org.fxmisc.undo.undofx') {
requires('javafx.base') requires('javafx.base')
requires('javafx.controls') requires('javafx.controls')
requires('javafx.graphics') requires('javafx.graphics')

View file

@ -4,12 +4,13 @@ plugins {
dependencies { dependencies {
implementation 'com.google.gradle:osdetector-gradle-plugin:1.7.3' implementation 'com.google.gradle:osdetector-gradle-plugin:1.7.3'
implementation 'org.javamodularity:moduleplugin:1.8.14'
} }
repositories { repositories {
mavenCentral() mavenCentral()
maven { maven {
url = uri("https://plugins.gradle.org/m2/") url "https://plugins.gradle.org/m2/"
} }
} }

View file

@ -32,6 +32,7 @@ package org.openjfx.gradle;
import com.google.gradle.osdetector.OsDetectorPlugin; import com.google.gradle.osdetector.OsDetectorPlugin;
import org.gradle.api.Plugin; import org.gradle.api.Plugin;
import org.gradle.api.Project; import org.gradle.api.Project;
import org.javamodularity.moduleplugin.ModuleSystemPlugin;
import org.openjfx.gradle.tasks.ExecTask; import org.openjfx.gradle.tasks.ExecTask;
public class JavaFXPlugin implements Plugin<Project> { public class JavaFXPlugin implements Plugin<Project> {
@ -39,9 +40,10 @@ public class JavaFXPlugin implements Plugin<Project> {
@Override @Override
public void apply(Project project) { public void apply(Project project) {
project.getPlugins().apply(OsDetectorPlugin.class); project.getPlugins().apply(OsDetectorPlugin.class);
project.getPlugins().apply(ModuleSystemPlugin.class);
project.getExtensions().create("javafx", JavaFXOptions.class, project); project.getExtensions().create("javafx", JavaFXOptions.class, project);
project.getTasks().register("configJavafxRun", ExecTask.class, project); project.getTasks().create("configJavafxRun", ExecTask.class, project);
} }
} }

View file

@ -33,19 +33,27 @@ import org.gradle.api.DefaultTask;
import org.gradle.api.GradleException; import org.gradle.api.GradleException;
import org.gradle.api.Project; import org.gradle.api.Project;
import org.gradle.api.file.FileCollection; 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.plugins.ApplicationPlugin;
import org.gradle.api.tasks.JavaExec; import org.gradle.api.tasks.JavaExec;
import org.gradle.api.tasks.TaskAction; import org.gradle.api.tasks.TaskAction;
import org.javamodularity.moduleplugin.extensions.RunModuleOptions;
import org.openjfx.gradle.JavaFXModule; import org.openjfx.gradle.JavaFXModule;
import org.openjfx.gradle.JavaFXOptions; import org.openjfx.gradle.JavaFXOptions;
import org.openjfx.gradle.JavaFXPlatform; import org.openjfx.gradle.JavaFXPlatform;
import javax.inject.Inject; import javax.inject.Inject;
import java.io.File; import java.io.File;
import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.List;
import java.util.TreeSet; import java.util.TreeSet;
public class ExecTask extends DefaultTask { public class ExecTask extends DefaultTask {
private static final Logger LOGGER = Logging.getLogger(ExecTask.class);
private final Project project; private final Project project;
private JavaExec execTask; private JavaExec execTask;
@ -70,11 +78,37 @@ public class ExecTask extends DefaultTask {
var definedJavaFXModuleNames = new TreeSet<>(javaFXOptions.getModules()); var definedJavaFXModuleNames = new TreeSet<>(javaFXOptions.getModules());
if (!definedJavaFXModuleNames.isEmpty()) { if (!definedJavaFXModuleNames.isEmpty()) {
RunModuleOptions moduleOptions = execTask.getExtensions().findByType(RunModuleOptions.class);
final FileCollection classpathWithoutJavaFXJars = execTask.getClasspath().filter( final FileCollection classpathWithoutJavaFXJars = execTask.getClasspath().filter(
jar -> Arrays.stream(JavaFXModule.values()).noneMatch(javaFXModule -> jar.getName().contains(javaFXModule.getArtifactName())) jar -> Arrays.stream(JavaFXModule.values()).noneMatch(javaFXModule -> jar.getName().contains(javaFXModule.getArtifactName()))
); );
final FileCollection javaFXPlatformJars = execTask.getClasspath().filter(jar -> isJavaFXJar(jar, javaFXOptions.getPlatform())); final FileCollection javaFXPlatformJars = execTask.getClasspath().filter(jar -> isJavaFXJar(jar, javaFXOptions.getPlatform()));
execTask.setClasspath(classpathWithoutJavaFXJars.plus(javaFXPlatformJars));
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 { } else {
throw new GradleException("Run task not found. Please, make sure the Application plugin is applied"); throw new GradleException("Run task not found. Please, make sure the Application plugin is applied");

View file

@ -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: First, assign a temporary variable in your shell for the specific release you want to build. For the current one specify:
```shell ```shell
GIT_TAG="2.3.0" GIT_TAG="2.2.2"
``` ```
The project can then be initially cloned as follows: The project can then be initially cloned as follows:

2
drongo

@ -1 +1 @@
Subproject commit e975cbe6f8d8574785124e6db5780d0541e20024 Subproject commit 13e1fafbe8892d7005a043ae561e09ed66f7cea6

Binary file not shown.

View file

@ -1,6 +1,6 @@
distributionBase=GRADLE_USER_HOME distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-9.1.0-bin.zip distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-bin.zip
networkTimeout=10000 networkTimeout=10000
validateDistributionUrl=true validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME

15
gradlew vendored
View file

@ -1,7 +1,7 @@
#!/bin/sh #!/bin/sh
# #
# Copyright © 2015 the original authors. # Copyright © 2015-2021 the original authors.
# #
# Licensed under the Apache License, Version 2.0 (the "License"); # Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License. # you may not use this file except in compliance with the License.
@ -15,8 +15,6 @@
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
# #
# SPDX-License-Identifier: Apache-2.0
#
############################################################################## ##############################################################################
# #
@ -57,7 +55,7 @@
# Darwin, MinGW, and NonStop. # Darwin, MinGW, and NonStop.
# #
# (3) This script is generated from the Groovy template # (3) This script is generated from the Groovy template
# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt # https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# within the Gradle project. # within the Gradle project.
# #
# You can find Gradle at https://github.com/gradle/gradle/. # You can find Gradle at https://github.com/gradle/gradle/.
@ -86,7 +84,7 @@ done
# shellcheck disable=SC2034 # shellcheck disable=SC2034
APP_BASE_NAME=${0##*/} APP_BASE_NAME=${0##*/}
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) # Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit
# Use the maximum available, or set MAX_FD != -1 to use that value. # Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD=maximum MAX_FD=maximum
@ -114,6 +112,7 @@ case "$( uname )" in #(
NONSTOP* ) nonstop=true ;; NONSTOP* ) nonstop=true ;;
esac esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
# Determine the Java command to use to start the JVM. # Determine the Java command to use to start the JVM.
@ -171,6 +170,7 @@ fi
# For Cygwin or MSYS, switch paths to Windows format before running java # For Cygwin or MSYS, switch paths to Windows format before running java
if "$cygwin" || "$msys" ; then if "$cygwin" || "$msys" ; then
APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
JAVACMD=$( cygpath --unix "$JAVACMD" ) JAVACMD=$( cygpath --unix "$JAVACMD" )
@ -203,14 +203,15 @@ fi
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Collect all arguments for the java command: # Collect all arguments for the java command:
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, # * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
# and any embedded shellness will be escaped. # 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 # * 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. # treated as '${Hostname}' itself on the command line.
set -- \ set -- \
"-Dorg.gradle.appname=$APP_BASE_NAME" \ "-Dorg.gradle.appname=$APP_BASE_NAME" \
-jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \ -classpath "$CLASSPATH" \
org.gradle.wrapper.GradleWrapperMain \
"$@" "$@"
# Stop when "xargs" is not available. # Stop when "xargs" is not available.

25
gradlew.bat vendored
View file

@ -13,8 +13,6 @@
@rem See the License for the specific language governing permissions and @rem See the License for the specific language governing permissions and
@rem limitations under the License. @rem limitations under the License.
@rem @rem
@rem SPDX-License-Identifier: Apache-2.0
@rem
@if "%DEBUG%"=="" @echo off @if "%DEBUG%"=="" @echo off
@rem ########################################################################## @rem ##########################################################################
@ -45,11 +43,11 @@ set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1 %JAVA_EXE% -version >NUL 2>&1
if %ERRORLEVEL% equ 0 goto execute if %ERRORLEVEL% equ 0 goto execute
echo. 1>&2 echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
echo. 1>&2 echo.
echo Please set the JAVA_HOME variable in your environment to match the 1>&2 echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation. 1>&2 echo location of your Java installation.
goto fail goto fail
@ -59,21 +57,22 @@ set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto execute if exist "%JAVA_EXE%" goto execute
echo. 1>&2 echo.
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
echo. 1>&2 echo.
echo Please set the JAVA_HOME variable in your environment to match the 1>&2 echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation. 1>&2 echo location of your Java installation.
goto fail goto fail
:execute :execute
@rem Setup the command line @rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute Gradle @rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
:end :end
@rem End local scope for the variables with windows NT shell @rem End local scope for the variables with windows NT shell

2
lark

@ -1 +1 @@
Subproject commit 10e8d9cd4bbe9fde4dd93c059e2a9faeec6be3e0 Subproject commit 5facb25ede49c30650a8460dc04982650edb397f

View file

@ -21,7 +21,7 @@
<key>CFBundlePackageType</key> <key>CFBundlePackageType</key>
<string>APPL</string> <string>APPL</string>
<key>CFBundleShortVersionString</key> <key>CFBundleShortVersionString</key>
<string>2.3.1</string> <string>2.2.3</string>
<key>CFBundleSignature</key> <key>CFBundleSignature</key>
<string>????</string> <string>????</string>
<!-- See https://developer.apple.com/app-store/categories/ for list of AppStore categories --> <!-- See https://developer.apple.com/app-store/categories/ for list of AppStore categories -->
@ -33,8 +33,6 @@
<string>Copyright (C) 2021</string> <string>Copyright (C) 2021</string>
<key>NSHighResolutionCapable</key> <key>NSHighResolutionCapable</key>
<string>true</string> <string>true</string>
<key>NSCameraUseContinuityCameraDeviceType</key>
<true/>
<key>NSCameraUsageDescription</key> <key>NSCameraUsageDescription</key>
<string>Sparrow requires access to the camera in order to scan QR codes</string> <string>Sparrow requires access to the camera in order to scan QR codes</string>
<key>NSLocalNetworkUsageDescription</key> <key>NSLocalNetworkUsageDescription</key>

View file

@ -3,14 +3,13 @@ package com.sparrowwallet.sparrow;
import com.beust.jcommander.JCommander; import com.beust.jcommander.JCommander;
import com.google.common.eventbus.Subscribe; import com.google.common.eventbus.Subscribe;
import com.sparrowwallet.drongo.*; import com.sparrowwallet.drongo.*;
import com.sparrowwallet.drongo.address.Address;
import com.sparrowwallet.drongo.crypto.*; 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.policy.PolicyType;
import com.sparrowwallet.drongo.protocol.*; import com.sparrowwallet.drongo.protocol.*;
import com.sparrowwallet.drongo.psbt.*; import com.sparrowwallet.drongo.psbt.PSBT;
import com.sparrowwallet.drongo.silentpayments.SilentPaymentAddress; import com.sparrowwallet.drongo.psbt.PSBTInput;
import com.sparrowwallet.drongo.psbt.PSBTParseException;
import com.sparrowwallet.drongo.psbt.PSBTSignatureException;
import com.sparrowwallet.drongo.wallet.*; import com.sparrowwallet.drongo.wallet.*;
import com.sparrowwallet.hummingbird.UR; import com.sparrowwallet.hummingbird.UR;
import com.sparrowwallet.hummingbird.registry.CryptoPSBT; import com.sparrowwallet.hummingbird.registry.CryptoPSBT;
@ -31,7 +30,7 @@ import com.sparrowwallet.sparrow.transaction.TransactionView;
import com.sparrowwallet.sparrow.wallet.Entry; import com.sparrowwallet.sparrow.wallet.Entry;
import com.sparrowwallet.sparrow.wallet.WalletController; import com.sparrowwallet.sparrow.wallet.WalletController;
import com.sparrowwallet.sparrow.wallet.WalletForm; import com.sparrowwallet.sparrow.wallet.WalletForm;
import de.jangassen.MenuToolkit; import de.codecentric.centerdevice.MenuToolkit;
import javafx.animation.*; import javafx.animation.*;
import javafx.application.Platform; import javafx.application.Platform;
import javafx.beans.binding.Bindings; import javafx.beans.binding.Bindings;
@ -50,14 +49,12 @@ import javafx.geometry.Side;
import javafx.scene.Node; import javafx.scene.Node;
import javafx.scene.Scene; import javafx.scene.Scene;
import javafx.scene.control.*; import javafx.scene.control.*;
import javafx.scene.control.Label; import javafx.scene.image.Image;
import javafx.scene.control.Menu; import javafx.scene.image.ImageView;
import javafx.scene.control.MenuItem;
import javafx.scene.input.*; import javafx.scene.input.*;
import javafx.scene.layout.*; import javafx.scene.layout.*;
import javafx.scene.paint.Color; import javafx.scene.paint.Color;
import javafx.stage.*; import javafx.stage.*;
import javafx.stage.Window;
import javafx.util.Duration; import javafx.util.Duration;
import org.controlsfx.control.Notifications; import org.controlsfx.control.Notifications;
import org.controlsfx.control.StatusBar; import org.controlsfx.control.StatusBar;
@ -72,7 +69,6 @@ import java.nio.charset.StandardCharsets;
import java.nio.file.Files; import java.nio.file.Files;
import java.text.ParseException; import java.text.ParseException;
import java.util.*; import java.util.*;
import java.util.List;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import static com.sparrowwallet.sparrow.AppServices.*; import static com.sparrowwallet.sparrow.AppServices.*;
@ -826,10 +822,10 @@ public class AppController implements Initializable {
try(FileOutputStream outputStream = new FileOutputStream(file)) { try(FileOutputStream outputStream = new FileOutputStream(file)) {
if(asText) { if(asText) {
PrintWriter writer = new PrintWriter(new OutputStreamWriter(outputStream, StandardCharsets.UTF_8)); PrintWriter writer = new PrintWriter(new OutputStreamWriter(outputStream, StandardCharsets.UTF_8));
writer.print(transactionTabData.getPsbt().getForExport().toBase64String(includeXpubs)); writer.print(transactionTabData.getPsbt().toBase64String(includeXpubs));
writer.flush(); writer.flush();
} else { } else {
outputStream.write(transactionTabData.getPsbt().getForExport().serialize(includeXpubs, true)); outputStream.write(transactionTabData.getPsbt().serialize(includeXpubs, true));
} }
} catch(IOException e) { } catch(IOException e) {
log.error("Error saving PSBT", e); log.error("Error saving PSBT", e);
@ -852,7 +848,7 @@ public class AppController implements Initializable {
TabData tabData = (TabData)selectedTab.getUserData(); TabData tabData = (TabData)selectedTab.getUserData();
if(tabData.getType() == TabData.TabType.TRANSACTION) { if(tabData.getType() == TabData.TabType.TRANSACTION) {
TransactionTabData transactionTabData = (TransactionTabData)tabData; TransactionTabData transactionTabData = (TransactionTabData)tabData;
String data = asBase64 ? transactionTabData.getPsbt().getForExport().toBase64String() : transactionTabData.getPsbt().getForExport().toString(); String data = asBase64 ? transactionTabData.getPsbt().toBase64String() : transactionTabData.getPsbt().toString();
ClipboardContent content = new ClipboardContent(); ClipboardContent content = new ClipboardContent();
content.putString(data); content.putString(data);
@ -866,7 +862,7 @@ public class AppController implements Initializable {
if(tabData.getType() == TabData.TabType.TRANSACTION) { if(tabData.getType() == TabData.TabType.TRANSACTION) {
TransactionTabData transactionTabData = (TransactionTabData)tabData; TransactionTabData transactionTabData = (TransactionTabData)tabData;
byte[] psbtBytes = transactionTabData.getPsbt().getForExport().serialize(); byte[] psbtBytes = transactionTabData.getPsbt().serialize();
CryptoPSBT cryptoPSBT = new CryptoPSBT(psbtBytes); CryptoPSBT cryptoPSBT = new CryptoPSBT(psbtBytes);
BBQR bbqr = new BBQR(BBQRType.PSBT, psbtBytes); BBQR bbqr = new BBQR(BBQRType.PSBT, psbtBytes);
QRDisplayDialog qrDisplayDialog = new QRDisplayDialog(cryptoPSBT.toUR(), bbqr, false, true, false); QRDisplayDialog qrDisplayDialog = new QRDisplayDialog(cryptoPSBT.toUR(), bbqr, false, true, false);
@ -1038,10 +1034,6 @@ public class AppController implements Initializable {
cmd.add(System.getProperty(JPACKAGE_APP_PATH)); cmd.add(System.getProperty(JPACKAGE_APP_PATH));
cmd.addAll(args.toParams()); cmd.addAll(args.toParams());
final ProcessBuilder builder = new ProcessBuilder(cmd); final ProcessBuilder builder = new ProcessBuilder(cmd);
if(OsType.getCurrent() == OsType.UNIX) {
Map<String, String> env = builder.environment();
env.remove("LD_LIBRARY_PATH");
}
builder.start(); builder.start();
quit(event); quit(event);
} catch(Exception e) { } catch(Exception e) {
@ -1901,11 +1893,6 @@ public class AppController implements Initializable {
} }
private void addTransactionTab(String name, File file, PSBT psbt) { 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 //Add any missing previous outputs if available in open wallets
for(PSBTInput psbtInput : psbt.getPsbtInputs()) { for(PSBTInput psbtInput : psbt.getPsbtInputs()) {
if(psbtInput.getUtxo() == null) { if(psbtInput.getUtxo() == null) {
@ -1925,39 +1912,6 @@ 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); Window psbtWalletWindow = AppServices.get().getWindowForPSBT(psbt);
if(psbtWalletWindow != null && !tabs.getScene().getWindow().equals(psbtWalletWindow)) { if(psbtWalletWindow != null && !tabs.getScene().getWindow().equals(psbtWalletWindow)) {
EventManager.get().post(new ViewPSBTEvent(psbtWalletWindow, name, file, psbt)); EventManager.get().post(new ViewPSBTEvent(psbtWalletWindow, name, file, psbt));
@ -2096,33 +2050,23 @@ public class AppController implements Initializable {
} }
MenuItem moveRight = new MenuItem("Move Right"); MenuItem moveRight = new MenuItem("Move Right");
moveRight.setAccelerator(new KeyCodeCombination(KeyCode.RIGHT, KeyCombination.CONTROL_DOWN, KeyCombination.SHIFT_DOWN));
moveRight.setOnAction(event -> { moveRight.setOnAction(event -> {
int currentIndex = tabs.getSelectionModel().getSelectedIndex(); int index = tabs.getTabs().indexOf(tab);
if(currentIndex + 1 >= tabs.getTabs().size()) {
return;
}
Tab selectedTab = tabs.getSelectionModel().getSelectedItem();
tabs.getTabs().removeListener(tabsChangeListener); tabs.getTabs().removeListener(tabsChangeListener);
tabs.getTabs().remove(selectedTab); tabs.getTabs().remove(tab);
tabs.getTabs().add(currentIndex + 1, selectedTab); tabs.getTabs().add(index + 1, tab);
tabs.getTabs().addListener(tabsChangeListener); tabs.getTabs().addListener(tabsChangeListener);
tabs.getSelectionModel().select(selectedTab); tabs.getSelectionModel().select(tab);
EventManager.get().post(new RequestOpenWalletsEvent()); //Rearrange recent files list EventManager.get().post(new RequestOpenWalletsEvent()); //Rearrange recent files list
}); });
MenuItem moveLeft = new MenuItem("Move Left"); MenuItem moveLeft = new MenuItem("Move Left");
moveLeft.setAccelerator(new KeyCodeCombination(KeyCode.LEFT, KeyCombination.CONTROL_DOWN, KeyCombination.SHIFT_DOWN));
moveLeft.setOnAction(event -> { moveLeft.setOnAction(event -> {
int currentIndex = tabs.getSelectionModel().getSelectedIndex(); int index = tabs.getTabs().indexOf(tab);
if(currentIndex == 0) {
return;
}
Tab selectedTab = tabs.getSelectionModel().getSelectedItem();
tabs.getTabs().removeListener(tabsChangeListener); tabs.getTabs().removeListener(tabsChangeListener);
tabs.getTabs().remove(selectedTab); tabs.getTabs().remove(tab);
tabs.getTabs().add(currentIndex - 1, selectedTab); tabs.getTabs().add(index - 1, tab);
tabs.getTabs().addListener(tabsChangeListener); tabs.getTabs().addListener(tabsChangeListener);
tabs.getSelectionModel().select(selectedTab); tabs.getSelectionModel().select(tab);
EventManager.get().post(new RequestOpenWalletsEvent()); //Rearrange recent files list EventManager.get().post(new RequestOpenWalletsEvent()); //Rearrange recent files list
}); });
contextMenu.getItems().addAll(moveRight, moveLeft); contextMenu.getItems().addAll(moveRight, moveLeft);

View file

@ -91,7 +91,8 @@ public class AppServices {
private static final String TOR_DEFAULT_PROXY_CIRCUIT_ID = "default"; 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<Integer> TARGET_BLOCKS_RANGE = List.of(1, 2, 3, 4, 5, 10, 25, 50);
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 List<Long> LONG_FEE_RATES_RANGE = List.of(1L, 2L, 4L, 8L, 16L, 32L, 64L, 128L, 256L, 512L, 1024L, 2048L, 4096L, 8192L);
public static final List<Long> FEE_RATES_RANGE = LONG_FEE_RATES_RANGE.subList(0, LONG_FEE_RATES_RANGE.size() - 3);
public static final double FALLBACK_FEE_RATE = 20000d / 1000; public static final double FALLBACK_FEE_RATE = 20000d / 1000;
public static final double TESTNET_FALLBACK_FEE_RATE = 1000d / 1000; public static final double TESTNET_FALLBACK_FEE_RATE = 1000d / 1000;
@ -141,8 +142,6 @@ public class AppServices {
private static Double minimumRelayFeeRate; private static Double minimumRelayFeeRate;
private static Double serverMinimumRelayFeeRate;
private static CurrencyRate fiatCurrencyExchangeRate; private static CurrencyRate fiatCurrencyExchangeRate;
private static List<Device> devices; private static List<Device> devices;
@ -212,7 +211,6 @@ public class AppServices {
preventSleepService = createPreventSleepService(); preventSleepService = createPreventSleepService();
onlineProperty.addListener(onlineServicesListener); onlineProperty.addListener(onlineServicesListener);
minimumRelayFeeRate = getConfiguredMinimumRelayFeeRate(config);
if(config.getMode() == Mode.ONLINE) { if(config.getMode() == Mode.ONLINE) {
if(config.requiresInternalTor()) { if(config.requiresInternalTor()) {
@ -752,26 +750,6 @@ public class AppServices {
return Math.max(minRate, Transaction.DUST_RELAY_TX_FEE); 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() { public static Double getNextBlockMedianFeeRate() {
return nextBlockMedianFeeRate == null ? getDefaultFeeRate() : nextBlockMedianFeeRate; return nextBlockMedianFeeRate == null ? getDefaultFeeRate() : nextBlockMedianFeeRate;
} }
@ -810,18 +788,10 @@ 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() { public static Double getMinimumRelayFeeRate() {
return minimumRelayFeeRate == null ? Transaction.DEFAULT_MIN_RELAY_FEE : minimumRelayFeeRate; return minimumRelayFeeRate == null ? Transaction.DEFAULT_MIN_RELAY_FEE : minimumRelayFeeRate;
} }
public static Double getServerMinimumRelayFeeRate() {
return serverMinimumRelayFeeRate;
}
public static CurrencyRate getFiatCurrencyExchangeRate() { public static CurrencyRate getFiatCurrencyExchangeRate() {
return fiatCurrencyExchangeRate; return fiatCurrencyExchangeRate;
} }
@ -835,8 +805,8 @@ public class AppServices {
} }
public static void addPayjoinURI(BitcoinURI bitcoinURI) { public static void addPayjoinURI(BitcoinURI bitcoinURI) {
if(bitcoinURI.getPayjoinUrl() == null || bitcoinURI.getAddress() == null) { if(bitcoinURI.getPayjoinUrl() == null) {
throw new IllegalArgumentException("Not a valid payjoin URI"); throw new IllegalArgumentException("Not a payjoin URI");
} }
payjoinURIs.put(bitcoinURI.getAddress(), bitcoinURI); payjoinURIs.put(bitcoinURI.getAddress(), bitcoinURI);
} }
@ -1233,7 +1203,7 @@ public class AppServices {
} }
public static Font getMonospaceFont() { public static Font getMonospaceFont() {
return Font.font("Fragment Mono Regular", 13); return Font.font("Roboto Mono", 13);
} }
public static boolean isOnWayland() { public static boolean isOnWayland() {
@ -1249,10 +1219,7 @@ public class AppServices {
public void newConnection(ConnectionEvent event) { public void newConnection(ConnectionEvent event) {
currentBlockHeight = event.getBlockHeight(); currentBlockHeight = event.getBlockHeight();
System.setProperty(Network.BLOCK_HEIGHT_PROPERTY, Integer.toString(currentBlockHeight)); System.setProperty(Network.BLOCK_HEIGHT_PROPERTY, Integer.toString(currentBlockHeight));
if(getConfiguredMinimumRelayFeeRate(Config.get()) == null) { minimumRelayFeeRate = Math.max(event.getMinimumRelayFeeRate(), Transaction.DEFAULT_MIN_RELAY_FEE);
minimumRelayFeeRate = event.getMinimumRelayFeeRate() == null ? Transaction.DEFAULT_MIN_RELAY_FEE : event.getMinimumRelayFeeRate();
}
serverMinimumRelayFeeRate = event.getMinimumRelayFeeRate();
latestBlockHeader = event.getBlockHeader(); latestBlockHeader = event.getBlockHeader();
Config.get().addRecentServer(); Config.get().addRecentServer();

View file

@ -113,8 +113,8 @@ public class SparrowDesktop extends Application {
private void initializeFonts() { private void initializeFonts() {
GlyphFontRegistry.register(new FontAwesome5()); GlyphFontRegistry.register(new FontAwesome5());
GlyphFontRegistry.register(new FontAwesome5Brands()); GlyphFontRegistry.register(new FontAwesome5Brands());
Font.loadFont(AppServices.class.getResourceAsStream("/font/FragmentMono-Regular.ttf"), 13); Font.loadFont(AppServices.class.getResourceAsStream("/font/RobotoMono-Regular.ttf"), 13);
Font.loadFont(AppServices.class.getResourceAsStream("/font/FragmentMono-Italic.ttf"), 11); Font.loadFont(AppServices.class.getResourceAsStream("/font/RobotoMono-Italic.ttf"), 11);
if(OsType.getCurrent() == OsType.MACOS) { if(OsType.getCurrent() == OsType.MACOS) {
Font.loadFont(AppServices.class.getResourceAsStream("/font/LiberationSans-Regular.ttf"), 13); Font.loadFont(AppServices.class.getResourceAsStream("/font/LiberationSans-Regular.ttf"), 13);
} }

View file

@ -18,7 +18,7 @@ import java.util.*;
public class SparrowWallet { public class SparrowWallet {
public static final String APP_ID = "sparrow"; public static final String APP_ID = "sparrow";
public static final String APP_NAME = "Sparrow"; public static final String APP_NAME = "Sparrow";
public static final String APP_VERSION = "2.3.1"; public static final String APP_VERSION = "2.2.3";
public static final String APP_VERSION_SUFFIX = ""; public static final String APP_VERSION_SUFFIX = "";
public static final String APP_HOME_PROPERTY = "sparrow.home"; public static final String APP_HOME_PROPERTY = "sparrow.home";
public static final String NETWORK_ENV_PROPERTY = "SPARROW_NETWORK"; public static final String NETWORK_ENV_PROPERTY = "SPARROW_NETWORK";

View file

@ -87,8 +87,6 @@ class CoinCell extends TreeTableCell<Entry, Number> implements ConfirmationsList
} else if(entry instanceof UtxoEntry) { } else if(entry instanceof UtxoEntry) {
setGraphic(null); setGraphic(null);
} else if(entry instanceof HashIndexEntry) { } else if(entry instanceof HashIndexEntry) {
tooltip.hideConfirmations();
Region node = new Region(); Region node = new Region();
node.setPrefWidth(10); node.setPrefWidth(10);
setGraphic(node); setGraphic(node);
@ -150,14 +148,6 @@ class CoinCell extends TreeTableCell<Entry, Number> implements ConfirmationsList
setTooltipText(); setTooltipText();
} }
public void hideConfirmations() {
showConfirmations = false;
isCoinbase = false;
confirmationsProperty.unbind();
setTooltipText();
}
private void setTooltipText() { private void setTooltipText() {
setText(value + (showConfirmations ? " (" + getConfirmationsDescription() + ")" : "")); setText(value + (showConfirmations ? " (" + getConfirmationsDescription() + ")" : ""));
} }

View file

@ -6,16 +6,10 @@ import javafx.beans.property.SimpleObjectProperty;
import javafx.scene.Cursor; import javafx.scene.Cursor;
import javafx.scene.Node; import javafx.scene.Node;
import javafx.scene.control.ComboBox; 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.Region;
import javafx.scene.layout.StackPane; import javafx.scene.layout.StackPane;
import org.controlsfx.control.textfield.CustomTextField; import org.controlsfx.control.textfield.CustomTextField;
import java.util.List;
public class ComboBoxTextField extends CustomTextField { public class ComboBoxTextField extends CustomTextField {
private final ObjectProperty<ComboBox<?>> comboProperty = new SimpleObjectProperty<>(); private final ObjectProperty<ComboBox<?>> comboProperty = new SimpleObjectProperty<>();
@ -74,53 +68,4 @@ public class ComboBoxTextField extends CustomTextField {
public void setComboProperty(ComboBox<?> comboProperty) { public void setComboProperty(ComboBox<?> comboProperty) {
this.comboProperty.set(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);
});
}
}
} }

View file

@ -11,7 +11,6 @@ import javafx.scene.control.Tooltip;
import javafx.scene.input.Clipboard; import javafx.scene.input.Clipboard;
import javafx.scene.input.ClipboardContent; import javafx.scene.input.ClipboardContent;
import javafx.event.EventHandler; import javafx.event.EventHandler;
import javafx.scene.input.MouseButton;
import javafx.scene.input.MouseEvent; import javafx.scene.input.MouseEvent;
public class CopyableCoinLabel extends CopyableLabel { public class CopyableCoinLabel extends CopyableLabel {
@ -30,10 +29,6 @@ public class CopyableCoinLabel extends CopyableLabel {
valueProperty().addListener((observable, oldValue, newValue) -> setValueAsText((Long)newValue, Config.get().getUnitFormat(), Config.get().getBitcoinUnit())); valueProperty().addListener((observable, oldValue, newValue) -> setValueAsText((Long)newValue, Config.get().getUnitFormat(), Config.get().getBitcoinUnit()));
setOnMouseClicked(event -> { setOnMouseClicked(event -> {
if(!event.getButton().equals(MouseButton.PRIMARY)) {
return;
}
if(bitcoinUnit == null) { if(bitcoinUnit == null) {
bitcoinUnit = Config.get().getBitcoinUnit(); bitcoinUnit = Config.get().getBitcoinUnit();
} }

View file

@ -453,26 +453,20 @@ public class DevicePane extends TitledDescriptionPane {
}); });
vBox.getChildren().addAll(pinField, enterPinButton); vBox.getChildren().addAll(pinField, enterPinButton);
GridPane gridPane = new GridPane(); TilePane tilePane = new TilePane();
gridPane.setHgap(10); tilePane.setPrefColumns(3);
gridPane.setVgap(10); tilePane.setHgap(10);
gridPane.setMaxWidth(150); tilePane.setVgap(10);
gridPane.setMaxHeight(device.getModel().hasZeroInPin() ? 160 : 120); tilePane.setMaxWidth(150);
tilePane.setMaxHeight(120);
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}; int[] digits = new int[] {7, 8, 9, 4, 5, 6, 1, 2, 3};
for(int i = 0; i < digits.length; i++) { for(int i = 0; i < digits.length; i++) {
Button pinButton = new Button(); Button pinButton = new Button();
Glyph circle = new Glyph(FontAwesome5.FONT_NAME, "CIRCLE"); Glyph circle = new Glyph(FontAwesome5.FONT_NAME, "CIRCLE");
pinButton.setGraphic(circle); pinButton.setGraphic(circle);
pinButton.setUserData(digits[i]); pinButton.setUserData(digits[i]);
GridPane.setRowIndex(pinButton, i / 3); tilePane.getChildren().add(pinButton);
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 -> { pinButton.setOnAction(event -> {
pinField.setText(pinField.getText() + pinButton.getUserData()); pinField.setText(pinField.getText() + pinButton.getUserData());
}); });
@ -480,7 +474,7 @@ public class DevicePane extends TitledDescriptionPane {
HBox contentBox = new HBox(); HBox contentBox = new HBox();
contentBox.setSpacing(50); contentBox.setSpacing(50);
contentBox.getChildren().add(gridPane); contentBox.getChildren().add(tilePane);
contentBox.getChildren().add(vBox); contentBox.getChildren().add(vBox);
contentBox.setPadding(new Insets(10, 0, 10, 0)); contentBox.setPadding(new Insets(10, 0, 10, 0));
contentBox.setAlignment(Pos.TOP_CENTER); contentBox.setAlignment(Pos.TOP_CENTER);

View file

@ -5,8 +5,6 @@ import com.sparrowwallet.drongo.OsType;
import com.sparrowwallet.drongo.Utils; import com.sparrowwallet.drongo.Utils;
import com.sparrowwallet.drongo.address.Address; import com.sparrowwallet.drongo.address.Address;
import com.sparrowwallet.drongo.protocol.*; 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.drongo.wallet.*;
import com.sparrowwallet.sparrow.AppServices; import com.sparrowwallet.sparrow.AppServices;
import com.sparrowwallet.sparrow.EventManager; import com.sparrowwallet.sparrow.EventManager;
@ -57,7 +55,7 @@ public class EntryCell extends TreeTableCell<Entry, Entry> implements Confirmati
super.updateItem(entry, empty); 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) //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() && isTableSizeRecalculation()) { if(this == lastCell && !getTableRow().isVisible()) {
return; return;
} }
lastCell = this; lastCell = this;
@ -68,7 +66,8 @@ public class EntryCell extends TreeTableCell<Entry, Entry> implements Confirmati
setText(null); setText(null);
setGraphic(null); setGraphic(null);
} else { } else {
if(entry instanceof TransactionEntry transactionEntry) { if(entry instanceof TransactionEntry) {
TransactionEntry transactionEntry = (TransactionEntry)entry;
if(transactionEntry.getBlockTransaction().getHeight() == -1) { if(transactionEntry.getBlockTransaction().getHeight() == -1) {
setText("Unconfirmed Parent"); setText("Unconfirmed Parent");
setContextMenu(new UnconfirmedTransactionContextMenu(transactionEntry)); setContextMenu(new UnconfirmedTransactionContextMenu(transactionEntry));
@ -102,7 +101,7 @@ public class EntryCell extends TreeTableCell<Entry, Entry> implements Confirmati
actionBox.getChildren().add(viewTransactionButton); actionBox.getChildren().add(viewTransactionButton);
BlockTransaction blockTransaction = transactionEntry.getBlockTransaction(); BlockTransaction blockTransaction = transactionEntry.getBlockTransaction();
if(blockTransaction.getHeight() <= 0 && canRBF(blockTransaction, transactionEntry.getWallet()) && if(blockTransaction.getHeight() <= 0 && canRBF(blockTransaction) &&
Config.get().isIncludeMempoolOutputs() && transactionEntry.getWallet().allInputsFromWallet(blockTransaction.getHash())) { Config.get().isIncludeMempoolOutputs() && transactionEntry.getWallet().allInputsFromWallet(blockTransaction.getHash())) {
Button increaseFeeButton = new Button(""); Button increaseFeeButton = new Button("");
increaseFeeButton.setGraphic(getIncreaseFeeRBFGlyph()); increaseFeeButton.setGraphic(getIncreaseFeeRBFGlyph());
@ -122,7 +121,8 @@ public class EntryCell extends TreeTableCell<Entry, Entry> implements Confirmati
} }
setGraphic(actionBox); setGraphic(actionBox);
} else if(entry instanceof NodeEntry nodeEntry) { } else if(entry instanceof NodeEntry) {
NodeEntry nodeEntry = (NodeEntry)entry;
Address address = nodeEntry.getAddress(); Address address = nodeEntry.getAddress();
setText(address.toString()); setText(address.toString());
setContextMenu(new AddressContextMenu(address, nodeEntry.getOutputDescriptor(), nodeEntry, true, getTreeTableView())); setContextMenu(new AddressContextMenu(address, nodeEntry.getOutputDescriptor(), nodeEntry, true, getTreeTableView()));
@ -163,7 +163,8 @@ public class EntryCell extends TreeTableCell<Entry, Entry> implements Confirmati
setContextMenu(null); setContextMenu(null);
setGraphic(new HBox()); setGraphic(new HBox());
} }
} else if(entry instanceof HashIndexEntry hashIndexEntry) { } else if(entry instanceof HashIndexEntry) {
HashIndexEntry hashIndexEntry = (HashIndexEntry)entry;
setText(hashIndexEntry.getDescription()); setText(hashIndexEntry.getDescription());
setContextMenu(getTreeTableView().getStyleClass().contains("bip47") ? null : new HashIndexEntryContextMenu(getTreeTableView(), hashIndexEntry)); setContextMenu(getTreeTableView().getStyleClass().contains("bip47") ? null : new HashIndexEntryContextMenu(getTreeTableView(), hashIndexEntry));
Tooltip tooltip = new Tooltip(); Tooltip tooltip = new Tooltip();
@ -211,14 +212,13 @@ public class EntryCell extends TreeTableCell<Entry, Entry> implements Confirmati
private static void increaseFee(TransactionEntry transactionEntry, boolean cancelTransaction) { private static void increaseFee(TransactionEntry transactionEntry, boolean cancelTransaction) {
BlockTransaction blockTransaction = transactionEntry.getBlockTransaction(); BlockTransaction blockTransaction = transactionEntry.getBlockTransaction();
boolean silentPaymentTransaction = transactionEntry.getWallet().isSilentPaymentsTransaction(blockTransaction);
Map<BlockTransactionHashIndex, WalletNode> walletTxos = transactionEntry.getWallet().getWalletTxos(); Map<BlockTransactionHashIndex, WalletNode> walletTxos = transactionEntry.getWallet().getWalletTxos();
List<BlockTransactionHashIndex> utxos = transactionEntry.getChildren().stream() List<BlockTransactionHashIndex> utxos = transactionEntry.getChildren().stream()
.filter(e -> e instanceof HashIndexEntry) .filter(e -> e instanceof HashIndexEntry)
.map(e -> (HashIndexEntry)e) .map(e -> (HashIndexEntry)e)
.filter(e -> e.getType().equals(HashIndexEntry.Type.INPUT) && e.isSpendable()) .filter(e -> e.getType().equals(HashIndexEntry.Type.INPUT) && e.isSpendable())
.map(e -> blockTransaction.getTransaction().getInputs().get((int)e.getHashIndex().getIndex())) .map(e -> blockTransaction.getTransaction().getInputs().get((int)e.getHashIndex().getIndex()))
.filter(i -> Config.get().isMempoolFullRbf() || i.isReplaceByFeeEnabled() || silentPaymentTransaction) .filter(i -> Config.get().isMempoolFullRbf() || i.isReplaceByFeeEnabled())
.map(txInput -> walletTxos.keySet().stream().filter(txo -> txo.getHash().equals(txInput.getOutpoint().getHash()) && txo.getIndex() == txInput.getOutpoint().getIndex()).findFirst().get()) .map(txInput -> walletTxos.keySet().stream().filter(txo -> txo.getHash().equals(txInput.getOutpoint().getHash()) && txo.getIndex() == txInput.getOutpoint().getIndex()).findFirst().get())
.collect(Collectors.toList()); .collect(Collectors.toList());
@ -243,7 +243,6 @@ public class EntryCell extends TreeTableCell<Entry, Entry> implements Confirmati
.collect(Collectors.toList()); .collect(Collectors.toList());
boolean consolidationTransaction = consolidationOutputs.size() == blockTransaction.getTransaction().getOutputs().size() && consolidationOutputs.size() == 1; 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(); long changeTotal = ourOutputs.stream().mapToLong(TransactionOutput::getValue).sum() - consolidationOutputs.stream().mapToLong(TransactionOutput::getValue).sum();
Transaction tx = blockTransaction.getTransaction(); Transaction tx = blockTransaction.getTransaction();
double vSize = tx.getVirtualSize(); double vSize = tx.getVirtualSize();
@ -258,7 +257,7 @@ public class EntryCell extends TreeTableCell<Entry, Entry> implements Confirmati
List<OutputGroup> outputGroups = transactionEntry.getWallet().getGroupedUtxos(txoFilters, feeRate, AppServices.getMinimumRelayFeeRate(), Config.get().isGroupByAddress()) List<OutputGroup> outputGroups = transactionEntry.getWallet().getGroupedUtxos(txoFilters, feeRate, AppServices.getMinimumRelayFeeRate(), Config.get().isGroupByAddress())
.stream().filter(outputGroup -> outputGroup.getEffectiveValue() >= 0).collect(Collectors.toList()); .stream().filter(outputGroup -> outputGroup.getEffectiveValue() >= 0).collect(Collectors.toList());
Collections.shuffle(outputGroups); Collections.shuffle(outputGroups);
while((double)changeTotal / vSize < getMaxFeeRate() && !outputGroups.isEmpty() && !cancelTransaction && !consolidationTransaction && safeToAddInputsOrOutputs) { while((double)changeTotal / vSize < getMaxFeeRate() && !outputGroups.isEmpty() && !cancelTransaction && !consolidationTransaction) {
//If there is insufficient change output, include another random output group so the fee can be increased //If there is insufficient change output, include another random output group so the fee can be increased
OutputGroup outputGroup = outputGroups.remove(0); OutputGroup outputGroup = outputGroups.remove(0);
for(BlockTransactionHashIndex utxo : outputGroup.getUtxos()) { for(BlockTransactionHashIndex utxo : outputGroup.getUtxos()) {
@ -299,13 +298,9 @@ public class EntryCell extends TreeTableCell<Entry, Entry> implements Confirmati
label += " (Replaced By Fee)"; label += " (Replaced By Fee)";
} }
Address address = txOutput.getScript().getToAddress(); if(txOutput.getScript().getToAddress() != null) {
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 //Disable change creation by enabling max payment when there is only one output and no additional UTXOs included
boolean sendMax = blockTransaction.getTransaction().getOutputs().size() == 1 && rbfChange == 0; return new Payment(txOutput.getScript().getToAddress(), label, txOutput.getValue(), 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; return null;
@ -342,7 +337,7 @@ public class EntryCell extends TreeTableCell<Entry, Entry> implements Confirmati
} }
EventManager.get().post(new SendActionEvent(transactionEntry.getWallet(), utxos)); 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, safeToAddInputsOrOutputs))); Platform.runLater(() -> EventManager.get().post(new SpendUtxoEvent(transactionEntry.getWallet(), utxos, payments, opReturns.isEmpty() ? null : opReturns, rbfFee, true, blockTransaction)));
} }
private static Double getMaxFeeRate() { private static Double getMaxFeeRate() {
@ -399,11 +394,11 @@ public class EntryCell extends TreeTableCell<Entry, Entry> implements Confirmati
Payment payment = new Payment(freshAddress, label, inputTotal, true); Payment payment = new Payment(freshAddress, label, inputTotal, true);
EventManager.get().post(new SendActionEvent(transactionEntry.getWallet(), utxos)); 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, true))); Platform.runLater(() -> EventManager.get().post(new SpendUtxoEvent(transactionEntry.getWallet(), utxos, List.of(payment), null, blockTransaction.getFee(), true, null)));
} }
private static boolean canRBF(BlockTransaction blockTransaction, Wallet wallet) { private static boolean canRBF(BlockTransaction blockTransaction) {
return Config.get().isMempoolFullRbf() || blockTransaction.getTransaction().isReplaceByFee() || wallet.isSilentPaymentsTransaction(blockTransaction); return Config.get().isMempoolFullRbf() || blockTransaction.getTransaction().isReplaceByFee();
} }
private static boolean canSignMessage(WalletNode walletNode) { private static boolean canSignMessage(WalletNode walletNode) {
@ -481,7 +476,7 @@ public class EntryCell extends TreeTableCell<Entry, Entry> implements Confirmati
tooltip += "\nFee rate: " + String.format("%.2f", feeRate) + " sats/vB"; tooltip += "\nFee rate: " + String.format("%.2f", feeRate) + " sats/vB";
} }
tooltip += "\nRBF: " + (canRBF(transactionEntry.getBlockTransaction(), transactionEntry.getWallet()) ? "Enabled" : "Disabled"); tooltip += "\nRBF: " + (canRBF(transactionEntry.getBlockTransaction()) ? "Enabled" : "Disabled");
} }
return tooltip; return tooltip;
@ -549,7 +544,6 @@ public class EntryCell extends TreeTableCell<Entry, Entry> implements Confirmati
private static class UnconfirmedTransactionContextMenu extends ContextMenu { private static class UnconfirmedTransactionContextMenu extends ContextMenu {
public UnconfirmedTransactionContextMenu(TransactionEntry transactionEntry) { public UnconfirmedTransactionContextMenu(TransactionEntry transactionEntry) {
Wallet wallet = transactionEntry.getWallet();
BlockTransaction blockTransaction = transactionEntry.getBlockTransaction(); BlockTransaction blockTransaction = transactionEntry.getBlockTransaction();
MenuItem viewTransaction = new MenuItem("View Transaction"); MenuItem viewTransaction = new MenuItem("View Transaction");
viewTransaction.setGraphic(getViewTransactionGlyph()); viewTransaction.setGraphic(getViewTransactionGlyph());
@ -559,7 +553,7 @@ public class EntryCell extends TreeTableCell<Entry, Entry> implements Confirmati
}); });
getItems().add(viewTransaction); getItems().add(viewTransaction);
if(canRBF(blockTransaction, wallet) && Config.get().isIncludeMempoolOutputs() && transactionEntry.getWallet().allInputsFromWallet(blockTransaction.getHash())) { if(canRBF(blockTransaction) && Config.get().isIncludeMempoolOutputs() && transactionEntry.getWallet().allInputsFromWallet(blockTransaction.getHash())) {
MenuItem increaseFee = new MenuItem("Increase Fee (RBF)"); MenuItem increaseFee = new MenuItem("Increase Fee (RBF)");
increaseFee.setGraphic(getIncreaseFeeRBFGlyph()); increaseFee.setGraphic(getIncreaseFeeRBFGlyph());
increaseFee.setOnAction(AE -> { increaseFee.setOnAction(AE -> {
@ -570,7 +564,7 @@ public class EntryCell extends TreeTableCell<Entry, Entry> implements Confirmati
getItems().add(increaseFee); getItems().add(increaseFee);
} }
if(canRBF(blockTransaction, wallet) && Config.get().isIncludeMempoolOutputs() && transactionEntry.getWallet().allInputsFromWallet(blockTransaction.getHash())) { if(canRBF(blockTransaction) && Config.get().isIncludeMempoolOutputs() && transactionEntry.getWallet().allInputsFromWallet(blockTransaction.getHash())) {
MenuItem cancelTx = new MenuItem("Cancel Transaction (RBF)"); MenuItem cancelTx = new MenuItem("Cancel Transaction (RBF)");
cancelTx.setGraphic(getCancelTransactionRBFGlyph()); cancelTx.setGraphic(getCancelTransactionRBFGlyph());
cancelTx.setOnAction(AE -> { cancelTx.setOnAction(AE -> {
@ -856,11 +850,4 @@ 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")));
}
} }

View file

@ -1,6 +1,5 @@
package com.sparrowwallet.sparrow.control; package com.sparrowwallet.sparrow.control;
import com.sparrowwallet.drongo.protocol.Transaction;
import com.sparrowwallet.sparrow.AppServices; import com.sparrowwallet.sparrow.AppServices;
import com.sparrowwallet.sparrow.net.FeeRatesSource; import com.sparrowwallet.sparrow.net.FeeRatesSource;
import javafx.application.Platform; import javafx.application.Platform;
@ -8,7 +7,6 @@ import javafx.scene.Node;
import javafx.scene.control.Slider; import javafx.scene.control.Slider;
import javafx.util.StringConverter; import javafx.util.StringConverter;
import java.text.DecimalFormat;
import java.util.*; import java.util.*;
import java.util.stream.Collectors; import java.util.stream.Collectors;
@ -16,11 +14,9 @@ import static com.sparrowwallet.sparrow.AppServices.*;
public class FeeRangeSlider extends Slider { public class FeeRangeSlider extends Slider {
private static final double FEE_RATE_SCROLL_INCREMENT = 0.01; 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() { public FeeRangeSlider() {
super(0, AppServices.getFeeRatesRange().size() - 1, 0); super(0, FEE_RATES_RANGE.size() - 1, 0);
setMajorTickUnit(1); setMajorTickUnit(1);
setMinorTickCount(0); setMinorTickCount(0);
setSnapToTicks(false); setSnapToTicks(false);
@ -31,11 +27,11 @@ public class FeeRangeSlider extends Slider {
setLabelFormatter(new StringConverter<>() { setLabelFormatter(new StringConverter<>() {
@Override @Override
public String toString(Double object) { public String toString(Double object) {
Double feeRate = AppServices.getLongFeeRatesRange().get(object.intValue()); Long feeRate = LONG_FEE_RATES_RANGE.get(object.intValue());
if(isLongFeeRange() && feeRate >= 1000) { if(isLongFeeRange() && feeRate >= 1000) {
return INTEGER_FEE_RATE_FORMAT.format(feeRate / 1000) + "k"; return feeRate / 1000 + "k";
} }
return feeRate > 0d && feeRate < Transaction.DEFAULT_MIN_RELAY_FEE ? FRACTIONAL_FEE_RATE_FORMAT.format(feeRate) : INTEGER_FEE_RATE_FORMAT.format(feeRate); return Long.toString(feeRate);
} }
@Override @Override
@ -55,10 +51,10 @@ public class FeeRangeSlider extends Slider {
setOnScroll(event -> { setOnScroll(event -> {
if(event.getDeltaY() != 0) { if(event.getDeltaY() != 0) {
double newFeeRate = getFeeRate() + (event.getDeltaY() > 0 ? FEE_RATE_SCROLL_INCREMENT : -FEE_RATE_SCROLL_INCREMENT); double newFeeRate = getFeeRate() + (event.getDeltaY() > 0 ? FEE_RATE_SCROLL_INCREMENT : -FEE_RATE_SCROLL_INCREMENT);
if(newFeeRate < AppServices.getLongFeeRatesRange().getFirst()) { if(newFeeRate < LONG_FEE_RATES_RANGE.get(0)) {
newFeeRate = AppServices.getLongFeeRatesRange().getFirst(); newFeeRate = LONG_FEE_RATES_RANGE.get(0);
} else if(newFeeRate > AppServices.getLongFeeRatesRange().getLast()) { } else if(newFeeRate > LONG_FEE_RATES_RANGE.get(LONG_FEE_RATES_RANGE.size() - 1)) {
newFeeRate = AppServices.getLongFeeRatesRange().getLast(); newFeeRate = LONG_FEE_RATES_RANGE.get(LONG_FEE_RATES_RANGE.size() - 1);
} }
setFeeRate(newFeeRate); setFeeRate(newFeeRate);
} }
@ -66,79 +62,27 @@ public class FeeRangeSlider extends Slider {
} }
public double getFeeRate() { public double getFeeRate() {
return getFeeRate(AppServices.getMinimumRelayFeeRate()); return Math.pow(2.0, getValue());
}
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) { public void setFeeRate(double feeRate) {
setFeeRate(feeRate, AppServices.getMinimumRelayFeeRate()); double value = Math.log(feeRate) / Math.log(2);
}
public void setFeeRate(double feeRate, Double minRelayFeeRate) {
double value = getValue(feeRate, minRelayFeeRate);
updateMaxFeeRange(value); updateMaxFeeRange(value);
setValue(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) { private void updateMaxFeeRange(double value) {
if(value >= getMax() && !isLongFeeRange()) { if(value >= getMax() && !isLongFeeRange()) {
if(AppServices.getMinimumRelayFeeRate() < Transaction.DEFAULT_MIN_RELAY_FEE) { setMax(LONG_FEE_RATES_RANGE.size() - 1);
setMin(1.0d);
}
setMax(AppServices.getLongFeeRatesRange().size() - 1);
updateTrackHighlight(); updateTrackHighlight();
} else if(value == getMin() && isLongFeeRange()) { } else if(value == getMin() && isLongFeeRange()) {
if(AppServices.getMinimumRelayFeeRate() < Transaction.DEFAULT_MIN_RELAY_FEE) { setMax(FEE_RATES_RANGE.size() - 1);
setMin(0.0d);
}
setMax(AppServices.getFeeRatesRange().size() - 1);
updateTrackHighlight(); updateTrackHighlight();
} }
} }
public boolean isLongFeeRange() { private boolean isLongFeeRange() {
return getMax() > AppServices.getFeeRatesRange().size() - 1; return getMax() > FEE_RATES_RANGE.size() - 1;
} }
public void updateTrackHighlight() { public void updateTrackHighlight() {
@ -193,9 +137,9 @@ public class FeeRangeSlider extends Slider {
} }
private int getPercentageOfFeeRange(Double feeRate) { private int getPercentageOfFeeRange(Double feeRate) {
double index = getValue(feeRate, AppServices.getMinimumRelayFeeRate()); double index = Math.log(feeRate) / Math.log(2);
if(isLongFeeRange()) { if(isLongFeeRange()) {
index *= ((double)AppServices.getFeeRatesRange().size() / (AppServices.getLongFeeRatesRange().size())) * 0.99; index *= ((double)FEE_RATES_RANGE.size() / (LONG_FEE_RATES_RANGE.size())) * 0.99;
} }
return (int)Math.round(index * 10.0); return (int)Math.round(index * 10.0);
} }

View file

@ -398,14 +398,14 @@ public class PrivateKeySweepDialog extends Dialog<Transaction> {
double feeRate = feeRange.getFeeRate(); double feeRate = feeRange.getFeeRate();
long fee = (long)Math.ceil(noFeeTransaction.getVirtualSize() * feeRate); long fee = (long)Math.ceil(noFeeTransaction.getVirtualSize() * feeRate);
if(feeRate == AppServices.getMinimumRelayFeeRate() && feeRate > 0d) { if(feeRate == Transaction.DEFAULT_MIN_RELAY_FEE) {
fee++; fee++;
} }
long dustThreshold = destAddress.getScriptType().getDustThreshold(sweepOutput, Transaction.DUST_RELAY_TX_FEE); long dustThreshold = destAddress.getScriptType().getDustThreshold(sweepOutput, Transaction.DUST_RELAY_TX_FEE);
if(total - fee <= dustThreshold) { if(total - fee <= dustThreshold) {
feeRate = AppServices.getMinimumRelayFeeRate(); feeRate = Transaction.DEFAULT_MIN_RELAY_FEE;
fee = (long)Math.ceil(noFeeTransaction.getVirtualSize() * feeRate) + (feeRate > 0d ? 1 : 0); fee = (long)Math.ceil(noFeeTransaction.getVirtualSize() * feeRate) + 1;
if(total - fee <= dustThreshold) { if(total - fee <= dustThreshold) {
AppServices.showErrorDialog("Insufficient funds", "The unspent outputs for this private key contain insufficient funds to spend (" + total + " sats)."); AppServices.showErrorDialog("Insufficient funds", "The unspent outputs for this private key contain insufficient funds to spend (" + total + " sats).");

View file

@ -189,7 +189,6 @@ public class QRScanDialog extends Dialog<QRScanDialog.Result> {
}); });
webcamDeviceProperty.addListener((_, _, newValue) -> { webcamDeviceProperty.addListener((_, _, newValue) -> {
Config.get().setWebcamDevice(newValue.getName()); Config.get().setWebcamDevice(newValue.getName());
Config.get().setWebcamDeviceId(newValue.getUniqueId());
if(!Objects.equals(webcamService.getDevice(), newValue)) { if(!Objects.equals(webcamService.getDevice(), newValue)) {
webcamService.cancel(); webcamService.cancel();
} }

View file

@ -2,7 +2,6 @@ package com.sparrowwallet.sparrow.control;
import com.sparrowwallet.drongo.protocol.Script; import com.sparrowwallet.drongo.protocol.Script;
import com.sparrowwallet.drongo.protocol.ScriptChunk; import com.sparrowwallet.drongo.protocol.ScriptChunk;
import com.sparrowwallet.drongo.protocol.ScriptOpCodes;
import javafx.geometry.Pos; import javafx.geometry.Pos;
import org.controlsfx.control.decoration.Decorator; import org.controlsfx.control.decoration.Decorator;
import org.controlsfx.control.decoration.GraphicDecoration; import org.controlsfx.control.decoration.GraphicDecoration;
@ -54,11 +53,7 @@ public class ScriptArea extends CodeArea {
for (int i = 0; i < script.getChunks().size(); i++) { for (int i = 0; i < script.getChunks().size(); i++) {
ScriptChunk chunk = script.getChunks().get(i); ScriptChunk chunk = script.getChunks().get(i);
if(chunk.isOpCode()) { if(chunk.isOpCode()) {
if(chunk.getOpcode() == ScriptOpCodes.OP_0 && witnessScript != null) { append(chunk.toString(), "script-opcode");
append("<empty>", "script-other");
} else {
append(chunk.toString(), "script-opcode");
}
} else if(chunk.isPubKey()) { } else if(chunk.isPubKey()) {
append("<pubkey" + pubKeyCount++ + ">", "script-pubkey"); append("<pubkey" + pubKeyCount++ + ">", "script-pubkey");
} else if(chunk.isSignature()) { } else if(chunk.isSignature()) {

View file

@ -5,26 +5,12 @@ import com.sparrowwallet.drongo.BitcoinUnit;
import com.sparrowwallet.drongo.OsType; import com.sparrowwallet.drongo.OsType;
import com.sparrowwallet.drongo.address.Address; import com.sparrowwallet.drongo.address.Address;
import com.sparrowwallet.drongo.address.InvalidAddressException; 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.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.drongo.wallet.Payment;
import com.sparrowwallet.sparrow.AppServices; import com.sparrowwallet.sparrow.AppServices;
import com.sparrowwallet.sparrow.EventManager; import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
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.FXCollections;
import javafx.collections.ObservableList; import javafx.collections.ObservableList;
import javafx.concurrent.Service;
import javafx.concurrent.Task;
import javafx.event.ActionEvent;
import javafx.scene.Node; import javafx.scene.Node;
import javafx.scene.control.*; import javafx.scene.control.*;
import javafx.scene.input.Clipboard; import javafx.scene.input.Clipboard;
@ -32,20 +18,19 @@ import javafx.scene.layout.StackPane;
import javafx.stage.FileChooser; import javafx.stage.FileChooser;
import javafx.util.StringConverter; import javafx.util.StringConverter;
import org.controlsfx.control.spreadsheet.*; import org.controlsfx.control.spreadsheet.*;
import org.controlsfx.glyphfont.Glyph;
import java.io.*; import java.io.*;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.Optional;
import java.util.concurrent.ExecutionException;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import java.util.stream.IntStream; import java.util.stream.IntStream;
public class SendToManyDialog extends Dialog<List<Payment>> { public class SendToManyDialog extends Dialog<List<Payment>> {
private final BitcoinUnit bitcoinUnit; private final BitcoinUnit bitcoinUnit;
private final SpreadsheetView spreadsheetView; private final SpreadsheetView spreadsheetView;
public static final SendToAddressCellType SEND_TO_ADDRESS = new SendToAddressCellType(); public static final AddressCellType ADDRESS = new AddressCellType();
public SendToManyDialog(BitcoinUnit bitcoinUnit, List<Payment> payments) { public SendToManyDialog(BitcoinUnit bitcoinUnit, List<Payment> payments) {
this.bitcoinUnit = bitcoinUnit; this.bitcoinUnit = bitcoinUnit;
@ -83,16 +68,14 @@ public class SendToManyDialog extends Dialog<List<Payment>> {
dialogPane.setContent(stackPane); dialogPane.setContent(stackPane);
dialogPane.getButtonTypes().addAll(ButtonType.OK, ButtonType.CANCEL); 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); final ButtonType loadCsvButtonType = new javafx.scene.control.ButtonType("Load CSV", ButtonBar.ButtonData.LEFT);
dialogPane.getButtonTypes().add(loadCsvButtonType); dialogPane.getButtonTypes().add(loadCsvButtonType);
setResultConverter((_) -> null); setResultConverter((dialogButton) -> {
ButtonBar.ButtonData data = dialogButton == null ? null : dialogButton.getButtonData();
return data == ButtonBar.ButtonData.OK_DONE ? getPayments() : null;
});
dialogPane.setPrefWidth(850); dialogPane.setPrefWidth(850);
dialogPane.setPrefHeight(500); dialogPane.setPrefHeight(500);
@ -102,24 +85,18 @@ public class SendToManyDialog extends Dialog<List<Payment>> {
} }
private Grid getGrid(List<Payment> payments) { private Grid getGrid(List<Payment> payments) {
return createGrid(payments.stream().map(payment -> new SendToPayment(payment, SendToAddress.fromPayment(payment))).collect(Collectors.toList())); int rowCount = payments.size();
}
private Grid createGrid(List<SendToPayment> sendToPayments) {
int rowCount = sendToPayments.size();
int columnCount = 3; int columnCount = 3;
GridBase grid = new GridBase(rowCount, columnCount); GridBase grid = new GridBase(rowCount, columnCount);
ObservableList<ObservableList<SpreadsheetCell>> rows = FXCollections.observableArrayList(); ObservableList<ObservableList<SpreadsheetCell>> rows = FXCollections.observableArrayList();
for(int row = 0; row < grid.getRowCount(); ++row) { for(int row = 0; row < grid.getRowCount(); ++row) {
SendToPayment sendToPayment = sendToPayments.get(row);
final ObservableList<SpreadsheetCell> list = FXCollections.observableArrayList(); final ObservableList<SpreadsheetCell> list = FXCollections.observableArrayList();
SendToAddress sendToAddress = sendToPayment.sendToAddress(); SpreadsheetCell addressCell = ADDRESS.createCell(row, 0, 1, 1, payments.get(row).getAddress());
SpreadsheetCell addressCell = SEND_TO_ADDRESS.createCell(row, 0, 1, 1, sendToAddress);
addressCell.getStyleClass().add("fixed-width"); addressCell.getStyleClass().add("fixed-width");
list.add(addressCell); list.add(addressCell);
double amount = (double)sendToPayment.payment().getAmount(); double amount = (double)payments.get(row).getAmount();
if(bitcoinUnit == BitcoinUnit.BTC) { if(bitcoinUnit == BitcoinUnit.BTC) {
amount = amount / Transaction.SATOSHIS_PER_BITCOIN; amount = amount / Transaction.SATOSHIS_PER_BITCOIN;
} }
@ -131,7 +108,7 @@ public class SendToManyDialog extends Dialog<List<Payment>> {
} }
list.add(amountCell); list.add(amountCell);
list.add(SpreadsheetCellType.STRING.createCell(row, 2, 1, 1, sendToPayment.payment().getLabel())); list.add(SpreadsheetCellType.STRING.createCell(row, 2, 1, 1, payments.get(row).getLabel()));
rows.add(list); rows.add(list);
} }
grid.setRows(rows); grid.setRows(rows);
@ -140,49 +117,32 @@ public class SendToManyDialog extends Dialog<List<Payment>> {
return grid; return grid;
} }
private void getPayments() { private List<Payment> getPayments() {
if(needsResolution() && Config.get().hasServer() && !AppServices.isConnected() && !AppServices.isConnecting()) { List<Payment> payments = new ArrayList<>();
if(Config.get().getConnectToResolve() == null || Config.get().getConnectToResolve() == Boolean.FALSE) { Grid grid = spreadsheetView.getGrid();
Platform.runLater(() -> { String firstLabel = null;
ConfirmationAlert confirmationAlert = new ConfirmationAlert("Connect to resolve?", "You are currently offline. Connect to resolve the addresses?", ButtonType.NO, ButtonType.YES); for(int row = 0; row < grid.getRowCount(); row++) {
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); ObservableList<SpreadsheetCell> rowCells = spreadsheetView.getItems().get(row);
SendToAddress sendToAddress = (SendToAddress)rowCells.getFirst().getItem(); Address address = (Address)rowCells.get(0).getItem();
if(sendToAddress.hrn != null && DnsPaymentCache.getDnsPayment(sendToAddress.hrn) == null) { Double value = (Double)rowCells.get(1).getItem();
return true; 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));
} }
} }
return false; return payments;
} }
private class SendToManyDialogPane extends DialogPane { private class SendToManyDialogPane extends DialogPane {
@ -192,7 +152,7 @@ public class SendToManyDialog extends Dialog<List<Payment>> {
if(buttonType.getButtonData() == ButtonBar.ButtonData.LEFT) { if(buttonType.getButtonData() == ButtonBar.ButtonData.LEFT) {
Button loadButton = new Button(buttonType.getText()); Button loadButton = new Button(buttonType.getText());
loadButton.setGraphicTextGap(5); loadButton.setGraphicTextGap(5);
loadButton.setGraphic(GlyphUtils.getUpArrowGlyph()); loadButton.setGraphic(getGlyph(FontAwesome5.Glyph.ARROW_UP));
final ButtonBar.ButtonData buttonData = buttonType.getButtonData(); final ButtonBar.ButtonData buttonData = buttonType.getButtonData();
ButtonBar.setButtonData(loadButton, buttonData); ButtonBar.setButtonData(loadButton, buttonData);
loadButton.setOnAction(event -> { loadButton.setOnAction(event -> {
@ -207,7 +167,7 @@ public class SendToManyDialog extends Dialog<List<Payment>> {
File file = fileChooser.showOpenDialog(this.getScene().getWindow()); File file = fileChooser.showOpenDialog(this.getScene().getWindow());
if(file != null) { if(file != null) {
try { try {
List<SendToPayment> csvPayments = new ArrayList<>(); List<Payment> csvPayments = new ArrayList<>();
try(Reader reader = new FileReader(file, StandardCharsets.UTF_8)) { try(Reader reader = new FileReader(file, StandardCharsets.UTF_8)) {
CsvReader csvReader = new CsvReader(reader); CsvReader csvReader = new CsvReader(reader);
while(csvReader.readRecord()) { while(csvReader.readRecord()) {
@ -223,22 +183,9 @@ public class SendToManyDialog extends Dialog<List<Payment>> {
} else { } else {
amount = Long.parseLong(csvReader.get(1).replace(",", "")); amount = Long.parseLong(csvReader.get(1).replace(",", ""));
} }
Address address = Address.fromString(csvReader.get(0));
String label = csvReader.get(2); String label = csvReader.get(2);
Optional<String> optDnsPaymentHrn = DnsPayment.getHrn(csvReader.get(0)); csvPayments.add(new Payment(address, label, amount, false));
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) { } catch(NumberFormatException e) {
//ignore and continue - probably a header line //ignore and continue - probably a header line
} catch(InvalidAddressException e) { } catch(InvalidAddressException e) {
@ -251,7 +198,7 @@ public class SendToManyDialog extends Dialog<List<Payment>> {
return; return;
} }
spreadsheetView.setGrid(createGrid(csvPayments)); spreadsheetView.setGrid(getGrid(csvPayments));
} }
} catch(IOException e) { } catch(IOException e) {
AppServices.showErrorDialog("Cannot load CSV", e.getMessage()); AppServices.showErrorDialog("Cannot load CSV", e.getMessage());
@ -266,18 +213,24 @@ public class SendToManyDialog extends Dialog<List<Payment>> {
return button; return button;
} }
private Glyph getGlyph(FontAwesome5.Glyph glyphName) {
Glyph glyph = new Glyph(FontAwesome5.FONT_NAME, glyphName);
glyph.setFontSize(11);
return glyph;
}
} }
public static class SendToAddressCellType extends SpreadsheetCellType<SendToAddress> { public static class AddressCellType extends SpreadsheetCellType<Address> {
public SendToAddressCellType() { public AddressCellType() {
this(new StringConverterWithFormat<>(new SendToAddressStringConverter()) { this(new StringConverterWithFormat<>(new AddressStringConverter()) {
@Override @Override
public String toString(SendToAddress item) { public String toString(Address item) {
return toStringFormat(item, ""); //$NON-NLS-1$ return toStringFormat(item, ""); //$NON-NLS-1$
} }
@Override @Override
public SendToAddress fromString(String str) { public Address fromString(String str) {
if(str == null || str.isEmpty()) { //$NON-NLS-1$ if(str == null || str.isEmpty()) { //$NON-NLS-1$
return null; return null;
} else { } else {
@ -286,7 +239,7 @@ public class SendToManyDialog extends Dialog<List<Payment>> {
} }
@Override @Override
public String toStringFormat(SendToAddress item, String format) { public String toStringFormat(Address item, String format) {
try { try {
if(item == null) { if(item == null) {
return ""; //$NON-NLS-1$ return ""; //$NON-NLS-1$
@ -300,7 +253,7 @@ public class SendToManyDialog extends Dialog<List<Payment>> {
}); });
} }
public SendToAddressCellType(StringConverter<SendToAddress> converter) { public AddressCellType(StringConverter<Address> converter) {
super(converter); super(converter);
} }
@ -310,7 +263,7 @@ public class SendToManyDialog extends Dialog<List<Payment>> {
} }
public SpreadsheetCell createCell(final int row, final int column, final int rowSpan, final int columnSpan, public SpreadsheetCell createCell(final int row, final int column, final int rowSpan, final int columnSpan,
final SendToAddress value) { final Address value) {
SpreadsheetCell cell = new SpreadsheetCellBase(row, column, rowSpan, columnSpan, this); SpreadsheetCell cell = new SpreadsheetCellBase(row, column, rowSpan, columnSpan, this);
cell.setItem(value); cell.setItem(value);
return cell; return cell;
@ -323,7 +276,7 @@ public class SendToManyDialog extends Dialog<List<Payment>> {
@Override @Override
public boolean match(Object value, Object... options) { public boolean match(Object value, Object... options) {
if(value instanceof SendToAddress) if(value instanceof Address)
return true; return true;
else { else {
try { try {
@ -336,9 +289,9 @@ public class SendToManyDialog extends Dialog<List<Payment>> {
} }
@Override @Override
public SendToAddress convertValue(Object value) { public Address convertValue(Object value) {
if(value instanceof SendToAddress) if(value instanceof Address)
return (SendToAddress)value; return (Address)value;
else { else {
try { try {
return converter.fromString(value == null ? null : value.toString()); return converter.fromString(value == null ? null : value.toString());
@ -349,155 +302,13 @@ public class SendToManyDialog extends Dialog<List<Payment>> {
} }
@Override @Override
public String toString(SendToAddress item) { public String toString(Address item) {
return converter.toString(item); return converter.toString(item);
} }
@Override @Override
public String toString(SendToAddress item, String format) { public String toString(Address item, String format) {
return ((StringConverterWithFormat<SendToAddress>)converter).toStringFormat(item, format); return ((StringConverterWithFormat<Address>)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) {}
} }

View file

@ -4,12 +4,8 @@ import com.sparrowwallet.drongo.KeyPurpose;
import com.sparrowwallet.drongo.OsType; import com.sparrowwallet.drongo.OsType;
import com.sparrowwallet.drongo.address.Address; import com.sparrowwallet.drongo.address.Address;
import com.sparrowwallet.drongo.bip47.PaymentCode; 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.Sha256Hash;
import com.sparrowwallet.drongo.protocol.TransactionOutput; 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.uri.BitcoinURI;
import com.sparrowwallet.drongo.wallet.*; import com.sparrowwallet.drongo.wallet.*;
import com.sparrowwallet.sparrow.*; import com.sparrowwallet.sparrow.*;
@ -26,7 +22,6 @@ import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleObjectProperty; import javafx.beans.property.SimpleObjectProperty;
import javafx.embed.swing.SwingFXUtils; import javafx.embed.swing.SwingFXUtils;
import javafx.event.EventHandler; import javafx.event.EventHandler;
import javafx.geometry.HPos;
import javafx.geometry.Insets; import javafx.geometry.Insets;
import javafx.geometry.Pos; import javafx.geometry.Pos;
import javafx.scene.Group; import javafx.scene.Group;
@ -108,7 +103,6 @@ public class TransactionDiagram extends GridPane {
expandedDiagram.setId("transactionDiagram"); expandedDiagram.setId("transactionDiagram");
expandedDiagram.setExpanded(true); expandedDiagram.setExpanded(true);
expandedDiagram.setFinal(isFinal()); expandedDiagram.setFinal(isFinal());
expandedDiagram.setMaxWidth(AppServices.getActiveWindow().getWidth() - 200);
updateDerivedDiagram(expandedDiagram); updateDerivedDiagram(expandedDiagram);
HBox buttonBox = new HBox(); HBox buttonBox = new HBox();
@ -126,7 +120,7 @@ public class TransactionDiagram extends GridPane {
AppServices.setStageIcon(stage); AppServices.setStageIcon(stage);
stage.setScene(scene); stage.setScene(scene);
stage.setOnShowing(e -> { stage.setOnShowing(e -> {
AppServices.moveToActiveWindowScreen(stage, expandedDiagram.getMaxWidth(), 460); AppServices.moveToActiveWindowScreen(stage, 600, 460);
}); });
stage.setOnHidden(e -> { stage.setOnHidden(e -> {
expandedDiagram = null; expandedDiagram = null;
@ -143,39 +137,6 @@ 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) { public void update(WalletTransaction walletTx) {
setMinHeight(getDiagramHeight()); setMinHeight(getDiagramHeight());
setMaxHeight(getDiagramHeight()); setMaxHeight(getDiagramHeight());
@ -204,7 +165,7 @@ public class TransactionDiagram extends GridPane {
VBox messagePane = new VBox(); VBox messagePane = new VBox();
messagePane.setPrefHeight(getDiagramHeight()); messagePane.setPrefHeight(getDiagramHeight());
messagePane.setPadding(new Insets(0, 10, 0, 10)); messagePane.setPadding(new Insets(0, 10, 0, 280));
messagePane.setAlignment(Pos.CENTER); messagePane.setAlignment(Pos.CENTER);
messagePane.getChildren().add(createSpacer()); messagePane.getChildren().add(createSpacer());
@ -264,6 +225,7 @@ public class TransactionDiagram extends GridPane {
GridPane.setConstraints(outputsPane, 5, 0); GridPane.setConstraints(outputsPane, 5, 0);
getChildren().clear(); getChildren().clear();
getChildren().addAll(inputsTypePane, inputsPane, inputsLinesPane, txPane, outputsLinesPane, outputsPane);
List<Payment> userPayments = getUserPayments(); List<Payment> userPayments = getUserPayments();
if(!isFinal() && userPayments.size() > 1) { if(!isFinal() && userPayments.size() > 1) {
@ -272,8 +234,6 @@ public class TransactionDiagram extends GridPane {
getChildren().add(totalsPane); getChildren().add(totalsPane);
} }
getChildren().addAll(inputsTypePane, inputsPane, inputsLinesPane, txPane, outputsLinesPane, outputsPane);
if(contextMenu == null) { if(contextMenu == null) {
contextMenu = new ContextMenu(); contextMenu = new ContextMenu();
MenuItem menuItem = new MenuItem("Save as Image..."); MenuItem menuItem = new MenuItem("Save as Image...");
@ -447,6 +407,8 @@ public class TransactionDiagram extends GridPane {
private Pane getInputsLabels(List<Map<BlockTransactionHashIndex, WalletNode>> displayedUtxoSets) { private Pane getInputsLabels(List<Map<BlockTransactionHashIndex, WalletNode>> displayedUtxoSets) {
VBox inputsBox = new VBox(); VBox inputsBox = new VBox();
inputsBox.setMaxWidth(isExpanded() ? 300 : 150);
inputsBox.setPrefWidth(isExpanded() ? 230 : 150);
inputsBox.setPadding(new Insets(0, 10, 0, 10)); inputsBox.setPadding(new Insets(0, 10, 0, 10));
inputsBox.minHeightProperty().bind(minHeightProperty()); inputsBox.minHeightProperty().bind(minHeightProperty());
inputsBox.setAlignment(Pos.BASELINE_RIGHT); inputsBox.setAlignment(Pos.BASELINE_RIGHT);
@ -678,8 +640,7 @@ public class TransactionDiagram extends GridPane {
double width = 140.0; double width = 140.0;
long sum = walletTx.getTotal(); long sum = walletTx.getTotal();
List<Long> values = walletTx.getOutputs().stream().filter(output -> !(output instanceof WalletTransaction.NonAddressOutput)) List<Long> values = walletTx.getTransaction().getOutputs().stream().filter(txo -> txo.getScript().getToAddress() != null).map(TransactionOutput::getValue).collect(Collectors.toList());
.map(output -> output.getTransactionOutput().getValue()).collect(Collectors.toList());
values.add(walletTx.getFee()); values.add(walletTx.getFee());
int numOutputs = displayedPayments.size() + walletTx.getChangeMap().size() + 1; int numOutputs = displayedPayments.size() + walletTx.getChangeMap().size() + 1;
for(int i = 1; i <= numOutputs; i++) { for(int i = 1; i <= numOutputs; i++) {
@ -715,6 +676,8 @@ public class TransactionDiagram extends GridPane {
private Pane getOutputsLabels(List<Payment> displayedPayments) { private Pane getOutputsLabels(List<Payment> displayedPayments) {
VBox outputsBox = new VBox(); VBox outputsBox = new VBox();
outputsBox.setMaxWidth(isExpanded() ? 350 : 150);
outputsBox.setPrefWidth(isExpanded() ? 230 : 150);
outputsBox.setPadding(new Insets(0, 20, 0, 10)); outputsBox.setPadding(new Insets(0, 20, 0, 10));
outputsBox.setAlignment(Pos.BASELINE_LEFT); outputsBox.setAlignment(Pos.BASELINE_LEFT);
outputsBox.getChildren().add(createSpacer()); outputsBox.getChildren().add(createSpacer());
@ -723,16 +686,15 @@ public class TransactionDiagram extends GridPane {
for(Payment payment : displayedPayments) { for(Payment payment : displayedPayments) {
Glyph outputGlyph = GlyphUtils.getOutputGlyph(walletTx, payment); 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; 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.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.getAddress().toString().substring(0, 8) + "..." : payment.getLabel(), outputGlyph);
recipientLabel.getStyleClass().add("output-label"); recipientLabel.getStyleClass().add("output-label");
recipientLabel.getStyleClass().add(labelledPayment ? "payment-label" : "recipient-label"); recipientLabel.getStyleClass().add(labelledPayment ? "payment-label" : "recipient-label");
Wallet toWallet = walletTx.getToWallet(AppServices.get().getOpenWallets().keySet(), payment); Wallet toWallet = walletTx.getToWallet(AppServices.get().getOpenWallets().keySet(), payment);
WalletNode toNode = payment instanceof WalletNodePayment walletNodePayment ? walletNodePayment.getWalletNode() : null; WalletNode toNode = walletTx.getWallet() != null && !walletTx.getWallet().isBip47() ? walletTx.getAddressNodeMap().get(payment.getAddress()) : null;
Wallet toBip47Wallet = getBip47SendWallet(payment); Wallet toBip47Wallet = getBip47SendWallet(payment);
DnsPayment dnsPayment = DnsPaymentCache.getDnsPayment(payment);
Tooltip recipientTooltip = new Tooltip((toWallet == null ? (toNode != null ? "Consolidate " : "Pay ") : "Receive ") Tooltip recipientTooltip = new Tooltip((toWallet == null ? (toNode != null ? "Consolidate " : "Pay ") : "Receive ")
+ getSatsValue(payment.getAmount()) + " sats to " + getSatsValue(payment.getAmount()) + " sats to "
+ (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()) + (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())
+ (walletTx.isDuplicateAddress(payment) ? " (Duplicate)" : "")); + (walletTx.isDuplicateAddress(payment) ? " (Duplicate)" : ""));
recipientTooltip.getStyleClass().add("recipient-label"); recipientTooltip.getStyleClass().add("recipient-label");
recipientTooltip.setShowDelay(new Duration(TOOLTIP_SHOW_DELAY)); recipientTooltip.setShowDelay(new Duration(TOOLTIP_SHOW_DELAY));
@ -757,13 +719,9 @@ public class TransactionDiagram extends GridPane {
paymentBox.getChildren().addAll(region, amountLabel); paymentBox.getChildren().addAll(region, amountLabel);
} }
if(payment instanceof SilentPayment silentPayment) { Wallet bip47Wallet = toWallet != null && toWallet.isBip47() ? toWallet : (toBip47Wallet != null && toBip47Wallet.isBip47() ? toBip47Wallet : null);
outputNodes.add(new OutputNode(paymentBox, silentPayment.isAddressComputed() ? silentPayment.getAddress() : null, payment.getAmount(), null, silentPayment.getSilentPaymentAddress())); PaymentCode paymentCode = bip47Wallet == null ? null : bip47Wallet.getKeystores().getFirst().getExternalPaymentCode();
} else { outputNodes.add(new OutputNode(paymentBox, payment.getAddress(), payment.getAmount(), paymentCode));
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, null));
}
} }
Set<Integer> seenIndexes = new HashSet<>(); Set<Integer> seenIndexes = new HashSet<>();
@ -827,7 +785,7 @@ public class TransactionDiagram extends GridPane {
outputsBox.getChildren().add(outputNode.outputLabel); outputsBox.getChildren().add(outputNode.outputLabel);
outputsBox.getChildren().add(createSpacer()); outputsBox.getChildren().add(createSpacer());
ContextMenu contextMenu = new LabelContextMenu(outputNode.address, outputNode.amount, outputNode.paymentCode, outputNode.silentPaymentAddress); ContextMenu contextMenu = new LabelContextMenu(outputNode.address, outputNode.amount, outputNode.paymentCode);
if(!outputNode.outputLabel.getChildren().isEmpty() && outputNode.outputLabel.getChildren().get(0) instanceof Label outputLabelControl) { if(!outputNode.outputLabel.getChildren().isEmpty() && outputNode.outputLabel.getChildren().get(0) instanceof Label outputLabelControl) {
outputLabelControl.setContextMenu(contextMenu); outputLabelControl.setContextMenu(contextMenu);
} }
@ -1002,11 +960,8 @@ public class TransactionDiagram extends GridPane {
} }
private int getOutputIndex(Address address, long amount, Collection<Integer> seenIndexes) { private int getOutputIndex(Address address, long amount, Collection<Integer> seenIndexes) {
List<TransactionOutput> addressOutputs = walletTx.getOutputs().stream().filter(output -> !(output instanceof WalletTransaction.NonAddressOutput)) List<TransactionOutput> addressOutputs = walletTx.getTransaction().getOutputs().stream().filter(txOutput -> txOutput.getScript().getToAddress() != null).collect(Collectors.toList());
.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();
TransactionOutput output = addressOutputs.stream()
.filter(txOutput -> address.equals(txOutput.getScript().getToAddress()) && txOutput.getValue() == amount && !seenIndexes.contains(txOutput.getIndex()))
.findFirst().orElseThrow();
return addressOutputs.indexOf(output); return addressOutputs.indexOf(output);
} }
@ -1156,7 +1111,7 @@ public class TransactionDiagram extends GridPane {
} }
public String toString() { public String toString() {
return additionalPayments.stream().map(Payment::toString).collect(Collectors.joining("\n")); return additionalPayments.stream().map(payment -> payment.getAddress().toString()).collect(Collectors.joining("\n"));
} }
} }
@ -1165,27 +1120,25 @@ public class TransactionDiagram extends GridPane {
public Address address; public Address address;
public long amount; public long amount;
public PaymentCode paymentCode; public PaymentCode paymentCode;
public SilentPaymentAddress silentPaymentAddress;
public OutputNode(Pane outputLabel, Address address, long amount) { public OutputNode(Pane outputLabel, Address address, long amount) {
this(outputLabel, address, amount, null, null); this(outputLabel, address, amount, null);
} }
public OutputNode(Pane outputLabel, Address address, long amount, PaymentCode paymentCode, SilentPaymentAddress silentPaymentAddress) { public OutputNode(Pane outputLabel, Address address, long amount, PaymentCode paymentCode) {
this.outputLabel = outputLabel; this.outputLabel = outputLabel;
this.address = address; this.address = address;
this.amount = amount; this.amount = amount;
this.paymentCode = paymentCode; this.paymentCode = paymentCode;
this.silentPaymentAddress = silentPaymentAddress;
} }
} }
private class LabelContextMenu extends ContextMenu { private class LabelContextMenu extends ContextMenu {
public LabelContextMenu(Address address, long value) { public LabelContextMenu(Address address, long value) {
this(address, value, null, null); this(address, value, null);
} }
public LabelContextMenu(Address address, long value, PaymentCode paymentCode, SilentPaymentAddress silentPaymentAddress) { public LabelContextMenu(Address address, long value, PaymentCode paymentCode) {
if(address != null) { if(address != null) {
MenuItem copyAddress = new MenuItem("Copy Address"); MenuItem copyAddress = new MenuItem("Copy Address");
copyAddress.setOnAction(event -> { copyAddress.setOnAction(event -> {
@ -1233,17 +1186,6 @@ public class TransactionDiagram extends GridPane {
}); });
getItems().add(copyPaymentCode); 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);
}
} }
} }
} }

View file

@ -90,20 +90,20 @@ public class TransactionDiagramLabel extends HBox {
outputLabels.add(mixOutputLabel); outputLabels.add(mixOutputLabel);
} }
} else if(walletTx.getPayments().size() >= 5 && walletTx.getPayments().stream().mapToLong(Payment::getAmount).distinct().count() <= 1 && walletTx.getWallet() != null } 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.getWalletNodePayments().isEmpty()) { && walletTx.getWallet().getStandardAccountType() == StandardAccount.WHIRLPOOL_POSTMIX && walletTx.getPayments().stream().anyMatch(walletTx::isConsolidationSend)) {
OutputLabel remixOutputLabel = getRemixOutputLabel(transactionDiagram, walletTx.getPayments()); OutputLabel remixOutputLabel = getRemixOutputLabel(transactionDiagram, walletTx.getPayments());
if(remixOutputLabel != null) { if(remixOutputLabel != null) {
outputLabels.add(remixOutputLabel); outputLabels.add(remixOutputLabel);
} }
} else { } else {
List<Payment> payments = walletTx.getExternalPayments().stream().filter(payment -> payment.getType() == Payment.Type.DEFAULT).collect(Collectors.toList()); List<Payment> payments = walletTx.getPayments().stream().filter(payment -> payment.getType() == Payment.Type.DEFAULT && !walletTx.isConsolidationSend(payment)).collect(Collectors.toList());
List<OutputLabel> paymentLabels = payments.stream().map(payment -> getOutputLabel(transactionDiagram, payment)).collect(Collectors.toList()); List<OutputLabel> paymentLabels = payments.stream().map(payment -> getOutputLabel(transactionDiagram, payment)).collect(Collectors.toList());
if(walletTx.getSelectedUtxos().values().stream().allMatch(Objects::isNull)) { if(walletTx.getSelectedUtxos().values().stream().allMatch(Objects::isNull)) {
paymentLabels.sort(Comparator.comparingInt(paymentLabel -> (paymentLabel.text.startsWith("Receive") ? 0 : 1))); paymentLabels.sort(Comparator.comparingInt(paymentLabel -> (paymentLabel.text.startsWith("Receive") ? 0 : 1)));
} }
outputLabels.addAll(paymentLabels); outputLabels.addAll(paymentLabels);
List<Payment> consolidations = walletTx.getWalletNodePayments().stream().filter(payment -> payment.getType() == Payment.Type.DEFAULT).collect(Collectors.toList()); List<Payment> consolidations = walletTx.getPayments().stream().filter(payment -> payment.getType() == Payment.Type.DEFAULT && walletTx.isConsolidationSend(payment)).collect(Collectors.toList());
outputLabels.addAll(consolidations.stream().map(consolidation -> getOutputLabel(transactionDiagram, consolidation)).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()); 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) { private OutputLabel getOutputLabel(TransactionDiagram transactionDiagram, Payment payment) {
WalletTransaction walletTx = transactionDiagram.getWalletTransaction(); WalletTransaction walletTx = transactionDiagram.getWalletTransaction();
Wallet toWallet = walletTx.getToWallet(AppServices.get().getOpenWallets().keySet(), payment); Wallet toWallet = walletTx.getToWallet(AppServices.get().getOpenWallets().keySet(), payment);
WalletNode toNode = payment instanceof WalletNodePayment walletNodePayment ? walletNodePayment.getWalletNode() : null; WalletNode toNode = walletTx.getWallet() != null && !walletTx.getWallet().isBip47() ? walletTx.getAddressNodeMap().get(payment.getAddress()) : null;
Glyph glyph = GlyphUtils.getOutputGlyph(transactionDiagram.getWalletTransaction(), payment); Glyph glyph = GlyphUtils.getOutputGlyph(transactionDiagram.getWalletTransaction(), payment);
String text = (toWallet == null ? (toNode != null ? "Consolidate " : "Pay ") : "Receive ") + transactionDiagram.getSatsValue(payment.getAmount()) + " sats to " + payment; String text = (toWallet == null ? (toNode != null ? "Consolidate " : "Pay ") : "Receive ") + transactionDiagram.getSatsValue(payment.getAmount()) + " sats to " + payment.getAddress().toString();
return getOutputLabel(glyph, text); return getOutputLabel(glyph, text);
} }
@ -240,7 +240,7 @@ public class TransactionDiagramLabel extends HBox {
icon.setGraphic(glyph); icon.setGraphic(glyph);
CopyableLabel label = new CopyableLabel(); CopyableLabel label = new CopyableLabel();
label.setFont(Font.font("Fragment Mono Italic", 13)); label.setFont(Font.font("Roboto Mono Italic", 13));
label.setText(text); label.setText(text);
HBox output = new HBox(5); HBox output = new HBox(5);

View file

@ -7,7 +7,6 @@ public enum WebcamPixelFormat {
PIX_FMT_RGB24("RGB3", true), PIX_FMT_RGB24("RGB3", true),
PIX_FMT_YUYV("YUYV", true), PIX_FMT_YUYV("YUYV", true),
PIX_FMT_NV12("NV12", true), PIX_FMT_NV12("NV12", true),
PIX_FMT_YU12("YU12", true),
PIX_FMT_MJPG("MJPG", true); PIX_FMT_MJPG("MJPG", true);
private final String name; private final String name;

View file

@ -7,7 +7,6 @@ import com.google.zxing.qrcode.QRCodeReader;
import com.sparrowwallet.bokmakierie.Bokmakierie; import com.sparrowwallet.bokmakierie.Bokmakierie;
import com.sparrowwallet.drongo.OsType; import com.sparrowwallet.drongo.OsType;
import com.sparrowwallet.sparrow.io.Config; import com.sparrowwallet.sparrow.io.Config;
import com.sparrowwallet.sparrow.io.ZBar;
import javafx.beans.property.BooleanProperty; import javafx.beans.property.BooleanProperty;
import javafx.beans.property.ObjectProperty; import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleBooleanProperty; import javafx.beans.property.SimpleBooleanProperty;
@ -16,6 +15,7 @@ import javafx.concurrent.ScheduledService;
import javafx.concurrent.Task; import javafx.concurrent.Task;
import javafx.embed.swing.SwingFXUtils; import javafx.embed.swing.SwingFXUtils;
import javafx.scene.image.Image; import javafx.scene.image.Image;
import net.sourceforge.zbar.ZBar;
import org.openpnp.capture.*; import org.openpnp.capture.*;
import org.openpnp.capture.library.OpenpnpCaptureLibrary; import org.openpnp.capture.library.OpenpnpCaptureLibrary;
import org.slf4j.Logger; import org.slf4j.Logger;
@ -137,20 +137,14 @@ public class WebcamService extends ScheduledService<Image> {
if(device != null) { if(device != null) {
for(CaptureDevice webcam : availableDevices) { for(CaptureDevice webcam : availableDevices) {
if(webcam.equals(device)) { if(webcam.getName().equals(device.getName())) {
selectedDevice = webcam; selectedDevice = webcam;
break;
} }
} }
} else if(Config.get().getWebcamDevice() != null) { } else if(Config.get().getWebcamDevice() != null) {
for(CaptureDevice webcam : availableDevices) { for(CaptureDevice webcam : availableDevices) {
if(webcam.getUniqueId().equals(Config.get().getWebcamDeviceId())) {
selectedDevice = webcam;
break;
}
if(webcam.getName().equals(Config.get().getWebcamDevice())) { if(webcam.getName().equals(Config.get().getWebcamDevice())) {
selectedDevice = webcam; selectedDevice = webcam;
break;
} }
} }
} }
@ -319,6 +313,9 @@ public class WebcamService extends ScheduledService<Image> {
} }
private Result readQR(BufferedImage bufferedImage) { private Result readQR(BufferedImage bufferedImage) {
LuminanceSource source = new BufferedImageLuminanceSource(bufferedImage);
BinaryBitmap bitmap = new BinaryBitmap(new HybridBinarizer(source));
try { try {
com.sparrowwallet.bokmakierie.Result result = bokmakierie.scan(bufferedImage); com.sparrowwallet.bokmakierie.Result result = bokmakierie.scan(bufferedImage);
if(result != null) { if(result != null) {
@ -336,8 +333,6 @@ public class WebcamService extends ScheduledService<Image> {
} }
try { try {
LuminanceSource source = new BufferedImageLuminanceSource(bufferedImage);
BinaryBitmap bitmap = new BinaryBitmap(new HybridBinarizer(source));
return qrReader.decode(bitmap, Map.of(DecodeHintType.TRY_HARDER, Boolean.TRUE)); return qrReader.decode(bitmap, Map.of(DecodeHintType.TRY_HARDER, Boolean.TRUE));
} catch(ReaderException e) { } catch(ReaderException e) {
// fall thru, it means there is no QR code in image // fall thru, it means there is no QR code in image
@ -449,27 +444,10 @@ public class WebcamService extends ScheduledService<Image> {
} }
public static <T extends Enum<T>> T getNearestEnum(T target, T[] values) { public static <T extends Enum<T>> T getNearestEnum(T target, T[] values) {
if(values == null || values.length == 0) { int ordinal = target.ordinal();
return null; 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);
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 { private static class CroppedDimension {

View file

@ -1,7 +1,6 @@
package com.sparrowwallet.sparrow.event; package com.sparrowwallet.sparrow.event;
import com.sparrowwallet.drongo.protocol.BlockHeader; import com.sparrowwallet.drongo.protocol.BlockHeader;
import com.sparrowwallet.sparrow.AppServices;
import com.sparrowwallet.sparrow.net.MempoolRateSize; import com.sparrowwallet.sparrow.net.MempoolRateSize;
import java.util.List; import java.util.List;
@ -14,7 +13,6 @@ public class ConnectionEvent extends FeeRatesUpdatedEvent {
private final int blockHeight; private final int blockHeight;
private final BlockHeader blockHeader; private final BlockHeader blockHeader;
private final Double minimumRelayFeeRate; 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) { public ConnectionEvent(List<String> serverVersion, String serverBanner, int blockHeight, BlockHeader blockHeader, Map<Integer, Double> targetBlockFeeRates, Set<MempoolRateSize> mempoolRateSizes, Double minimumRelayFeeRate) {
super(targetBlockFeeRates, mempoolRateSizes); super(targetBlockFeeRates, mempoolRateSizes);
@ -23,7 +21,6 @@ public class ConnectionEvent extends FeeRatesUpdatedEvent {
this.blockHeight = blockHeight; this.blockHeight = blockHeight;
this.blockHeader = blockHeader; this.blockHeader = blockHeader;
this.minimumRelayFeeRate = minimumRelayFeeRate; this.minimumRelayFeeRate = minimumRelayFeeRate;
this.previousMinimumRelayFeeRate = AppServices.getMinimumRelayFeeRate();
} }
public List<String> getServerVersion() { public List<String> getServerVersion() {
@ -45,8 +42,4 @@ public class ConnectionEvent extends FeeRatesUpdatedEvent {
public Double getMinimumRelayFeeRate() { public Double getMinimumRelayFeeRate() {
return minimumRelayFeeRate; return minimumRelayFeeRate;
} }
public Double getPreviousMinimumRelayFeeRate() {
return previousMinimumRelayFeeRate;
}
} }

View file

@ -17,13 +17,12 @@ public class SpendUtxoEvent {
private final boolean requireAllUtxos; private final boolean requireAllUtxos;
private final BlockTransaction replacedTransaction; private final BlockTransaction replacedTransaction;
private final PaymentCode paymentCode; private final PaymentCode paymentCode;
private final boolean allowPaymentChanges;
public SpendUtxoEvent(Wallet wallet, List<BlockTransactionHashIndex> utxos) { public SpendUtxoEvent(Wallet wallet, List<BlockTransactionHashIndex> utxos) {
this(wallet, utxos, null, null, null, false, null, true); this(wallet, utxos, null, null, null, false, null);
} }
public SpendUtxoEvent(Wallet wallet, List<BlockTransactionHashIndex> utxos, List<Payment> payments, List<byte[]> opReturns, Long fee, boolean requireAllUtxos, BlockTransaction replacedTransaction, boolean allowPaymentChanges) { public SpendUtxoEvent(Wallet wallet, List<BlockTransactionHashIndex> utxos, List<Payment> payments, List<byte[]> opReturns, Long fee, boolean requireAllUtxos, BlockTransaction replacedTransaction) {
this.wallet = wallet; this.wallet = wallet;
this.utxos = utxos; this.utxos = utxos;
this.payments = payments; this.payments = payments;
@ -32,7 +31,6 @@ public class SpendUtxoEvent {
this.requireAllUtxos = requireAllUtxos; this.requireAllUtxos = requireAllUtxos;
this.replacedTransaction = replacedTransaction; this.replacedTransaction = replacedTransaction;
this.paymentCode = null; this.paymentCode = null;
this.allowPaymentChanges = allowPaymentChanges;
} }
public SpendUtxoEvent(Wallet wallet, List<Payment> payments, List<byte[]> opReturns, PaymentCode paymentCode) { public SpendUtxoEvent(Wallet wallet, List<Payment> payments, List<byte[]> opReturns, PaymentCode paymentCode) {
@ -44,7 +42,6 @@ public class SpendUtxoEvent {
this.requireAllUtxos = false; this.requireAllUtxos = false;
this.replacedTransaction = null; this.replacedTransaction = null;
this.paymentCode = paymentCode; this.paymentCode = paymentCode;
this.allowPaymentChanges = false;
} }
public Wallet getWallet() { public Wallet getWallet() {
@ -78,8 +75,4 @@ public class SpendUtxoEvent {
public PaymentCode getPaymentCode() { public PaymentCode getPaymentCode() {
return paymentCode; return paymentCode;
} }
public boolean allowPaymentChanges() {
return allowPaymentChanges;
}
} }

View file

@ -1,9 +0,0 @@
package com.sparrowwallet.sparrow.event;
import com.sparrowwallet.drongo.protocol.Transaction;
public class TransactionOutputsChangedEvent extends TransactionChangedEvent {
public TransactionOutputsChangedEvent(Transaction transaction) {
super(transaction);
}
}

View file

@ -1,7 +1,6 @@
package com.sparrowwallet.sparrow.glyphfont; package com.sparrowwallet.sparrow.glyphfont;
import com.sparrowwallet.drongo.wallet.Payment; import com.sparrowwallet.drongo.wallet.Payment;
import com.sparrowwallet.drongo.wallet.WalletNodePayment;
import com.sparrowwallet.drongo.wallet.WalletTransaction; import com.sparrowwallet.drongo.wallet.WalletTransaction;
import com.sparrowwallet.sparrow.AppServices; import com.sparrowwallet.sparrow.AppServices;
import com.sparrowwallet.sparrow.control.TransactionDiagram; import com.sparrowwallet.sparrow.control.TransactionDiagram;
@ -16,7 +15,7 @@ public class GlyphUtils {
return getFakeMixGlyph(); return getFakeMixGlyph();
} else if(payment.getType().equals(Payment.Type.ANCHOR)) { } else if(payment.getType().equals(Payment.Type.ANCHOR)) {
return getAnchorGlyph(); return getAnchorGlyph();
} else if(payment instanceof WalletNodePayment) { } else if(walletTx.isConsolidationSend(payment)) {
return getConsolidationGlyph(); return getConsolidationGlyph();
} else if(walletTx.isPremixSend(payment)) { } else if(walletTx.isPremixSend(payment)) {
return getPremixGlyph(); return getPremixGlyph();
@ -214,13 +213,6 @@ public class GlyphUtils {
return busyGlyph; 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() { public static Glyph getDownArrowGlyph() {
Glyph downGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.ARROW_DOWN); Glyph downGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.ARROW_DOWN);
downGlyph.getStyleClass().add("arrow-down"); downGlyph.getStyleClass().add("arrow-down");

View file

@ -192,7 +192,7 @@ public class Bip129 implements KeystoreFileExport, KeystoreFileImport, WalletExp
public void exportWallet(Wallet wallet, OutputStream outputStream, String password) throws ExportException { public void exportWallet(Wallet wallet, OutputStream outputStream, String password) throws ExportException {
try { try {
String record = "BSMS 1.0\n" + String record = "BSMS 1.0\n" +
OutputDescriptor.getOutputDescriptor(wallet, KeyPurpose.DEFAULT_PURPOSES, null) + OutputDescriptor.getOutputDescriptor(wallet) +
"\n/0/*,/1/*\n" + "\n/0/*,/1/*\n" +
wallet.getNode(KeyPurpose.RECEIVE).getChildren().iterator().next().getAddress(); wallet.getNode(KeyPurpose.RECEIVE).getChildren().iterator().next().getAddress();
outputStream.write(record.getBytes(StandardCharsets.UTF_8)); outputStream.write(record.getBytes(StandardCharsets.UTF_8));

View file

@ -9,7 +9,7 @@ import java.io.InputStream;
public class BlueWalletMultisig extends ColdcardMultisig { public class BlueWalletMultisig extends ColdcardMultisig {
@Override @Override
public String getName() { public String getName() {
return "BlueWallet Vault Multisig"; return "Blue Wallet Vault Multisig";
} }
@Override @Override
@ -21,7 +21,7 @@ public class BlueWalletMultisig extends ColdcardMultisig {
public Wallet importWallet(InputStream inputStream, String password) throws ImportException { public Wallet importWallet(InputStream inputStream, String password) throws ImportException {
Wallet wallet = super.importWallet(inputStream, password); Wallet wallet = super.importWallet(inputStream, password);
for(Keystore keystore : wallet.getKeystores()) { for(Keystore keystore : wallet.getKeystores()) {
keystore.setLabel(keystore.getLabel().replace("Coldcard", "BlueWallet")); keystore.setLabel(keystore.getLabel().replace("Coldcard", "Blue Wallet"));
keystore.setWalletModel(WalletModel.BLUE_WALLET); keystore.setWalletModel(WalletModel.BLUE_WALLET);
} }
@ -30,12 +30,12 @@ public class BlueWalletMultisig extends ColdcardMultisig {
@Override @Override
public String getWalletImportDescription() { public String getWalletImportDescription() {
return "Import file or QR created by using the Wallet > Export Coordination Setup feature on your BlueWallet Vault wallet."; return "Import file or QR created by using the Wallet > Export Coordination Setup feature on your Blue Wallet Vault wallet.";
} }
@Override @Override
public String getWalletExportDescription() { public String getWalletExportDescription() {
return "Export file that can be read by BlueWallet using the Add Wallet > Vault > Import wallet feature."; return "Export file that can be read by Blue Wallet using the Add Wallet > Vault > Import wallet feature.";
} }
@Override @Override

View file

@ -2,7 +2,6 @@ package com.sparrowwallet.sparrow.io;
import com.google.gson.*; import com.google.gson.*;
import com.sparrowwallet.drongo.BitcoinUnit; import com.sparrowwallet.drongo.BitcoinUnit;
import com.sparrowwallet.drongo.protocol.Transaction;
import com.sparrowwallet.sparrow.UnitFormat; import com.sparrowwallet.sparrow.UnitFormat;
import com.sparrowwallet.sparrow.Mode; import com.sparrowwallet.sparrow.Mode;
import com.sparrowwallet.sparrow.Theme; import com.sparrowwallet.sparrow.Theme;
@ -54,7 +53,6 @@ public class Config {
private boolean signBsmsExports = false; private boolean signBsmsExports = false;
private boolean preventSleep = false; private boolean preventSleep = false;
private Boolean connectToBroadcast; private Boolean connectToBroadcast;
private Boolean connectToResolve;
private Boolean suggestSendToMany; private Boolean suggestSendToMany;
private List<File> recentWalletFiles; private List<File> recentWalletFiles;
private Integer keyDerivationPeriod; private Integer keyDerivationPeriod;
@ -65,7 +63,6 @@ public class Config {
private boolean mirrorCapture = true; private boolean mirrorCapture = true;
private boolean useZbar = true; private boolean useZbar = true;
private String webcamDevice; private String webcamDevice;
private String webcamDeviceId;
private ServerType serverType; private ServerType serverType;
private Server publicElectrumServer; private Server publicElectrumServer;
private Server coreServer; private Server coreServer;
@ -85,7 +82,6 @@ public class Config {
private int maxPageSize = DEFAULT_PAGE_SIZE; private int maxPageSize = DEFAULT_PAGE_SIZE;
private boolean usePayNym; private boolean usePayNym;
private boolean mempoolFullRbf; private boolean mempoolFullRbf;
private double minRelayFeeRate = Transaction.DEFAULT_MIN_RELAY_FEE;
private Double appWidth; private Double appWidth;
private Double appHeight; private Double appHeight;
@ -366,15 +362,6 @@ public class Config {
flush(); flush();
} }
public Boolean getConnectToResolve() {
return connectToResolve;
}
public void setConnectToResolve(Boolean connectToResolve) {
this.connectToResolve = connectToResolve;
flush();
}
public Boolean getSuggestSendToMany() { public Boolean getSuggestSendToMany() {
return suggestSendToMany; return suggestSendToMany;
} }
@ -450,15 +437,6 @@ public class Config {
flush(); flush();
} }
public String getWebcamDeviceId() {
return webcamDeviceId;
}
public void setWebcamDeviceId(String webcamDeviceId) {
this.webcamDeviceId = webcamDeviceId;
flush();
}
public ServerType getServerType() { public ServerType getServerType() {
return serverType; return serverType;
} }
@ -720,14 +698,6 @@ public class Config {
flush(); flush();
} }
public double getMinRelayFeeRate() {
return minRelayFeeRate;
}
public void setMinRelayFeeRate(double minRelayFeeRate) {
this.minRelayFeeRate = minRelayFeeRate;
}
public Double getAppWidth() { public Double getAppWidth() {
return appWidth; return appWidth;
} }

View file

@ -684,7 +684,7 @@ public class Storage {
public static Executor getSingleThreadedExecutor() { public static Executor getSingleThreadedExecutor() {
if(singleThreadedExecutor == null) { if(singleThreadedExecutor == null) {
BasicThreadFactory factory = BasicThreadFactory.builder().namingPattern("LoadWalletService-single").daemon(true).priority(Thread.MIN_PRIORITY).build(); BasicThreadFactory factory = new BasicThreadFactory.Builder().namingPattern("LoadWalletService-single").daemon(true).priority(Thread.MIN_PRIORITY).build();
singleThreadedExecutor = Executors.newSingleThreadScheduledExecutor(factory); singleThreadedExecutor = Executors.newSingleThreadScheduledExecutor(factory);
} }

View file

@ -171,7 +171,7 @@ public class DbPersistence implements Persistence {
private synchronized void createUpdateExecutor(Wallet masterWallet) { private synchronized void createUpdateExecutor(Wallet masterWallet) {
if(updateExecutor == null) { if(updateExecutor == null) {
BasicThreadFactory factory = BasicThreadFactory.builder().namingPattern(masterWallet.getFullName() + "-dbupdater").daemon(true).priority(Thread.NORM_PRIORITY).build(); BasicThreadFactory factory = new BasicThreadFactory.Builder().namingPattern(masterWallet.getFullName() + "-dbupdater").daemon(true).priority(Thread.NORM_PRIORITY).build();
updateExecutor = Executors.newSingleThreadExecutor(factory); updateExecutor = Executors.newSingleThreadExecutor(factory);
} }
} }

View file

@ -124,7 +124,7 @@ public enum ExchangeSource {
return historicalRates; return historicalRates;
} }
}, },
COINGECKO("Coingecko", "Historical rates for the last 365 days") { COINGECKO("Coingecko", "No historical rates") {
@Override @Override
public List<Currency> getSupportedCurrencies() { public List<Currency> getSupportedCurrencies() {
return getRates().rates.entrySet().stream().filter(rate -> "fiat".equals(rate.getValue().type) && isValidISO4217Code(rate.getKey().toUpperCase(Locale.ROOT))) return getRates().rates.entrySet().stream().filter(rate -> "fiat".equals(rate.getValue().type) && isValidISO4217Code(rate.getKey().toUpperCase(Locale.ROOT)))
@ -167,11 +167,6 @@ public enum ExchangeSource {
long startDate = start.getTime() / 1000; long startDate = start.getTime() / 1000;
long endDate = end.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; String url = "https://api.coingecko.com/api/v3/coins/bitcoin/market_chart/range?vs_currency=" + currency.getCurrencyCode() + "&from=" + startDate + "&to=" + endDate;
if(log.isInfoEnabled()) { if(log.isInfoEnabled()) {

View file

@ -1,17 +1,11 @@
package com.sparrowwallet.sparrow.net; package com.sparrowwallet.sparrow.net;
import com.sparrowwallet.drongo.OsType;
import java.io.*; import java.io.*;
import java.nio.file.FileSystemNotFoundException; import java.nio.file.FileSystemNotFoundException;
import java.nio.file.FileSystems; import java.nio.file.FileSystems;
import java.nio.file.Files; import java.nio.file.Files;
import java.nio.file.ProviderNotFoundException; import java.nio.file.ProviderNotFoundException;
import java.nio.file.StandardCopyOption; 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 * A simple library class which helps with loading dynamic libraries stored in the
@ -117,33 +111,9 @@ public class NativeUtils {
String tempDir = System.getProperty("java.io.tmpdir"); String tempDir = System.getProperty("java.io.tmpdir");
File generatedDir = new File(tempDir, prefix + System.nanoTime()); File generatedDir = new File(tempDir, prefix + System.nanoTime());
if(!createOwnerOnlyDirectory(generatedDir)) { if (!generatedDir.mkdir())
throw new IOException("Failed to create temp directory " + generatedDir.getName()); throw new IOException("Failed to create temp directory " + generatedDir.getName());
}
return generatedDir; 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;
}
} }

View file

@ -16,7 +16,6 @@ import java.security.cert.Certificate;
public class TcpOverTlsTransport extends TcpTransport { public class TcpOverTlsTransport extends TcpTransport {
private static final Logger log = LoggerFactory.getLogger(TcpOverTlsTransport.class); private static final Logger log = LoggerFactory.getLogger(TcpOverTlsTransport.class);
public static final int PAD_TO_MULTIPLE_OF_BYTES = 96;
protected final SSLSocketFactory sslSocketFactory; protected final SSLSocketFactory sslSocketFactory;
@ -42,24 +41,6 @@ public class TcpOverTlsTransport extends TcpTransport {
sslSocketFactory = sslContext.getSocketFactory(); 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 { private TrustManager[] getTrustManagers(File crtFile) throws IOException, CertificateException, NoSuchAlgorithmException, KeyStoreException {
if(crtFile == null) { if(crtFile == null) {
return new TrustManager[] { return new TrustManager[] {

View file

@ -97,7 +97,7 @@ public class TcpTransport implements CloseableTransport, TimeoutCounter {
} }
} }
protected void writeRequest(String request) throws IOException { private void writeRequest(String request) throws IOException {
if(log.isTraceEnabled()) { if(log.isTraceEnabled()) {
log.trace("Sending to electrum server at " + server + ": " + request); 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."); throw new IllegalStateException("Socket connection has not been established.");
} }
PrintWriter out = new PrintWriter(new BufferedWriter(new OutputStreamWriter(socket.getOutputStream(), StandardCharsets.UTF_8))); PrintWriter out = new PrintWriter(new BufferedWriter(new OutputStreamWriter(socket.getOutputStream())));
out.println(request); out.println(request);
out.flush(); out.flush();
} }

View file

@ -149,9 +149,6 @@ public class BitcoindClient {
List<String> loadedWallets; List<String> loadedWallets;
try { try {
loadedWallets = getBitcoindService().listWallets(); loadedWallets = getBitcoindService().listWallets();
if(loadedWallets == null) {
throw new BitcoinRPCException("Wallet support must be enabled in Bitcoin Core");
}
legacyWalletExists = loadedWallets.contains(Bwt.DEFAULT_CORE_WALLET); legacyWalletExists = loadedWallets.contains(Bwt.DEFAULT_CORE_WALLET);
} catch(JsonRpcException e) { } catch(JsonRpcException e) {
if(e.getErrorMessage().getCode() == RPC_METHOD_NOT_FOUND) { if(e.getErrorMessage().getCode() == RPC_METHOD_NOT_FOUND) {

View file

@ -40,10 +40,6 @@ public class Payjoin {
this.wallet = wallet; this.wallet = wallet;
this.psbt = psbt; this.psbt = psbt;
if(payjoinURI.getAddress() == null) {
throw new IllegalArgumentException("Payjoin URI must have an address");
}
for(PSBTInput psbtInput : psbt.getPsbtInputs()) { for(PSBTInput psbtInput : psbt.getPsbtInputs()) {
if(psbtInput.getUtxo() == null) { if(psbtInput.getUtxo() == null) {
throw new IllegalArgumentException("Original PSBT for payjoin transaction must have non_witness_utxo or witness_utxo fields for all inputs"); throw new IllegalArgumentException("Original PSBT for payjoin transaction must have non_witness_utxo or witness_utxo fields for all inputs");
@ -108,9 +104,6 @@ public class Payjoin {
} catch(PSBTParseException e) { } catch(PSBTParseException e) {
log.error("Error parsing received PSBT", e); log.error("Error parsing received PSBT", e);
throw new PayjoinReceiverException("Payjoin receiver returned invalid 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) { } catch(Exception e) {
log.error("Payjoin error", e); log.error("Payjoin error", e);
throw new PayjoinReceiverException("Payjoin error", e); throw new PayjoinReceiverException("Payjoin error", e);

View file

@ -616,7 +616,6 @@ public class PayNymController {
List<byte[]> opReturns = List.of(blindedPaymentCode); List<byte[]> opReturns = List.of(blindedPaymentCode);
Double feeRate = AppServices.getDefaultFeeRate(); Double feeRate = AppServices.getDefaultFeeRate();
Double minimumFeeRate = AppServices.getMinimumFeeRate(); Double minimumFeeRate = AppServices.getMinimumFeeRate();
Double minRelayFeeRate = AppServices.getMinimumRelayFeeRate();
boolean groupByAddress = Config.get().isGroupByAddress(); boolean groupByAddress = Config.get().isGroupByAddress();
boolean includeMempoolOutputs = Config.get().isIncludeMempoolOutputs(); boolean includeMempoolOutputs = Config.get().isIncludeMempoolOutputs();
@ -624,9 +623,7 @@ public class PayNymController {
List<UtxoSelector> utxoSelectors = List.of(utxos == null ? new KnapsackUtxoSelector(noInputsFee) : new PresetUtxoSelector(utxos, true, false)); 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)); List<TxoFilter> txoFilters = List.of(new SpentTxoFilter(), new FrozenTxoFilter(), new CoinbaseTxoFilter(wallet));
TransactionParameters params = new TransactionParameters(utxoSelectors, txoFilters, payments, opReturns, Collections.emptySet(), return wallet.createWalletTransaction(utxoSelectors, txoFilters, payments, opReturns, Collections.emptySet(), feeRate, minimumFeeRate, null, AppServices.getCurrentBlockHeight(), groupByAddress, includeMempoolOutputs);
feeRate, minimumFeeRate, minRelayFeeRate, null, AppServices.getCurrentBlockHeight(), groupByAddress, includeMempoolOutputs, true);
return wallet.createWalletTransaction(params);
} }
private Map<BlockTransaction, WalletNode> getNotificationTransaction(PaymentCode externalPaymentCode) { private Map<BlockTransaction, WalletNode> getNotificationTransaction(PaymentCode externalPaymentCode) {

View file

@ -4,12 +4,9 @@ import com.sparrowwallet.drongo.KeyPurpose;
import com.sparrowwallet.drongo.SecureString; import com.sparrowwallet.drongo.SecureString;
import com.sparrowwallet.drongo.Utils; import com.sparrowwallet.drongo.Utils;
import com.sparrowwallet.drongo.address.Address; import com.sparrowwallet.drongo.address.Address;
import com.sparrowwallet.drongo.policy.PolicyType;
import com.sparrowwallet.drongo.protocol.*; import com.sparrowwallet.drongo.protocol.*;
import com.sparrowwallet.drongo.psbt.PSBT; import com.sparrowwallet.drongo.psbt.PSBT;
import com.sparrowwallet.drongo.psbt.PSBTInput; 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.uri.BitcoinURI;
import com.sparrowwallet.drongo.wallet.*; import com.sparrowwallet.drongo.wallet.*;
import com.sparrowwallet.hummingbird.UR; import com.sparrowwallet.hummingbird.UR;
@ -60,6 +57,7 @@ import tornadofx.control.Fieldset;
import com.google.common.eventbus.Subscribe; import com.google.common.eventbus.Subscribe;
import tornadofx.control.Form; import tornadofx.control.Form;
import javax.swing.text.html.Option;
import java.io.*; import java.io.*;
import java.net.*; import java.net.*;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
@ -182,15 +180,6 @@ public class HeadersController extends TransactionFormController implements Init
@FXML @FXML
private CopyableLabel blockTimestamp; private CopyableLabel blockTimestamp;
@FXML
private Field signedByField;
@FXML
private CopyableLabel signedBy;
@FXML
private Form blockchainSpacerForm;
@FXML @FXML
private Form signingWalletForm; private Form signingWalletForm;
@ -462,7 +451,6 @@ public class HeadersController extends TransactionFormController implements Init
headersForm.setWalletTransaction(getWalletTransaction(headersForm.getInputTransactions())); headersForm.setWalletTransaction(getWalletTransaction(headersForm.getInputTransactions()));
blockchainForm.managedProperty().bind(blockchainForm.visibleProperty()); blockchainForm.managedProperty().bind(blockchainForm.visibleProperty());
blockchainSpacerForm.managedProperty().bind(blockchainForm.managedProperty());
signingWalletForm.managedProperty().bind(signingWalletForm.visibleProperty()); signingWalletForm.managedProperty().bind(signingWalletForm.visibleProperty());
sigHashForm.managedProperty().bind(sigHashForm.visibleProperty()); sigHashForm.managedProperty().bind(sigHashForm.visibleProperty());
@ -648,27 +636,24 @@ public class HeadersController extends TransactionFormController implements Init
} }
List<Payment> payments = new ArrayList<>(); List<Payment> payments = new ArrayList<>();
List<WalletTransaction.Output> outputs = new ArrayList<>();
Map<WalletNode, Long> changeMap = new LinkedHashMap<>(); Map<WalletNode, Long> changeMap = new LinkedHashMap<>();
Map<Script, WalletNode> receiveOutputScripts = wallet.getWalletOutputScripts(KeyPurpose.RECEIVE);
Map<Script, WalletNode> changeOutputScripts = wallet.getWalletOutputScripts(wallet.getChangeKeyPurpose()); Map<Script, WalletNode> changeOutputScripts = wallet.getWalletOutputScripts(wallet.getChangeKeyPurpose());
for(TransactionOutput txOutput : headersForm.getTransaction().getOutputs()) { for(TransactionOutput txOutput : headersForm.getTransaction().getOutputs()) {
WalletNode changeNode = changeOutputScripts.get(txOutput.getScript()); WalletNode changeNode = changeOutputScripts.get(txOutput.getScript());
if(changeNode != null) { if(changeNode != null) {
if(headersForm.getTransaction().getOutputs().size() == 4 && headersForm.getTransaction().getOutputs().stream().anyMatch(txo -> txo != txOutput && txo.getValue() == txOutput.getValue())) { 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)) { if(selectedTxos.values().stream().allMatch(Objects::nonNull)) {
payments.add(new WalletNodePayment(changeNode, ".." + changeNode + " (Fake Mix)", txOutput.getValue(), false, Payment.Type.FAKE_MIX)); payments.add(new Payment(txOutput.getScript().getToAddress(), ".." + changeNode + " (Fake Mix)", txOutput.getValue(), false, Payment.Type.FAKE_MIX));
} else { } else {
payments.add(new WalletNodePayment(changeNode, ".." + changeNode + " (Mix)", txOutput.getValue(), false, Payment.Type.MIX)); payments.add(new Payment(txOutput.getScript().getToAddress(), ".." + changeNode + " (Mix)", txOutput.getValue(), false, Payment.Type.MIX));
} }
} else { } else {
if(changeMap.containsKey(changeNode)) { if(changeMap.containsKey(changeNode)) {
payments.add(new WalletNodePayment(changeNode, headersForm.getName(), txOutput.getValue(), false, Payment.Type.DEFAULT)); payments.add(new Payment(txOutput.getScript().getToAddress(), headersForm.getName(), txOutput.getValue(), false, Payment.Type.DEFAULT));
} else { } else {
changeMap.put(changeNode, txOutput.getValue()); changeMap.put(changeNode, txOutput.getValue());
} }
} }
outputs.add(new WalletTransaction.ChangeOutput(txOutput, changeNode, txOutput.getValue()));
} else { } else {
Payment.Type paymentType = Payment.Type.DEFAULT; Payment.Type paymentType = Payment.Type.DEFAULT;
Wallet masterWallet = wallet.isMasterWallet() ? wallet : wallet.getMasterWallet(); Wallet masterWallet = wallet.isMasterWallet() ? wallet : wallet.getMasterWallet();
@ -679,44 +664,24 @@ 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); 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(); String label = headersForm.getName() == null || (headersForm.getName().startsWith("[") && headersForm.getName().endsWith("]") && headersForm.getName().length() == 8) ? null : headersForm.getName();
Address address = txOutput.getScript().getToAddress(); try {
WalletNode receiveNode = receiveOutputScripts.get(txOutput.getScript()); Payment payment = new Payment(txOutput.getScript().getToAddresses()[0], receivedTxo != null ? receivedTxo.getLabel() : label, txOutput.getValue(), false, paymentType);
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()); WalletTransaction createdTx = AppServices.get().getCreatedTransaction(selectedTxos.keySet());
if(createdTx != null) { if(createdTx != null) {
Optional<String> optLabel = createdTx.getPayments().stream() Optional<String> optLabel = createdTx.getPayments().stream().filter(pymt -> pymt.getAddress().equals(payment.getAddress()) && pymt.getAmount() == payment.getAmount()).map(Payment::getLabel).findFirst();
.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()) { if(optLabel.isPresent()) {
payment.setLabel(optLabel.get()); payment.setLabel(optLabel.get());
outputIndexLabels.put(txOutput.getIndex(), optLabel.get()); outputIndexLabels.put(txOutput.getIndex(), optLabel.get());
} }
} }
payments.add(payment); payments.add(payment);
if(payment instanceof SilentPayment silentPayment) { } catch(Exception e) {
outputs.add(new WalletTransaction.SilentPaymentOutput(txOutput, silentPayment)); //ignore
} 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, outputs, changeMap, fee.getValue(), walletInputTransactions); return new WalletTransaction(wallet, headersForm.getTransaction(), Collections.emptyList(), List.of(selectedTxos), payments, changeMap, fee.getValue(), walletInputTransactions);
} else { } else {
Map<BlockTransactionHashIndex, WalletNode> selectedTxos = headersForm.getTransaction().getInputs().stream() Map<BlockTransactionHashIndex, WalletNode> selectedTxos = headersForm.getTransaction().getInputs().stream()
.collect(Collectors.toMap(txInput -> getBlockTransactionInput(inputTransactions, txInput), .collect(Collectors.toMap(txInput -> getBlockTransactionInput(inputTransactions, txInput),
@ -726,25 +691,16 @@ public class HeadersController extends TransactionFormController implements Init
selectedTxos.entrySet().forEach(entry -> entry.setValue(null)); selectedTxos.entrySet().forEach(entry -> entry.setValue(null));
List<Payment> payments = new ArrayList<>(); List<Payment> payments = new ArrayList<>();
List<WalletTransaction.Output> outputs = new ArrayList<>();
for(TransactionOutput txOutput : headersForm.getTransaction().getOutputs()) { for(TransactionOutput txOutput : headersForm.getTransaction().getOutputs()) {
Address address = txOutput.getScript().getToAddress(); try {
SilentPaymentAddress silentPaymentAddress = headersForm.getSilentPaymentAddress(txOutput); BlockTransactionHashIndex receivedTxo = getBlockTransactionOutput(txOutput);
BlockTransactionHashIndex receivedTxo = getBlockTransactionOutput(txOutput); payments.add(new Payment(txOutput.getScript().getToAddresses()[0], receivedTxo != null ? receivedTxo.getLabel() : null, txOutput.getValue(), false));
String label = receivedTxo != null ? receivedTxo.getLabel() : null; } catch(Exception e) {
if(address != null || silentPaymentAddress != null) { //ignore
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, outputs, Collections.emptyMap(), fee.getValue(), inputTransactions); return new WalletTransaction(null, headersForm.getTransaction(), Collections.emptyList(), List.of(selectedTxos), payments, Collections.emptyMap(), fee.getValue(), inputTransactions);
} }
} }
@ -818,7 +774,6 @@ public class HeadersController extends TransactionFormController implements Init
blockHeightField.managedProperty().bind(blockHeightField.visibleProperty()); blockHeightField.managedProperty().bind(blockHeightField.visibleProperty());
blockTimestampField.managedProperty().bind(blockTimestampField.visibleProperty()); blockTimestampField.managedProperty().bind(blockTimestampField.visibleProperty());
signedByField.managedProperty().bind(signedByField.visibleProperty());
if(blockTransaction.getHeight() > 0) { if(blockTransaction.getHeight() > 0) {
blockHeightField.setVisible(true); blockHeightField.setVisible(true);
@ -836,19 +791,6 @@ public class HeadersController extends TransactionFormController implements Init
} else { } else {
blockTimestampField.setVisible(false); 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) { private void initializeSignButton(Wallet signingWallet) {
@ -985,7 +927,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 //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()); boolean includeNonWitnessUtxos = !Arrays.asList(ScriptType.WITNESS_TYPES).contains(headersForm.getSigningWallet().getScriptType());
byte[] psbtBytes = headersForm.getPsbt().getForExport().serialize(true, includeNonWitnessUtxos); byte[] psbtBytes = headersForm.getPsbt().serialize(true, includeNonWitnessUtxos);
CryptoPSBT cryptoPSBT = new CryptoPSBT(psbtBytes); CryptoPSBT cryptoPSBT = new CryptoPSBT(psbtBytes);
BBQR bbqr = addBbqrOption ? new BBQR(BBQRType.PSBT, psbtBytes) : null; BBQR bbqr = addBbqrOption ? new BBQR(BBQRType.PSBT, psbtBytes) : null;
@ -1068,7 +1010,7 @@ public class HeadersController extends TransactionFormController implements Init
} }
try(FileOutputStream outputStream = new FileOutputStream(file)) { try(FileOutputStream outputStream = new FileOutputStream(file)) {
outputStream.write(headersForm.getPsbt().getForExport().serialize()); outputStream.write(headersForm.getPsbt().serialize());
} catch(IOException e) { } catch(IOException e) {
log.error("Error saving PSBT", e); log.error("Error saving PSBT", e);
AppServices.showErrorDialog("Error saving PSBT", "Cannot write to " + file.getAbsolutePath()); AppServices.showErrorDialog("Error saving PSBT", "Cannot write to " + file.getAbsolutePath());
@ -1125,12 +1067,7 @@ public class HeadersController extends TransactionFormController implements Init
private void signUnencryptedKeystores(Wallet unencryptedWallet) { private void signUnencryptedKeystores(Wallet unencryptedWallet) {
try { try {
Map<PSBTInput, WalletNode> signingNodes = unencryptedWallet.getSigningNodes(headersForm.getPsbt()); unencryptedWallet.sign(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()); updateSignedKeystores(headersForm.getSigningWallet());
} catch(Exception e) { } catch(Exception e) {
log.warn("Failed to Sign", e); log.warn("Failed to Sign", e);
@ -1202,7 +1139,7 @@ public class HeadersController extends TransactionFormController implements Init
if(fee.getValue() > 0) { if(fee.getValue() > 0) {
double feeRateAmt = fee.getValue() / headersForm.getTransaction().getVirtualSize(); double feeRateAmt = fee.getValue() / headersForm.getTransaction().getVirtualSize();
if(feeRateAmt > AppServices.getLongFeeRatesRange().getLast()) { if(feeRateAmt > AppServices.LONG_FEE_RATES_RANGE.get(AppServices.LONG_FEE_RATES_RANGE.size() - 1)) {
Optional<ButtonType> optType = AppServices.showWarningDialog("Very high fee rate!", 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); "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) { if(optType.isPresent() && optType.get() == ButtonType.NO) {
@ -1288,17 +1225,9 @@ public class HeadersController extends TransactionFormController implements Init
UnitFormat format = Config.get().getUnitFormat() == null ? UnitFormat.DOT : Config.get().getUnitFormat(); UnitFormat format = Config.get().getUnitFormat() == null ? UnitFormat.DOT : Config.get().getUnitFormat();
if(failMessage.startsWith("min relay fee not met")) { if(failMessage.startsWith("min relay fee not met")) {
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 " + format.getCurrencyFormat().format(AppServices.getMinimumRelayFeeRate()) + " sats/vB. " +
AppServices.showErrorDialog("Error broadcasting transaction", "The fee rate for the signed transaction is below the minimum configured relay fee rate for the server of " + "This usually happens because a keystore has created a signature that is larger than necessary.\n\n" +
format.getCurrencyFormat().format(AppServices.getServerMinimumRelayFeeRate()) + " sats/vB."); "You can solve this by recreating the transaction with a slightly increased fee rate.");
} 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")) { } 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."); 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")) { } else if(failMessage.contains("mempool min fee not met")) {
@ -1499,7 +1428,6 @@ public class HeadersController extends TransactionFormController implements Init
errorGlyph.getStyleClass().add("failure"); errorGlyph.getStyleClass().add("failure");
blockHeightField.setVisible(false); blockHeightField.setVisible(false);
blockTimestampField.setVisible(false); blockTimestampField.setVisible(false);
signedByField.setVisible(false);
} }
} }
@ -1659,13 +1587,6 @@ 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 @Subscribe
public void transactionExtracted(TransactionExtractedEvent event) { public void transactionExtracted(TransactionExtractedEvent event) {
if(event.getPsbt().equals(headersForm.getPsbt())) { if(event.getPsbt().equals(headersForm.getPsbt())) {

View file

@ -337,7 +337,7 @@ public class InputController extends TransactionFormController implements Initia
} }
} else { } else {
if(txInput.isAbsoluteTimeLocked()) { if(txInput.isAbsoluteTimeLocked()) {
txInput.setSequenceNumber(TransactionInput.SEQUENCE_RBF_DISABLED); txInput.setSequenceNumber(TransactionInput.SEQUENCE_LOCKTIME_DISABLED - 1);
if(oldValue != null) { if(oldValue != null) {
EventManager.get().post(new TransactionChangedEvent(transaction)); EventManager.get().post(new TransactionChangedEvent(transaction));
} }
@ -389,7 +389,7 @@ public class InputController extends TransactionFormController implements Initia
if(rbf.selectedProperty().getValue()) { if(rbf.selectedProperty().getValue()) {
txInput.setSequenceNumber(TransactionInput.SEQUENCE_RBF_ENABLED); txInput.setSequenceNumber(TransactionInput.SEQUENCE_RBF_ENABLED);
} else { } else {
txInput.setSequenceNumber(TransactionInput.SEQUENCE_RBF_DISABLED); txInput.setSequenceNumber(TransactionInput.SEQUENCE_LOCKTIME_DISABLED - 1);
} }
if(old_toggle != null) { if(old_toggle != null) {
EventManager.get().post(new TransactionChangedEvent(transaction)); EventManager.get().post(new TransactionChangedEvent(transaction));

View file

@ -5,12 +5,14 @@ import com.sparrowwallet.drongo.address.Address;
import com.sparrowwallet.drongo.protocol.NonStandardScriptException; import com.sparrowwallet.drongo.protocol.NonStandardScriptException;
import com.sparrowwallet.drongo.protocol.TransactionInput; import com.sparrowwallet.drongo.protocol.TransactionInput;
import com.sparrowwallet.drongo.protocol.TransactionOutput; import com.sparrowwallet.drongo.protocol.TransactionOutput;
import com.sparrowwallet.drongo.silentpayments.SilentPaymentAddress;
import com.sparrowwallet.drongo.wallet.*; import com.sparrowwallet.drongo.wallet.*;
import com.sparrowwallet.sparrow.AppServices; import com.sparrowwallet.sparrow.AppServices;
import com.sparrowwallet.sparrow.EventManager; import com.sparrowwallet.sparrow.EventManager;
import com.sparrowwallet.sparrow.control.*; import com.sparrowwallet.sparrow.control.*;
import com.sparrowwallet.sparrow.event.*; 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.net.ElectrumServer; import com.sparrowwallet.sparrow.net.ElectrumServer;
import javafx.fxml.FXML; import javafx.fxml.FXML;
import javafx.fxml.Initializable; import javafx.fxml.Initializable;
@ -68,7 +70,20 @@ public class OutputController extends TransactionFormController implements Initi
updateOutputLegendFromWallet(txOutput, walletTransaction != null ? walletTransaction.getWallet() : null); updateOutputLegendFromWallet(txOutput, walletTransaction != null ? walletTransaction.getWallet() : null);
}); });
updateOutputLegendFromWallet(txOutput, outputForm.getWallet()); updateOutputLegendFromWallet(txOutput, outputForm.getWallet());
updateSends(txOutput);
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
}
spentField.managedProperty().bind(spentField.visibleProperty()); spentField.managedProperty().bind(spentField.visibleProperty());
spentByField.managedProperty().bind(spentByField.visibleProperty()); spentByField.managedProperty().bind(spentByField.visibleProperty());
@ -83,32 +98,6 @@ public class OutputController extends TransactionFormController implements Initi
} }
initializeScriptField(scriptPubKeyArea); 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.clear();
scriptPubKeyArea.appendScript(txOutput.getScript(), null, null); scriptPubKeyArea.appendScript(txOutput.getScript(), null, null);
} }
@ -126,14 +115,11 @@ public class OutputController extends TransactionFormController implements Initi
WalletTransaction.Output output = outputs.get(outputForm.getIndex()); WalletTransaction.Output output = outputs.get(outputForm.getIndex());
if(output instanceof WalletTransaction.NonAddressOutput) { if(output instanceof WalletTransaction.NonAddressOutput) {
outputFieldset.setText(baseText); 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) { } else if(output instanceof WalletTransaction.PaymentOutput paymentOutput) {
Payment payment = paymentOutput.getPayment(); Payment payment = paymentOutput.getPayment();
Wallet toWallet = walletTx.getToWallet(AppServices.get().getOpenWallets().keySet(), payment); Wallet toWallet = walletTx.getToWallet(AppServices.get().getOpenWallets().keySet(), payment);
outputFieldset.setText(baseText + (toWallet == null ? " - Payment" : " - Received to " + toWallet.getFullDisplayName())); 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()));
} else if(output instanceof WalletTransaction.ChangeOutput changeOutput) { } else if(output instanceof WalletTransaction.ChangeOutput changeOutput) {
outputFieldset.setText(baseText + " - Change to " + changeOutput.getWalletNode().toString()); outputFieldset.setText(baseText + " - Change to " + changeOutput.getWalletNode().toString());
} else { } else {
@ -220,12 +206,4 @@ public class OutputController extends TransactionFormController implements Initi
updateOutputLegendFromWallet(outputForm.getTransactionOutput(), null); updateOutputLegendFromWallet(outputForm.getTransactionOutput(), null);
} }
} }
@Subscribe
public void transactionOutputsChanged(TransactionOutputsChangedEvent event) {
if(event.getTransaction().equals(outputForm.getTransaction())) {
updateSends(outputForm.getTransactionOutput());
updateScriptPubKey(outputForm.getTransactionOutput());
}
}
} }

View file

@ -89,11 +89,7 @@ public class OutputForm extends IndexedTransactionForm {
} }
} else if(output instanceof WalletTransaction.PaymentOutput paymentOutput) { } else if(output instanceof WalletTransaction.PaymentOutput paymentOutput) {
Payment payment = paymentOutput.getPayment(); Payment payment = paymentOutput.getPayment();
return new Label(payment.getLabel() != null && payment.getType() != Payment.Type.FAKE_MIX && payment.getType() != Payment.Type.MIX ? payment.getLabel() : payment.toString(), return new Label(payment.getLabel() != null && payment.getType() != Payment.Type.FAKE_MIX && payment.getType() != Payment.Type.MIX ? payment.getLabel() : payment.getAddress().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)); GlyphUtils.getOutputGlyph(getWalletTransaction(), payment));
} else if(output instanceof WalletTransaction.ChangeOutput changeOutput) { } else if(output instanceof WalletTransaction.ChangeOutput changeOutput) {
return new Label("Change", GlyphUtils.getChangeGlyph()); return new Label("Change", GlyphUtils.getChangeGlyph());

View file

@ -6,7 +6,6 @@ import com.sparrowwallet.drongo.protocol.TransactionOutput;
import com.sparrowwallet.sparrow.EventManager; import com.sparrowwallet.sparrow.EventManager;
import com.sparrowwallet.sparrow.control.CopyableCoinLabel; import com.sparrowwallet.sparrow.control.CopyableCoinLabel;
import com.sparrowwallet.sparrow.control.CopyableLabel; import com.sparrowwallet.sparrow.control.CopyableLabel;
import com.sparrowwallet.sparrow.event.TransactionOutputsChangedEvent;
import com.sparrowwallet.sparrow.event.UnitFormatChangedEvent; import com.sparrowwallet.sparrow.event.UnitFormatChangedEvent;
import javafx.fxml.FXML; import javafx.fxml.FXML;
import javafx.fxml.Initializable; import javafx.fxml.Initializable;
@ -61,11 +60,4 @@ public class OutputsController extends TransactionFormController implements Init
public void unitFormatChanged(UnitFormatChangedEvent event) { public void unitFormatChanged(UnitFormatChangedEvent event) {
total.refresh(event.getUnitFormat(), event.getBitcoinUnit()); total.refresh(event.getUnitFormat(), event.getBitcoinUnit());
} }
@Subscribe
public void transactionOutputsChanged(TransactionOutputsChangedEvent event) {
if(event.getTransaction().equals(outputsForm.getTransaction())) {
updatePieData(outputsPie, outputsForm.getTransaction().getOutputs());
}
}
} }

View file

@ -3,8 +3,6 @@ package com.sparrowwallet.sparrow.transaction;
import com.sparrowwallet.drongo.KeyPurpose; import com.sparrowwallet.drongo.KeyPurpose;
import com.sparrowwallet.drongo.protocol.*; import com.sparrowwallet.drongo.protocol.*;
import com.sparrowwallet.drongo.psbt.PSBT; 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.drongo.wallet.*;
import com.sparrowwallet.sparrow.io.Storage; import com.sparrowwallet.sparrow.io.Storage;
import javafx.beans.property.SimpleObjectProperty; import javafx.beans.property.SimpleObjectProperty;
@ -195,16 +193,4 @@ public class TransactionData {
public Wallet getWallet() { public Wallet getWallet() {
return getSigningWallet() != null ? getSigningWallet() : (getWalletTransaction() != null ? getWalletTransaction().getWallet() : null); 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;
}
} }

View file

@ -2,10 +2,8 @@ package com.sparrowwallet.sparrow.transaction;
import com.sparrowwallet.drongo.protocol.Sha256Hash; import com.sparrowwallet.drongo.protocol.Sha256Hash;
import com.sparrowwallet.drongo.protocol.Transaction; import com.sparrowwallet.drongo.protocol.Transaction;
import com.sparrowwallet.drongo.protocol.TransactionOutput;
import com.sparrowwallet.drongo.protocol.TransactionSignature; import com.sparrowwallet.drongo.protocol.TransactionSignature;
import com.sparrowwallet.drongo.psbt.PSBT; import com.sparrowwallet.drongo.psbt.PSBT;
import com.sparrowwallet.drongo.silentpayments.SilentPaymentAddress;
import com.sparrowwallet.drongo.wallet.*; import com.sparrowwallet.drongo.wallet.*;
import com.sparrowwallet.sparrow.io.Storage; import com.sparrowwallet.sparrow.io.Storage;
import javafx.beans.property.SimpleObjectProperty; import javafx.beans.property.SimpleObjectProperty;
@ -114,10 +112,6 @@ public abstract class TransactionForm {
return txdata.getWallet(); return txdata.getWallet();
} }
public SilentPaymentAddress getSilentPaymentAddress(TransactionOutput output) {
return txdata.getSilentPaymentAddress(output);
}
public boolean isEditable() { public boolean isEditable() {
if(getBlockTransaction() != null) { if(getBlockTransaction() != null) {
return false; return false;

View file

@ -5,7 +5,6 @@ import com.sparrowwallet.drongo.BitcoinUnit;
import com.sparrowwallet.drongo.address.Address; import com.sparrowwallet.drongo.address.Address;
import com.sparrowwallet.drongo.protocol.NonStandardScriptException; import com.sparrowwallet.drongo.protocol.NonStandardScriptException;
import com.sparrowwallet.drongo.protocol.TransactionOutput; import com.sparrowwallet.drongo.protocol.TransactionOutput;
import com.sparrowwallet.drongo.silentpayments.SilentPaymentAddress;
import com.sparrowwallet.sparrow.UnitFormat; import com.sparrowwallet.sparrow.UnitFormat;
import com.sparrowwallet.sparrow.BaseController; import com.sparrowwallet.sparrow.BaseController;
import com.sparrowwallet.sparrow.EventManager; import com.sparrowwallet.sparrow.EventManager;
@ -34,7 +33,17 @@ public abstract class TransactionFormController extends BaseController {
long totalAmt = 0; long totalAmt = 0;
for(int i = 0; i < outputs.size(); i++) { for(int i = 0; i < outputs.size(); i++) {
TransactionOutput output = outputs.get(i); TransactionOutput output = outputs.get(i);
String name = getPieDataName(i, output); 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
}
totalAmt += output.getValue(); totalAmt += output.getValue();
outputsPieData.add(new PieChart.Data(name, output.getValue())); outputsPieData.add(new PieChart.Data(name, output.getValue()));
@ -43,34 +52,6 @@ public abstract class TransactionFormController extends BaseController {
addPieData(pie, outputsPieData); 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) { protected void addCoinbasePieData(PieChart pie, long value) {
ObservableList<PieChart.Data> outputsPieData = FXCollections.observableList(List.of(new PieChart.Data("Coinbase", value))); ObservableList<PieChart.Data> outputsPieData = FXCollections.observableList(List.of(new PieChart.Data("Coinbase", value)));
addPieData(pie, outputsPieData); addPieData(pie, outputsPieData);

View file

@ -1,6 +1,5 @@
package com.sparrowwallet.sparrow.wallet; package com.sparrowwallet.sparrow.wallet;
import com.google.common.base.Throwables;
import com.google.common.eventbus.Subscribe; import com.google.common.eventbus.Subscribe;
import com.sparrowwallet.drongo.BitcoinUnit; import com.sparrowwallet.drongo.BitcoinUnit;
import com.sparrowwallet.drongo.KeyPurpose; import com.sparrowwallet.drongo.KeyPurpose;
@ -10,15 +9,15 @@ import com.sparrowwallet.drongo.address.P2PKHAddress;
import com.sparrowwallet.drongo.bip47.InvalidPaymentCodeException; import com.sparrowwallet.drongo.bip47.InvalidPaymentCodeException;
import com.sparrowwallet.drongo.bip47.PaymentCode; import com.sparrowwallet.drongo.bip47.PaymentCode;
import com.sparrowwallet.drongo.crypto.ECKey; import com.sparrowwallet.drongo.crypto.ECKey;
import com.sparrowwallet.drongo.dns.DnsPaymentCache;
import com.sparrowwallet.drongo.protocol.ScriptType; import com.sparrowwallet.drongo.protocol.ScriptType;
import com.sparrowwallet.drongo.protocol.Transaction; import com.sparrowwallet.drongo.protocol.Transaction;
import com.sparrowwallet.drongo.protocol.TransactionOutput; 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.uri.BitcoinURI;
import com.sparrowwallet.drongo.wallet.*; import com.sparrowwallet.drongo.wallet.*;
import com.sparrowwallet.sparrow.*; import com.sparrowwallet.sparrow.UnitFormat;
import com.sparrowwallet.sparrow.AppServices;
import com.sparrowwallet.sparrow.CurrencyRate;
import com.sparrowwallet.sparrow.EventManager;
import com.sparrowwallet.sparrow.control.*; import com.sparrowwallet.sparrow.control.*;
import com.sparrowwallet.sparrow.event.*; import com.sparrowwallet.sparrow.event.*;
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5; import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
@ -26,8 +25,6 @@ import com.sparrowwallet.sparrow.io.CardApi;
import com.sparrowwallet.sparrow.io.Config; import com.sparrowwallet.sparrow.io.Config;
import com.sparrowwallet.sparrow.io.Storage; import com.sparrowwallet.sparrow.io.Storage;
import com.sparrowwallet.sparrow.net.ExchangeSource; 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.PayNym;
import com.sparrowwallet.sparrow.paynym.PayNymDialog; import com.sparrowwallet.sparrow.paynym.PayNymDialog;
import javafx.application.Platform; import javafx.application.Platform;
@ -39,32 +36,21 @@ import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue; import javafx.beans.value.ObservableValue;
import javafx.collections.FXCollections; import javafx.collections.FXCollections;
import javafx.collections.ListChangeListener; import javafx.collections.ListChangeListener;
import javafx.concurrent.Service;
import javafx.concurrent.Task;
import javafx.event.ActionEvent; import javafx.event.ActionEvent;
import javafx.fxml.FXML; import javafx.fxml.FXML;
import javafx.fxml.Initializable; import javafx.fxml.Initializable;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Node; import javafx.scene.Node;
import javafx.scene.control.*; 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.glyphfont.Glyph;
import org.controlsfx.validation.ValidationResult; import org.controlsfx.validation.ValidationResult;
import org.controlsfx.validation.ValidationSupport; import org.controlsfx.validation.ValidationSupport;
import org.controlsfx.validation.Validator; import org.controlsfx.validation.Validator;
import org.girod.javafx.svgimage.SVGImage;
import org.girod.javafx.svgimage.SVGLoader;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import java.net.URL; import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.text.DecimalFormat; import java.text.DecimalFormat;
import java.util.*; import java.util.*;
import java.util.concurrent.TimeoutException;
import java.util.regex.Pattern; import java.util.regex.Pattern;
import java.util.stream.Collectors; import java.util.stream.Collectors;
@ -143,14 +129,8 @@ public class PaymentController extends WalletFormController implements Initializ
} }
}; };
private final ObjectProperty<WalletNode> consolidationNodeProperty = new SimpleObjectProperty<>();
private final ObjectProperty<PayNym> payNymProperty = 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() { private static final Wallet payNymWallet = new Wallet() {
@Override @Override
public String getFullDisplayName() { public String getFullDisplayName() {
@ -165,127 +145,6 @@ 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 @Override
public void initialize(URL location, ResourceBundle resources) { public void initialize(URL location, ResourceBundle resources) {
EventManager.get().register(this); EventManager.get().register(this);
@ -351,28 +210,6 @@ public class PaymentController extends WalletFormController implements Initializ
revalidateAmount(); 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 -> { address.setTextFormatter(new TextFormatter<>(change -> {
String controlNewText = change.getControlNewText(); String controlNewText = change.getControlNewText();
if(!controlNewText.equals(controlNewText.trim())) { if(!controlNewText.equals(controlNewText.trim())) {
@ -385,8 +222,55 @@ public class PaymentController extends WalletFormController implements Initializ
return change; return change;
})); }));
address.textProperty().addListener(addressListener); address.textProperty().addListener((observable, oldValue, newValue) -> {
address.setContextMenu(address.getCustomContextMenu(Collections.emptyList())); 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);
}
});
label.textProperty().addListener((observable, oldValue, newValue) -> { label.textProperty().addListener((observable, oldValue, newValue) -> {
maxButton.setDisable(!isMaxButtonEnabled()); maxButton.setDisable(!isMaxButtonEnabled());
@ -444,37 +328,6 @@ 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() { private void updateOpenWallets() {
updateOpenWallets(AppServices.get().getOpenWallets().keySet()); updateOpenWallets(AppServices.get().getOpenWallets().keySet());
} }
@ -546,16 +399,6 @@ public class PaymentController extends WalletFormController implements Initializ
} }
private Address getRecipientAddress() throws InvalidAddressException { 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(); PayNym payNym = payNymProperty.get();
if(payNym == null) { if(payNym == null) {
return Address.fromString(address.getText()); return Address.fromString(address.getText());
@ -673,17 +516,7 @@ public class PaymentController extends WalletFormController implements Initializ
Long value = sendAll ? Long.valueOf(getRecipientDustThreshold() + 1) : getRecipientValueSats(); Long value = sendAll ? Long.valueOf(getRecipientDustThreshold() + 1) : getRecipientValueSats();
if(!label.getText().isEmpty() && value != null && value >= getRecipientDustThreshold()) { if(!label.getText().isEmpty() && value != null && value >= getRecipientDustThreshold()) {
Payment payment; Payment payment = new Payment(recipientAddress, label.getText(), value, sendAll);
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) { if(address.getUserData() != null) {
payment.setType((Payment.Type)address.getUserData()); payment.setType((Payment.Type)address.getUserData());
} }
@ -700,14 +533,7 @@ public class PaymentController extends WalletFormController implements Initializ
public void setPayment(Payment payment) { public void setPayment(Payment payment) {
if(getRecipientValueSats() == null || payment.getAmount() != getRecipientValueSats()) { if(getRecipientValueSats() == null || payment.getAmount() != getRecipientValueSats()) {
if(payment.getAddress() != null) { if(payment.getAddress() != null) {
DnsPayment dnsPayment = DnsPaymentCache.getDnsPayment(payment); address.setText(payment.getAddress().toString());
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()); address.setUserData(payment.getType());
} }
if(payment.getLabel() != null && !label.getText().equals(payment.getLabel())) { if(payment.getLabel() != null && !label.getText().equals(payment.getLabel())) {
@ -738,10 +564,7 @@ public class PaymentController extends WalletFormController implements Initializ
setSendMax(false); setSendMax(false);
dustAmountProperty.set(false); dustAmountProperty.set(false);
consolidationNodeProperty.set(null);
payNymProperty.set(null); payNymProperty.set(null);
dnsPaymentProperty.set(null);
silentPaymentAddressProperty.set(null);
} }
public void setMaxInput(ActionEvent event) { public void setMaxInput(ActionEvent event) {
@ -749,7 +572,8 @@ public class PaymentController extends WalletFormController implements Initializ
if(utxoSelector == null) { if(utxoSelector == null) {
MaxUtxoSelector maxUtxoSelector = new MaxUtxoSelector(); MaxUtxoSelector maxUtxoSelector = new MaxUtxoSelector();
sendController.utxoSelectorProperty().set(maxUtxoSelector); sendController.utxoSelectorProperty().set(maxUtxoSelector);
} else if(utxoSelector instanceof PresetUtxoSelector presetUtxoSelector && !isValidAddressAndLabel() && sendController.getPaymentTabs().getTabs().size() == 1) { } else if(utxoSelector instanceof PresetUtxoSelector && !isValidAddressAndLabel() && sendController.getPaymentTabs().getTabs().size() == 1) {
PresetUtxoSelector presetUtxoSelector = (PresetUtxoSelector)utxoSelector;
Payment payment = new Payment(null, null, presetUtxoSelector.getPresetUtxos().stream().mapToLong(BlockTransactionHashIndex::getValue).sum(), true); Payment payment = new Payment(null, null, presetUtxoSelector.getPresetUtxos().stream().mapToLong(BlockTransactionHashIndex::getValue).sum(), true);
setPayment(payment); setPayment(payment);
return; return;
@ -801,7 +625,7 @@ public class PaymentController extends WalletFormController implements Initializ
setRecipientValueSats(bitcoinURI.getAmount()); setRecipientValueSats(bitcoinURI.getAmount());
setFiatAmount(AppServices.getFiatCurrencyExchangeRate(), bitcoinURI.getAmount()); setFiatAmount(AppServices.getFiatCurrencyExchangeRate(), bitcoinURI.getAmount());
} }
if(bitcoinURI.getAddress() != null && bitcoinURI.getPayjoinUrl() != null) { if(bitcoinURI.getPayjoinUrl() != null) {
AppServices.addPayjoinURI(bitcoinURI); AppServices.addPayjoinURI(bitcoinURI);
} }
sendController.updateTransaction(); sendController.updateTransaction();
@ -852,33 +676,10 @@ public class PaymentController extends WalletFormController implements Initializ
public static Glyph getPayNymGlyph() { public static Glyph getPayNymGlyph() {
Glyph payNymGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.ROBOT); Glyph payNymGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.ROBOT);
payNymGlyph.getStyleClass().add("paynym-icon"); payNymGlyph.getStyleClass().add("paynym-icon");
payNymGlyph.setFontSize(10); payNymGlyph.setFontSize(12);
return payNymGlyph; 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() { public static Glyph getNfcCardGlyph() {
Glyph nfcCardGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.WIFI); Glyph nfcCardGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.WIFI);
nfcCardGlyph.getStyleClass().add("nfccard-icon"); nfcCardGlyph.getStyleClass().add("nfccard-icon");
@ -922,23 +723,4 @@ public class PaymentController extends WalletFormController implements Initializ
public void openWallets(OpenWalletsEvent event) { public void openWallets(OpenWalletsEvent event) {
updateOpenWallets(event.getWallets()); 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();
}
};
}
}
} }

View file

@ -6,12 +6,12 @@ import com.sparrowwallet.drongo.KeyPurpose;
import com.sparrowwallet.drongo.Network; import com.sparrowwallet.drongo.Network;
import com.sparrowwallet.drongo.SecureString; import com.sparrowwallet.drongo.SecureString;
import com.sparrowwallet.drongo.address.Address; import com.sparrowwallet.drongo.address.Address;
import com.sparrowwallet.drongo.address.InvalidAddressException;
import com.sparrowwallet.drongo.bip47.PaymentCode; import com.sparrowwallet.drongo.bip47.PaymentCode;
import com.sparrowwallet.drongo.bip47.SecretPoint; import com.sparrowwallet.drongo.bip47.SecretPoint;
import com.sparrowwallet.drongo.crypto.ECKey; import com.sparrowwallet.drongo.crypto.ECKey;
import com.sparrowwallet.drongo.protocol.*; import com.sparrowwallet.drongo.protocol.*;
import com.sparrowwallet.drongo.psbt.PSBT; import com.sparrowwallet.drongo.psbt.PSBT;
import com.sparrowwallet.drongo.silentpayments.SilentPayment;
import com.sparrowwallet.drongo.wallet.*; import com.sparrowwallet.drongo.wallet.*;
import com.sparrowwallet.sparrow.*; import com.sparrowwallet.sparrow.*;
import com.sparrowwallet.sparrow.control.*; 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 Set<WalletNode> excludedChangeNodes = new HashSet<>();
private final Map<Address, WalletNode> walletAddresses = new HashMap<>(); private final Map<Wallet, Map<Address, WalletNode>> addressNodeMap = new HashMap<>();
private final ChangeListener<String> feeListener = new ChangeListener<>() { private final ChangeListener<String> feeListener = new ChangeListener<>() {
@Override @Override
@ -484,6 +484,7 @@ public class SendController extends WalletFormController implements Initializabl
validationSupport.setValidationDecorator(new StyleClassValidationDecoration()); validationSupport.setValidationDecorator(new StyleClassValidationDecoration());
validationSupport.registerValidator(fee, Validator.combine( 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 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()) (Control c, String newValue) -> ValidationResult.fromErrorIf( c, "Insufficient Fee Rate", isInsufficientFeeRate())
)); ));
@ -605,25 +606,18 @@ public class SendController extends WalletFormController implements Initializabl
try { try {
List<Payment> payments = transactionPayments != null ? transactionPayments : getPayments(); List<Payment> payments = transactionPayments != null ? transactionPayments : getPayments();
updateOptimizationButtons(payments); updateOptimizationButtons(payments);
if(!userFeeSet.get() || getFeeValueSats() != null) { if(!userFeeSet.get() || (getFeeValueSats() != null && getFeeValueSats() > 0)) {
Wallet wallet = getWalletForm().getWallet(); Wallet wallet = getWalletForm().getWallet();
Long userFee = userFeeSet.get() ? getFeeValueSats() : null; Long userFee = userFeeSet.get() ? getFeeValueSats() : null;
double feeRate = getUserFeeRate(); double feeRate = getUserFeeRate();
double minRelayFeeRate = AppServices.getMinimumRelayFeeRate();
Integer currentBlockHeight = AppServices.getCurrentBlockHeight(); Integer currentBlockHeight = AppServices.getCurrentBlockHeight();
boolean groupByAddress = Config.get().isGroupByAddress(); boolean groupByAddress = Config.get().isGroupByAddress();
boolean includeMempoolOutputs = Config.get().isIncludeMempoolOutputs(); boolean includeMempoolOutputs = Config.get().isIncludeMempoolOutputs();
BlockTransaction replacedTransaction = replacedTransactionProperty.get(); BlockTransaction replacedTransaction = replacedTransactionProperty.get();
//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 walletTransactionService = new WalletTransactionService(addressNodeMap, wallet, getUtxoSelectors(payments), getTxoFilters(),
boolean allowRbf = (replacedTransaction == null || replacedTransaction.getTransaction().isReplaceByFee())
&& payments.stream().noneMatch(payment -> payment instanceof SilentPayment);
TransactionParameters params = new TransactionParameters(getUtxoSelectors(payments), getTxoFilters(),
payments, opReturnsList, excludedChangeNodes, payments, opReturnsList, excludedChangeNodes,
feeRate, getMinimumFeeRate(), minRelayFeeRate, userFee, feeRate, getMinimumFeeRate(), userFee, currentBlockHeight, groupByAddress, includeMempoolOutputs, replacedTransaction);
currentBlockHeight, groupByAddress, includeMempoolOutputs, allowRbf);
walletTransactionService = new WalletTransactionService(wallet, params, replacedTransaction);
walletTransactionService.setOnSucceeded(event -> { walletTransactionService.setOnSucceeded(event -> {
if(!walletTransactionService.isIgnoreResult()) { if(!walletTransactionService.isIgnoreResult()) {
walletTransactionProperty.setValue(walletTransactionService.getValue()); walletTransactionProperty.setValue(walletTransactionService.getValue());
@ -658,12 +652,12 @@ public class SendController extends WalletFormController implements Initializabl
walletTransactionService.start(); walletTransactionService.start();
} }
} catch(IllegalStateException e) { } catch(InvalidAddressException | IllegalStateException e) {
walletTransactionProperty.setValue(null); walletTransactionProperty.setValue(null);
} }
} }
private List<UtxoSelector> getUtxoSelectors(List<Payment> payments) { private List<UtxoSelector> getUtxoSelectors(List<Payment> payments) throws InvalidAddressException {
if(utxoSelectorProperty.get() != null) { if(utxoSelectorProperty.get() != null) {
return List.of(utxoSelectorProperty.get()); return List.of(utxoSelectorProperty.get());
} }
@ -685,14 +679,39 @@ public class SendController extends WalletFormController implements Initializabl
} }
private static class WalletTransactionService extends Service<WalletTransaction> { private static class WalletTransactionService extends Service<WalletTransaction> {
private final Map<Wallet, Map<Address, WalletNode>> addressNodeMap;
private final Wallet wallet; private final Wallet wallet;
private final TransactionParameters params; 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 BlockTransaction replacedTransaction; private final BlockTransaction replacedTransaction;
private boolean ignoreResult; private boolean ignoreResult;
public WalletTransactionService(Wallet wallet, TransactionParameters params, BlockTransaction replacedTransaction) { 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;
this.wallet = wallet; this.wallet = wallet;
this.params = params; 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.replacedTransaction = replacedTransaction; this.replacedTransaction = replacedTransaction;
} }
@ -703,17 +722,16 @@ public class SendController extends WalletFormController implements Initializabl
try { try {
return getWalletTransaction(); return getWalletTransaction();
} catch(InsufficientFundsException e) { } catch(InsufficientFundsException e) {
if(e.getTargetValue() != null && replacedTransaction != null && wallet.isSafeToAddInputsOrOutputs(replacedTransaction) if(e.getTargetValue() != null && replacedTransaction != null && utxoSelectors.size() == 1 && utxoSelectors.get(0) instanceof PresetUtxoSelector presetUtxoSelector) {
&& params.utxoSelectors().size() == 1 && params.utxoSelectors().getFirst() instanceof PresetUtxoSelector presetUtxoSelector) {
//Creating RBF transaction - include additional UTXOs if available to pay desired fee //Creating RBF transaction - include additional UTXOs if available to pay desired fee
List<TxoFilter> filters = new ArrayList<>(params.txoFilters()); List<TxoFilter> filters = new ArrayList<>(txoFilters);
filters.add(presetUtxoSelector.asExcludeTxoFilter()); filters.add(presetUtxoSelector.asExcludeTxoFilter());
List<OutputGroup> outputGroups = wallet.getGroupedUtxos(filters, params.feeRate(), AppServices.getMinimumRelayFeeRate(), Config.get().isGroupByAddress()) List<OutputGroup> outputGroups = wallet.getGroupedUtxos(filters, feeRate, AppServices.getMinimumRelayFeeRate(), Config.get().isGroupByAddress())
.stream().filter(outputGroup -> outputGroup.getEffectiveValue() >= 0).collect(Collectors.toList()); .stream().filter(outputGroup -> outputGroup.getEffectiveValue() >= 0).collect(Collectors.toList());
Collections.shuffle(outputGroups); Collections.shuffle(outputGroups);
while(!outputGroups.isEmpty() && presetUtxoSelector.getPresetUtxos().stream().mapToLong(BlockTransactionHashIndex::getValue).sum() < e.getTargetValue()) { while(!outputGroups.isEmpty() && presetUtxoSelector.getPresetUtxos().stream().mapToLong(BlockTransactionHashIndex::getValue).sum() < e.getTargetValue()) {
OutputGroup outputGroup = outputGroups.removeFirst(); OutputGroup outputGroup = outputGroups.remove(0);
for(BlockTransactionHashIndex utxo : outputGroup.getUtxos()) { for(BlockTransactionHashIndex utxo : outputGroup.getUtxos()) {
presetUtxoSelector.getPresetUtxos().add(utxo); presetUtxoSelector.getPresetUtxos().add(utxo);
} }
@ -727,12 +745,12 @@ public class SendController extends WalletFormController implements Initializabl
} }
private WalletTransaction getWalletTransaction() throws InsufficientFundsException { private WalletTransaction getWalletTransaction() throws InsufficientFundsException {
try { updateMessage("Selecting UTXOs...");
updateMessage("Selecting UTXOs..."); WalletTransaction walletTransaction = wallet.createWalletTransaction(utxoSelectors, txoFilters, payments, opReturns, excludedChangeNodes,
return wallet.createWalletTransaction(params); feeRate, longTermFeeRate, fee, currentBlockHeight, groupByAddress, includeMempoolOutputs);
} finally { updateMessage("Deriving keys...");
updateMessage(""); walletTransaction.updateAddressNodeMap(addressNodeMap, walletTransaction.getWallet());
} return walletTransaction;
} }
}; };
} }
@ -860,7 +878,7 @@ public class SendController extends WalletFormController implements Initializabl
* @return the fee rate to use when constructing a transaction * @return the fee rate to use when constructing a transaction
*/ */
public Double getUserFeeRate() { public Double getUserFeeRate() {
return (userFeeSet.get() ? AppServices.getMinimumRelayFeeRate() : getFeeRate()); return (userFeeSet.get() ? Transaction.DEFAULT_MIN_RELAY_FEE : getFeeRate());
} }
public Double getFeeRate() { public Double getFeeRate() {
@ -924,6 +942,7 @@ public class SendController extends WalletFormController implements Initializabl
private void setFeeRatePriority(Double feeRateAmt) { private void setFeeRatePriority(Double feeRateAmt) {
Map<Integer, Double> targetBlocksFeeRates = getTargetBlocksFeeRates(); Map<Integer, Double> targetBlocksFeeRates = getTargetBlocksFeeRates();
Integer targetBlocks = getTargetBlocks(feeRateAmt);
if(targetBlocksFeeRates.get(Integer.MAX_VALUE) != null) { if(targetBlocksFeeRates.get(Integer.MAX_VALUE) != null) {
Double minFeeRate = targetBlocksFeeRates.get(Integer.MAX_VALUE); Double minFeeRate = targetBlocksFeeRates.get(Integer.MAX_VALUE);
if(minFeeRate > 1.0 && feeRateAmt < minFeeRate) { if(minFeeRate > 1.0 && feeRateAmt < minFeeRate) {
@ -944,10 +963,9 @@ public class SendController extends WalletFormController implements Initializabl
} }
} }
Integer targetBlocks = getTargetBlocks(feeRateAmt);
if(targetBlocks != null) { if(targetBlocks != null) {
if(targetBlocks < FeeRatesSource.BLOCKS_IN_HALF_HOUR) { if(targetBlocks < FeeRatesSource.BLOCKS_IN_HALF_HOUR) {
Double maxFeeRate = AppServices.getFeeRatesRange().getLast(); Double maxFeeRate = FEE_RATES_RANGE.get(FEE_RATES_RANGE.size() - 1).doubleValue();
Double highestBlocksRate = targetBlocksFeeRates.get(TARGET_BLOCKS_RANGE.get(0)); Double highestBlocksRate = targetBlocksFeeRates.get(TARGET_BLOCKS_RANGE.get(0));
if(highestBlocksRate < maxFeeRate && feeRateAmt > (highestBlocksRate + ((maxFeeRate - highestBlocksRate) / 10))) { if(highestBlocksRate < maxFeeRate && feeRateAmt > (highestBlocksRate + ((maxFeeRate - highestBlocksRate) / 10))) {
feeRatePriority.setText("Overpaid"); feeRatePriority.setText("Overpaid");
@ -1097,7 +1115,7 @@ public class SendController extends WalletFormController implements Initializabl
paymentCodeProperty.set(null); paymentCodeProperty.set(null);
walletAddresses.clear(); addressNodeMap.clear();
} }
public UtxoSelector getUtxoSelector() { public UtxoSelector getUtxoSelector() {
@ -1175,20 +1193,13 @@ public class SendController extends WalletFormController implements Initializabl
WalletTransaction walletTransaction = walletTransactionProperty.get(); WalletTransaction walletTransaction = walletTransactionProperty.get();
Set<WalletNode> nodes = new LinkedHashSet<>(walletTransaction.getSelectedUtxos().values()); Set<WalletNode> nodes = new LinkedHashSet<>(walletTransaction.getSelectedUtxos().values());
nodes.addAll(walletTransaction.getChangeMap().keySet()); nodes.addAll(walletTransaction.getChangeMap().keySet());
nodes.addAll(walletTransaction.getWalletNodePayments().stream().map(WalletNodePayment::getWalletNode).collect(Collectors.toList())); Map<Address, WalletNode> addressNodeMap = walletTransaction.getAddressNodeMap();
nodes.addAll(addressNodeMap.values().stream().filter(Objects::nonNull).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 //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); 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) { public void broadcastNotification(ActionEvent event) {
Wallet wallet = getWalletForm().getWallet(); Wallet wallet = getWalletForm().getWallet();
Storage storage = AppServices.get().getOpenWallets().get(wallet); Storage storage = AppServices.get().getOpenWallets().get(wallet);
@ -1232,14 +1243,11 @@ public class SendController extends WalletFormController implements Initializabl
List<UtxoSelector> utxoSelectors = List.of(new PresetUtxoSelector(walletTransaction.getSelectedUtxos().keySet(), true, false)); List<UtxoSelector> utxoSelectors = List.of(new PresetUtxoSelector(walletTransaction.getSelectedUtxos().keySet(), true, false));
Long userFee = userFeeSet.get() ? getFeeValueSats() : null; Long userFee = userFeeSet.get() ? getFeeValueSats() : null;
double feeRate = getUserFeeRate(); double feeRate = getUserFeeRate();
Double minRelayFeeRate = AppServices.getMinimumRelayFeeRate();
Integer currentBlockHeight = AppServices.getCurrentBlockHeight(); Integer currentBlockHeight = AppServices.getCurrentBlockHeight();
boolean groupByAddress = Config.get().isGroupByAddress(); boolean groupByAddress = Config.get().isGroupByAddress();
boolean includeMempoolOutputs = Config.get().isIncludeMempoolOutputs(); boolean includeMempoolOutputs = Config.get().isIncludeMempoolOutputs();
TransactionParameters params = new TransactionParameters(utxoSelectors, getTxoFilters(), walletTransaction.getPayments(), List.of(blindedPaymentCode), WalletTransaction finalWalletTx = decryptedWallet.createWalletTransaction(utxoSelectors, getTxoFilters(), walletTransaction.getPayments(), List.of(blindedPaymentCode), excludedChangeNodes, feeRate, getMinimumFeeRate(), userFee, currentBlockHeight, groupByAddress, includeMempoolOutputs);
excludedChangeNodes, feeRate, getMinimumFeeRate(), minRelayFeeRate, userFee, currentBlockHeight, groupByAddress, includeMempoolOutputs, true);
WalletTransaction finalWalletTx = decryptedWallet.createWalletTransaction(params);
PSBT psbt = finalWalletTx.createPSBT(); PSBT psbt = finalWalletTx.createPSBT();
decryptedWallet.sign(psbt); decryptedWallet.sign(psbt);
decryptedWallet.finalise(psbt); decryptedWallet.finalise(psbt);
@ -1498,7 +1506,7 @@ public class SendController extends WalletFormController implements Initializabl
notificationButton.setVisible(isNotificationTransaction); notificationButton.setVisible(isNotificationTransaction);
notificationButton.setDefaultButton(isNotificationTransaction); notificationButton.setDefaultButton(isNotificationTransaction);
setInputFieldsDisabled(!event.allowPaymentChanges(), false); setInputFieldsDisabled(isNotificationTransaction, false);
} }
} }
@ -1627,26 +1635,18 @@ public class SendController extends WalletFormController implements Initializabl
recentBlocksView.updateFeeRatesSource(event.getFeeRateSource()); 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 class PrivacyAnalysisTooltip extends VBox {
private final List<Label> analysisLabels = new ArrayList<>(); private final List<Label> analysisLabels = new ArrayList<>();
public PrivacyAnalysisTooltip(WalletTransaction walletTransaction) { public PrivacyAnalysisTooltip(WalletTransaction walletTransaction) {
List<Payment> payments = walletTransaction.getPayments(); List<Payment> payments = walletTransaction.getPayments();
List<Payment> userPayments = payments.stream().filter(payment -> payment.getType() != Payment.Type.FAKE_MIX).collect(Collectors.toList()); List<Payment> userPayments = payments.stream().filter(payment -> payment.getType() != Payment.Type.FAKE_MIX).collect(Collectors.toList());
List<WalletNodePayment> walletNodePayments = walletTransaction.getWalletNodePayments(); Map<Address, WalletNode> walletAddresses = walletTransaction.getAddressNodeMap();
OptimizationStrategy optimizationStrategy = getPreferredOptimizationStrategy(); OptimizationStrategy optimizationStrategy = getPreferredOptimizationStrategy();
boolean fakeMixPresent = payments.stream().anyMatch(payment -> payment.getType() == Payment.Type.FAKE_MIX); boolean fakeMixPresent = payments.stream().anyMatch(payment -> payment.getType() == Payment.Type.FAKE_MIX);
boolean roundPaymentAmounts = userPayments.stream().anyMatch(payment -> payment.getAmount() % 100 == 0); 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 mixedAddressTypes = userPayments.stream().anyMatch(payment -> payment.getAddress().getScriptType() != getWalletForm().getWallet().getFreshNode(KeyPurpose.RECEIVE).getAddress().getScriptType());
boolean addressReuse = walletNodePayments.stream().anyMatch(walletNodePayment -> !walletNodePayment.getWalletNode().getTransactionOutputs().isEmpty()); boolean addressReuse = userPayments.stream().anyMatch(payment -> walletAddresses.get(payment.getAddress()) != null && !walletAddresses.get(payment.getAddress()).getTransactionOutputs().isEmpty());
boolean payjoinPresent = userPayments.stream().anyMatch(payment -> AppServices.getPayjoinURI(payment.getAddress()) != null); boolean payjoinPresent = userPayments.stream().anyMatch(payment -> AppServices.getPayjoinURI(payment.getAddress()) != null);
if(optimizationStrategy == OptimizationStrategy.PRIVACY) { if(optimizationStrategy == OptimizationStrategy.PRIVACY) {

View file

@ -41,11 +41,8 @@ import java.io.IOException;
import java.net.URL; import java.net.URL;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.util.*; import java.util.*;
import java.util.regex.Matcher;
import java.util.stream.Collectors; 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.showErrorDialog;
import static com.sparrowwallet.sparrow.AppServices.showWarningDialog; import static com.sparrowwallet.sparrow.AppServices.showWarningDialog;
@ -458,26 +455,6 @@ 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."); 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", "")); setDescriptorText(text.get().replace("\n", ""));
} }
} }

View file

@ -27,14 +27,13 @@ open module com.sparrowwallet.sparrow {
requires com.google.gson; requires com.google.gson;
requires org.jdbi.v3.core; requires org.jdbi.v3.core;
requires org.jdbi.v3.sqlobject; requires org.jdbi.v3.sqlobject;
requires io.leangen.geantyref;
requires org.flywaydb.core; requires org.flywaydb.core;
requires com.zaxxer.hikari; requires com.zaxxer.hikari;
requires com.h2database; requires com.h2database;
requires com.sparrowwallet.hummingbird; requires com.sparrowwallet.hummingbird;
requires org.fxmisc.flowless; requires org.fxmisc.flowless;
requires openpnp.capture.java; requires openpnp.capture.java;
requires nsmenufx; requires centerdevice.nsmenufx;
requires org.jcommander; requires org.jcommander;
requires jul.to.slf4j; requires jul.to.slf4j;
requires net.sourceforge.javacsv; requires net.sourceforge.javacsv;
@ -57,5 +56,4 @@ open module com.sparrowwallet.sparrow {
requires com.sparrowwallet.tern; requires com.sparrowwallet.tern;
requires com.sparrowwallet.lark; requires com.sparrowwallet.lark;
requires com.sun.jna; requires com.sun.jna;
requires io.github.doblon8.jzbar;
} }

View file

@ -0,0 +1,76 @@
/*------------------------------------------------------------------------
* 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;
}

View file

@ -0,0 +1,197 @@
/*------------------------------------------------------------------------
* 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);
}

View file

@ -0,0 +1,110 @@
/*------------------------------------------------------------------------
* 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);
}

View file

@ -0,0 +1,44 @@
/*------------------------------------------------------------------------
* 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;
}

View file

@ -0,0 +1,52 @@
/*------------------------------------------------------------------------
* 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;
}

View file

@ -0,0 +1,265 @@
/*------------------------------------------------------------------------
* 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();
}

View file

@ -0,0 +1,75 @@
/*------------------------------------------------------------------------
* 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"));
}
}

View file

@ -0,0 +1,93 @@
/*------------------------------------------------------------------------
* 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);
}

View file

@ -1,21 +1,29 @@
package com.sparrowwallet.sparrow.io; package net.sourceforge.zbar;
import io.github.doblon8.jzbar.Config; import com.sparrowwallet.sparrow.net.NativeUtils;
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.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import java.awt.*; import java.awt.*;
import java.awt.image.BufferedImage; import java.awt.image.BufferedImage;
import java.awt.image.DataBufferByte; import java.awt.image.DataBufferByte;
import java.util.Iterator;
public class ZBar { public class ZBar {
private static final Logger log = LoggerFactory.getLogger(ZBar.class); 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() { public static boolean isEnabled() {
return com.sparrowwallet.sparrow.io.Config.get().isUseZbar(); return enabled;
} }
public static Scan scan(BufferedImage bufferedImage) { public static Scan scan(BufferedImage bufferedImage) {
@ -33,12 +41,19 @@ public class ZBar {
image.setData(data); image.setData(data);
try(ImageScanner scanner = new ImageScanner()) { try(ImageScanner scanner = new ImageScanner()) {
scanner.setConfig(SymbolType.NONE, Config.ENABLE, 0); scanner.setConfig(Symbol.NONE, Config.ENABLE, 0);
scanner.setConfig(SymbolType.QRCODE, Config.ENABLE, 1); scanner.setConfig(Symbol.QRCODE, Config.ENABLE, 1);
int result = scanner.scanImage(image); int result = scanner.scanImage(image);
if(result != 0) { if(result != 0) {
String symbolData = image.getFirstSymbol().getData(); try(SymbolSet results = scanner.getResults()) {
return new Scan(getRawBytes(symbolData), symbolData); 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;
}
} }
} }
} }
@ -82,6 +97,31 @@ public class ZBar {
return outputData; 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) { private static byte[] getRawBytes(String str) {
char[] chars = str.toCharArray(); char[] chars = str.toCharArray();
byte[] bytes = new byte[chars.length]; byte[] bytes = new byte[chars.length];

View file

@ -329,10 +329,6 @@ HorizontalHeaderColumn > TableColumnHeader.column-header.table-column{
-fx-stroke: #696c77; -fx-stroke: #696c77;
} }
#blockchainForm #blockStatus {
-fx-text-fill: white;
}
.root .progress-indicator.progress-timer.warn > .determinate-indicator > .indicator { .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%); -fx-background-color: -fx-box-border, radial-gradient(center 50% 50%, radius 50%, #e06c75 70%, derive(-fx-control-inner-background, -9%) 100%);
} }

View file

@ -46,7 +46,7 @@
.id, .fixed-width { .id, .fixed-width {
-fx-font-size: 13px; -fx-font-size: 13px;
-fx-font-family: 'Fragment Mono Regular'; -fx-font-family: 'Roboto Mono';
} }
.form-separator { .form-separator {

View file

@ -29,7 +29,7 @@
.virtualized-scroll-pane .code-area, .uneditable-codearea { .virtualized-scroll-pane .code-area, .uneditable-codearea {
-fx-font-size: 13px; -fx-font-size: 13px;
-fx-font-family: 'Fragment Mono Regular'; -fx-font-family: 'Roboto Mono';
-fx-padding: 4; -fx-padding: 4;
-fx-fill: -fx-text-inner-color; -fx-fill: -fx-text-inner-color;
} }

View file

@ -44,7 +44,7 @@
#transactionDiagram .input-label, #transactionDiagram .recipient-label, #transactionDiagram .change-label, #transactionDiagram .fee-tooltip, #transactionDiagram .transaction-tooltip { #transactionDiagram .input-label, #transactionDiagram .recipient-label, #transactionDiagram .change-label, #transactionDiagram .fee-tooltip, #transactionDiagram .transaction-tooltip {
-fx-font-size: 13px; -fx-font-size: 13px;
-fx-font-family: 'Fragment Mono Regular'; -fx-font-family: 'Roboto Mono';
} }
#transactionDiagram .fee-warning-icon { #transactionDiagram .fee-warning-icon {

View file

@ -40,9 +40,7 @@
<ColumnConstraints percentWidth="50" /> <ColumnConstraints percentWidth="50" />
</columnConstraints> </columnConstraints>
<rowConstraints> <rowConstraints>
<RowConstraints /> <RowConstraints />
<RowConstraints />
<RowConstraints vgrow="SOMETIMES" />
</rowConstraints> </rowConstraints>
<Form GridPane.columnIndex="0" GridPane.rowIndex="0" GridPane.columnSpan="2"> <Form GridPane.columnIndex="0" GridPane.rowIndex="0" GridPane.columnSpan="2">
<Fieldset text="Transaction" inputGrow="SOMETIMES" wrapWidth="620"> <Fieldset text="Transaction" inputGrow="SOMETIMES" wrapWidth="620">
@ -76,11 +74,9 @@
<TabPane side="RIGHT" GridPane.columnIndex="0" GridPane.rowIndex="2" GridPane.columnSpan="2" styleClass="headers-tabs"> <TabPane side="RIGHT" GridPane.columnIndex="0" GridPane.rowIndex="2" GridPane.columnSpan="2" styleClass="headers-tabs">
<Tab text="Overview" closable="false"> <Tab text="Overview" closable="false">
<VBox spacing="8" alignment="CENTER"> <VBox spacing="8">
<Region VBox.vgrow="SOMETIMES" /> <TransactionDiagram fx:id="transactionDiagram" maxWidth="700" final="true"/>
<TransactionDiagram fx:id="transactionDiagram" final="true"/> <TransactionDiagramLabel fx:id="transactionDiagramLabel" maxWidth="640" prefWidth="640" />
<TransactionDiagramLabel fx:id="transactionDiagramLabel" />
<Region VBox.vgrow="SOMETIMES" />
</VBox> </VBox>
</Tab> </Tab>
<Tab text="Detail" closable="false"> <Tab text="Detail" closable="false">
@ -180,44 +176,19 @@
<Separator GridPane.columnIndex="0" GridPane.rowIndex="5" GridPane.columnSpan="2" styleClass="form-separator"/> <Separator GridPane.columnIndex="0" GridPane.rowIndex="5" GridPane.columnSpan="2" styleClass="form-separator"/>
<GridPane GridPane.columnIndex="0" GridPane.columnSpan="2" GridPane.rowIndex="6"> <DynamicForm fx:id="blockchainForm" GridPane.columnIndex="0" GridPane.columnSpan="2" GridPane.rowIndex="6">
<columnConstraints> <Fieldset text="Blockchain" inputGrow="SOMETIMES">
<ColumnConstraints percentWidth="80" /> <Field text="Status:">
<ColumnConstraints percentWidth="20" /> <Label fx:id="blockStatus" contentDisplay="RIGHT" graphicTextGap="5" />
</columnConstraints> </Field>
<DynamicForm fx:id="blockchainForm" GridPane.columnIndex="0" GridPane.rowIndex="0"> <Field fx:id="blockHeightField" text="Block Height:">
<Fieldset text="Blockchain" inputGrow="SOMETIMES"> <CopyableLabel fx:id="blockHeight" />
<Field text="Status:"> </Field>
<Label fx:id="blockStatus" contentDisplay="RIGHT" graphicTextGap="5" /> <Field fx:id="blockTimestampField" text="Timestamp:">
</Field> <CopyableLabel fx:id="blockTimestamp" />
<Field fx:id="blockHeightField" text="Block Height:"> </Field>
<CopyableLabel fx:id="blockHeight" /> </Fieldset>
</Field> </DynamicForm>
<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"> <Form fx:id="signingWalletForm" GridPane.columnIndex="0" GridPane.rowIndex="6">
<Fieldset text="Signatures" inputGrow="SOMETIMES" styleClass="relaxedLabelFieldSet"> <Fieldset text="Signatures" inputGrow="SOMETIMES" styleClass="relaxedLabelFieldSet">
@ -265,7 +236,7 @@
<Fieldset text="Signatures" inputGrow="SOMETIMES"> <Fieldset text="Signatures" inputGrow="SOMETIMES">
<VBox> <VBox>
<SignaturesProgressBar fx:id="signaturesProgressBar" /> <SignaturesProgressBar fx:id="signaturesProgressBar" />
<ProgressBar fx:id="broadcastProgressBar" maxWidth="Infinity" minHeight="50" prefHeight="50" /> <ProgressBar fx:id="broadcastProgressBar" maxWidth="Infinity" prefHeight="50" />
</VBox> </VBox>
<VBox> <VBox>
<HBox fx:id="signButtonBox" styleClass="signatures-buttons" spacing="20"> <HBox fx:id="signButtonBox" styleClass="signatures-buttons" spacing="20">

View file

@ -23,7 +23,7 @@
.chart-legend-item { .chart-legend-item {
-fx-font-size: 13px; -fx-font-size: 13px;
-fx-font-family: 'Fragment Mono Regular'; -fx-font-family: 'Roboto Mono';
} }
.default-color0.chart-pie { -fx-pie-color: #ca1243 } .default-color0.chart-pie { -fx-pie-color: #ca1243 }

View file

@ -23,7 +23,7 @@
.chart-legend-item { .chart-legend-item {
-fx-font-size: 13; -fx-font-size: 13;
-fx-font-family: 'Fragment Mono Regular'; -fx-font-family: 'Roboto Mono';
} }
.default-color7.chart-pie { -fx-pie-color: #0184bc } .default-color7.chart-pie { -fx-pie-color: #0184bc }

View file

@ -1,7 +1,7 @@
#txhex { #txhex {
-fx-background-color: -fx-control-inner-background; -fx-background-color: -fx-control-inner-background;
-fx-font-size: 13px; -fx-font-size: 13px;
-fx-font-family: 'Fragment Mono Regular'; -fx-font-family: 'Roboto Mono';
-fx-padding: 2; -fx-padding: 2;
color-0: #ca1243; color-0: #ca1243;
color-1: #d75f00; color-1: #d75f00;

View file

@ -15,7 +15,7 @@
#fingerprint, #derivation, #xpub { #fingerprint, #derivation, #xpub {
-fx-font-size: 13px; -fx-font-size: 13px;
-fx-font-family: 'Fragment Mono Regular'; -fx-font-family: 'Roboto Mono';
} }
#type { #type {

View file

@ -21,7 +21,7 @@
<Insets top="10.0" bottom="10.0" /> <Insets top="10.0" bottom="10.0" />
</padding> </padding>
<columnConstraints> <columnConstraints>
<ColumnConstraints prefWidth="410" hgrow="SOMETIMES" /> <ColumnConstraints prefWidth="410" />
<ColumnConstraints prefWidth="200" /> <ColumnConstraints prefWidth="200" />
<ColumnConstraints prefWidth="105" /> <ColumnConstraints prefWidth="105" />
</columnConstraints> </columnConstraints>

View file

@ -26,7 +26,7 @@
<Insets left="25.0" right="25.0" top="25.0" /> <Insets left="25.0" right="25.0" top="25.0" />
</padding> </padding>
<columnConstraints> <columnConstraints>
<ColumnConstraints prefWidth="620" hgrow="SOMETIMES" /> <ColumnConstraints prefWidth="620" />
<ColumnConstraints prefWidth="140" /> <ColumnConstraints prefWidth="140" />
</columnConstraints> </columnConstraints>
<rowConstraints> <rowConstraints>

View file

@ -71,7 +71,7 @@
#transactionDiagram .input-label, #transactionDiagram .recipient-label, #transactionDiagram .change-label, #transactionDiagram .fee-tooltip, #transactionDiagram .transaction-tooltip { #transactionDiagram .input-label, #transactionDiagram .recipient-label, #transactionDiagram .change-label, #transactionDiagram .fee-tooltip, #transactionDiagram .transaction-tooltip {
-fx-font-size: 13px; -fx-font-size: 13px;
-fx-font-family: 'Fragment Mono Regular'; -fx-font-family: 'Roboto Mono';
} }
#transactionDiagram .fee-warning-icon { #transactionDiagram .fee-warning-icon {

View file

@ -36,7 +36,7 @@
<Insets left="25.0" right="25.0" top="25.0" /> <Insets left="25.0" right="25.0" top="25.0" />
</padding> </padding>
<columnConstraints> <columnConstraints>
<ColumnConstraints prefWidth="410" hgrow="SOMETIMES" /> <ColumnConstraints prefWidth="410" />
<ColumnConstraints prefWidth="200" /> <ColumnConstraints prefWidth="200" />
<ColumnConstraints prefWidth="140" /> <ColumnConstraints prefWidth="140" />
</columnConstraints> </columnConstraints>
@ -152,9 +152,9 @@
<RecentBlocksView fx:id="recentBlocksView" styleClass="feeRatesChart" AnchorPane.topAnchor="10" AnchorPane.leftAnchor="74" translateY="30" minHeight="135"/> <RecentBlocksView fx:id="recentBlocksView" styleClass="feeRatesChart" AnchorPane.topAnchor="10" AnchorPane.leftAnchor="74" translateY="30" minHeight="135"/>
</AnchorPane> </AnchorPane>
</GridPane> </GridPane>
<StackPane VBox.vgrow="SOMETIMES"> <AnchorPane>
<TransactionDiagram fx:id="transactionDiagram" /> <TransactionDiagram fx:id="transactionDiagram" maxWidth="700" AnchorPane.leftAnchor="100" />
</StackPane> </AnchorPane>
</VBox> </VBox>
</center> </center>
<bottom> <bottom>

View file

@ -31,7 +31,7 @@
.address-cell, .utxo-row.entry-cell { .address-cell, .utxo-row.entry-cell {
-fx-font-size: 13px; -fx-font-size: 13px;
-fx-font-family: 'Fragment Mono Regular'; -fx-font-family: 'Roboto Mono';
} }
.cell > .hyperlink { .cell > .hyperlink {
@ -149,7 +149,7 @@
.address-text-field { .address-text-field {
-fx-font-size: 13px; -fx-font-size: 13px;
-fx-font-family: 'Fragment Mono Regular'; -fx-font-family: 'Roboto Mono';
} }
.unconfirmed-row { .unconfirmed-row {

Binary file not shown.

Binary file not shown.

View file

@ -1,11 +0,0 @@
<?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>

Before

Width:  |  Height:  |  Size: 4.1 KiB

View file

@ -1,11 +0,0 @@
<?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>

Before

Width:  |  Height:  |  Size: 4.2 KiB

View file

@ -36,7 +36,6 @@
<logger name="org.springframework.web.HttpLogging" level="OFF" /> <logger name="org.springframework.web.HttpLogging" level="OFF" />
<logger name="org.springframework.web.socket.sockjs.client.SockJsClient" level="OFF" /> <logger name="org.springframework.web.socket.sockjs.client.SockJsClient" level="OFF" />
<logger name="org.springframework.web.socket.sockjs.client.DefaultTransportRequest" level="OFF" /> <logger name="org.springframework.web.socket.sockjs.client.DefaultTransportRequest" level="OFF" />
<logger name="org.xbill.DNS.dnssec.DnsSecVerifier" level="ERROR" />
<contextListener class="ch.qos.logback.classic.jul.LevelChangePropagator"/> <contextListener class="ch.qos.logback.classic.jul.LevelChangePropagator"/>

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.