mirror of
https://github.com/sparrowwallet/sparrow.git
synced 2025-11-05 11:56:37 +00:00
Compare commits
91 commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3a5fa69fb6 | ||
|
|
4774830ce4 | ||
|
|
2f62a9e9c8 | ||
|
|
75bcfe2253 | ||
|
|
bedf1399ea | ||
|
|
58575793ea | ||
|
|
6c9b580d4f | ||
|
|
31909b7a15 | ||
|
|
092267339a | ||
|
|
0974918cff | ||
|
|
0f4c36b3c2 | ||
|
|
e1fe35fb74 | ||
|
|
d37fd00c4b | ||
|
|
5f54f86df7 | ||
|
|
e2fa3df08d | ||
|
|
6d6ede9abe | ||
|
|
cca9ab1056 | ||
|
|
9e33861110 | ||
|
|
c3d3fd1fda | ||
|
|
ca8553ecb8 | ||
|
|
d23ee8c086 | ||
|
|
e776a17ad4 | ||
|
|
480ce1e476 | ||
|
|
656cd90b08 | ||
|
|
8df0777959 | ||
|
|
84566b92e6 | ||
|
|
7802510e58 | ||
|
|
efb1eb1051 | ||
|
|
6240667478 | ||
|
|
2c27112dad | ||
|
|
6d53e1ed1d | ||
|
|
e8c5660897 | ||
|
|
bef6c750bd | ||
|
|
4ec3603789 | ||
|
|
90c9f9733f | ||
|
|
64efcf67d3 | ||
|
|
385d173948 | ||
|
|
d81b868049 | ||
|
|
2ff7a15d1e | ||
|
|
f48fa7e23c | ||
|
|
4632850e1e | ||
|
|
5f62523710 | ||
|
|
9dcf210762 | ||
|
|
c7e9a0a161 | ||
|
|
fa10714844 | ||
|
|
80105aee62 | ||
|
|
3c5fa58a16 | ||
|
|
2a2be2617c | ||
|
|
6c9a0d14cd | ||
|
|
f82fcb58bb | ||
|
|
5ec3bff6a4 | ||
|
|
134dc826ba | ||
|
|
cd2a6623a4 | ||
|
|
49ab9e40e3 | ||
|
|
cec7eac9ac | ||
|
|
33e043fd9a | ||
|
|
3aae26b196 | ||
|
|
73d4fd5049 | ||
|
|
a94380e882 | ||
|
|
e4dd4950bf | ||
|
|
26ce1b3469 | ||
|
|
ebce34f3d1 | ||
|
|
f28e00b97e | ||
|
|
25770c2426 | ||
|
|
799cac7b1f | ||
|
|
c265fd1969 | ||
|
|
890f0476b1 | ||
|
|
4d93381124 | ||
|
|
364909cfa3 | ||
|
|
38f0068411 | ||
|
|
8885e48ed9 | ||
|
|
31ce3ce68a | ||
|
|
b0d0514617 | ||
|
|
d7d23f9b58 | ||
|
|
3fdf093a26 | ||
|
|
74c298fd93 | ||
|
|
4298bfb053 | ||
|
|
231eb13cee | ||
|
|
52470ee6d8 | ||
|
|
853949675e | ||
|
|
098afebbe0 | ||
|
|
63c0a6d6e2 | ||
|
|
77c305f90b | ||
|
|
276f8b4148 | ||
|
|
b3c92617c9 | ||
|
|
58635801fc | ||
|
|
8c32bb3903 | ||
|
|
55a2c86a83 | ||
|
|
345e018eb9 | ||
|
|
45d2dee764 | ||
|
|
250bc84060 |
125 changed files with 2447 additions and 1601 deletions
10
.github/workflows/package.yaml
vendored
10
.github/workflows/package.yaml
vendored
|
|
@ -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@v4
|
- uses: actions/checkout@v5
|
||||||
with:
|
with:
|
||||||
submodules: true
|
submodules: true
|
||||||
- name: Set up JDK 22.0.2
|
- name: Set up JDK 22.0.2
|
||||||
uses: actions/setup-java@v4
|
uses: actions/setup-java@v5
|
||||||
with:
|
with:
|
||||||
distribution: 'temurin'
|
distribution: 'temurin'
|
||||||
java-version: '22.0.2'
|
java-version: '22.0.2'
|
||||||
|
|
@ -30,6 +30,9 @@ jobs:
|
||||||
- name: Package tar distribution
|
- name: Package tar distribution
|
||||||
if: ${{ runner.os == 'Linux' }}
|
if: ${{ runner.os == 'Linux' }}
|
||||||
run: ./gradlew packageTarDistribution
|
run: ./gradlew packageTarDistribution
|
||||||
|
- name: Repackage deb distribution
|
||||||
|
if: ${{ runner.os == 'Linux' }}
|
||||||
|
run: ./repackage.sh
|
||||||
- name: Upload Artifact
|
- name: Upload Artifact
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
|
|
@ -43,6 +46,9 @@ jobs:
|
||||||
- name: Package headless tar distribution
|
- name: Package headless tar distribution
|
||||||
if: ${{ runner.os == 'Linux' }}
|
if: ${{ runner.os == 'Linux' }}
|
||||||
run: ./gradlew -Djava.awt.headless=true packageTarDistribution
|
run: ./gradlew -Djava.awt.headless=true packageTarDistribution
|
||||||
|
- name: Repackage headless deb distribution
|
||||||
|
if: ${{ runner.os == 'Linux' }}
|
||||||
|
run: ./repackage.sh
|
||||||
- name: Upload Headless Artifact
|
- name: Upload Headless Artifact
|
||||||
if: ${{ runner.os == 'Linux' }}
|
if: ${{ runner.os == 'Linux' }}
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v4
|
||||||
|
|
|
||||||
95
build.gradle
95
build.gradle
|
|
@ -1,9 +1,9 @@
|
||||||
plugins {
|
plugins {
|
||||||
id 'application'
|
id 'application'
|
||||||
id 'org-openjfx-javafxplugin'
|
id 'org-openjfx-javafxplugin'
|
||||||
id 'org.beryx.jlink' version '3.1.1'
|
id 'org.beryx.jlink' version '3.1.3'
|
||||||
id 'org.gradlex.extra-java-module-info' version '1.9'
|
id 'org.gradlex.extra-java-module-info' version '1.13'
|
||||||
id 'io.matthewnelson.kmp.tor.resource-filterjar' version '408.16.2'
|
id 'io.matthewnelson.kmp.tor.resource-filterjar' version '408.16.3'
|
||||||
}
|
}
|
||||||
|
|
||||||
def os = org.gradle.internal.os.OperatingSystem.current()
|
def os = org.gradle.internal.os.OperatingSystem.current()
|
||||||
|
|
@ -19,17 +19,16 @@ 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.2.0'
|
version = '2.3.1'
|
||||||
|
|
||||||
repositories {
|
repositories {
|
||||||
mavenCentral()
|
mavenCentral()
|
||||||
maven { url 'https://code.sparrowwallet.com/api/packages/sparrowwallet/maven' }
|
maven { url = uri('https://code.sparrowwallet.com/api/packages/sparrowwallet/maven') }
|
||||||
}
|
}
|
||||||
|
|
||||||
tasks.withType(AbstractArchiveTask) {
|
tasks.withType(AbstractArchiveTask).configureEach {
|
||||||
preserveFileTimestamps = false
|
useFileSystemPermissions()
|
||||||
reproducibleFileOrder = true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
javafx {
|
javafx {
|
||||||
|
|
@ -45,20 +44,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.0.0-jre')
|
implementation('com.google.guava:guava:33.5.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.20.0') {
|
implementation('org.jdbi:jdbi3-core:3.49.5') {
|
||||||
exclude group: 'org.slf4j'
|
exclude group: 'org.slf4j'
|
||||||
}
|
}
|
||||||
implementation('org.jdbi:jdbi3-sqlobject:3.20.0') {
|
implementation('org.jdbi:jdbi3-sqlobject:3.49.5') {
|
||||||
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.10.4')
|
implementation('org.fxmisc.richtext:richtextfx:0.11.6')
|
||||||
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'
|
||||||
|
|
@ -74,13 +73,15 @@ 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.28-5')
|
implementation('org.openpnp:openpnp-capture-java:0.0.30-1')
|
||||||
implementation("io.matthewnelson.kmp-tor:runtime:2.2.1")
|
implementation("io.matthewnelson.kmp-tor:runtime:2.2.1")
|
||||||
implementation("io.matthewnelson.kmp-tor:resource-exec-tor-gpl:408.16.2")
|
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.codecentric.centerdevice:centerdevice-nsmenufx:2.1.7')
|
implementation('de.jangassen:nsmenufx:3.1.0') {
|
||||||
|
exclude group: 'net.java.dev.jna', module: 'jna'
|
||||||
|
}
|
||||||
implementation('org.controlsfx:controlsfx:11.1.0' ) {
|
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'
|
||||||
|
|
@ -100,7 +101,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.7')
|
implementation('org.apache.commons:commons-lang3:3.19.0')
|
||||||
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')
|
||||||
|
|
@ -109,6 +110,7 @@ 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')
|
||||||
|
|
@ -141,6 +143,12 @@ 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",
|
||||||
|
|
@ -150,11 +158,6 @@ 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",
|
||||||
|
|
@ -165,8 +168,7 @@ 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"]
|
||||||
|
|
@ -189,7 +191,14 @@ jlink {
|
||||||
options = ['--strip-native-commands', '--strip-java-debug-attributes', '--compress', 'zip-6', '--no-header-files', '--no-man-pages', '--ignore-signing-information', '--exclude-files', '**.png', '--exclude-resources', 'glob:/com.sparrowwallet.merged.module/META-INF/*']
|
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 = ["--add-opens=javafx.graphics/com.sun.javafx.css=org.controlsfx.controls",
|
jvmArgs = ["--enable-native-access=com.sparrowwallet.drongo",
|
||||||
|
"--enable-native-access=com.sun.jna",
|
||||||
|
"--enable-native-access=javafx.graphics",
|
||||||
|
"--enable-native-access=com.sparrowwallet.merged.module",
|
||||||
|
"--enable-native-access=com.fazecast.jSerialComm",
|
||||||
|
"--enable-native-access=org.usb4java",
|
||||||
|
"--enable-native-access=io.github.doblon8.jzbar",
|
||||||
|
"--add-opens=javafx.graphics/com.sun.javafx.css=org.controlsfx.controls",
|
||||||
"--add-opens=javafx.graphics/javafx.scene=org.controlsfx.controls",
|
"--add-opens=javafx.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",
|
||||||
|
|
@ -198,11 +207,6 @@ 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",
|
||||||
|
|
@ -221,6 +225,7 @@ 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"]
|
||||||
|
|
||||||
|
|
@ -228,7 +233,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", "--add-opens=javafx.graphics/com.sun.glass.ui.mac=centerdevice.nsmenufx"]
|
jvmArgs += ["-Dprism.lcdtext=false", "--add-opens=javafx.graphics/com.sun.glass.ui.mac=com.sparrowwallet.merged.module"]
|
||||||
}
|
}
|
||||||
if(headless) {
|
if(headless) {
|
||||||
jvmArgs += ["-Dglass.platform=Monocle", "-Dmonocle.platform=Headless", "-Dprism.order=sw"]
|
jvmArgs += ["-Dglass.platform=Monocle", "-Dmonocle.platform=Headless", "-Dprism.order=sw"]
|
||||||
|
|
@ -280,7 +285,8 @@ if(os.linux) {
|
||||||
|
|
||||||
tasks.register('addUserWritePermission', Exec) {
|
tasks.register('addUserWritePermission', Exec) {
|
||||||
if(os.windows) {
|
if(os.windows) {
|
||||||
commandLine 'icacls', "$buildDir\\image\\legal", '/grant', 'Users:(OI)(CI)F', '/T'
|
def usersGroup = '*S-1-5-32-545' // Windows "Users" group SID (language-independent)
|
||||||
|
commandLine 'icacls', "$buildDir\\image\\legal", '/grant', "${usersGroup}:(OI)(CI)F", '/T'
|
||||||
} else {
|
} else {
|
||||||
commandLine 'chmod', '-R', 'u+w', "$buildDir/image/legal"
|
commandLine 'chmod', '-R', 'u+w', "$buildDir/image/legal"
|
||||||
}
|
}
|
||||||
|
|
@ -380,33 +386,12 @@ 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')
|
||||||
|
|
@ -416,10 +401,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.undofx')
|
requires('org.fxmisc.undo')
|
||||||
requires('org.fxmisc.wellbehaved')
|
requires('org.fxmisc.wellbehaved')
|
||||||
}
|
}
|
||||||
module('org.fxmisc.undo:undofx', 'org.fxmisc.undo.undofx') {
|
module('org.fxmisc.undo:undofx', 'org.fxmisc.undo') {
|
||||||
requires('javafx.base')
|
requires('javafx.base')
|
||||||
requires('javafx.controls')
|
requires('javafx.controls')
|
||||||
requires('javafx.graphics')
|
requires('javafx.graphics')
|
||||||
|
|
|
||||||
|
|
@ -4,13 +4,12 @@ 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 "https://plugins.gradle.org/m2/"
|
url = uri("https://plugins.gradle.org/m2/")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -32,7 +32,6 @@ package org.openjfx.gradle;
|
||||||
import com.google.gradle.osdetector.OsDetectorPlugin;
|
import 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> {
|
||||||
|
|
@ -40,10 +39,9 @@ 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().create("configJavafxRun", ExecTask.class, project);
|
project.getTasks().register("configJavafxRun", ExecTask.class, project);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -33,27 +33,19 @@ import org.gradle.api.DefaultTask;
|
||||||
import org.gradle.api.GradleException;
|
import org.gradle.api.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;
|
||||||
|
|
||||||
|
|
@ -78,37 +70,11 @@ 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");
|
||||||
|
|
|
||||||
|
|
@ -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.1.3"
|
GIT_TAG="2.3.0"
|
||||||
```
|
```
|
||||||
|
|
||||||
The project can then be initially cloned as follows:
|
The project can then be initially cloned as follows:
|
||||||
|
|
|
||||||
2
drongo
2
drongo
|
|
@ -1 +1 @@
|
||||||
Subproject commit abb598d3b041a9d0b3d0ba41b5fb9785e2100193
|
Subproject commit e975cbe6f8d8574785124e6db5780d0541e20024
|
||||||
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
Binary file not shown.
2
gradle/wrapper/gradle-wrapper.properties
vendored
2
gradle/wrapper/gradle-wrapper.properties
vendored
|
|
@ -1,6 +1,6 @@
|
||||||
distributionBase=GRADLE_USER_HOME
|
distributionBase=GRADLE_USER_HOME
|
||||||
distributionPath=wrapper/dists
|
distributionPath=wrapper/dists
|
||||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-bin.zip
|
distributionUrl=https\://services.gradle.org/distributions/gradle-9.1.0-bin.zip
|
||||||
networkTimeout=10000
|
networkTimeout=10000
|
||||||
validateDistributionUrl=true
|
validateDistributionUrl=true
|
||||||
zipStoreBase=GRADLE_USER_HOME
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
|
|
|
||||||
15
gradlew
vendored
15
gradlew
vendored
|
|
@ -1,7 +1,7 @@
|
||||||
#!/bin/sh
|
#!/bin/sh
|
||||||
|
|
||||||
#
|
#
|
||||||
# Copyright © 2015-2021 the original authors.
|
# Copyright © 2015 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,6 +15,8 @@
|
||||||
# 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
|
||||||
|
#
|
||||||
|
|
||||||
##############################################################################
|
##############################################################################
|
||||||
#
|
#
|
||||||
|
|
@ -55,7 +57,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/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
|
# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
|
||||||
# within the Gradle project.
|
# within the Gradle project.
|
||||||
#
|
#
|
||||||
# You can find Gradle at https://github.com/gradle/gradle/.
|
# You can find Gradle at https://github.com/gradle/gradle/.
|
||||||
|
|
@ -84,7 +86,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 "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit
|
APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit
|
||||||
|
|
||||||
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
||||||
MAX_FD=maximum
|
MAX_FD=maximum
|
||||||
|
|
@ -112,7 +114,6 @@ 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.
|
||||||
|
|
@ -170,7 +171,6 @@ 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,15 +203,14 @@ 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, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
|
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
|
||||||
# and any embedded shellness will be escaped.
|
# 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" \
|
||||||
-classpath "$CLASSPATH" \
|
-jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \
|
||||||
org.gradle.wrapper.GradleWrapperMain \
|
|
||||||
"$@"
|
"$@"
|
||||||
|
|
||||||
# Stop when "xargs" is not available.
|
# Stop when "xargs" is not available.
|
||||||
|
|
|
||||||
25
gradlew.bat
vendored
25
gradlew.bat
vendored
|
|
@ -13,6 +13,8 @@
|
||||||
@rem See the License for the specific language governing permissions and
|
@rem 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 ##########################################################################
|
||||||
|
|
@ -43,11 +45,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.
|
echo. 1>&2
|
||||||
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
|
||||||
echo.
|
echo. 1>&2
|
||||||
echo Please set the JAVA_HOME variable in your environment to match the
|
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
|
||||||
echo location of your Java installation.
|
echo location of your Java installation. 1>&2
|
||||||
|
|
||||||
goto fail
|
goto fail
|
||||||
|
|
||||||
|
|
@ -57,22 +59,21 @@ set JAVA_EXE=%JAVA_HOME%/bin/java.exe
|
||||||
|
|
||||||
if exist "%JAVA_EXE%" goto execute
|
if exist "%JAVA_EXE%" goto execute
|
||||||
|
|
||||||
echo.
|
echo. 1>&2
|
||||||
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
|
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
|
||||||
echo.
|
echo. 1>&2
|
||||||
echo Please set the JAVA_HOME variable in your environment to match the
|
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
|
||||||
echo location of your Java installation.
|
echo location of your Java installation. 1>&2
|
||||||
|
|
||||||
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%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
|
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %*
|
||||||
|
|
||||||
:end
|
: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
2
lark
|
|
@ -1 +1 @@
|
||||||
Subproject commit 5facb25ede49c30650a8460dc04982650edb397f
|
Subproject commit 10e8d9cd4bbe9fde4dd93c059e2a9faeec6be3e0
|
||||||
48
repackage.sh
Executable file
48
repackage.sh
Executable file
|
|
@ -0,0 +1,48 @@
|
||||||
|
#!/bin/bash
|
||||||
|
set -e # Exit on any error
|
||||||
|
|
||||||
|
# Define paths
|
||||||
|
BUILD_DIR="build"
|
||||||
|
JPACKAGE_DIR="$BUILD_DIR/jpackage"
|
||||||
|
TEMP_DIR="$BUILD_DIR/repackage"
|
||||||
|
|
||||||
|
# Find the .deb file in build/jpackage (assuming there is only one)
|
||||||
|
DEB_FILE=$(find "$JPACKAGE_DIR" -type f -name "*.deb" -print -quit)
|
||||||
|
|
||||||
|
# Check if a .deb file was found
|
||||||
|
if [ -z "$DEB_FILE" ]; then
|
||||||
|
echo "Error: No .deb file found in $JPACKAGE_DIR"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Extract the filename from the path for later use
|
||||||
|
DEB_FILENAME=$(basename "$DEB_FILE")
|
||||||
|
|
||||||
|
echo "Found .deb file: $DEB_FILENAME"
|
||||||
|
|
||||||
|
# Create a temp directory inside build to avoid file conflicts
|
||||||
|
mkdir -p "$TEMP_DIR"
|
||||||
|
cd "$TEMP_DIR"
|
||||||
|
|
||||||
|
# Extract the .deb file contents
|
||||||
|
ar x "../../$DEB_FILE"
|
||||||
|
|
||||||
|
# Decompress zst files to tar
|
||||||
|
unzstd control.tar.zst
|
||||||
|
unzstd data.tar.zst
|
||||||
|
|
||||||
|
# Compress tar files to xz
|
||||||
|
xz -c control.tar > control.tar.xz
|
||||||
|
xz -c data.tar > data.tar.xz
|
||||||
|
|
||||||
|
# Remove the original .deb file
|
||||||
|
rm "../../$DEB_FILE"
|
||||||
|
|
||||||
|
# Create the new .deb file with xz compression in the original location
|
||||||
|
ar cr "../../$DEB_FILE" debian-binary control.tar.xz data.tar.xz
|
||||||
|
|
||||||
|
# Clean up temp files
|
||||||
|
cd ../..
|
||||||
|
rm -rf "$TEMP_DIR"
|
||||||
|
|
||||||
|
echo "Repackaging complete: $DEB_FILENAME"
|
||||||
|
|
@ -21,7 +21,7 @@
|
||||||
<key>CFBundlePackageType</key>
|
<key>CFBundlePackageType</key>
|
||||||
<string>APPL</string>
|
<string>APPL</string>
|
||||||
<key>CFBundleShortVersionString</key>
|
<key>CFBundleShortVersionString</key>
|
||||||
<string>2.2.0</string>
|
<string>2.3.1</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,6 +33,8 @@
|
||||||
<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>
|
||||||
|
|
|
||||||
|
|
@ -3,13 +3,14 @@ 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.PSBT;
|
import com.sparrowwallet.drongo.psbt.*;
|
||||||
import com.sparrowwallet.drongo.psbt.PSBTInput;
|
import com.sparrowwallet.drongo.silentpayments.SilentPaymentAddress;
|
||||||
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;
|
||||||
|
|
@ -30,7 +31,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.codecentric.centerdevice.MenuToolkit;
|
import de.jangassen.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;
|
||||||
|
|
@ -49,12 +50,14 @@ 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.image.Image;
|
import javafx.scene.control.Label;
|
||||||
import javafx.scene.image.ImageView;
|
import javafx.scene.control.Menu;
|
||||||
|
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;
|
||||||
|
|
@ -69,6 +72,7 @@ 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.*;
|
||||||
|
|
@ -822,10 +826,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().toBase64String(includeXpubs));
|
writer.print(transactionTabData.getPsbt().getForExport().toBase64String(includeXpubs));
|
||||||
writer.flush();
|
writer.flush();
|
||||||
} else {
|
} else {
|
||||||
outputStream.write(transactionTabData.getPsbt().serialize(includeXpubs, true));
|
outputStream.write(transactionTabData.getPsbt().getForExport().serialize(includeXpubs, true));
|
||||||
}
|
}
|
||||||
} catch(IOException e) {
|
} catch(IOException e) {
|
||||||
log.error("Error saving PSBT", e);
|
log.error("Error saving PSBT", e);
|
||||||
|
|
@ -848,7 +852,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().toBase64String() : transactionTabData.getPsbt().toString();
|
String data = asBase64 ? transactionTabData.getPsbt().getForExport().toBase64String() : transactionTabData.getPsbt().getForExport().toString();
|
||||||
|
|
||||||
ClipboardContent content = new ClipboardContent();
|
ClipboardContent content = new ClipboardContent();
|
||||||
content.putString(data);
|
content.putString(data);
|
||||||
|
|
@ -862,7 +866,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().serialize();
|
byte[] psbtBytes = transactionTabData.getPsbt().getForExport().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);
|
||||||
|
|
@ -1034,6 +1038,10 @@ 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) {
|
||||||
|
|
@ -1422,6 +1430,10 @@ public class AppController implements Initializable {
|
||||||
}
|
}
|
||||||
|
|
||||||
public void sendToMany(ActionEvent event) {
|
public void sendToMany(ActionEvent event) {
|
||||||
|
sendToMany(Collections.emptyList());
|
||||||
|
}
|
||||||
|
|
||||||
|
private void sendToMany(List<Payment> initialPayments) {
|
||||||
if(sendToManyDialog != null) {
|
if(sendToManyDialog != null) {
|
||||||
Stage stage = (Stage)sendToManyDialog.getDialogPane().getScene().getWindow();
|
Stage stage = (Stage)sendToManyDialog.getDialogPane().getScene().getWindow();
|
||||||
stage.setAlwaysOnTop(true);
|
stage.setAlwaysOnTop(true);
|
||||||
|
|
@ -1437,7 +1449,7 @@ public class AppController implements Initializable {
|
||||||
bitcoinUnit = wallet.getAutoUnit();
|
bitcoinUnit = wallet.getAutoUnit();
|
||||||
}
|
}
|
||||||
|
|
||||||
sendToManyDialog = new SendToManyDialog(bitcoinUnit);
|
sendToManyDialog = new SendToManyDialog(bitcoinUnit, initialPayments);
|
||||||
sendToManyDialog.initModality(Modality.NONE);
|
sendToManyDialog.initModality(Modality.NONE);
|
||||||
Optional<List<Payment>> optPayments = sendToManyDialog.showAndWait();
|
Optional<List<Payment>> optPayments = sendToManyDialog.showAndWait();
|
||||||
sendToManyDialog = null;
|
sendToManyDialog = null;
|
||||||
|
|
@ -1889,6 +1901,11 @@ 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) {
|
||||||
|
|
@ -1908,6 +1925,39 @@ public class AppController implements Initializable {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//Add DNS payment information if not already cached
|
||||||
|
for(PSBTOutput psbtOutput : psbt.getPsbtOutputs()) {
|
||||||
|
if(psbtOutput.getDnssecProof() != null && !psbtOutput.getDnssecProof().isEmpty()) {
|
||||||
|
Address address = psbtOutput.getScript() != null ? psbtOutput.getScript().getToAddress() : null;
|
||||||
|
if(address != null && DnsPaymentCache.getDnsPayment(address) == null) {
|
||||||
|
try {
|
||||||
|
Optional<DnsPayment> optDnsPayment = psbtOutput.getDnsPayment();
|
||||||
|
if(optDnsPayment.isPresent() && address.equals(optDnsPayment.get().bitcoinURI().getAddress())) {
|
||||||
|
DnsPaymentCache.putDnsPayment(address, optDnsPayment.get());
|
||||||
|
}
|
||||||
|
} catch(Exception e) {
|
||||||
|
log.debug("Error resolving DNS payment", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
SilentPaymentAddress silentPaymentAddress = psbtOutput.getSilentPaymentAddress();
|
||||||
|
if(address != null && silentPaymentAddress == null) {
|
||||||
|
silentPaymentAddress = AppServices.get().getOpenWallets().keySet().stream()
|
||||||
|
.map(wallet -> wallet.getSilentPaymentAddress(address)).filter(Objects::nonNull).findFirst().orElse(null);
|
||||||
|
}
|
||||||
|
if(silentPaymentAddress != null && DnsPaymentCache.getDnsPayment(silentPaymentAddress) == null) {
|
||||||
|
try {
|
||||||
|
Optional<DnsPayment> optDnsPayment = psbtOutput.getDnsPayment();
|
||||||
|
if(optDnsPayment.isPresent() && silentPaymentAddress.equals(optDnsPayment.get().bitcoinURI().getSilentPaymentAddress())) {
|
||||||
|
DnsPaymentCache.putDnsPayment(silentPaymentAddress, optDnsPayment.get());
|
||||||
|
}
|
||||||
|
} catch(Exception e) {
|
||||||
|
log.debug("Error resolving DNS payment", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Window psbtWalletWindow = AppServices.get().getWindowForPSBT(psbt);
|
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));
|
||||||
|
|
@ -2046,23 +2096,33 @@ 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 index = tabs.getTabs().indexOf(tab);
|
int currentIndex = tabs.getSelectionModel().getSelectedIndex();
|
||||||
|
if(currentIndex + 1 >= tabs.getTabs().size()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Tab selectedTab = tabs.getSelectionModel().getSelectedItem();
|
||||||
tabs.getTabs().removeListener(tabsChangeListener);
|
tabs.getTabs().removeListener(tabsChangeListener);
|
||||||
tabs.getTabs().remove(tab);
|
tabs.getTabs().remove(selectedTab);
|
||||||
tabs.getTabs().add(index + 1, tab);
|
tabs.getTabs().add(currentIndex + 1, selectedTab);
|
||||||
tabs.getTabs().addListener(tabsChangeListener);
|
tabs.getTabs().addListener(tabsChangeListener);
|
||||||
tabs.getSelectionModel().select(tab);
|
tabs.getSelectionModel().select(selectedTab);
|
||||||
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 index = tabs.getTabs().indexOf(tab);
|
int currentIndex = tabs.getSelectionModel().getSelectedIndex();
|
||||||
|
if(currentIndex == 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Tab selectedTab = tabs.getSelectionModel().getSelectedItem();
|
||||||
tabs.getTabs().removeListener(tabsChangeListener);
|
tabs.getTabs().removeListener(tabsChangeListener);
|
||||||
tabs.getTabs().remove(tab);
|
tabs.getTabs().remove(selectedTab);
|
||||||
tabs.getTabs().add(index - 1, tab);
|
tabs.getTabs().add(currentIndex - 1, selectedTab);
|
||||||
tabs.getTabs().addListener(tabsChangeListener);
|
tabs.getTabs().addListener(tabsChangeListener);
|
||||||
tabs.getSelectionModel().select(tab);
|
tabs.getSelectionModel().select(selectedTab);
|
||||||
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);
|
||||||
|
|
@ -3107,6 +3167,11 @@ public class AppController implements Initializable {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Subscribe
|
||||||
|
public void requestSendToMany(RequestSendToManyEvent event) {
|
||||||
|
sendToMany(event.getPayments());
|
||||||
|
}
|
||||||
|
|
||||||
@Subscribe
|
@Subscribe
|
||||||
public void functionAction(FunctionActionEvent event) {
|
public void functionAction(FunctionActionEvent event) {
|
||||||
selectTab(event.getWallet());
|
selectTab(event.getWallet());
|
||||||
|
|
|
||||||
|
|
@ -91,8 +91,7 @@ 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);
|
||||||
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);
|
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> 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;
|
||||||
|
|
||||||
|
|
@ -136,10 +135,14 @@ public class AppServices {
|
||||||
|
|
||||||
private static Map<Integer, Double> targetBlockFeeRates;
|
private static Map<Integer, Double> targetBlockFeeRates;
|
||||||
|
|
||||||
|
private static Double nextBlockMedianFeeRate;
|
||||||
|
|
||||||
private static final TreeMap<Date, Set<MempoolRateSize>> mempoolHistogram = new TreeMap<>();
|
private static final TreeMap<Date, Set<MempoolRateSize>> mempoolHistogram = new TreeMap<>();
|
||||||
|
|
||||||
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;
|
||||||
|
|
@ -209,6 +212,7 @@ 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()) {
|
||||||
|
|
@ -748,6 +752,30 @@ 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() {
|
||||||
|
return nextBlockMedianFeeRate == null ? getDefaultFeeRate() : nextBlockMedianFeeRate;
|
||||||
|
}
|
||||||
|
|
||||||
public static double getFallbackFeeRate() {
|
public static double getFallbackFeeRate() {
|
||||||
return Network.get() == Network.MAINNET ? FALLBACK_FEE_RATE : TESTNET_FALLBACK_FEE_RATE;
|
return Network.get() == Network.MAINNET ? FALLBACK_FEE_RATE : TESTNET_FALLBACK_FEE_RATE;
|
||||||
}
|
}
|
||||||
|
|
@ -782,10 +810,18 @@ public class AppServices {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static Double getConfiguredMinimumRelayFeeRate(Config config) {
|
||||||
|
return config.getMinRelayFeeRate() >= 0d && config.getMinRelayFeeRate() < Transaction.DEFAULT_MIN_RELAY_FEE ? config.getMinRelayFeeRate() : null;
|
||||||
|
}
|
||||||
|
|
||||||
public static Double getMinimumRelayFeeRate() {
|
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;
|
||||||
}
|
}
|
||||||
|
|
@ -799,8 +835,8 @@ public class AppServices {
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void addPayjoinURI(BitcoinURI bitcoinURI) {
|
public static void addPayjoinURI(BitcoinURI bitcoinURI) {
|
||||||
if(bitcoinURI.getPayjoinUrl() == null) {
|
if(bitcoinURI.getPayjoinUrl() == null || bitcoinURI.getAddress() == null) {
|
||||||
throw new IllegalArgumentException("Not a payjoin URI");
|
throw new IllegalArgumentException("Not a valid payjoin URI");
|
||||||
}
|
}
|
||||||
payjoinURIs.put(bitcoinURI.getAddress(), bitcoinURI);
|
payjoinURIs.put(bitcoinURI.getAddress(), bitcoinURI);
|
||||||
}
|
}
|
||||||
|
|
@ -1197,7 +1233,7 @@ public class AppServices {
|
||||||
}
|
}
|
||||||
|
|
||||||
public static Font getMonospaceFont() {
|
public static Font getMonospaceFont() {
|
||||||
return Font.font("Roboto Mono", 13);
|
return Font.font("Fragment Mono Regular", 13);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static boolean isOnWayland() {
|
public static boolean isOnWayland() {
|
||||||
|
|
@ -1213,7 +1249,10 @@ 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));
|
||||||
minimumRelayFeeRate = Math.max(event.getMinimumRelayFeeRate(), Transaction.DEFAULT_MIN_RELAY_FEE);
|
if(getConfiguredMinimumRelayFeeRate(Config.get()) == null) {
|
||||||
|
minimumRelayFeeRate = event.getMinimumRelayFeeRate() == null ? Transaction.DEFAULT_MIN_RELAY_FEE : event.getMinimumRelayFeeRate();
|
||||||
|
}
|
||||||
|
serverMinimumRelayFeeRate = event.getMinimumRelayFeeRate();
|
||||||
latestBlockHeader = event.getBlockHeader();
|
latestBlockHeader = event.getBlockHeader();
|
||||||
Config.get().addRecentServer();
|
Config.get().addRecentServer();
|
||||||
|
|
||||||
|
|
@ -1249,11 +1288,13 @@ public class AppServices {
|
||||||
if(AppServices.currentBlockHeight != null) {
|
if(AppServices.currentBlockHeight != null) {
|
||||||
blockSummaries.keySet().removeIf(height -> AppServices.currentBlockHeight - height > 5);
|
blockSummaries.keySet().removeIf(height -> AppServices.currentBlockHeight - height > 5);
|
||||||
}
|
}
|
||||||
|
nextBlockMedianFeeRate = event.getNextBlockMedianFeeRate();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Subscribe
|
@Subscribe
|
||||||
public void feesUpdated(FeeRatesUpdatedEvent event) {
|
public void feesUpdated(FeeRatesUpdatedEvent event) {
|
||||||
targetBlockFeeRates = event.getTargetBlockFeeRates();
|
targetBlockFeeRates = event.getTargetBlockFeeRates();
|
||||||
|
nextBlockMedianFeeRate = event.getNextBlockMedianFeeRate();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Subscribe
|
@Subscribe
|
||||||
|
|
|
||||||
|
|
@ -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/RobotoMono-Regular.ttf"), 13);
|
Font.loadFont(AppServices.class.getResourceAsStream("/font/FragmentMono-Regular.ttf"), 13);
|
||||||
Font.loadFont(AppServices.class.getResourceAsStream("/font/RobotoMono-Italic.ttf"), 11);
|
Font.loadFont(AppServices.class.getResourceAsStream("/font/FragmentMono-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);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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.2.0";
|
public static final String APP_VERSION = "2.3.1";
|
||||||
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";
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,8 @@ package com.sparrowwallet.sparrow.control;
|
||||||
import com.sparrowwallet.drongo.Network;
|
import com.sparrowwallet.drongo.Network;
|
||||||
import com.sparrowwallet.drongo.protocol.Transaction;
|
import com.sparrowwallet.drongo.protocol.Transaction;
|
||||||
import com.sparrowwallet.sparrow.BlockSummary;
|
import com.sparrowwallet.sparrow.BlockSummary;
|
||||||
|
import com.sparrowwallet.sparrow.io.Config;
|
||||||
|
import com.sparrowwallet.sparrow.net.FeeRatesSource;
|
||||||
import javafx.animation.KeyFrame;
|
import javafx.animation.KeyFrame;
|
||||||
import javafx.animation.KeyValue;
|
import javafx.animation.KeyValue;
|
||||||
import javafx.animation.Timeline;
|
import javafx.animation.Timeline;
|
||||||
|
|
@ -15,6 +17,7 @@ import javafx.scene.text.FontWeight;
|
||||||
import javafx.scene.text.Text;
|
import javafx.scene.text.Text;
|
||||||
import javafx.scene.text.TextFlow;
|
import javafx.scene.text.TextFlow;
|
||||||
import javafx.util.Duration;
|
import javafx.util.Duration;
|
||||||
|
import org.girod.javafx.svgimage.SVGImage;
|
||||||
|
|
||||||
import java.time.Instant;
|
import java.time.Instant;
|
||||||
import java.time.temporal.ChronoUnit;
|
import java.time.temporal.ChronoUnit;
|
||||||
|
|
@ -26,12 +29,13 @@ public class BlockCube extends Group {
|
||||||
public static final double CUBE_SIZE = 60;
|
public static final double CUBE_SIZE = 60;
|
||||||
|
|
||||||
private final IntegerProperty weightProperty = new SimpleIntegerProperty(0);
|
private final IntegerProperty weightProperty = new SimpleIntegerProperty(0);
|
||||||
private final DoubleProperty medianFeeProperty = new SimpleDoubleProperty(-1.0d);
|
private final DoubleProperty medianFeeProperty = new SimpleDoubleProperty(-Double.MAX_VALUE);
|
||||||
private final IntegerProperty heightProperty = new SimpleIntegerProperty(0);
|
private final IntegerProperty heightProperty = new SimpleIntegerProperty(0);
|
||||||
private final IntegerProperty txCountProperty = new SimpleIntegerProperty(0);
|
private final IntegerProperty txCountProperty = new SimpleIntegerProperty(0);
|
||||||
private final LongProperty timestampProperty = new SimpleLongProperty(System.currentTimeMillis());
|
private final LongProperty timestampProperty = new SimpleLongProperty(System.currentTimeMillis());
|
||||||
private final StringProperty elapsedProperty = new SimpleStringProperty("");
|
private final StringProperty elapsedProperty = new SimpleStringProperty("");
|
||||||
private final BooleanProperty confirmedProperty = new SimpleBooleanProperty(false);
|
private final BooleanProperty confirmedProperty = new SimpleBooleanProperty(false);
|
||||||
|
private final ObjectProperty<FeeRatesSource> feeRatesSource = new SimpleObjectProperty<>(null);
|
||||||
|
|
||||||
private Polygon front;
|
private Polygon front;
|
||||||
private Rectangle unusedArea;
|
private Rectangle unusedArea;
|
||||||
|
|
@ -43,21 +47,27 @@ public class BlockCube extends Group {
|
||||||
private final TextFlow medianFeeTextFlow = new TextFlow();
|
private final TextFlow medianFeeTextFlow = new TextFlow();
|
||||||
private final Text txCountText = new Text();
|
private final Text txCountText = new Text();
|
||||||
private final Text elapsedText = new Text();
|
private final Text elapsedText = new Text();
|
||||||
|
private final Group feeRateIcon = new Group();
|
||||||
|
|
||||||
public BlockCube(Integer weight, Double medianFee, Integer height, Integer txCount, Long timestamp, boolean confirmed) {
|
public BlockCube(Integer weight, Double medianFee, Integer height, Integer txCount, Long timestamp, boolean confirmed) {
|
||||||
getStyleClass().addAll("block-" + Network.getCanonical().getName(), "block-cube");
|
getStyleClass().addAll("block-" + Network.getCanonical().getName(), "block-cube");
|
||||||
this.confirmedProperty.set(confirmed);
|
this.confirmedProperty.set(confirmed);
|
||||||
|
|
||||||
|
FeeRatesSource feeRatesSource = Config.get().getFeeRatesSource();
|
||||||
|
feeRatesSource = (feeRatesSource == null ? FeeRatesSource.MEMPOOL_SPACE : feeRatesSource);
|
||||||
|
this.feeRatesSource.set(feeRatesSource);
|
||||||
|
|
||||||
this.weightProperty.addListener((_, _, _) -> {
|
this.weightProperty.addListener((_, _, _) -> {
|
||||||
if(front != null) {
|
if(front != null) {
|
||||||
updateFill();
|
updateFill();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
this.medianFeeProperty.addListener((_, _, newValue) -> {
|
this.medianFeeProperty.addListener((_, _, newValue) -> {
|
||||||
medianFeeText.setText("~" + Math.round(Math.max(newValue.doubleValue(), 1.0d)));
|
medianFeeText.setText(newValue.doubleValue() < 0.0d ? "" : "~" + Math.round(Math.max(newValue.doubleValue(), 1.0d)));
|
||||||
unitsText.setText(" s/vb");
|
unitsText.setText(newValue.doubleValue() < 0.0d ? "" : " s/vb");
|
||||||
|
double medianFeeWidth = TextUtils.computeTextWidth(medianFeeText.getFont(), medianFeeText.getText(), 0.0d);
|
||||||
double unitsWidth = TextUtils.computeTextWidth(unitsText.getFont(), unitsText.getText(), 0.0d);
|
double unitsWidth = TextUtils.computeTextWidth(unitsText.getFont(), unitsText.getText(), 0.0d);
|
||||||
medianFeeTextFlow.setTranslateX((CUBE_SIZE - (medianFeeText.getLayoutBounds().getWidth() + unitsWidth)) / 2);
|
medianFeeTextFlow.setTranslateX((CUBE_SIZE - (medianFeeWidth + unitsWidth)) / 2);
|
||||||
});
|
});
|
||||||
this.txCountProperty.addListener((_, _, newValue) -> {
|
this.txCountProperty.addListener((_, _, newValue) -> {
|
||||||
txCountText.setText(newValue.intValue() == 0 ? "" : newValue + " txes");
|
txCountText.setText(newValue.intValue() == 0 ? "" : newValue + " txes");
|
||||||
|
|
@ -79,6 +89,11 @@ public class BlockCube extends Group {
|
||||||
updateFill();
|
updateFill();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
this.feeRatesSource.addListener((_, _, _) -> {
|
||||||
|
if(front != null) {
|
||||||
|
updateFill();
|
||||||
|
}
|
||||||
|
});
|
||||||
this.medianFeeText.textProperty().addListener((_, _, _) -> {
|
this.medianFeeText.textProperty().addListener((_, _, _) -> {
|
||||||
pulse();
|
pulse();
|
||||||
});
|
});
|
||||||
|
|
@ -145,12 +160,15 @@ public class BlockCube extends Group {
|
||||||
txCountText.setX((CUBE_SIZE - txCountText.getLayoutBounds().getWidth()) / 2);
|
txCountText.setX((CUBE_SIZE - txCountText.getLayoutBounds().getWidth()) / 2);
|
||||||
txCountText.setY(34);
|
txCountText.setY(34);
|
||||||
|
|
||||||
|
feeRateIcon.setTranslateX(((CUBE_SIZE * 0.7) - 14) / 2);
|
||||||
|
feeRateIcon.setTranslateY(-36);
|
||||||
|
|
||||||
elapsedText.getStyleClass().add("block-text");
|
elapsedText.getStyleClass().add("block-text");
|
||||||
elapsedText.setFont(new Font(10));
|
elapsedText.setFont(new Font(10));
|
||||||
elapsedText.setX((CUBE_SIZE - elapsedText.getLayoutBounds().getWidth()) / 2);
|
elapsedText.setX((CUBE_SIZE - elapsedText.getLayoutBounds().getWidth()) / 2);
|
||||||
elapsedText.setY(50);
|
elapsedText.setY(50);
|
||||||
|
|
||||||
getChildren().addAll(frontFaceGroup, top, left, heightText, medianFeeTextFlow, txCountText, elapsedText);
|
getChildren().addAll(frontFaceGroup, top, left, heightText, medianFeeTextFlow, txCountText, feeRateIcon, elapsedText);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void updateFill() {
|
private void updateFill() {
|
||||||
|
|
@ -167,6 +185,7 @@ public class BlockCube extends Group {
|
||||||
usedArea.setHeight(CUBE_SIZE - startYAbsolute);
|
usedArea.setHeight(CUBE_SIZE - startYAbsolute);
|
||||||
usedArea.setVisible(true);
|
usedArea.setVisible(true);
|
||||||
heightText.setVisible(true);
|
heightText.setVisible(true);
|
||||||
|
feeRateIcon.getChildren().clear();
|
||||||
} else {
|
} else {
|
||||||
getStyleClass().removeAll("block-confirmed");
|
getStyleClass().removeAll("block-confirmed");
|
||||||
if(!getStyleClass().contains("block-unconfirmed")) {
|
if(!getStyleClass().contains("block-unconfirmed")) {
|
||||||
|
|
@ -175,6 +194,16 @@ public class BlockCube extends Group {
|
||||||
usedArea.setVisible(false);
|
usedArea.setVisible(false);
|
||||||
unusedArea.setStyle("-fx-fill: " + getFeeRateStyleName() + ";");
|
unusedArea.setStyle("-fx-fill: " + getFeeRateStyleName() + ";");
|
||||||
heightText.setVisible(false);
|
heightText.setVisible(false);
|
||||||
|
if(feeRatesSource.get() != null) {
|
||||||
|
SVGImage svgImage = feeRatesSource.get().getSVGImage();
|
||||||
|
if(svgImage != null) {
|
||||||
|
feeRateIcon.getChildren().setAll(feeRatesSource.get().getSVGImage());
|
||||||
|
} else {
|
||||||
|
feeRateIcon.getChildren().clear();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
feeRateIcon.getChildren().clear();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -324,8 +353,20 @@ public class BlockCube extends Group {
|
||||||
confirmedProperty.set(confirmed);
|
confirmedProperty.set(confirmed);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public FeeRatesSource getFeeRatesSource() {
|
||||||
|
return feeRatesSource.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
public ObjectProperty<FeeRatesSource> feeRatesSourceProperty() {
|
||||||
|
return feeRatesSource;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setFeeRatesSource(FeeRatesSource feeRatesSource) {
|
||||||
|
this.feeRatesSource.set(feeRatesSource);
|
||||||
|
}
|
||||||
|
|
||||||
public static BlockCube fromBlockSummary(BlockSummary blockSummary) {
|
public static BlockCube fromBlockSummary(BlockSummary blockSummary) {
|
||||||
return new BlockCube(blockSummary.getWeight().orElse(0), blockSummary.getMedianFee().orElse(1.0d), blockSummary.getHeight(),
|
return new BlockCube(blockSummary.getWeight().orElse(0), blockSummary.getMedianFee().orElse(-1.0d), blockSummary.getHeight(),
|
||||||
blockSummary.getTransactionCount().orElse(0), blockSummary.getTimestamp().getTime(), true);
|
blockSummary.getTransactionCount().orElse(0), blockSummary.getTimestamp().getTime(), true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -87,6 +87,8 @@ 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);
|
||||||
|
|
@ -148,6 +150,14 @@ 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() + ")" : ""));
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,10 +6,16 @@ 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<>();
|
||||||
|
|
||||||
|
|
@ -68,4 +74,53 @@ 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);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,39 @@
|
||||||
|
package com.sparrowwallet.sparrow.control;
|
||||||
|
|
||||||
|
import com.sparrowwallet.sparrow.AppServices;
|
||||||
|
import javafx.geometry.Insets;
|
||||||
|
import javafx.scene.control.Alert;
|
||||||
|
import javafx.scene.control.ButtonType;
|
||||||
|
import javafx.scene.control.CheckBox;
|
||||||
|
import javafx.scene.control.Label;
|
||||||
|
import javafx.scene.layout.VBox;
|
||||||
|
|
||||||
|
import static com.sparrowwallet.sparrow.AppServices.getActiveWindow;
|
||||||
|
import static com.sparrowwallet.sparrow.AppServices.setStageIcon;
|
||||||
|
|
||||||
|
public class ConfirmationAlert extends Alert {
|
||||||
|
private final CheckBox dontAskAgain;
|
||||||
|
|
||||||
|
public ConfirmationAlert(String title, String contentText, ButtonType... buttons) {
|
||||||
|
super(AlertType.CONFIRMATION, contentText, buttons);
|
||||||
|
|
||||||
|
initOwner(getActiveWindow());
|
||||||
|
setStageIcon(getDialogPane().getScene().getWindow());
|
||||||
|
getDialogPane().getScene().getStylesheets().add(AppServices.class.getResource("general.css").toExternalForm());
|
||||||
|
setTitle(title);
|
||||||
|
setHeaderText(title);
|
||||||
|
|
||||||
|
VBox contentBox = new VBox(20);
|
||||||
|
contentBox.setPadding(new Insets(10, 20, 10, 20));
|
||||||
|
Label contentLabel = new Label(contentText);
|
||||||
|
contentLabel.setWrapText(true);
|
||||||
|
dontAskAgain = new CheckBox("Don't ask again");
|
||||||
|
contentBox.getChildren().addAll(contentLabel, dontAskAgain);
|
||||||
|
|
||||||
|
getDialogPane().setContent(contentBox);
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isDontAskAgain() {
|
||||||
|
return dontAskAgain.isSelected();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -11,6 +11,7 @@ import javafx.scene.control.Tooltip;
|
||||||
import javafx.scene.input.Clipboard;
|
import javafx.scene.input.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 {
|
||||||
|
|
@ -29,6 +30,10 @@ 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();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -453,20 +453,26 @@ public class DevicePane extends TitledDescriptionPane {
|
||||||
});
|
});
|
||||||
vBox.getChildren().addAll(pinField, enterPinButton);
|
vBox.getChildren().addAll(pinField, enterPinButton);
|
||||||
|
|
||||||
TilePane tilePane = new TilePane();
|
GridPane gridPane = new GridPane();
|
||||||
tilePane.setPrefColumns(3);
|
gridPane.setHgap(10);
|
||||||
tilePane.setHgap(10);
|
gridPane.setVgap(10);
|
||||||
tilePane.setVgap(10);
|
gridPane.setMaxWidth(150);
|
||||||
tilePane.setMaxWidth(150);
|
gridPane.setMaxHeight(device.getModel().hasZeroInPin() ? 160 : 120);
|
||||||
tilePane.setMaxHeight(120);
|
|
||||||
|
|
||||||
int[] digits = new int[] {7, 8, 9, 4, 5, 6, 1, 2, 3};
|
int[] digits = device.getModel().hasZeroInPin() ? new int[] {7, 8, 9, 4, 5, 6, 1, 2, 3, 0} : new int[] {7, 8, 9, 4, 5, 6, 1, 2, 3};
|
||||||
for(int i = 0; i < digits.length; i++) {
|
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]);
|
||||||
tilePane.getChildren().add(pinButton);
|
GridPane.setRowIndex(pinButton, i / 3);
|
||||||
|
GridPane.setColumnIndex(pinButton, i % 3);
|
||||||
|
if((i / 3) == 3) {
|
||||||
|
GridPane.setHgrow(pinButton, Priority.ALWAYS);
|
||||||
|
GridPane.setColumnSpan(pinButton, 3);
|
||||||
|
pinButton.setMaxWidth(Double.MAX_VALUE);
|
||||||
|
}
|
||||||
|
gridPane.getChildren().add(pinButton);
|
||||||
pinButton.setOnAction(event -> {
|
pinButton.setOnAction(event -> {
|
||||||
pinField.setText(pinField.getText() + pinButton.getUserData());
|
pinField.setText(pinField.getText() + pinButton.getUserData());
|
||||||
});
|
});
|
||||||
|
|
@ -474,7 +480,7 @@ public class DevicePane extends TitledDescriptionPane {
|
||||||
|
|
||||||
HBox contentBox = new HBox();
|
HBox contentBox = new HBox();
|
||||||
contentBox.setSpacing(50);
|
contentBox.setSpacing(50);
|
||||||
contentBox.getChildren().add(tilePane);
|
contentBox.getChildren().add(gridPane);
|
||||||
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);
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,8 @@ 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;
|
||||||
|
|
@ -55,7 +57,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()) {
|
if(this == lastCell && !getTableRow().isVisible() && isTableSizeRecalculation()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
lastCell = this;
|
lastCell = this;
|
||||||
|
|
@ -66,8 +68,7 @@ public class EntryCell extends TreeTableCell<Entry, Entry> implements Confirmati
|
||||||
setText(null);
|
setText(null);
|
||||||
setGraphic(null);
|
setGraphic(null);
|
||||||
} else {
|
} else {
|
||||||
if(entry instanceof TransactionEntry) {
|
if(entry instanceof TransactionEntry 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));
|
||||||
|
|
@ -101,7 +102,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) &&
|
if(blockTransaction.getHeight() <= 0 && canRBF(blockTransaction, transactionEntry.getWallet()) &&
|
||||||
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());
|
||||||
|
|
@ -121,8 +122,7 @@ public class EntryCell extends TreeTableCell<Entry, Entry> implements Confirmati
|
||||||
}
|
}
|
||||||
|
|
||||||
setGraphic(actionBox);
|
setGraphic(actionBox);
|
||||||
} else if(entry instanceof NodeEntry) {
|
} else if(entry instanceof NodeEntry 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,8 +163,7 @@ public class EntryCell extends TreeTableCell<Entry, Entry> implements Confirmati
|
||||||
setContextMenu(null);
|
setContextMenu(null);
|
||||||
setGraphic(new HBox());
|
setGraphic(new HBox());
|
||||||
}
|
}
|
||||||
} else if(entry instanceof HashIndexEntry) {
|
} else if(entry instanceof HashIndexEntry 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();
|
||||||
|
|
@ -212,13 +211,14 @@ 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())
|
.filter(i -> Config.get().isMempoolFullRbf() || i.isReplaceByFeeEnabled() || silentPaymentTransaction)
|
||||||
.map(txInput -> walletTxos.keySet().stream().filter(txo -> txo.getHash().equals(txInput.getOutpoint().getHash()) && txo.getIndex() == txInput.getOutpoint().getIndex()).findFirst().get())
|
.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,6 +243,7 @@ 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();
|
||||||
|
|
@ -257,7 +258,7 @@ public class EntryCell extends TreeTableCell<Entry, Entry> implements Confirmati
|
||||||
List<OutputGroup> outputGroups = transactionEntry.getWallet().getGroupedUtxos(txoFilters, feeRate, AppServices.getMinimumRelayFeeRate(), Config.get().isGroupByAddress())
|
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) {
|
while((double)changeTotal / vSize < getMaxFeeRate() && !outputGroups.isEmpty() && !cancelTransaction && !consolidationTransaction && safeToAddInputsOrOutputs) {
|
||||||
//If there is insufficient change output, include another random output group so the fee can be increased
|
//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()) {
|
||||||
|
|
@ -298,9 +299,13 @@ public class EntryCell extends TreeTableCell<Entry, Entry> implements Confirmati
|
||||||
label += " (Replaced By Fee)";
|
label += " (Replaced By Fee)";
|
||||||
}
|
}
|
||||||
|
|
||||||
if(txOutput.getScript().getToAddress() != null) {
|
Address address = txOutput.getScript().getToAddress();
|
||||||
|
if(address != null) {
|
||||||
|
long value = txOutput.getValue();
|
||||||
//Disable change creation by enabling max payment when there is only one output and no additional UTXOs included
|
//Disable change creation by enabling max payment when there is only one output and no additional UTXOs included
|
||||||
return new Payment(txOutput.getScript().getToAddress(), label, txOutput.getValue(), blockTransaction.getTransaction().getOutputs().size() == 1 && rbfChange == 0);
|
boolean sendMax = blockTransaction.getTransaction().getOutputs().size() == 1 && rbfChange == 0;
|
||||||
|
SilentPaymentAddress silentPaymentAddress = transactionEntry.getWallet().getSilentPaymentAddress(address);
|
||||||
|
return silentPaymentAddress == null ? new Payment(address, label, value, sendMax) : new SilentPayment(silentPaymentAddress, label, value, sendMax);
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
|
|
@ -337,7 +342,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)));
|
Platform.runLater(() -> EventManager.get().post(new SpendUtxoEvent(transactionEntry.getWallet(), utxos, payments, opReturns.isEmpty() ? null : opReturns, rbfFee, true, blockTransaction, safeToAddInputsOrOutputs)));
|
||||||
}
|
}
|
||||||
|
|
||||||
private static Double getMaxFeeRate() {
|
private static Double getMaxFeeRate() {
|
||||||
|
|
@ -394,11 +399,11 @@ public class EntryCell extends TreeTableCell<Entry, Entry> implements Confirmati
|
||||||
Payment payment = new Payment(freshAddress, label, inputTotal, true);
|
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)));
|
Platform.runLater(() -> EventManager.get().post(new SpendUtxoEvent(transactionEntry.getWallet(), utxos, List.of(payment), null, blockTransaction.getFee(), true, null, true)));
|
||||||
}
|
}
|
||||||
|
|
||||||
private static boolean canRBF(BlockTransaction blockTransaction) {
|
private static boolean canRBF(BlockTransaction blockTransaction, Wallet wallet) {
|
||||||
return Config.get().isMempoolFullRbf() || blockTransaction.getTransaction().isReplaceByFee();
|
return Config.get().isMempoolFullRbf() || blockTransaction.getTransaction().isReplaceByFee() || wallet.isSilentPaymentsTransaction(blockTransaction);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static boolean canSignMessage(WalletNode walletNode) {
|
private static boolean canSignMessage(WalletNode walletNode) {
|
||||||
|
|
@ -476,7 +481,7 @@ public class EntryCell extends TreeTableCell<Entry, Entry> implements Confirmati
|
||||||
tooltip += "\nFee rate: " + String.format("%.2f", feeRate) + " sats/vB";
|
tooltip += "\nFee rate: " + String.format("%.2f", feeRate) + " sats/vB";
|
||||||
}
|
}
|
||||||
|
|
||||||
tooltip += "\nRBF: " + (canRBF(transactionEntry.getBlockTransaction()) ? "Enabled" : "Disabled");
|
tooltip += "\nRBF: " + (canRBF(transactionEntry.getBlockTransaction(), transactionEntry.getWallet()) ? "Enabled" : "Disabled");
|
||||||
}
|
}
|
||||||
|
|
||||||
return tooltip;
|
return tooltip;
|
||||||
|
|
@ -544,6 +549,7 @@ 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());
|
||||||
|
|
@ -553,7 +559,7 @@ public class EntryCell extends TreeTableCell<Entry, Entry> implements Confirmati
|
||||||
});
|
});
|
||||||
getItems().add(viewTransaction);
|
getItems().add(viewTransaction);
|
||||||
|
|
||||||
if(canRBF(blockTransaction) && Config.get().isIncludeMempoolOutputs() && transactionEntry.getWallet().allInputsFromWallet(blockTransaction.getHash())) {
|
if(canRBF(blockTransaction, wallet) && Config.get().isIncludeMempoolOutputs() && transactionEntry.getWallet().allInputsFromWallet(blockTransaction.getHash())) {
|
||||||
MenuItem increaseFee = new MenuItem("Increase Fee (RBF)");
|
MenuItem increaseFee = new MenuItem("Increase Fee (RBF)");
|
||||||
increaseFee.setGraphic(getIncreaseFeeRBFGlyph());
|
increaseFee.setGraphic(getIncreaseFeeRBFGlyph());
|
||||||
increaseFee.setOnAction(AE -> {
|
increaseFee.setOnAction(AE -> {
|
||||||
|
|
@ -564,7 +570,7 @@ public class EntryCell extends TreeTableCell<Entry, Entry> implements Confirmati
|
||||||
getItems().add(increaseFee);
|
getItems().add(increaseFee);
|
||||||
}
|
}
|
||||||
|
|
||||||
if(canRBF(blockTransaction) && Config.get().isIncludeMempoolOutputs() && transactionEntry.getWallet().allInputsFromWallet(blockTransaction.getHash())) {
|
if(canRBF(blockTransaction, wallet) && Config.get().isIncludeMempoolOutputs() && transactionEntry.getWallet().allInputsFromWallet(blockTransaction.getHash())) {
|
||||||
MenuItem cancelTx = new MenuItem("Cancel Transaction (RBF)");
|
MenuItem cancelTx = new MenuItem("Cancel Transaction (RBF)");
|
||||||
cancelTx.setGraphic(getCancelTransactionRBFGlyph());
|
cancelTx.setGraphic(getCancelTransactionRBFGlyph());
|
||||||
cancelTx.setOnAction(AE -> {
|
cancelTx.setOnAction(AE -> {
|
||||||
|
|
@ -850,4 +856,11 @@ public class EntryCell extends TreeTableCell<Entry, Entry> implements Confirmati
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private boolean isTableSizeRecalculation() {
|
||||||
|
//As per https://bugs.openjdk.org/browse/JDK-8265669 we check for cell visibility to avoid unnecessary recalculation, but this can result in false positives
|
||||||
|
//The method releaseCell in VirtualFlow is responsible for setting accumCell visibility to false after use, so check this method is calling updateItem
|
||||||
|
return StackWalker.getInstance().walk(frames -> frames.anyMatch(frame -> frame.getClassName().equals("javafx.scene.control.skin.VirtualFlow")
|
||||||
|
&& frame.getMethodName().equals("releaseCell")));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
package com.sparrowwallet.sparrow.control;
|
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;
|
||||||
|
|
@ -7,6 +8,7 @@ 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;
|
||||||
|
|
||||||
|
|
@ -14,9 +16,11 @@ 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, FEE_RATES_RANGE.size() - 1, 0);
|
super(0, AppServices.getFeeRatesRange().size() - 1, 0);
|
||||||
setMajorTickUnit(1);
|
setMajorTickUnit(1);
|
||||||
setMinorTickCount(0);
|
setMinorTickCount(0);
|
||||||
setSnapToTicks(false);
|
setSnapToTicks(false);
|
||||||
|
|
@ -27,11 +31,11 @@ public class FeeRangeSlider extends Slider {
|
||||||
setLabelFormatter(new StringConverter<>() {
|
setLabelFormatter(new StringConverter<>() {
|
||||||
@Override
|
@Override
|
||||||
public String toString(Double object) {
|
public String toString(Double object) {
|
||||||
Long feeRate = LONG_FEE_RATES_RANGE.get(object.intValue());
|
Double feeRate = AppServices.getLongFeeRatesRange().get(object.intValue());
|
||||||
if(isLongFeeRange() && feeRate >= 1000) {
|
if(isLongFeeRange() && feeRate >= 1000) {
|
||||||
return feeRate / 1000 + "k";
|
return INTEGER_FEE_RATE_FORMAT.format(feeRate / 1000) + "k";
|
||||||
}
|
}
|
||||||
return Long.toString(feeRate);
|
return feeRate > 0d && feeRate < Transaction.DEFAULT_MIN_RELAY_FEE ? FRACTIONAL_FEE_RATE_FORMAT.format(feeRate) : INTEGER_FEE_RATE_FORMAT.format(feeRate);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
@ -51,10 +55,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 < LONG_FEE_RATES_RANGE.get(0)) {
|
if(newFeeRate < AppServices.getLongFeeRatesRange().getFirst()) {
|
||||||
newFeeRate = LONG_FEE_RATES_RANGE.get(0);
|
newFeeRate = AppServices.getLongFeeRatesRange().getFirst();
|
||||||
} else if(newFeeRate > LONG_FEE_RATES_RANGE.get(LONG_FEE_RATES_RANGE.size() - 1)) {
|
} else if(newFeeRate > AppServices.getLongFeeRatesRange().getLast()) {
|
||||||
newFeeRate = LONG_FEE_RATES_RANGE.get(LONG_FEE_RATES_RANGE.size() - 1);
|
newFeeRate = AppServices.getLongFeeRatesRange().getLast();
|
||||||
}
|
}
|
||||||
setFeeRate(newFeeRate);
|
setFeeRate(newFeeRate);
|
||||||
}
|
}
|
||||||
|
|
@ -62,27 +66,79 @@ public class FeeRangeSlider extends Slider {
|
||||||
}
|
}
|
||||||
|
|
||||||
public double getFeeRate() {
|
public double getFeeRate() {
|
||||||
return Math.pow(2.0, getValue());
|
return getFeeRate(AppServices.getMinimumRelayFeeRate());
|
||||||
|
}
|
||||||
|
|
||||||
|
public double getFeeRate(Double minRelayFeeRate) {
|
||||||
|
if(minRelayFeeRate >= Transaction.DEFAULT_MIN_RELAY_FEE) {
|
||||||
|
return Math.pow(2.0, getValue());
|
||||||
|
}
|
||||||
|
|
||||||
|
if(getValue() < 1.0d) {
|
||||||
|
if(minRelayFeeRate == 0.0d) {
|
||||||
|
return getValue();
|
||||||
|
}
|
||||||
|
return Math.pow(minRelayFeeRate, 1.0d - getValue());
|
||||||
|
}
|
||||||
|
|
||||||
|
return Math.pow(2.0, getValue() - 1.0d);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setFeeRate(double feeRate) {
|
public void setFeeRate(double feeRate) {
|
||||||
double value = Math.log(feeRate) / Math.log(2);
|
setFeeRate(feeRate, AppServices.getMinimumRelayFeeRate());
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setFeeRate(double feeRate, Double minRelayFeeRate) {
|
||||||
|
double value = getValue(feeRate, minRelayFeeRate);
|
||||||
updateMaxFeeRange(value);
|
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()) {
|
||||||
setMax(LONG_FEE_RATES_RANGE.size() - 1);
|
if(AppServices.getMinimumRelayFeeRate() < Transaction.DEFAULT_MIN_RELAY_FEE) {
|
||||||
|
setMin(1.0d);
|
||||||
|
}
|
||||||
|
setMax(AppServices.getLongFeeRatesRange().size() - 1);
|
||||||
updateTrackHighlight();
|
updateTrackHighlight();
|
||||||
} else if(value == getMin() && isLongFeeRange()) {
|
} else if(value == getMin() && isLongFeeRange()) {
|
||||||
setMax(FEE_RATES_RANGE.size() - 1);
|
if(AppServices.getMinimumRelayFeeRate() < Transaction.DEFAULT_MIN_RELAY_FEE) {
|
||||||
|
setMin(0.0d);
|
||||||
|
}
|
||||||
|
setMax(AppServices.getFeeRatesRange().size() - 1);
|
||||||
updateTrackHighlight();
|
updateTrackHighlight();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean isLongFeeRange() {
|
public boolean isLongFeeRange() {
|
||||||
return getMax() > FEE_RATES_RANGE.size() - 1;
|
return getMax() > AppServices.getFeeRatesRange().size() - 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void updateTrackHighlight() {
|
public void updateTrackHighlight() {
|
||||||
|
|
@ -137,9 +193,9 @@ public class FeeRangeSlider extends Slider {
|
||||||
}
|
}
|
||||||
|
|
||||||
private int getPercentageOfFeeRange(Double feeRate) {
|
private int getPercentageOfFeeRange(Double feeRate) {
|
||||||
double index = Math.log(feeRate) / Math.log(2);
|
double index = getValue(feeRate, AppServices.getMinimumRelayFeeRate());
|
||||||
if(isLongFeeRange()) {
|
if(isLongFeeRange()) {
|
||||||
index *= ((double)FEE_RATES_RANGE.size() / (LONG_FEE_RATES_RANGE.size())) * 0.99;
|
index *= ((double)AppServices.getFeeRatesRange().size() / (AppServices.getLongFeeRatesRange().size())) * 0.99;
|
||||||
}
|
}
|
||||||
return (int)Math.round(index * 10.0);
|
return (int)Math.round(index * 10.0);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -240,6 +240,9 @@ public class MessageSignDialog extends Dialog<ButtonBar.ButtonData> {
|
||||||
setFormatFromScriptType(address.getScriptType());
|
setFormatFromScriptType(address.getScriptType());
|
||||||
if(wallet != null) {
|
if(wallet != null) {
|
||||||
setWalletNodeFromAddress(wallet, address);
|
setWalletNodeFromAddress(wallet, address);
|
||||||
|
if(walletNode != null) {
|
||||||
|
setFormatFromScriptType(getSigningScriptType(walletNode));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch(InvalidAddressException e) {
|
} catch(InvalidAddressException e) {
|
||||||
//can't happen
|
//can't happen
|
||||||
|
|
@ -273,7 +276,7 @@ public class MessageSignDialog extends Dialog<ButtonBar.ButtonData> {
|
||||||
}
|
}
|
||||||
|
|
||||||
if(wallet != null && walletNode != null) {
|
if(wallet != null && walletNode != null) {
|
||||||
setFormatFromScriptType(wallet.getScriptType());
|
setFormatFromScriptType(getSigningScriptType(walletNode));
|
||||||
} else {
|
} else {
|
||||||
formatGroup.selectToggle(formatElectrum);
|
formatGroup.selectToggle(formatElectrum);
|
||||||
}
|
}
|
||||||
|
|
@ -287,9 +290,13 @@ public class MessageSignDialog extends Dialog<ButtonBar.ButtonData> {
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean canSign(Wallet wallet) {
|
private boolean canSign(Wallet wallet) {
|
||||||
return wallet.getKeystores().get(0).hasPrivateKey()
|
return wallet.getKeystores().getFirst().hasPrivateKey()
|
||||||
|| wallet.getKeystores().get(0).getSource() == KeystoreSource.HW_USB
|
|| wallet.getKeystores().getFirst().getSource() == KeystoreSource.HW_USB
|
||||||
|| wallet.getKeystores().get(0).getWalletModel().isCard();
|
|| wallet.getKeystores().getFirst().getWalletModel().isCard();
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean canSignBip322(Wallet wallet) {
|
||||||
|
return wallet.getKeystores().getFirst().hasPrivateKey();
|
||||||
}
|
}
|
||||||
|
|
||||||
private Address getAddress()throws InvalidAddressException {
|
private Address getAddress()throws InvalidAddressException {
|
||||||
|
|
@ -313,6 +320,11 @@ public class MessageSignDialog extends Dialog<ButtonBar.ButtonData> {
|
||||||
walletNode = wallet.getWalletAddresses().get(address);
|
walletNode = wallet.getWalletAddresses().get(address);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private ScriptType getSigningScriptType(WalletNode walletNode) {
|
||||||
|
ScriptType scriptType = walletNode.getWallet().getScriptType();
|
||||||
|
return canSign(walletNode.getWallet()) && !canSignBip322(walletNode.getWallet()) ? ScriptType.P2PKH : scriptType;
|
||||||
|
}
|
||||||
|
|
||||||
private void setFormatFromScriptType(ScriptType scriptType) {
|
private void setFormatFromScriptType(ScriptType scriptType) {
|
||||||
formatElectrum.setDisable(scriptType == ScriptType.P2TR);
|
formatElectrum.setDisable(scriptType == ScriptType.P2TR);
|
||||||
formatTrezor.setDisable(scriptType == ScriptType.P2TR || scriptType == ScriptType.P2PKH);
|
formatTrezor.setDisable(scriptType == ScriptType.P2TR || scriptType == ScriptType.P2PKH);
|
||||||
|
|
@ -345,7 +357,7 @@ public class MessageSignDialog extends Dialog<ButtonBar.ButtonData> {
|
||||||
|
|
||||||
//Note we can expect a single keystore due to the check in the constructor
|
//Note we can expect a single keystore due to the check in the constructor
|
||||||
Wallet signingWallet = walletNode.getWallet();
|
Wallet signingWallet = walletNode.getWallet();
|
||||||
if(signingWallet.getKeystores().get(0).hasPrivateKey()) {
|
if(signingWallet.getKeystores().getFirst().hasPrivateKey()) {
|
||||||
if(signingWallet.isEncrypted()) {
|
if(signingWallet.isEncrypted()) {
|
||||||
EventManager.get().post(new RequestOpenWalletsEvent());
|
EventManager.get().post(new RequestOpenWalletsEvent());
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -358,7 +370,7 @@ public class MessageSignDialog extends Dialog<ButtonBar.ButtonData> {
|
||||||
|
|
||||||
private void signUnencryptedKeystore(Wallet decryptedWallet) {
|
private void signUnencryptedKeystore(Wallet decryptedWallet) {
|
||||||
try {
|
try {
|
||||||
Keystore keystore = decryptedWallet.getKeystores().get(0);
|
Keystore keystore = decryptedWallet.getKeystores().getFirst();
|
||||||
ECKey privKey = keystore.getKey(walletNode);
|
ECKey privKey = keystore.getKey(walletNode);
|
||||||
String signatureText;
|
String signatureText;
|
||||||
if(isBip322()) {
|
if(isBip322()) {
|
||||||
|
|
@ -378,8 +390,8 @@ public class MessageSignDialog extends Dialog<ButtonBar.ButtonData> {
|
||||||
}
|
}
|
||||||
|
|
||||||
private void signDeviceKeystore(Wallet deviceWallet) {
|
private void signDeviceKeystore(Wallet deviceWallet) {
|
||||||
List<String> fingerprints = List.of(deviceWallet.getKeystores().get(0).getKeyDerivation().getMasterFingerprint());
|
List<String> fingerprints = List.of(deviceWallet.getKeystores().getFirst().getKeyDerivation().getMasterFingerprint());
|
||||||
KeyDerivation fullDerivation = deviceWallet.getKeystores().get(0).getKeyDerivation().extend(walletNode.getDerivation());
|
KeyDerivation fullDerivation = deviceWallet.getKeystores().getFirst().getKeyDerivation().extend(walletNode.getDerivation());
|
||||||
DeviceSignMessageDialog deviceSignMessageDialog = new DeviceSignMessageDialog(fingerprints, deviceWallet, message.getText().trim(), fullDerivation);
|
DeviceSignMessageDialog deviceSignMessageDialog = new DeviceSignMessageDialog(fingerprints, deviceWallet, message.getText().trim(), fullDerivation);
|
||||||
deviceSignMessageDialog.initOwner(getDialogPane().getScene().getWindow());
|
deviceSignMessageDialog.initOwner(getDialogPane().getScene().getWindow());
|
||||||
Optional<String> optSignature = deviceSignMessageDialog.showAndWait();
|
Optional<String> optSignature = deviceSignMessageDialog.showAndWait();
|
||||||
|
|
|
||||||
|
|
@ -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 == Transaction.DEFAULT_MIN_RELAY_FEE) {
|
if(feeRate == AppServices.getMinimumRelayFeeRate() && feeRate > 0d) {
|
||||||
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 = Transaction.DEFAULT_MIN_RELAY_FEE;
|
feeRate = AppServices.getMinimumRelayFeeRate();
|
||||||
fee = (long)Math.ceil(noFeeTransaction.getVirtualSize() * feeRate) + 1;
|
fee = (long)Math.ceil(noFeeTransaction.getVirtualSize() * feeRate) + (feeRate > 0d ? 1 : 0);
|
||||||
|
|
||||||
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).");
|
||||||
|
|
|
||||||
|
|
@ -122,19 +122,21 @@ public class QRScanDialog extends Dialog<QRScanDialog.Result> {
|
||||||
if(percentComplete.get() <= 0.0) {
|
if(percentComplete.get() <= 0.0) {
|
||||||
Platform.runLater(() -> percentComplete.set(opening ? 0.0 : -1.0));
|
Platform.runLater(() -> percentComplete.set(opening ? 0.0 : -1.0));
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
|
||||||
if(opening) {
|
webcamService.openedProperty().addListener((_, _, opened) -> {
|
||||||
|
if(opened) {
|
||||||
Platform.runLater(() -> {
|
Platform.runLater(() -> {
|
||||||
try {
|
try {
|
||||||
postOpenUpdate = true;
|
postOpenUpdate = true;
|
||||||
List<CaptureDevice> newDevices = new ArrayList<>(webcamService.getDevices());
|
List<CaptureDevice> newDevices = new ArrayList<>(webcamService.getAvailableDevices());
|
||||||
newDevices.removeAll(foundDevices);
|
newDevices.removeAll(foundDevices);
|
||||||
foundDevices.addAll(newDevices);
|
foundDevices.addAll(newDevices);
|
||||||
foundDevices.removeIf(device -> !webcamService.getDevices().contains(device));
|
foundDevices.removeIf(device -> !webcamService.getDevices().contains(device));
|
||||||
|
|
||||||
if(Config.get().getWebcamDevice() != null && webcamDeviceProperty.get() == null) {
|
if(webcamService.getDevice() != null) {
|
||||||
for(CaptureDevice device : foundDevices) {
|
for(CaptureDevice device : foundDevices) {
|
||||||
if(device.getName().equals(Config.get().getWebcamDevice())) {
|
if(device.equals(webcamService.getDevice())) {
|
||||||
webcamDeviceProperty.set(device);
|
webcamDeviceProperty.set(device);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -146,10 +148,7 @@ public class QRScanDialog extends Dialog<QRScanDialog.Result> {
|
||||||
postOpenUpdate = false;
|
postOpenUpdate = false;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
} else if(webcamResolutionProperty.get() != null) {
|
||||||
});
|
|
||||||
webcamService.closedProperty().addListener((_, _, closed) -> {
|
|
||||||
if(closed && webcamResolutionProperty.get() != null) {
|
|
||||||
webcamService.setResolution(webcamResolutionProperty.get());
|
webcamService.setResolution(webcamResolutionProperty.get());
|
||||||
webcamService.setDevice(webcamDeviceProperty.get());
|
webcamService.setDevice(webcamDeviceProperty.get());
|
||||||
Platform.runLater(() -> {
|
Platform.runLater(() -> {
|
||||||
|
|
@ -190,6 +189,7 @@ 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();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,15 @@
|
||||||
package com.sparrowwallet.sparrow.control;
|
package com.sparrowwallet.sparrow.control;
|
||||||
|
|
||||||
import com.sparrowwallet.sparrow.BlockSummary;
|
import com.sparrowwallet.sparrow.BlockSummary;
|
||||||
|
import com.sparrowwallet.sparrow.io.Config;
|
||||||
|
import com.sparrowwallet.sparrow.net.FeeRatesSource;
|
||||||
import io.reactivex.Observable;
|
import io.reactivex.Observable;
|
||||||
import io.reactivex.disposables.CompositeDisposable;
|
import io.reactivex.disposables.CompositeDisposable;
|
||||||
import io.reactivex.rxjavafx.schedulers.JavaFxScheduler;
|
import io.reactivex.rxjavafx.schedulers.JavaFxScheduler;
|
||||||
import javafx.animation.TranslateTransition;
|
import javafx.animation.TranslateTransition;
|
||||||
import javafx.beans.property.ObjectProperty;
|
import javafx.beans.property.ObjectProperty;
|
||||||
import javafx.beans.property.SimpleObjectProperty;
|
import javafx.beans.property.SimpleObjectProperty;
|
||||||
|
import javafx.scene.control.Tooltip;
|
||||||
import javafx.scene.layout.Pane;
|
import javafx.scene.layout.Pane;
|
||||||
import javafx.scene.shape.Line;
|
import javafx.scene.shape.Line;
|
||||||
import javafx.scene.shape.Rectangle;
|
import javafx.scene.shape.Rectangle;
|
||||||
|
|
@ -14,8 +17,12 @@ import javafx.util.Duration;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
|
import static com.sparrowwallet.sparrow.AppServices.TARGET_BLOCKS_RANGE;
|
||||||
|
import static com.sparrowwallet.sparrow.control.BlockCube.CUBE_SIZE;
|
||||||
|
|
||||||
public class RecentBlocksView extends Pane {
|
public class RecentBlocksView extends Pane {
|
||||||
private static final double CUBE_SPACING = 100;
|
private static final double CUBE_SPACING = 100;
|
||||||
private static final double ANIMATION_DURATION_MILLIS = 1000;
|
private static final double ANIMATION_DURATION_MILLIS = 1000;
|
||||||
|
|
@ -24,6 +31,7 @@ public class RecentBlocksView extends Pane {
|
||||||
private final CompositeDisposable disposables = new CompositeDisposable();
|
private final CompositeDisposable disposables = new CompositeDisposable();
|
||||||
|
|
||||||
private final ObjectProperty<List<BlockCube>> cubesProperty = new SimpleObjectProperty<>(new ArrayList<>());
|
private final ObjectProperty<List<BlockCube>> cubesProperty = new SimpleObjectProperty<>(new ArrayList<>());
|
||||||
|
private final Tooltip tooltip = new Tooltip();
|
||||||
|
|
||||||
public RecentBlocksView() {
|
public RecentBlocksView() {
|
||||||
cubesProperty.addListener((_, _, newValue) -> {
|
cubesProperty.addListener((_, _, newValue) -> {
|
||||||
|
|
@ -41,6 +49,18 @@ public class RecentBlocksView extends Pane {
|
||||||
cube.setElapsed(BlockCube.getElapsed(cube.getTimestamp()));
|
cube.setElapsed(BlockCube.getElapsed(cube.getTimestamp()));
|
||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
FeeRatesSource feeRatesSource = Config.get().getFeeRatesSource();
|
||||||
|
feeRatesSource = (feeRatesSource == null ? FeeRatesSource.MEMPOOL_SPACE : feeRatesSource);
|
||||||
|
updateFeeRatesSource(feeRatesSource);
|
||||||
|
Tooltip.install(this, tooltip);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void updateFeeRatesSource(FeeRatesSource feeRatesSource) {
|
||||||
|
tooltip.setText("Fee rate estimate from " + feeRatesSource.getDescription());
|
||||||
|
if(getCubes() != null && !getCubes().isEmpty()) {
|
||||||
|
getCubes().getFirst().setFeeRatesSource(feeRatesSource);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void drawView() {
|
public void drawView() {
|
||||||
|
|
@ -54,7 +74,7 @@ public class RecentBlocksView extends Pane {
|
||||||
}
|
}
|
||||||
|
|
||||||
private void createSeparator() {
|
private void createSeparator() {
|
||||||
Line separator = new Line(SEPARATOR_X, -9, SEPARATOR_X, 80);
|
Line separator = new Line(SEPARATOR_X, -9, SEPARATOR_X, CUBE_SIZE);
|
||||||
separator.getStyleClass().add("blocks-separator");
|
separator.getStyleClass().add("blocks-separator");
|
||||||
separator.getStrokeDashArray().addAll(5.0, 5.0); // Create dotted line pattern
|
separator.getStrokeDashArray().addAll(5.0, 5.0); // Create dotted line pattern
|
||||||
separator.setStrokeWidth(1.0);
|
separator.setStrokeWidth(1.0);
|
||||||
|
|
@ -73,14 +93,14 @@ public class RecentBlocksView extends Pane {
|
||||||
if(latestTip > knownTip) {
|
if(latestTip > knownTip) {
|
||||||
addNewBlock(latestBlocks, currentFeeRate);
|
addNewBlock(latestBlocks, currentFeeRate);
|
||||||
} else {
|
} else {
|
||||||
for(int i = 1; i < getCubes().size() && i < latestBlocks.size(); i++) {
|
for(int i = 1; i < getCubes().size() && i <= latestBlocks.size(); i++) {
|
||||||
BlockCube blockCube = getCubes().get(i);
|
BlockCube blockCube = getCubes().get(i);
|
||||||
BlockSummary latestBlock = latestBlocks.get(i);
|
BlockSummary latestBlock = latestBlocks.get(i - 1);
|
||||||
blockCube.setConfirmed(true);
|
blockCube.setConfirmed(true);
|
||||||
blockCube.setHeight(latestBlock.getHeight());
|
blockCube.setHeight(latestBlock.getHeight());
|
||||||
blockCube.setTimestamp(latestBlock.getTimestamp().getTime());
|
blockCube.setTimestamp(latestBlock.getTimestamp().getTime());
|
||||||
blockCube.setWeight(latestBlock.getWeight().orElse(0));
|
blockCube.setWeight(latestBlock.getWeight().orElse(0));
|
||||||
blockCube.setMedianFee(latestBlock.getMedianFee().orElse(0.0d));
|
blockCube.setMedianFee(latestBlock.getMedianFee().orElse(-1.0d));
|
||||||
blockCube.setTxCount(latestBlock.getTransactionCount().orElse(0));
|
blockCube.setTxCount(latestBlock.getTransactionCount().orElse(0));
|
||||||
}
|
}
|
||||||
updateFeeRate(currentFeeRate);
|
updateFeeRate(currentFeeRate);
|
||||||
|
|
@ -88,7 +108,7 @@ public class RecentBlocksView extends Pane {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void addNewBlock(List<BlockSummary> latestBlocks, Double currentFeeRate) {
|
private void addNewBlock(List<BlockSummary> latestBlocks, Double currentFeeRate) {
|
||||||
if(getCubes().isEmpty()) {
|
if(getCubes().isEmpty()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -100,7 +120,7 @@ public class RecentBlocksView extends Pane {
|
||||||
blockCube.setHeight(latestBlock.getHeight());
|
blockCube.setHeight(latestBlock.getHeight());
|
||||||
blockCube.setTimestamp(latestBlock.getTimestamp().getTime());
|
blockCube.setTimestamp(latestBlock.getTimestamp().getTime());
|
||||||
blockCube.setWeight(latestBlock.getWeight().orElse(0));
|
blockCube.setWeight(latestBlock.getWeight().orElse(0));
|
||||||
blockCube.setMedianFee(latestBlock.getMedianFee().orElse(0.0d));
|
blockCube.setMedianFee(latestBlock.getMedianFee().orElse(-1.0d));
|
||||||
blockCube.setTxCount(latestBlock.getTransactionCount().orElse(0));
|
blockCube.setTxCount(latestBlock.getTransactionCount().orElse(0));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -120,6 +140,14 @@ public class RecentBlocksView extends Pane {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void updateFeeRate(Map<Integer, Double> targetBlockFeeRates) {
|
||||||
|
int defaultTarget = TARGET_BLOCKS_RANGE.get((TARGET_BLOCKS_RANGE.size() / 2) - 1);
|
||||||
|
if(targetBlockFeeRates.get(defaultTarget) != null) {
|
||||||
|
Double defaultRate = targetBlockFeeRates.get(defaultTarget);
|
||||||
|
updateFeeRate(defaultRate);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public void updateFeeRate(Double currentFeeRate) {
|
public void updateFeeRate(Double currentFeeRate) {
|
||||||
if(!getCubes().isEmpty()) {
|
if(!getCubes().isEmpty()) {
|
||||||
BlockCube firstCube = getCubes().getFirst();
|
BlockCube firstCube = getCubes().getFirst();
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ 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;
|
||||||
|
|
@ -53,7 +54,11 @@ 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()) {
|
||||||
append(chunk.toString(), "script-opcode");
|
if(chunk.getOpcode() == ScriptOpCodes.OP_0 && witnessScript != null) {
|
||||||
|
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()) {
|
||||||
|
|
|
||||||
|
|
@ -5,36 +5,49 @@ 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.glyphfont.FontAwesome5;
|
import com.sparrowwallet.sparrow.EventManager;
|
||||||
|
import com.sparrowwallet.sparrow.event.RequestConnectEvent;
|
||||||
|
import com.sparrowwallet.sparrow.glyphfont.GlyphUtils;
|
||||||
|
import com.sparrowwallet.sparrow.io.Config;
|
||||||
|
import javafx.application.Platform;
|
||||||
import javafx.collections.FXCollections;
|
import javafx.collections.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.image.Image;
|
|
||||||
import javafx.scene.image.ImageView;
|
|
||||||
import javafx.scene.input.Clipboard;
|
import javafx.scene.input.Clipboard;
|
||||||
import javafx.scene.layout.StackPane;
|
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 AddressCellType ADDRESS = new AddressCellType();
|
public static final SendToAddressCellType SEND_TO_ADDRESS = new SendToAddressCellType();
|
||||||
|
|
||||||
public SendToManyDialog(BitcoinUnit bitcoinUnit) {
|
public SendToManyDialog(BitcoinUnit bitcoinUnit, List<Payment> payments) {
|
||||||
this.bitcoinUnit = bitcoinUnit;
|
this.bitcoinUnit = bitcoinUnit;
|
||||||
|
|
||||||
final DialogPane dialogPane = new SendToManyDialogPane();
|
final DialogPane dialogPane = new SendToManyDialogPane();
|
||||||
|
|
@ -44,7 +57,8 @@ public class SendToManyDialog extends Dialog<List<Payment>> {
|
||||||
dialogPane.setHeaderText("Send to many recipients by specifying addresses and amounts.\nOnly the first row's label is necessary.");
|
dialogPane.setHeaderText("Send to many recipients by specifying addresses and amounts.\nOnly the first row's label is necessary.");
|
||||||
dialogPane.setGraphic(new DialogImage(DialogImage.Type.SPARROW));
|
dialogPane.setGraphic(new DialogImage(DialogImage.Type.SPARROW));
|
||||||
|
|
||||||
List<Payment> initialPayments = IntStream.range(0, 100).mapToObj(i -> new Payment(null, null, -1, false)).collect(Collectors.toList());
|
List<Payment> initialPayments = IntStream.range(0, 100)
|
||||||
|
.mapToObj(i -> i < payments.size() ? payments.get(i) : new Payment(null, null, -1, false)).collect(Collectors.toList());
|
||||||
Grid grid = getGrid(initialPayments);
|
Grid grid = getGrid(initialPayments);
|
||||||
|
|
||||||
spreadsheetView = new SpreadsheetView(grid) {
|
spreadsheetView = new SpreadsheetView(grid) {
|
||||||
|
|
@ -69,14 +83,16 @@ 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((dialogButton) -> {
|
setResultConverter((_) -> null);
|
||||||
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);
|
||||||
|
|
@ -86,18 +102,24 @@ public class SendToManyDialog extends Dialog<List<Payment>> {
|
||||||
}
|
}
|
||||||
|
|
||||||
private Grid getGrid(List<Payment> payments) {
|
private Grid getGrid(List<Payment> payments) {
|
||||||
int rowCount = payments.size();
|
return createGrid(payments.stream().map(payment -> new SendToPayment(payment, SendToAddress.fromPayment(payment))).collect(Collectors.toList()));
|
||||||
|
}
|
||||||
|
|
||||||
|
private Grid createGrid(List<SendToPayment> sendToPayments) {
|
||||||
|
int rowCount = sendToPayments.size();
|
||||||
int columnCount = 3;
|
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();
|
||||||
|
|
||||||
SpreadsheetCell addressCell = ADDRESS.createCell(row, 0, 1, 1, payments.get(row).getAddress());
|
SendToAddress sendToAddress = sendToPayment.sendToAddress();
|
||||||
|
SpreadsheetCell addressCell = SEND_TO_ADDRESS.createCell(row, 0, 1, 1, sendToAddress);
|
||||||
addressCell.getStyleClass().add("fixed-width");
|
addressCell.getStyleClass().add("fixed-width");
|
||||||
list.add(addressCell);
|
list.add(addressCell);
|
||||||
|
|
||||||
double amount = (double)payments.get(row).getAmount();
|
double amount = (double)sendToPayment.payment().getAmount();
|
||||||
if(bitcoinUnit == BitcoinUnit.BTC) {
|
if(bitcoinUnit == BitcoinUnit.BTC) {
|
||||||
amount = amount / Transaction.SATOSHIS_PER_BITCOIN;
|
amount = amount / Transaction.SATOSHIS_PER_BITCOIN;
|
||||||
}
|
}
|
||||||
|
|
@ -109,7 +131,7 @@ public class SendToManyDialog extends Dialog<List<Payment>> {
|
||||||
}
|
}
|
||||||
list.add(amountCell);
|
list.add(amountCell);
|
||||||
|
|
||||||
list.add(SpreadsheetCellType.STRING.createCell(row, 2, 1, 1, payments.get(row).getLabel()));
|
list.add(SpreadsheetCellType.STRING.createCell(row, 2, 1, 1, sendToPayment.payment().getLabel()));
|
||||||
rows.add(list);
|
rows.add(list);
|
||||||
}
|
}
|
||||||
grid.setRows(rows);
|
grid.setRows(rows);
|
||||||
|
|
@ -118,32 +140,49 @@ public class SendToManyDialog extends Dialog<List<Payment>> {
|
||||||
return grid;
|
return grid;
|
||||||
}
|
}
|
||||||
|
|
||||||
private List<Payment> getPayments() {
|
private void getPayments() {
|
||||||
List<Payment> payments = new ArrayList<>();
|
if(needsResolution() && Config.get().hasServer() && !AppServices.isConnected() && !AppServices.isConnecting()) {
|
||||||
Grid grid = spreadsheetView.getGrid();
|
if(Config.get().getConnectToResolve() == null || Config.get().getConnectToResolve() == Boolean.FALSE) {
|
||||||
String firstLabel = null;
|
Platform.runLater(() -> {
|
||||||
for(int row = 0; row < grid.getRowCount(); row++) {
|
ConfirmationAlert confirmationAlert = new ConfirmationAlert("Connect to resolve?", "You are currently offline. Connect to resolve the addresses?", ButtonType.NO, ButtonType.YES);
|
||||||
|
Optional<ButtonType> optType = confirmationAlert.showAndWait();
|
||||||
|
if(confirmationAlert.isDontAskAgain() && optType.isPresent()) {
|
||||||
|
Config.get().setConnectToResolve(optType.get() == ButtonType.YES);
|
||||||
|
}
|
||||||
|
if(optType.isPresent() && optType.get() == ButtonType.YES) {
|
||||||
|
EventManager.get().post(new RequestConnectEvent());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
Platform.runLater(() -> EventManager.get().post(new RequestConnectEvent()));
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
CreatePaymentsService createPaymentsService = new CreatePaymentsService();
|
||||||
|
createPaymentsService.setOnSucceeded(_ -> {
|
||||||
|
List<Payment> payments = createPaymentsService.getValue();
|
||||||
|
if(payments != null) {
|
||||||
|
setResult(payments);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
createPaymentsService.setOnFailed(event -> {
|
||||||
|
Throwable ex = event.getSource().getException();
|
||||||
|
AppServices.showErrorDialog("Error creating payments", ex.getMessage());
|
||||||
|
});
|
||||||
|
createPaymentsService.start();
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean needsResolution() {
|
||||||
|
for(int row = 0; row < spreadsheetView.getGrid().getRowCount(); row++) {
|
||||||
ObservableList<SpreadsheetCell> rowCells = spreadsheetView.getItems().get(row);
|
ObservableList<SpreadsheetCell> rowCells = spreadsheetView.getItems().get(row);
|
||||||
Address address = (Address)rowCells.get(0).getItem();
|
SendToAddress sendToAddress = (SendToAddress)rowCells.getFirst().getItem();
|
||||||
Double value = (Double)rowCells.get(1).getItem();
|
if(sendToAddress.hrn != null && DnsPaymentCache.getDnsPayment(sendToAddress.hrn) == null) {
|
||||||
String label = (String)rowCells.get(2).getItem();
|
return true;
|
||||||
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 payments;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
private class SendToManyDialogPane extends DialogPane {
|
private class SendToManyDialogPane extends DialogPane {
|
||||||
|
|
@ -153,7 +192,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(getGlyph(FontAwesome5.Glyph.ARROW_UP));
|
loadButton.setGraphic(GlyphUtils.getUpArrowGlyph());
|
||||||
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 -> {
|
||||||
|
|
@ -168,7 +207,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<Payment> csvPayments = new ArrayList<>();
|
List<SendToPayment> 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()) {
|
||||||
|
|
@ -184,9 +223,22 @@ 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);
|
||||||
csvPayments.add(new Payment(address, label, amount, false));
|
Optional<String> optDnsPaymentHrn = DnsPayment.getHrn(csvReader.get(0));
|
||||||
|
if(optDnsPaymentHrn.isPresent()) {
|
||||||
|
Payment payment = new Payment(null, label, amount, false);
|
||||||
|
csvPayments.add(new SendToPayment(payment, new SendToAddress(optDnsPaymentHrn.get())));
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
SilentPaymentAddress silentPaymentAddress = SilentPaymentAddress.from(csvReader.get(0));
|
||||||
|
Payment payment = new SilentPayment(silentPaymentAddress, label, amount, false);
|
||||||
|
csvPayments.add(new SendToPayment(payment, SendToAddress.fromPayment(payment)));
|
||||||
|
} catch(Exception e) {
|
||||||
|
Address address = Address.fromString(csvReader.get(0));
|
||||||
|
Payment payment = new Payment(address, label, amount, false);
|
||||||
|
csvPayments.add(new SendToPayment(payment, SendToAddress.fromPayment(payment)));
|
||||||
|
}
|
||||||
|
}
|
||||||
} catch(NumberFormatException e) {
|
} catch(NumberFormatException e) {
|
||||||
//ignore and continue - probably a header line
|
//ignore and continue - probably a header line
|
||||||
} catch(InvalidAddressException e) {
|
} catch(InvalidAddressException e) {
|
||||||
|
|
@ -199,7 +251,7 @@ public class SendToManyDialog extends Dialog<List<Payment>> {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
spreadsheetView.setGrid(getGrid(csvPayments));
|
spreadsheetView.setGrid(createGrid(csvPayments));
|
||||||
}
|
}
|
||||||
} catch(IOException e) {
|
} catch(IOException e) {
|
||||||
AppServices.showErrorDialog("Cannot load CSV", e.getMessage());
|
AppServices.showErrorDialog("Cannot load CSV", e.getMessage());
|
||||||
|
|
@ -214,24 +266,18 @@ 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 AddressCellType extends SpreadsheetCellType<Address> {
|
public static class SendToAddressCellType extends SpreadsheetCellType<SendToAddress> {
|
||||||
public AddressCellType() {
|
public SendToAddressCellType() {
|
||||||
this(new StringConverterWithFormat<>(new AddressStringConverter()) {
|
this(new StringConverterWithFormat<>(new SendToAddressStringConverter()) {
|
||||||
@Override
|
@Override
|
||||||
public String toString(Address item) {
|
public String toString(SendToAddress item) {
|
||||||
return toStringFormat(item, ""); //$NON-NLS-1$
|
return toStringFormat(item, ""); //$NON-NLS-1$
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Address fromString(String str) {
|
public SendToAddress 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 {
|
||||||
|
|
@ -240,7 +286,7 @@ public class SendToManyDialog extends Dialog<List<Payment>> {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String toStringFormat(Address item, String format) {
|
public String toStringFormat(SendToAddress item, String format) {
|
||||||
try {
|
try {
|
||||||
if(item == null) {
|
if(item == null) {
|
||||||
return ""; //$NON-NLS-1$
|
return ""; //$NON-NLS-1$
|
||||||
|
|
@ -254,7 +300,7 @@ public class SendToManyDialog extends Dialog<List<Payment>> {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public AddressCellType(StringConverter<Address> converter) {
|
public SendToAddressCellType(StringConverter<SendToAddress> converter) {
|
||||||
super(converter);
|
super(converter);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -264,7 +310,7 @@ public class SendToManyDialog extends Dialog<List<Payment>> {
|
||||||
}
|
}
|
||||||
|
|
||||||
public SpreadsheetCell createCell(final int row, final int column, final int rowSpan, final int columnSpan,
|
public SpreadsheetCell createCell(final int row, final int column, final int rowSpan, final int columnSpan,
|
||||||
final Address value) {
|
final SendToAddress value) {
|
||||||
SpreadsheetCell cell = new SpreadsheetCellBase(row, column, rowSpan, columnSpan, this);
|
SpreadsheetCell cell = new SpreadsheetCellBase(row, column, rowSpan, columnSpan, this);
|
||||||
cell.setItem(value);
|
cell.setItem(value);
|
||||||
return cell;
|
return cell;
|
||||||
|
|
@ -277,7 +323,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 Address)
|
if(value instanceof SendToAddress)
|
||||||
return true;
|
return true;
|
||||||
else {
|
else {
|
||||||
try {
|
try {
|
||||||
|
|
@ -290,9 +336,9 @@ public class SendToManyDialog extends Dialog<List<Payment>> {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Address convertValue(Object value) {
|
public SendToAddress convertValue(Object value) {
|
||||||
if(value instanceof Address)
|
if(value instanceof SendToAddress)
|
||||||
return (Address)value;
|
return (SendToAddress)value;
|
||||||
else {
|
else {
|
||||||
try {
|
try {
|
||||||
return converter.fromString(value == null ? null : value.toString());
|
return converter.fromString(value == null ? null : value.toString());
|
||||||
|
|
@ -303,13 +349,155 @@ public class SendToManyDialog extends Dialog<List<Payment>> {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String toString(Address item) {
|
public String toString(SendToAddress item) {
|
||||||
return converter.toString(item);
|
return converter.toString(item);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String toString(Address item, String format) {
|
public String toString(SendToAddress item, String format) {
|
||||||
return ((StringConverterWithFormat<Address>)converter).toStringFormat(item, format);
|
return ((StringConverterWithFormat<SendToAddress>)converter).toStringFormat(item, format);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
public static class SendToAddress {
|
||||||
|
private final String hrn;
|
||||||
|
private final Address address;
|
||||||
|
private final SilentPaymentAddress silentPaymentAddress;
|
||||||
|
|
||||||
|
public SendToAddress(String hrn) {
|
||||||
|
this.hrn = hrn;
|
||||||
|
this.address = null;
|
||||||
|
this.silentPaymentAddress = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public SendToAddress(Address address) {
|
||||||
|
this.hrn = null;
|
||||||
|
this.address = address;
|
||||||
|
this.silentPaymentAddress = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public SendToAddress(SilentPaymentAddress silentPaymentAddress) {
|
||||||
|
this.hrn = null;
|
||||||
|
this.address = null;
|
||||||
|
this.silentPaymentAddress = silentPaymentAddress;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String toString() {
|
||||||
|
return hrn == null ? silentPaymentAddress == null ? (address == null ? null : address.toString()) : silentPaymentAddress.toString() : hrn;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static SendToAddress fromPayment(Payment payment) {
|
||||||
|
DnsPayment dnsPayment = DnsPaymentCache.getDnsPayment(payment);
|
||||||
|
if(dnsPayment != null) {
|
||||||
|
return new SendToAddress(dnsPayment.hrn());
|
||||||
|
}
|
||||||
|
return payment instanceof SilentPayment ? new SendToAddress(((SilentPayment)payment).getSilentPaymentAddress()) : new SendToAddress(payment.getAddress());
|
||||||
|
}
|
||||||
|
|
||||||
|
public Payment toPayment(String label, long value, boolean sendMax) throws DnsPaymentValidationException, IOException, ExecutionException, InterruptedException, BitcoinURIParseException {
|
||||||
|
if(hrn != null) {
|
||||||
|
DnsPayment dnsPayment = DnsPaymentCache.getDnsPayment(hrn);
|
||||||
|
if(dnsPayment == null) {
|
||||||
|
DnsPaymentResolver resolver = new DnsPaymentResolver(hrn);
|
||||||
|
Optional<DnsPayment> optDnsPayment = resolver.resolve();
|
||||||
|
if(optDnsPayment.isPresent()) {
|
||||||
|
dnsPayment = optDnsPayment.get();
|
||||||
|
if(dnsPayment.hasAddress()) {
|
||||||
|
DnsPaymentCache.putDnsPayment(dnsPayment.bitcoinURI().getAddress(), dnsPayment);
|
||||||
|
} else if(dnsPayment.hasSilentPaymentAddress()) {
|
||||||
|
DnsPaymentCache.putDnsPayment(dnsPayment.bitcoinURI().getSilentPaymentAddress(), dnsPayment);
|
||||||
|
}
|
||||||
|
return getPayment(optDnsPayment.get(), label, value, sendMax);
|
||||||
|
} else {
|
||||||
|
throw new IllegalArgumentException("Payment to " + hrn + " could not be resolved.");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return getPayment(dnsPayment, label, value, sendMax);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if(silentPaymentAddress != null) {
|
||||||
|
return new SilentPayment(silentPaymentAddress, label, value, sendMax);
|
||||||
|
} else {
|
||||||
|
return new Payment(address, label, value, sendMax);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Payment getPayment(DnsPayment dnsPayment, String label, long value, boolean sendMax) {
|
||||||
|
if(dnsPayment.hasAddress()) {
|
||||||
|
return new Payment(dnsPayment.bitcoinURI().getAddress(), label, value, sendMax);
|
||||||
|
} else if(dnsPayment.hasSilentPaymentAddress()) {
|
||||||
|
return new SilentPayment(dnsPayment.bitcoinURI().getSilentPaymentAddress(), label, value, sendMax);
|
||||||
|
} else {
|
||||||
|
throw new IllegalArgumentException("Payment to " + dnsPayment + " has no associated address.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class SendToAddressStringConverter extends StringConverter<SendToAddress> {
|
||||||
|
private final AddressStringConverter addressStringConverter = new AddressStringConverter();
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public SendToAddress fromString(String value) {
|
||||||
|
Optional<String> optDnsPaymentHrn = DnsPayment.getHrn(value);
|
||||||
|
if(optDnsPaymentHrn.isPresent()) {
|
||||||
|
return new SendToAddress(optDnsPaymentHrn.get());
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
SilentPaymentAddress silentPaymentAddress = SilentPaymentAddress.from(value);
|
||||||
|
return new SendToAddress(silentPaymentAddress);
|
||||||
|
} catch(Exception e) {
|
||||||
|
Address address = addressStringConverter.fromString(value);
|
||||||
|
return address == null ? null : new SendToAddress(address);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString(SendToAddress value) {
|
||||||
|
return value.toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class CreatePaymentsService extends Service<List<Payment>> {
|
||||||
|
@Override
|
||||||
|
protected Task<List<Payment>> createTask() {
|
||||||
|
return new Task<>() {
|
||||||
|
@Override
|
||||||
|
protected List<Payment> call() throws Exception {
|
||||||
|
return getPayments();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<Payment> getPayments() throws DnsPaymentValidationException, IOException, ExecutionException, InterruptedException, BitcoinURIParseException {
|
||||||
|
List<Payment> payments = new ArrayList<>();
|
||||||
|
Grid grid = spreadsheetView.getGrid();
|
||||||
|
String firstLabel = null;
|
||||||
|
for(int row = 0; row < grid.getRowCount(); row++) {
|
||||||
|
ObservableList<SpreadsheetCell> rowCells = spreadsheetView.getItems().get(row);
|
||||||
|
SendToAddress sendToAddress = (SendToAddress)rowCells.get(0).getItem();
|
||||||
|
Double value = (Double)rowCells.get(1).getItem();
|
||||||
|
String label = (String)rowCells.get(2).getItem();
|
||||||
|
if(firstLabel == null) {
|
||||||
|
firstLabel = label;
|
||||||
|
}
|
||||||
|
if(label == null || label.isEmpty()) {
|
||||||
|
label = firstLabel;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(sendToAddress != null && value != null) {
|
||||||
|
if(bitcoinUnit == BitcoinUnit.BTC) {
|
||||||
|
value = value * Transaction.SATOSHIS_PER_BITCOIN;
|
||||||
|
}
|
||||||
|
|
||||||
|
payments.add(sendToAddress.toPayment(label, value.longValue(), false));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return payments;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private record SendToPayment(Payment payment, SendToAddress sendToAddress) {}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,8 +4,12 @@ import com.sparrowwallet.drongo.KeyPurpose;
|
||||||
import com.sparrowwallet.drongo.OsType;
|
import com.sparrowwallet.drongo.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.*;
|
||||||
|
|
@ -22,6 +26,7 @@ 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;
|
||||||
|
|
@ -103,6 +108,7 @@ 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();
|
||||||
|
|
@ -120,7 +126,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, 600, 460);
|
AppServices.moveToActiveWindowScreen(stage, expandedDiagram.getMaxWidth(), 460);
|
||||||
});
|
});
|
||||||
stage.setOnHidden(e -> {
|
stage.setOnHidden(e -> {
|
||||||
expandedDiagram = null;
|
expandedDiagram = null;
|
||||||
|
|
@ -137,6 +143,39 @@ public class TransactionDiagram extends GridPane {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
public TransactionDiagram() {
|
||||||
|
ColumnConstraints col1 = new ColumnConstraints();
|
||||||
|
col1.setPrefWidth(22);
|
||||||
|
col1.setHgrow(Priority.NEVER);
|
||||||
|
|
||||||
|
ColumnConstraints col2 = new ColumnConstraints();
|
||||||
|
col2.setHgrow(Priority.ALWAYS);
|
||||||
|
col2.setPercentWidth(25);
|
||||||
|
col2.setFillWidth(true);
|
||||||
|
|
||||||
|
ColumnConstraints col3 = new ColumnConstraints();
|
||||||
|
col3.setPrefWidth(140);
|
||||||
|
col3.setHgrow(Priority.NEVER);
|
||||||
|
|
||||||
|
ColumnConstraints col4 = new ColumnConstraints();
|
||||||
|
Label label = new Label();
|
||||||
|
col4.setMinWidth(TextUtils.computeTextWidth(label.getFont(), "Transaction", 0) + 20);
|
||||||
|
col4.setHgrow(Priority.NEVER);
|
||||||
|
col4.setHalignment(HPos.CENTER);
|
||||||
|
|
||||||
|
ColumnConstraints col5 = new ColumnConstraints();
|
||||||
|
col5.setPrefWidth(140);
|
||||||
|
col5.setHgrow(Priority.NEVER);
|
||||||
|
|
||||||
|
ColumnConstraints col6 = new ColumnConstraints();
|
||||||
|
col6.setHgrow(Priority.ALWAYS);
|
||||||
|
col6.setPercentWidth(25);
|
||||||
|
col6.setFillWidth(true);
|
||||||
|
|
||||||
|
getColumnConstraints().addAll(col1, col2, col3, col4, col5, col6);
|
||||||
|
setPadding(new Insets(0, 0, 0, 40));
|
||||||
|
}
|
||||||
|
|
||||||
public void update(WalletTransaction walletTx) {
|
public void update(WalletTransaction walletTx) {
|
||||||
setMinHeight(getDiagramHeight());
|
setMinHeight(getDiagramHeight());
|
||||||
setMaxHeight(getDiagramHeight());
|
setMaxHeight(getDiagramHeight());
|
||||||
|
|
@ -165,7 +204,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, 280));
|
messagePane.setPadding(new Insets(0, 10, 0, 10));
|
||||||
messagePane.setAlignment(Pos.CENTER);
|
messagePane.setAlignment(Pos.CENTER);
|
||||||
messagePane.getChildren().add(createSpacer());
|
messagePane.getChildren().add(createSpacer());
|
||||||
|
|
||||||
|
|
@ -225,7 +264,6 @@ 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) {
|
||||||
|
|
@ -234,6 +272,8 @@ 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...");
|
||||||
|
|
@ -407,8 +447,6 @@ 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);
|
||||||
|
|
@ -640,7 +678,8 @@ 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.getTransaction().getOutputs().stream().filter(txo -> txo.getScript().getToAddress() != null).map(TransactionOutput::getValue).collect(Collectors.toList());
|
List<Long> values = walletTx.getOutputs().stream().filter(output -> !(output instanceof WalletTransaction.NonAddressOutput))
|
||||||
|
.map(output -> output.getTransactionOutput().getValue()).collect(Collectors.toList());
|
||||||
values.add(walletTx.getFee());
|
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++) {
|
||||||
|
|
@ -676,8 +715,6 @@ 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());
|
||||||
|
|
@ -686,15 +723,16 @@ 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.getAddress().toString().substring(0, 8) + "..." : payment.getLabel(), outputGlyph);
|
Label recipientLabel = new Label(payment.getLabel() == null || payment.getType() == Payment.Type.FAKE_MIX || payment.getType() == Payment.Type.MIX ? payment.toString().substring(0, 8) + "..." : payment.getLabel(), outputGlyph);
|
||||||
recipientLabel.getStyleClass().add("output-label");
|
recipientLabel.getStyleClass().add("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 = walletTx.getWallet() != null && !walletTx.getWallet().isBip47() ? walletTx.getAddressNodeMap().get(payment.getAddress()) : null;
|
WalletNode toNode = payment instanceof WalletNodePayment walletNodePayment ? walletNodePayment.getWalletNode() : null;
|
||||||
Wallet toBip47Wallet = getBip47SendWallet(payment);
|
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 ? (payment.getLabel() == null ? (toNode != null ? toNode : (toBip47Wallet == null ? "external address" : toBip47Wallet.getDisplayName())) : payment.getLabel()) : toWallet.getFullDisplayName()) + "\n" + payment.getAddress().toString())
|
+ (payment instanceof AdditionalPayment ? (isExpanded() ? "\n" : "(click to expand)\n") + payment : (toWallet == null ? (dnsPayment == null ? (payment.getLabel() == null ? (toNode != null ? toNode : (toBip47Wallet == null ? "external address" : toBip47Wallet.getDisplayName())) : payment.getLabel()) : dnsPayment.toString()) : toWallet.getFullDisplayName()) + "\n" + payment.getDisplayAddress())
|
||||||
+ (walletTx.isDuplicateAddress(payment) ? " (Duplicate)" : ""));
|
+ (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));
|
||||||
|
|
@ -719,9 +757,13 @@ public class TransactionDiagram extends GridPane {
|
||||||
paymentBox.getChildren().addAll(region, amountLabel);
|
paymentBox.getChildren().addAll(region, amountLabel);
|
||||||
}
|
}
|
||||||
|
|
||||||
Wallet bip47Wallet = toWallet != null && toWallet.isBip47() ? toWallet : (toBip47Wallet != null && toBip47Wallet.isBip47() ? toBip47Wallet : null);
|
if(payment instanceof SilentPayment silentPayment) {
|
||||||
PaymentCode paymentCode = bip47Wallet == null ? null : bip47Wallet.getKeystores().getFirst().getExternalPaymentCode();
|
outputNodes.add(new OutputNode(paymentBox, silentPayment.isAddressComputed() ? silentPayment.getAddress() : null, payment.getAmount(), null, silentPayment.getSilentPaymentAddress()));
|
||||||
outputNodes.add(new OutputNode(paymentBox, payment.getAddress(), payment.getAmount(), paymentCode));
|
} else {
|
||||||
|
Wallet bip47Wallet = toWallet != null && toWallet.isBip47() ? toWallet : (toBip47Wallet != null && toBip47Wallet.isBip47() ? toBip47Wallet : null);
|
||||||
|
PaymentCode paymentCode = bip47Wallet == null ? null : bip47Wallet.getKeystores().getFirst().getExternalPaymentCode();
|
||||||
|
outputNodes.add(new OutputNode(paymentBox, payment.getAddress(), payment.getAmount(), paymentCode, null));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Set<Integer> seenIndexes = new HashSet<>();
|
Set<Integer> seenIndexes = new HashSet<>();
|
||||||
|
|
@ -785,7 +827,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);
|
ContextMenu contextMenu = new LabelContextMenu(outputNode.address, outputNode.amount, outputNode.paymentCode, outputNode.silentPaymentAddress);
|
||||||
if(!outputNode.outputLabel.getChildren().isEmpty() && outputNode.outputLabel.getChildren().get(0) instanceof Label outputLabelControl) {
|
if(!outputNode.outputLabel.getChildren().isEmpty() && outputNode.outputLabel.getChildren().get(0) instanceof Label outputLabelControl) {
|
||||||
outputLabelControl.setContextMenu(contextMenu);
|
outputLabelControl.setContextMenu(contextMenu);
|
||||||
}
|
}
|
||||||
|
|
@ -960,8 +1002,11 @@ 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.getTransaction().getOutputs().stream().filter(txOutput -> txOutput.getScript().getToAddress() != null).collect(Collectors.toList());
|
List<TransactionOutput> addressOutputs = walletTx.getOutputs().stream().filter(output -> !(output instanceof WalletTransaction.NonAddressOutput))
|
||||||
TransactionOutput output = addressOutputs.stream().filter(txOutput -> address.equals(txOutput.getScript().getToAddress()) && txOutput.getValue() == amount && !seenIndexes.contains(txOutput.getIndex())).findFirst().orElseThrow();
|
.map(WalletTransaction.Output::getTransactionOutput).collect(Collectors.toList());
|
||||||
|
TransactionOutput output = addressOutputs.stream()
|
||||||
|
.filter(txOutput -> address.equals(txOutput.getScript().getToAddress()) && txOutput.getValue() == amount && !seenIndexes.contains(txOutput.getIndex()))
|
||||||
|
.findFirst().orElseThrow();
|
||||||
return addressOutputs.indexOf(output);
|
return addressOutputs.indexOf(output);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1111,7 +1156,7 @@ public class TransactionDiagram extends GridPane {
|
||||||
}
|
}
|
||||||
|
|
||||||
public String toString() {
|
public String toString() {
|
||||||
return additionalPayments.stream().map(payment -> payment.getAddress().toString()).collect(Collectors.joining("\n"));
|
return additionalPayments.stream().map(Payment::toString).collect(Collectors.joining("\n"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1120,25 +1165,27 @@ public class TransactionDiagram extends GridPane {
|
||||||
public Address address;
|
public 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);
|
this(outputLabel, address, amount, null, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
public OutputNode(Pane outputLabel, Address address, long amount, PaymentCode paymentCode) {
|
public OutputNode(Pane outputLabel, Address address, long amount, PaymentCode paymentCode, SilentPaymentAddress silentPaymentAddress) {
|
||||||
this.outputLabel = outputLabel;
|
this.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);
|
this(address, value, null, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
public LabelContextMenu(Address address, long value, PaymentCode paymentCode) {
|
public LabelContextMenu(Address address, long value, PaymentCode paymentCode, SilentPaymentAddress silentPaymentAddress) {
|
||||||
if(address != null) {
|
if(address != null) {
|
||||||
MenuItem copyAddress = new MenuItem("Copy Address");
|
MenuItem copyAddress = new MenuItem("Copy Address");
|
||||||
copyAddress.setOnAction(event -> {
|
copyAddress.setOnAction(event -> {
|
||||||
|
|
@ -1186,6 +1233,17 @@ 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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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.getPayments().stream().anyMatch(walletTx::isConsolidationSend)) {
|
&& walletTx.getWallet().getStandardAccountType() == StandardAccount.WHIRLPOOL_POSTMIX && !walletTx.getWalletNodePayments().isEmpty()) {
|
||||||
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.getPayments().stream().filter(payment -> payment.getType() == Payment.Type.DEFAULT && !walletTx.isConsolidationSend(payment)).collect(Collectors.toList());
|
List<Payment> payments = walletTx.getExternalPayments().stream().filter(payment -> payment.getType() == Payment.Type.DEFAULT).collect(Collectors.toList());
|
||||||
List<OutputLabel> paymentLabels = payments.stream().map(payment -> getOutputLabel(transactionDiagram, payment)).collect(Collectors.toList());
|
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.getPayments().stream().filter(payment -> payment.getType() == Payment.Type.DEFAULT && walletTx.isConsolidationSend(payment)).collect(Collectors.toList());
|
List<Payment> consolidations = walletTx.getWalletNodePayments().stream().filter(payment -> payment.getType() == Payment.Type.DEFAULT).collect(Collectors.toList());
|
||||||
outputLabels.addAll(consolidations.stream().map(consolidation -> getOutputLabel(transactionDiagram, consolidation)).collect(Collectors.toList()));
|
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 = walletTx.getWallet() != null && !walletTx.getWallet().isBip47() ? walletTx.getAddressNodeMap().get(payment.getAddress()) : null;
|
WalletNode toNode = payment instanceof WalletNodePayment walletNodePayment ? walletNodePayment.getWalletNode() : null;
|
||||||
|
|
||||||
Glyph glyph = GlyphUtils.getOutputGlyph(transactionDiagram.getWalletTransaction(), payment);
|
Glyph glyph = GlyphUtils.getOutputGlyph(transactionDiagram.getWalletTransaction(), payment);
|
||||||
String text = (toWallet == null ? (toNode != null ? "Consolidate " : "Pay ") : "Receive ") + transactionDiagram.getSatsValue(payment.getAmount()) + " sats to " + payment.getAddress().toString();
|
String text = (toWallet == null ? (toNode != null ? "Consolidate " : "Pay ") : "Receive ") + transactionDiagram.getSatsValue(payment.getAmount()) + " sats to " + payment;
|
||||||
|
|
||||||
return getOutputLabel(glyph, text);
|
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("Roboto Mono Italic", 13));
|
label.setFont(Font.font("Fragment Mono Italic", 13));
|
||||||
label.setText(text);
|
label.setText(text);
|
||||||
|
|
||||||
HBox output = new HBox(5);
|
HBox output = new HBox(5);
|
||||||
|
|
|
||||||
|
|
@ -6,8 +6,9 @@ public enum WebcamPixelFormat {
|
||||||
//Only V4L2 formats defined in linux/videodev2.h are required here, declared in order of priority for supported formats
|
//Only V4L2 formats defined in linux/videodev2.h are required here, declared in order of priority for supported formats
|
||||||
PIX_FMT_RGB24("RGB3", true),
|
PIX_FMT_RGB24("RGB3", true),
|
||||||
PIX_FMT_YUYV("YUYV", true),
|
PIX_FMT_YUYV("YUYV", true),
|
||||||
PIX_FMT_MJPG("MJPG", true),
|
PIX_FMT_NV12("NV12", true),
|
||||||
PIX_FMT_NV12("NV12", false);
|
PIX_FMT_YU12("YU12", true),
|
||||||
|
PIX_FMT_MJPG("MJPG", true);
|
||||||
|
|
||||||
private final String name;
|
private final String name;
|
||||||
private final boolean supported;
|
private final boolean supported;
|
||||||
|
|
@ -25,6 +26,14 @@ public enum WebcamPixelFormat {
|
||||||
return supported;
|
return supported;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public int getFourCC() {
|
||||||
|
char a = name.charAt(0);
|
||||||
|
char b = name.charAt(1);
|
||||||
|
char c = name.charAt(2);
|
||||||
|
char d = name.charAt(3);
|
||||||
|
return ((int) a) | ((int) b << 8) | ((int) c << 16) | ((int) d << 24);
|
||||||
|
}
|
||||||
|
|
||||||
public String toString() {
|
public String toString() {
|
||||||
return name;
|
return name;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ 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;
|
||||||
|
|
@ -15,7 +16,6 @@ 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;
|
||||||
|
|
@ -27,6 +27,9 @@ import java.awt.image.BufferedImage;
|
||||||
import java.awt.image.WritableRaster;
|
import java.awt.image.WritableRaster;
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.concurrent.Semaphore;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
import java.util.function.Function;
|
import java.util.function.Function;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
import java.util.stream.Stream;
|
import java.util.stream.Stream;
|
||||||
|
|
@ -34,13 +37,18 @@ import java.util.stream.Stream;
|
||||||
public class WebcamService extends ScheduledService<Image> {
|
public class WebcamService extends ScheduledService<Image> {
|
||||||
private static final Logger log = LoggerFactory.getLogger(WebcamService.class);
|
private static final Logger log = LoggerFactory.getLogger(WebcamService.class);
|
||||||
|
|
||||||
|
private final Semaphore taskSemaphore = new Semaphore(1);
|
||||||
|
private final AtomicBoolean cancelRequested = new AtomicBoolean(false);
|
||||||
|
private final AtomicBoolean captureClosed = new AtomicBoolean(false);
|
||||||
|
|
||||||
private List<CaptureDevice> devices;
|
private List<CaptureDevice> devices;
|
||||||
|
private List<CaptureDevice> availableDevices;
|
||||||
private Set<WebcamResolution> resolutions;
|
private Set<WebcamResolution> resolutions;
|
||||||
|
|
||||||
private WebcamResolution resolution;
|
private WebcamResolution resolution;
|
||||||
private CaptureDevice device;
|
private CaptureDevice device;
|
||||||
private final BooleanProperty opening = new SimpleBooleanProperty(false);
|
private final BooleanProperty opening = new SimpleBooleanProperty(false);
|
||||||
private final BooleanProperty closed = new SimpleBooleanProperty(false);
|
private final BooleanProperty opened = new SimpleBooleanProperty(false);
|
||||||
|
|
||||||
private final ObjectProperty<Result> resultProperty = new SimpleObjectProperty<>(null);
|
private final ObjectProperty<Result> resultProperty = new SimpleObjectProperty<>(null);
|
||||||
|
|
||||||
|
|
@ -105,26 +113,44 @@ public class WebcamService extends ScheduledService<Image> {
|
||||||
return new Task<>() {
|
return new Task<>() {
|
||||||
@Override
|
@Override
|
||||||
protected Image call() throws Exception {
|
protected Image call() throws Exception {
|
||||||
|
if(cancelRequested.get() || isCancelled() || captureClosed.get()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(!taskSemaphore.tryAcquire()) {
|
||||||
|
log.warn("Skipped execution of webcam capture task, another task is running");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if(stream == null) {
|
if(devices == null) {
|
||||||
devices = capture.getDevices();
|
devices = capture.getDevices();
|
||||||
|
availableDevices = new ArrayList<>(devices);
|
||||||
|
|
||||||
if(devices.isEmpty()) {
|
if(devices.isEmpty()) {
|
||||||
throw new UnsupportedOperationException("No cameras available");
|
throw new UnsupportedOperationException("No cameras available");
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
CaptureDevice selectedDevice = devices.stream().filter(d -> !d.getFormats().isEmpty()).findFirst().orElse(devices.getFirst());
|
while(stream == null && !availableDevices.isEmpty()) {
|
||||||
|
CaptureDevice selectedDevice = availableDevices.stream().filter(d -> !d.getFormats().isEmpty()).findFirst().orElse(availableDevices.getFirst());
|
||||||
|
|
||||||
if(device != null) {
|
if(device != null) {
|
||||||
for(CaptureDevice webcam : devices) {
|
for(CaptureDevice webcam : availableDevices) {
|
||||||
if(webcam.getName().equals(device.getName())) {
|
if(webcam.equals(device)) {
|
||||||
selectedDevice = webcam;
|
selectedDevice = webcam;
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if(Config.get().getWebcamDevice() != null) {
|
} else if(Config.get().getWebcamDevice() != null) {
|
||||||
for(CaptureDevice webcam : devices) {
|
for(CaptureDevice webcam : availableDevices) {
|
||||||
|
if(webcam.getUniqueId().equals(Config.get().getWebcamDeviceId())) {
|
||||||
|
selectedDevice = webcam;
|
||||||
|
break;
|
||||||
|
}
|
||||||
if(webcam.getName().equals(Config.get().getWebcamDevice())) {
|
if(webcam.getName().equals(Config.get().getWebcamDevice())) {
|
||||||
selectedDevice = webcam;
|
selectedDevice = webcam;
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -163,22 +189,35 @@ public class WebcamService extends ScheduledService<Image> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//On Linux, formats not defined in WebcamPixelFormat are unsupported
|
||||||
|
if(OsType.getCurrent() == OsType.UNIX && WebcamPixelFormat.fromFourCC(format.getFormatInfo().fourcc) == null) {
|
||||||
|
log.warn("Unsupported camera pixel format " + WebcamPixelFormat.fourCCToString(format.getFormatInfo().fourcc));
|
||||||
|
}
|
||||||
|
|
||||||
if(log.isDebugEnabled()) {
|
if(log.isDebugEnabled()) {
|
||||||
log.debug("Opening capture stream on " + device + " with format " + format.getFormatInfo().width + "x" + format.getFormatInfo().height + " (" + WebcamPixelFormat.fourCCToString(format.getFormatInfo().fourcc) + ")");
|
log.debug("Opening capture stream on " + device + " with format " + format.getFormatInfo().width + "x" + format.getFormatInfo().height + " (" + WebcamPixelFormat.fourCCToString(format.getFormatInfo().fourcc) + ")");
|
||||||
}
|
}
|
||||||
|
|
||||||
opening.set(true);
|
opening.set(true);
|
||||||
stream = device.openStream(format);
|
stream = device.openStream(format);
|
||||||
opening.set(false);
|
opening.set(false);
|
||||||
closed.set(false);
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
zoomLimits = stream.getPropertyLimits(CaptureProperty.Zoom);
|
zoomLimits = stream.getPropertyLimits(CaptureProperty.Zoom);
|
||||||
} catch(Throwable e) {
|
} catch(Throwable e) {
|
||||||
log.debug("Error getting zoom limits on " + device + ", assuming no zoom function");
|
log.debug("Error getting zoom limits on " + device + ", assuming no zoom function");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if(stream == null) {
|
||||||
|
availableDevices.remove(device);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if(stream == null) {
|
||||||
|
throw new UnsupportedOperationException("No usable cameras available, tried " + devices);
|
||||||
|
}
|
||||||
|
|
||||||
|
opened.set(true);
|
||||||
BufferedImage originalImage = stream.capture();
|
BufferedImage originalImage = stream.capture();
|
||||||
CroppedDimension cropped = getCroppedDimension(originalImage);
|
CroppedDimension cropped = getCroppedDimension(originalImage);
|
||||||
BufferedImage croppedImage = originalImage.getSubimage(cropped.x, cropped.y, cropped.length, cropped.length);
|
BufferedImage croppedImage = originalImage.getSubimage(cropped.x, cropped.y, cropped.length, cropped.length);
|
||||||
|
|
@ -195,6 +234,7 @@ public class WebcamService extends ScheduledService<Image> {
|
||||||
return image;
|
return image;
|
||||||
} finally {
|
} finally {
|
||||||
opening.set(false);
|
opening.set(false);
|
||||||
|
taskSemaphore.release();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
@ -204,21 +244,38 @@ public class WebcamService extends ScheduledService<Image> {
|
||||||
public void reset() {
|
public void reset() {
|
||||||
stream = null;
|
stream = null;
|
||||||
zoomLimits = null;
|
zoomLimits = null;
|
||||||
|
cancelRequested.set(false);
|
||||||
super.reset();
|
super.reset();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean cancel() {
|
public boolean cancel() {
|
||||||
if(stream != null) {
|
cancelRequested.set(true);
|
||||||
stream.close();
|
boolean cancelled = super.cancel();
|
||||||
closed.set(true);
|
|
||||||
|
try {
|
||||||
|
if(taskSemaphore.tryAcquire(1, TimeUnit.SECONDS)) {
|
||||||
|
taskSemaphore.release();
|
||||||
|
} else {
|
||||||
|
log.error("Timed out waiting for task semaphore to be available to cancel, cancelling anyway");
|
||||||
|
}
|
||||||
|
} catch(InterruptedException e) {
|
||||||
|
log.error("Interrupted while waiting for task semaphore to be available to cancel, cancelling anyway");
|
||||||
}
|
}
|
||||||
|
|
||||||
return super.cancel();
|
if(stream != null) {
|
||||||
|
stream.close();
|
||||||
|
opened.set(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
return cancelled;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void close() {
|
public synchronized void close() {
|
||||||
capture.close();
|
if(!captureClosed.get()) {
|
||||||
|
captureClosed.set(true);
|
||||||
|
capture.close();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public PropertyLimits getZoomLimits() {
|
public PropertyLimits getZoomLimits() {
|
||||||
|
|
@ -262,9 +319,6 @@ 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) {
|
||||||
|
|
@ -282,6 +336,8 @@ 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
|
||||||
|
|
@ -336,6 +392,10 @@ public class WebcamService extends ScheduledService<Image> {
|
||||||
return devices;
|
return devices;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public List<CaptureDevice> getAvailableDevices() {
|
||||||
|
return availableDevices;
|
||||||
|
}
|
||||||
|
|
||||||
public Set<WebcamResolution> getResolutions() {
|
public Set<WebcamResolution> getResolutions() {
|
||||||
return resolutions;
|
return resolutions;
|
||||||
}
|
}
|
||||||
|
|
@ -376,8 +436,12 @@ public class WebcamService extends ScheduledService<Image> {
|
||||||
return opening;
|
return opening;
|
||||||
}
|
}
|
||||||
|
|
||||||
public BooleanProperty closedProperty() {
|
public BooleanProperty openedProperty() {
|
||||||
return closed;
|
return opened;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean getCancelRequested() {
|
||||||
|
return cancelRequested.get();
|
||||||
}
|
}
|
||||||
|
|
||||||
public static <T extends Enum<T>> T getNearestEnum(T target) {
|
public static <T extends Enum<T>> T getNearestEnum(T target) {
|
||||||
|
|
@ -385,10 +449,27 @@ public class WebcamService extends ScheduledService<Image> {
|
||||||
}
|
}
|
||||||
|
|
||||||
public static <T extends Enum<T>> T getNearestEnum(T target, T[] values) {
|
public static <T extends Enum<T>> T getNearestEnum(T target, T[] values) {
|
||||||
int ordinal = target.ordinal();
|
if(values == null || values.length == 0) {
|
||||||
return Stream.concat(ordinal > 0 ? Stream.of(values[ordinal - 1]) : Stream.empty(), ordinal < values.length - 1 ? Stream.of(values[ordinal + 1]) : Stream.empty())
|
return null;
|
||||||
.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 {
|
||||||
|
|
|
||||||
|
|
@ -61,7 +61,7 @@ public class WebcamView {
|
||||||
});
|
});
|
||||||
|
|
||||||
service.valueProperty().addListener((observable, oldValue, newValue) -> {
|
service.valueProperty().addListener((observable, oldValue, newValue) -> {
|
||||||
if(newValue != null) {
|
if(newValue != null && !service.getCancelRequested()) {
|
||||||
imageProperty.set(newValue);
|
imageProperty.set(newValue);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -6,12 +6,18 @@ import java.util.Map;
|
||||||
|
|
||||||
public class BlockSummaryEvent {
|
public class BlockSummaryEvent {
|
||||||
private final Map<Integer, BlockSummary> blockSummaryMap;
|
private final Map<Integer, BlockSummary> blockSummaryMap;
|
||||||
|
private final Double nextBlockMedianFeeRate;
|
||||||
|
|
||||||
public BlockSummaryEvent(Map<Integer, BlockSummary> blockSummaryMap) {
|
public BlockSummaryEvent(Map<Integer, BlockSummary> blockSummaryMap, Double nextBlockMedianFeeRate) {
|
||||||
this.blockSummaryMap = blockSummaryMap;
|
this.blockSummaryMap = blockSummaryMap;
|
||||||
|
this.nextBlockMedianFeeRate = nextBlockMedianFeeRate;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Map<Integer, BlockSummary> getBlockSummaryMap() {
|
public Map<Integer, BlockSummary> getBlockSummaryMap() {
|
||||||
return blockSummaryMap;
|
return blockSummaryMap;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Double getNextBlockMedianFeeRate() {
|
||||||
|
return nextBlockMedianFeeRate;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
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;
|
||||||
|
|
@ -13,6 +14,7 @@ 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);
|
||||||
|
|
@ -21,6 +23,7 @@ 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() {
|
||||||
|
|
@ -42,4 +45,8 @@ public class ConnectionEvent extends FeeRatesUpdatedEvent {
|
||||||
public Double getMinimumRelayFeeRate() {
|
public Double getMinimumRelayFeeRate() {
|
||||||
return minimumRelayFeeRate;
|
return minimumRelayFeeRate;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Double getPreviousMinimumRelayFeeRate() {
|
||||||
|
return previousMinimumRelayFeeRate;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,13 +7,23 @@ import java.util.Set;
|
||||||
|
|
||||||
public class FeeRatesUpdatedEvent extends MempoolRateSizesUpdatedEvent {
|
public class FeeRatesUpdatedEvent extends MempoolRateSizesUpdatedEvent {
|
||||||
private final Map<Integer, Double> targetBlockFeeRates;
|
private final Map<Integer, Double> targetBlockFeeRates;
|
||||||
|
private final Double nextBlockMedianFeeRate;
|
||||||
|
|
||||||
public FeeRatesUpdatedEvent(Map<Integer, Double> targetBlockFeeRates, Set<MempoolRateSize> mempoolRateSizes) {
|
public FeeRatesUpdatedEvent(Map<Integer, Double> targetBlockFeeRates, Set<MempoolRateSize> mempoolRateSizes) {
|
||||||
|
this(targetBlockFeeRates, mempoolRateSizes, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public FeeRatesUpdatedEvent(Map<Integer, Double> targetBlockFeeRates, Set<MempoolRateSize> mempoolRateSizes, Double nextBlockMedianFeeRate) {
|
||||||
super(mempoolRateSizes);
|
super(mempoolRateSizes);
|
||||||
this.targetBlockFeeRates = targetBlockFeeRates;
|
this.targetBlockFeeRates = targetBlockFeeRates;
|
||||||
|
this.nextBlockMedianFeeRate = nextBlockMedianFeeRate;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Map<Integer, Double> getTargetBlockFeeRates() {
|
public Map<Integer, Double> getTargetBlockFeeRates() {
|
||||||
return targetBlockFeeRates;
|
return targetBlockFeeRates;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Double getNextBlockMedianFeeRate() {
|
||||||
|
return nextBlockMedianFeeRate;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,17 @@
|
||||||
|
package com.sparrowwallet.sparrow.event;
|
||||||
|
|
||||||
|
import com.sparrowwallet.drongo.wallet.Payment;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public class RequestSendToManyEvent {
|
||||||
|
private final List<Payment> payments;
|
||||||
|
|
||||||
|
public RequestSendToManyEvent(List<Payment> payments) {
|
||||||
|
this.payments = payments;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<Payment> getPayments() {
|
||||||
|
return payments;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -17,12 +17,13 @@ public class SpendUtxoEvent {
|
||||||
private final boolean requireAllUtxos;
|
private final 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);
|
this(wallet, utxos, null, null, null, false, null, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
public SpendUtxoEvent(Wallet wallet, List<BlockTransactionHashIndex> utxos, List<Payment> payments, List<byte[]> opReturns, Long fee, boolean requireAllUtxos, BlockTransaction replacedTransaction) {
|
public SpendUtxoEvent(Wallet wallet, List<BlockTransactionHashIndex> utxos, List<Payment> payments, List<byte[]> opReturns, Long fee, boolean requireAllUtxos, BlockTransaction replacedTransaction, boolean allowPaymentChanges) {
|
||||||
this.wallet = wallet;
|
this.wallet = wallet;
|
||||||
this.utxos = utxos;
|
this.utxos = utxos;
|
||||||
this.payments = payments;
|
this.payments = payments;
|
||||||
|
|
@ -31,6 +32,7 @@ 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) {
|
||||||
|
|
@ -42,6 +44,7 @@ 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() {
|
||||||
|
|
@ -75,4 +78,8 @@ public class SpendUtxoEvent {
|
||||||
public PaymentCode getPaymentCode() {
|
public PaymentCode getPaymentCode() {
|
||||||
return paymentCode;
|
return paymentCode;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public boolean allowPaymentChanges() {
|
||||||
|
return allowPaymentChanges;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,9 @@
|
||||||
|
package com.sparrowwallet.sparrow.event;
|
||||||
|
|
||||||
|
import com.sparrowwallet.drongo.protocol.Transaction;
|
||||||
|
|
||||||
|
public class TransactionOutputsChangedEvent extends TransactionChangedEvent {
|
||||||
|
public TransactionOutputsChangedEvent(Transaction transaction) {
|
||||||
|
super(transaction);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -14,9 +14,16 @@ import java.util.List;
|
||||||
*/
|
*/
|
||||||
public class WalletNodeHistoryChangedEvent {
|
public class WalletNodeHistoryChangedEvent {
|
||||||
private final String scriptHash;
|
private final String scriptHash;
|
||||||
|
private final String status;
|
||||||
|
|
||||||
public WalletNodeHistoryChangedEvent(String scriptHash) {
|
public WalletNodeHistoryChangedEvent(String scriptHash) {
|
||||||
this.scriptHash = scriptHash;
|
this.scriptHash = scriptHash;
|
||||||
|
this.status = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public WalletNodeHistoryChangedEvent(String scriptHash, String status) {
|
||||||
|
this.scriptHash = scriptHash;
|
||||||
|
this.status = status;
|
||||||
}
|
}
|
||||||
|
|
||||||
public WalletNode getWalletNode(Wallet wallet) {
|
public WalletNode getWalletNode(Wallet wallet) {
|
||||||
|
|
@ -70,4 +77,8 @@ public class WalletNodeHistoryChangedEvent {
|
||||||
public String getScriptHash() {
|
public String getScriptHash() {
|
||||||
return scriptHash;
|
return scriptHash;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public String getStatus() {
|
||||||
|
return status;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
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;
|
||||||
|
|
@ -15,7 +16,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(walletTx.isConsolidationSend(payment)) {
|
} else if(payment instanceof WalletNodePayment) {
|
||||||
return getConsolidationGlyph();
|
return getConsolidationGlyph();
|
||||||
} else if(walletTx.isPremixSend(payment)) {
|
} else if(walletTx.isPremixSend(payment)) {
|
||||||
return getPremixGlyph();
|
return getPremixGlyph();
|
||||||
|
|
@ -213,6 +214,13 @@ 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");
|
||||||
|
|
|
||||||
|
|
@ -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) +
|
OutputDescriptor.getOutputDescriptor(wallet, KeyPurpose.DEFAULT_PURPOSES, null) +
|
||||||
"\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));
|
||||||
|
|
|
||||||
|
|
@ -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 "Blue Wallet Vault Multisig";
|
return "BlueWallet 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", "Blue Wallet"));
|
keystore.setLabel(keystore.getLabel().replace("Coldcard", "BlueWallet"));
|
||||||
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 Blue Wallet Vault wallet.";
|
return "Import file or QR created by using the Wallet > Export Coordination Setup feature on your BlueWallet Vault wallet.";
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String getWalletExportDescription() {
|
public String getWalletExportDescription() {
|
||||||
return "Export file that can be read by Blue Wallet using the Add Wallet > Vault > Import wallet feature.";
|
return "Export file that can be read by BlueWallet using the Add Wallet > Vault > Import wallet feature.";
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ 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;
|
||||||
|
|
@ -52,6 +53,9 @@ public class Config {
|
||||||
private boolean showDeprecatedImportExport = false;
|
private boolean showDeprecatedImportExport = false;
|
||||||
private boolean signBsmsExports = false;
|
private boolean signBsmsExports = false;
|
||||||
private boolean preventSleep = false;
|
private boolean preventSleep = false;
|
||||||
|
private Boolean connectToBroadcast;
|
||||||
|
private Boolean connectToResolve;
|
||||||
|
private Boolean suggestSendToMany;
|
||||||
private List<File> recentWalletFiles;
|
private List<File> recentWalletFiles;
|
||||||
private Integer keyDerivationPeriod;
|
private Integer keyDerivationPeriod;
|
||||||
private long dustAttackThreshold = DUST_ATTACK_THRESHOLD_SATS;
|
private long dustAttackThreshold = DUST_ATTACK_THRESHOLD_SATS;
|
||||||
|
|
@ -61,6 +65,7 @@ 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;
|
||||||
|
|
@ -69,6 +74,7 @@ public class Config {
|
||||||
private File coreDataDir;
|
private File coreDataDir;
|
||||||
private String coreAuth;
|
private String coreAuth;
|
||||||
private boolean useLegacyCoreWallet;
|
private boolean useLegacyCoreWallet;
|
||||||
|
private boolean legacyServer;
|
||||||
private Server electrumServer;
|
private Server electrumServer;
|
||||||
private List<Server> recentElectrumServers;
|
private List<Server> recentElectrumServers;
|
||||||
private File electrumServerCert;
|
private File electrumServerCert;
|
||||||
|
|
@ -79,6 +85,7 @@ 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;
|
||||||
|
|
||||||
|
|
@ -347,6 +354,34 @@ public class Config {
|
||||||
|
|
||||||
public void setPreventSleep(boolean preventSleep) {
|
public void setPreventSleep(boolean preventSleep) {
|
||||||
this.preventSleep = preventSleep;
|
this.preventSleep = preventSleep;
|
||||||
|
flush();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Boolean getConnectToBroadcast() {
|
||||||
|
return connectToBroadcast;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setConnectToBroadcast(Boolean connectToBroadcast) {
|
||||||
|
this.connectToBroadcast = connectToBroadcast;
|
||||||
|
flush();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Boolean getConnectToResolve() {
|
||||||
|
return connectToResolve;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setConnectToResolve(Boolean connectToResolve) {
|
||||||
|
this.connectToResolve = connectToResolve;
|
||||||
|
flush();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Boolean getSuggestSendToMany() {
|
||||||
|
return suggestSendToMany;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setSuggestSendToMany(Boolean suggestSendToMany) {
|
||||||
|
this.suggestSendToMany = suggestSendToMany;
|
||||||
|
flush();
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<File> getRecentWalletFiles() {
|
public List<File> getRecentWalletFiles() {
|
||||||
|
|
@ -415,6 +450,15 @@ 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;
|
||||||
}
|
}
|
||||||
|
|
@ -549,6 +593,15 @@ public class Config {
|
||||||
flush();
|
flush();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public boolean isLegacyServer() {
|
||||||
|
return legacyServer;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setLegacyServer(boolean legacyServer) {
|
||||||
|
this.legacyServer = legacyServer;
|
||||||
|
flush();
|
||||||
|
}
|
||||||
|
|
||||||
public Server getElectrumServer() {
|
public Server getElectrumServer() {
|
||||||
return electrumServer;
|
return electrumServer;
|
||||||
}
|
}
|
||||||
|
|
@ -667,6 +720,14 @@ 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;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -126,7 +126,7 @@ public class Descriptor implements WalletImport, WalletExport {
|
||||||
} else if(line.startsWith("#")) {
|
} else if(line.startsWith("#")) {
|
||||||
continue;
|
continue;
|
||||||
} else {
|
} else {
|
||||||
paragraph.append(line);
|
paragraph.append(line.replaceFirst("^.+:", "").trim());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -37,7 +37,8 @@ public class ElectrumPersonalServer implements WalletExport {
|
||||||
try {
|
try {
|
||||||
BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(outputStream, StandardCharsets.UTF_8));
|
BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(outputStream, StandardCharsets.UTF_8));
|
||||||
writer.write("# Electrum Personal Server configuration file fragments\n");
|
writer.write("# Electrum Personal Server configuration file fragments\n");
|
||||||
writer.write("# Copy the lines below into the relevant sections in your EPS config.ini file\n\n");
|
writer.write("# First close Sparrow and edit your config file in Sparrow home to set \"legacyServer\": true\n");
|
||||||
|
writer.write("# Then copy the lines below into the relevant sections in your EPS config.ini file\n\n");
|
||||||
writer.write("# Copy into [master-public-keys] section\n");
|
writer.write("# Copy into [master-public-keys] section\n");
|
||||||
Wallet masterWallet = wallet.isMasterWallet() ? wallet : wallet.getMasterWallet();
|
Wallet masterWallet = wallet.isMasterWallet() ? wallet : wallet.getMasterWallet();
|
||||||
writeWalletXpub(masterWallet, writer);
|
writeWalletXpub(masterWallet, writer);
|
||||||
|
|
|
||||||
|
|
@ -684,7 +684,7 @@ public class Storage {
|
||||||
|
|
||||||
public static Executor getSingleThreadedExecutor() {
|
public static Executor getSingleThreadedExecutor() {
|
||||||
if(singleThreadedExecutor == null) {
|
if(singleThreadedExecutor == null) {
|
||||||
BasicThreadFactory factory = new BasicThreadFactory.Builder().namingPattern("LoadWalletService-single").daemon(true).priority(Thread.MIN_PRIORITY).build();
|
BasicThreadFactory factory = BasicThreadFactory.builder().namingPattern("LoadWalletService-single").daemon(true).priority(Thread.MIN_PRIORITY).build();
|
||||||
singleThreadedExecutor = Executors.newSingleThreadScheduledExecutor(factory);
|
singleThreadedExecutor = Executors.newSingleThreadScheduledExecutor(factory);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,29 +1,21 @@
|
||||||
package net.sourceforge.zbar;
|
package com.sparrowwallet.sparrow.io;
|
||||||
|
|
||||||
import com.sparrowwallet.sparrow.net.NativeUtils;
|
import io.github.doblon8.jzbar.Config;
|
||||||
|
import io.github.doblon8.jzbar.Image;
|
||||||
|
import io.github.doblon8.jzbar.ImageScanner;
|
||||||
|
import io.github.doblon8.jzbar.SymbolType;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.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 enabled;
|
return com.sparrowwallet.sparrow.io.Config.get().isUseZbar();
|
||||||
}
|
}
|
||||||
|
|
||||||
public static Scan scan(BufferedImage bufferedImage) {
|
public static Scan scan(BufferedImage bufferedImage) {
|
||||||
|
|
@ -41,19 +33,12 @@ public class ZBar {
|
||||||
image.setData(data);
|
image.setData(data);
|
||||||
|
|
||||||
try(ImageScanner scanner = new ImageScanner()) {
|
try(ImageScanner scanner = new ImageScanner()) {
|
||||||
scanner.setConfig(Symbol.NONE, Config.ENABLE, 0);
|
scanner.setConfig(SymbolType.NONE, Config.ENABLE, 0);
|
||||||
scanner.setConfig(Symbol.QRCODE, Config.ENABLE, 1);
|
scanner.setConfig(SymbolType.QRCODE, Config.ENABLE, 1);
|
||||||
int result = scanner.scanImage(image);
|
int result = scanner.scanImage(image);
|
||||||
if(result != 0) {
|
if(result != 0) {
|
||||||
try(SymbolSet results = scanner.getResults()) {
|
String symbolData = image.getFirstSymbol().getData();
|
||||||
Scan scan = null;
|
return new Scan(getRawBytes(symbolData), symbolData);
|
||||||
for(Iterator<Symbol> iter = results.iterator(); iter.hasNext(); ) {
|
|
||||||
try(Symbol symbol = iter.next()) {
|
|
||||||
scan = new Scan(getRawBytes(symbol.getData()), symbol.getData());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return scan;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -97,31 +82,6 @@ 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];
|
||||||
|
|
@ -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 = new BasicThreadFactory.Builder().namingPattern(masterWallet.getFullName() + "-dbupdater").daemon(true).priority(Thread.NORM_PRIORITY).build();
|
BasicThreadFactory factory = BasicThreadFactory.builder().namingPattern(masterWallet.getFullName() + "-dbupdater").daemon(true).priority(Thread.NORM_PRIORITY).build();
|
||||||
updateExecutor = Executors.newSingleThreadExecutor(factory);
|
updateExecutor = Executors.newSingleThreadExecutor(factory);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,22 @@
|
||||||
package com.sparrowwallet.sparrow.net;
|
package com.sparrowwallet.sparrow.net;
|
||||||
|
|
||||||
|
import com.sparrowwallet.sparrow.AppServices;
|
||||||
import com.sparrowwallet.sparrow.io.Server;
|
import com.sparrowwallet.sparrow.io.Server;
|
||||||
|
import org.girod.javafx.svgimage.SVGImage;
|
||||||
|
import org.girod.javafx.svgimage.SVGLoader;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
import java.net.URL;
|
||||||
|
import java.util.Locale;
|
||||||
|
|
||||||
public enum BlockExplorer {
|
public enum BlockExplorer {
|
||||||
MEMPOOL_SPACE("https://mempool.space"),
|
MEMPOOL_SPACE("https://mempool.space"),
|
||||||
BLOCKSTREAM_INFO("https://blockstream.info"),
|
BLOCKSTREAM_INFO("https://blockstream.info"),
|
||||||
NONE("http://none");
|
NONE("http://none");
|
||||||
|
|
||||||
|
private static final Logger log = LoggerFactory.getLogger(BlockExplorer.class);
|
||||||
|
|
||||||
private final Server server;
|
private final Server server;
|
||||||
|
|
||||||
BlockExplorer(String url) {
|
BlockExplorer(String url) {
|
||||||
|
|
@ -16,4 +26,17 @@ public enum BlockExplorer {
|
||||||
public Server getServer() {
|
public Server getServer() {
|
||||||
return server;
|
return server;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static SVGImage getSVGImage(Server server) {
|
||||||
|
try {
|
||||||
|
URL url = AppServices.class.getResource("/image/blockexplorer/" + server.getHost().toLowerCase(Locale.ROOT) + "-icon.svg");
|
||||||
|
if(url != null) {
|
||||||
|
return SVGLoader.load(url);
|
||||||
|
}
|
||||||
|
} catch(Exception e) {
|
||||||
|
log.error("Could not load block explorer image for " + server.getHost());
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -76,6 +76,10 @@ public class ElectrumServer {
|
||||||
|
|
||||||
private static final Set<String> sameHeightTxioScriptHashes = ConcurrentHashMap.newKeySet();
|
private static final Set<String> sameHeightTxioScriptHashes = ConcurrentHashMap.newKeySet();
|
||||||
|
|
||||||
|
private final static Map<String, Integer> subscribedRecent = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
|
private final static Map<String, String> broadcastRecent = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
private static ElectrumServerRpc electrumServerRpc = new SimpleElectrumServerRpc();
|
private static ElectrumServerRpc electrumServerRpc = new SimpleElectrumServerRpc();
|
||||||
|
|
||||||
private static Cormorant cormorant;
|
private static Cormorant cormorant;
|
||||||
|
|
@ -261,7 +265,7 @@ public class ElectrumServer {
|
||||||
return 0;
|
return 0;
|
||||||
});
|
});
|
||||||
|
|
||||||
return txos.stream().map(txo -> new ScriptHashTx(txo.getHeight(), txo.getHashAsString(), txo.getFee())).toList();
|
return txos.stream().map(txo -> new ScriptHashTx(txo.getHeight(), txo.getHashAsString(), txo.getFee() == null ? 0 : txo.getFee())).toList();
|
||||||
}
|
}
|
||||||
|
|
||||||
private static String getScriptHashStatus(List<ScriptHashTx> scriptHashTxes) {
|
private static String getScriptHashStatus(List<ScriptHashTx> scriptHashTxes) {
|
||||||
|
|
@ -439,7 +443,7 @@ public class ElectrumServer {
|
||||||
blkTx.getTransaction().getInputs().stream().map(txInput -> getPrevOutput(wallet, txInput))
|
blkTx.getTransaction().getInputs().stream().map(txInput -> getPrevOutput(wallet, txInput))
|
||||||
.filter(Objects::nonNull).map(ElectrumServer::getScriptHash).anyMatch(scriptHash::equals)) {
|
.filter(Objects::nonNull).map(ElectrumServer::getScriptHash).anyMatch(scriptHash::equals)) {
|
||||||
List<ScriptHashTx> scriptHashTxes = new ArrayList<>(getScriptHashes(scriptHash, node));
|
List<ScriptHashTx> scriptHashTxes = new ArrayList<>(getScriptHashes(scriptHash, node));
|
||||||
scriptHashTxes.add(new ScriptHashTx(0, txid.toString(), blkTx.getFee()));
|
scriptHashTxes.add(new ScriptHashTx(0, txid.toString(), blkTx.getFee() == null ? 0 : blkTx.getFee()));
|
||||||
|
|
||||||
String status = getScriptHashStatus(scriptHashTxes);
|
String status = getScriptHashStatus(scriptHashTxes);
|
||||||
if(Objects.equals(status, statuses.getLast())) {
|
if(Objects.equals(status, statuses.getLast())) {
|
||||||
|
|
@ -936,6 +940,20 @@ public class ElectrumServer {
|
||||||
return targetBlocksFeeRatesSats;
|
return targetBlocksFeeRatesSats;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Double getNextBlockMedianFeeRate() {
|
||||||
|
FeeRatesSource feeRatesSource = Config.get().getFeeRatesSource();
|
||||||
|
feeRatesSource = (feeRatesSource == null ? FeeRatesSource.MEMPOOL_SPACE : feeRatesSource);
|
||||||
|
if(feeRatesSource.supportsNetwork(Network.get())) {
|
||||||
|
try {
|
||||||
|
return feeRatesSource.getNextBlockMedianFeeRate();
|
||||||
|
} catch(Exception e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
public Map<Integer, Double> getDefaultFeeEstimates(List<Integer> targetBlocks) throws ServerException {
|
public Map<Integer, Double> getDefaultFeeEstimates(List<Integer> targetBlocks) throws ServerException {
|
||||||
try {
|
try {
|
||||||
Map<Integer, Double> targetBlocksFeeRatesBtcKb = electrumServerRpc.getFeeEstimates(getTransport(), targetBlocks);
|
Map<Integer, Double> targetBlocksFeeRatesBtcKb = electrumServerRpc.getFeeEstimates(getTransport(), targetBlocks);
|
||||||
|
|
@ -1048,6 +1066,11 @@ public class ElectrumServer {
|
||||||
List<BlockTransactionHash> recentTransactions = feeRatesSource.getRecentMempoolTransactions();
|
List<BlockTransactionHash> recentTransactions = feeRatesSource.getRecentMempoolTransactions();
|
||||||
Map<BlockTransactionHash, Transaction> setReferences = new HashMap<>();
|
Map<BlockTransactionHash, Transaction> setReferences = new HashMap<>();
|
||||||
setReferences.put(recentTransactions.getFirst(), null);
|
setReferences.put(recentTransactions.getFirst(), null);
|
||||||
|
if(recentTransactions.size() > 1) {
|
||||||
|
Random random = new Random();
|
||||||
|
int halfSize = recentTransactions.size() / 2;
|
||||||
|
setReferences.put(recentTransactions.get(halfSize == 1 ? 1 : random.nextInt(halfSize) + 1), null);
|
||||||
|
}
|
||||||
Map<Sha256Hash, BlockTransaction> transactions = getTransactions(null, setReferences, Collections.emptyMap());
|
Map<Sha256Hash, BlockTransaction> transactions = getTransactions(null, setReferences, Collections.emptyMap());
|
||||||
return transactions.values().stream().filter(blxTx -> blxTx.getTransaction() != null).toList();
|
return transactions.values().stream().filter(blxTx -> blxTx.getTransaction() != null).toList();
|
||||||
} catch(Exception e) {
|
} catch(Exception e) {
|
||||||
|
|
@ -1232,11 +1255,11 @@ public class ElectrumServer {
|
||||||
if(!serverVersion.isEmpty()) {
|
if(!serverVersion.isEmpty()) {
|
||||||
String server = serverVersion.getFirst().toLowerCase(Locale.ROOT);
|
String server = serverVersion.getFirst().toLowerCase(Locale.ROOT);
|
||||||
if(server.contains("electrumx")) {
|
if(server.contains("electrumx")) {
|
||||||
return new ServerCapability(true);
|
return new ServerCapability(true, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
if(server.startsWith("cormorant")) {
|
if(server.startsWith("cormorant")) {
|
||||||
return new ServerCapability(true, false, true);
|
return new ServerCapability(true, false, true, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
if(server.startsWith("electrs/")) {
|
if(server.startsWith("electrs/")) {
|
||||||
|
|
@ -1248,7 +1271,7 @@ public class ElectrumServer {
|
||||||
try {
|
try {
|
||||||
Version version = new Version(electrsVersion);
|
Version version = new Version(electrsVersion);
|
||||||
if(version.compareTo(ELECTRS_MIN_BATCHING_VERSION) >= 0) {
|
if(version.compareTo(ELECTRS_MIN_BATCHING_VERSION) >= 0) {
|
||||||
return new ServerCapability(true);
|
return new ServerCapability(true, true);
|
||||||
}
|
}
|
||||||
} catch(Exception e) {
|
} catch(Exception e) {
|
||||||
//ignore
|
//ignore
|
||||||
|
|
@ -1264,7 +1287,7 @@ public class ElectrumServer {
|
||||||
try {
|
try {
|
||||||
Version version = new Version(fulcrumVersion);
|
Version version = new Version(fulcrumVersion);
|
||||||
if(version.compareTo(FULCRUM_MIN_BATCHING_VERSION) >= 0) {
|
if(version.compareTo(FULCRUM_MIN_BATCHING_VERSION) >= 0) {
|
||||||
return new ServerCapability(true);
|
return new ServerCapability(true, true);
|
||||||
}
|
}
|
||||||
} catch(Exception e) {
|
} catch(Exception e) {
|
||||||
//ignore
|
//ignore
|
||||||
|
|
@ -1283,15 +1306,19 @@ public class ElectrumServer {
|
||||||
Version version = new Version(mempoolElectrsVersion);
|
Version version = new Version(mempoolElectrsVersion);
|
||||||
if(version.compareTo(MEMPOOL_ELECTRS_MIN_BATCHING_VERSION) > 0 ||
|
if(version.compareTo(MEMPOOL_ELECTRS_MIN_BATCHING_VERSION) > 0 ||
|
||||||
(version.compareTo(MEMPOOL_ELECTRS_MIN_BATCHING_VERSION) == 0 && (!mempoolElectrsSuffix.contains("dev") || mempoolElectrsSuffix.contains("dev-249848d")))) {
|
(version.compareTo(MEMPOOL_ELECTRS_MIN_BATCHING_VERSION) == 0 && (!mempoolElectrsSuffix.contains("dev") || mempoolElectrsSuffix.contains("dev-249848d")))) {
|
||||||
return new ServerCapability(true, 25);
|
return new ServerCapability(true, 25, false);
|
||||||
}
|
}
|
||||||
} catch(Exception e) {
|
} catch(Exception e) {
|
||||||
//ignore
|
//ignore
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if(server.startsWith("electrumpersonalserver")) {
|
||||||
|
return new ServerCapability(false, false);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return new ServerCapability(false);
|
return new ServerCapability(false, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static class ServerVersionService extends Service<List<String>> {
|
public static class ServerVersionService extends Service<List<String>> {
|
||||||
|
|
@ -1456,8 +1483,9 @@ public class ElectrumServer {
|
||||||
if(elapsed > FEE_RATES_PERIOD) {
|
if(elapsed > FEE_RATES_PERIOD) {
|
||||||
Map<Integer, Double> blockTargetFeeRates = electrumServer.getFeeEstimates(AppServices.TARGET_BLOCKS_RANGE, false);
|
Map<Integer, Double> blockTargetFeeRates = electrumServer.getFeeEstimates(AppServices.TARGET_BLOCKS_RANGE, false);
|
||||||
Set<MempoolRateSize> mempoolRateSizes = electrumServer.getMempoolRateSizes();
|
Set<MempoolRateSize> mempoolRateSizes = electrumServer.getMempoolRateSizes();
|
||||||
|
Double nextBlockMedianFeeRate = electrumServer.getNextBlockMedianFeeRate();
|
||||||
feeRatesRetrievedAt = System.currentTimeMillis();
|
feeRatesRetrievedAt = System.currentTimeMillis();
|
||||||
return new FeeRatesUpdatedEvent(blockTargetFeeRates, mempoolRateSizes);
|
return new FeeRatesUpdatedEvent(blockTargetFeeRates, mempoolRateSizes, nextBlockMedianFeeRate);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
closeConnection();
|
closeConnection();
|
||||||
|
|
@ -1583,6 +1611,31 @@ public class ElectrumServer {
|
||||||
Set<MempoolRateSize> mempoolRateSizes = electrumServer.getMempoolRateSizes();
|
Set<MempoolRateSize> mempoolRateSizes = electrumServer.getMempoolRateSizes();
|
||||||
EventManager.get().post(new MempoolRateSizesUpdatedEvent(mempoolRateSizes));
|
EventManager.get().post(new MempoolRateSizesUpdatedEvent(mempoolRateSizes));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Subscribe
|
||||||
|
public void walletNodeHistoryChanged(WalletNodeHistoryChangedEvent event) {
|
||||||
|
String status = broadcastRecent.remove(event.getScriptHash());
|
||||||
|
if(status != null && status.equals(event.getStatus())) {
|
||||||
|
Map<String, String> subscribeScriptHashes = new HashMap<>();
|
||||||
|
Random random = new Random();
|
||||||
|
int subscriptions = random.nextInt(2) + 1;
|
||||||
|
for(int i = 0; i < subscriptions; i++) {
|
||||||
|
byte[] randomScriptHashBytes = new byte[32];
|
||||||
|
random.nextBytes(randomScriptHashBytes);
|
||||||
|
String randomScriptHash = Utils.bytesToHex(randomScriptHashBytes);
|
||||||
|
if(!subscribedScriptHashes.containsKey(randomScriptHash)) {
|
||||||
|
subscribeScriptHashes.put("m/" + subscribeScriptHashes.size(), randomScriptHash);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
electrumServerRpc.subscribeScriptHashes(transport, null, subscribeScriptHashes);
|
||||||
|
subscribeScriptHashes.values().forEach(scriptHash -> subscribedRecent.put(scriptHash, AppServices.getCurrentBlockHeight()));
|
||||||
|
} catch(ElectrumServerRpcException e) {
|
||||||
|
log.debug("Error subscribing to recent mempool transaction outputs", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static class ReadRunnable implements Runnable {
|
public static class ReadRunnable implements Runnable {
|
||||||
|
|
@ -1935,7 +1988,8 @@ public class ElectrumServer {
|
||||||
protected FeeRatesUpdatedEvent call() throws ServerException {
|
protected FeeRatesUpdatedEvent call() throws ServerException {
|
||||||
ElectrumServer electrumServer = new ElectrumServer();
|
ElectrumServer electrumServer = new ElectrumServer();
|
||||||
Map<Integer, Double> blockTargetFeeRates = electrumServer.getFeeEstimates(AppServices.TARGET_BLOCKS_RANGE, false);
|
Map<Integer, Double> blockTargetFeeRates = electrumServer.getFeeEstimates(AppServices.TARGET_BLOCKS_RANGE, false);
|
||||||
return new FeeRatesUpdatedEvent(blockTargetFeeRates, null);
|
Double nextBlockMedianFeeRate = electrumServer.getNextBlockMedianFeeRate();
|
||||||
|
return new FeeRatesUpdatedEvent(blockTargetFeeRates, null, nextBlockMedianFeeRate);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
@ -1982,10 +2036,14 @@ public class ElectrumServer {
|
||||||
Config config = Config.get();
|
Config config = Config.get();
|
||||||
if(!isBlockstorm(totalBlocks) && !AppServices.isUsingProxy() && config.getServer().getProtocol().equals(Protocol.SSL)
|
if(!isBlockstorm(totalBlocks) && !AppServices.isUsingProxy() && config.getServer().getProtocol().equals(Protocol.SSL)
|
||||||
&& (config.getServerType() == ServerType.PUBLIC_ELECTRUM_SERVER || config.getServerType() == ServerType.ELECTRUM_SERVER)) {
|
&& (config.getServerType() == ServerType.PUBLIC_ELECTRUM_SERVER || config.getServerType() == ServerType.ELECTRUM_SERVER)) {
|
||||||
subscribeRecent(electrumServer);
|
subscribeRecent(electrumServer, AppServices.getCurrentBlockHeight() == null ? endHeight : AppServices.getCurrentBlockHeight());
|
||||||
}
|
}
|
||||||
|
|
||||||
return new BlockSummaryEvent(blockSummaryMap);
|
Double nextBlockMedianFeeRate = null;
|
||||||
|
if(!isBlockstorm(totalBlocks)) {
|
||||||
|
nextBlockMedianFeeRate = electrumServer.getNextBlockMedianFeeRate();
|
||||||
|
}
|
||||||
|
return new BlockSummaryEvent(blockSummaryMap, nextBlockMedianFeeRate);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
@ -1994,43 +2052,72 @@ public class ElectrumServer {
|
||||||
return Network.get() != Network.MAINNET && totalBlocks > 2;
|
return Network.get() != Network.MAINNET && totalBlocks > 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
private final static Set<String> subscribedRecent = Collections.newSetFromMap(new ConcurrentHashMap<>());
|
private void subscribeRecent(ElectrumServer electrumServer, int currentHeight) {
|
||||||
|
Set<String> unsubscribeScriptHashes = subscribedRecent.entrySet().stream().filter(entry -> entry.getValue() == null || entry.getValue() <= currentHeight - 3)
|
||||||
private void subscribeRecent(ElectrumServer electrumServer) {
|
.map(Map.Entry::getKey).collect(Collectors.toSet());
|
||||||
Set<String> unsubscribeScriptHashes = new HashSet<>(subscribedRecent);
|
|
||||||
unsubscribeScriptHashes.removeIf(subscribedScriptHashes::containsKey);
|
unsubscribeScriptHashes.removeIf(subscribedScriptHashes::containsKey);
|
||||||
electrumServerRpc.unsubscribeScriptHashes(transport, unsubscribeScriptHashes);
|
if(!unsubscribeScriptHashes.isEmpty() && serverCapability.supportsUnsubscribe()) {
|
||||||
subscribedRecent.removeAll(unsubscribeScriptHashes);
|
electrumServerRpc.unsubscribeScriptHashes(transport, unsubscribeScriptHashes);
|
||||||
|
}
|
||||||
|
subscribedRecent.keySet().removeAll(unsubscribeScriptHashes);
|
||||||
|
broadcastRecent.keySet().removeAll(unsubscribeScriptHashes);
|
||||||
|
|
||||||
Map<String, String> subscribeScriptHashes = new HashMap<>();
|
Map<String, String> subscribeScriptHashes = new HashMap<>();
|
||||||
List<BlockTransaction> recentTransactions = electrumServer.getRecentMempoolTransactions();
|
List<BlockTransaction> recentTransactions = electrumServer.getRecentMempoolTransactions();
|
||||||
for(BlockTransaction blkTx : recentTransactions) {
|
for(BlockTransaction blkTx : recentTransactions) {
|
||||||
for(int i = 0; i < blkTx.getTransaction().getOutputs().size() && subscribeScriptHashes.size() < 10; i++) {
|
for(int i = 0; i < blkTx.getTransaction().getOutputs().size(); i++) {
|
||||||
TransactionOutput txOutput = blkTx.getTransaction().getOutputs().get(i);
|
TransactionOutput txOutput = blkTx.getTransaction().getOutputs().get(i);
|
||||||
String scriptHash = getScriptHash(txOutput);
|
String scriptHash = getScriptHash(txOutput);
|
||||||
if(!subscribedScriptHashes.containsKey(scriptHash)) {
|
if(!subscribedScriptHashes.containsKey(scriptHash)) {
|
||||||
subscribeScriptHashes.put("m/" + i, getScriptHash(txOutput));
|
subscribeScriptHashes.put("m/" + subscribeScriptHashes.size(), scriptHash);
|
||||||
|
}
|
||||||
|
if(Math.random() < 0.1d) {
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if(!subscribeScriptHashes.isEmpty()) {
|
if(!subscribeScriptHashes.isEmpty()) {
|
||||||
|
Random random = new Random();
|
||||||
|
int additionalRandomScriptHashes = random.nextInt(8);
|
||||||
|
for(int i = 0; i < additionalRandomScriptHashes; i++) {
|
||||||
|
byte[] randomScriptHashBytes = new byte[32];
|
||||||
|
random.nextBytes(randomScriptHashBytes);
|
||||||
|
String randomScriptHash = Utils.bytesToHex(randomScriptHashBytes);
|
||||||
|
if(!subscribedScriptHashes.containsKey(randomScriptHash)) {
|
||||||
|
subscribeScriptHashes.put("m/" + subscribeScriptHashes.size(), randomScriptHash);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
electrumServerRpc.subscribeScriptHashes(transport, null, subscribeScriptHashes);
|
electrumServerRpc.subscribeScriptHashes(transport, null, subscribeScriptHashes);
|
||||||
subscribedRecent.addAll(subscribeScriptHashes.values());
|
subscribeScriptHashes.values().forEach(scriptHash -> subscribedRecent.put(scriptHash, currentHeight));
|
||||||
} catch(ElectrumServerRpcException e) {
|
} catch(ElectrumServerRpcException e) {
|
||||||
log.debug("Error subscribing to recent mempool transactions", e);
|
log.debug("Error subscribing to recent mempool transactions", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if(!recentTransactions.isEmpty()) {
|
||||||
|
broadcastRecent(electrumServer, recentTransactions);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void broadcastRecent(ElectrumServer electrumServer, List<BlockTransaction> recentTransactions) {
|
||||||
ScheduledService<Void> broadcastService = new ScheduledService<>() {
|
ScheduledService<Void> broadcastService = new ScheduledService<>() {
|
||||||
@Override
|
@Override
|
||||||
protected Task<Void> createTask() {
|
protected Task<Void> createTask() {
|
||||||
return new Task<>() {
|
return new Task<>() {
|
||||||
@Override
|
@Override
|
||||||
protected Void call() throws Exception {
|
protected Void call() throws Exception {
|
||||||
for(BlockTransaction blkTx : recentTransactions) {
|
if(!recentTransactions.isEmpty()) {
|
||||||
electrumServer.broadcastTransaction(blkTx.getTransaction());
|
Random random = new Random();
|
||||||
|
if(random.nextBoolean()) {
|
||||||
|
BlockTransaction blkTx = recentTransactions.get(random.nextInt(recentTransactions.size()));
|
||||||
|
String scriptHash = getScriptHash(blkTx.getTransaction().getOutputs().getFirst());
|
||||||
|
String status = getScriptHashStatus(List.of(new ScriptHashTx(0, blkTx.getHashAsString(), blkTx.getFee())));
|
||||||
|
broadcastRecent.put(scriptHash, status);
|
||||||
|
electrumServer.broadcastTransaction(blkTx.getTransaction());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -9,9 +9,12 @@ import javafx.concurrent.ScheduledService;
|
||||||
import javafx.concurrent.Service;
|
import javafx.concurrent.Service;
|
||||||
import javafx.concurrent.Task;
|
import javafx.concurrent.Task;
|
||||||
import org.apache.commons.lang3.time.DateUtils;
|
import org.apache.commons.lang3.time.DateUtils;
|
||||||
|
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.text.DateFormat;
|
import java.text.DateFormat;
|
||||||
import java.text.SimpleDateFormat;
|
import java.text.SimpleDateFormat;
|
||||||
import java.time.Instant;
|
import java.time.Instant;
|
||||||
|
|
@ -121,7 +124,7 @@ public enum ExchangeSource {
|
||||||
return historicalRates;
|
return historicalRates;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
COINGECKO("Coingecko", "No historical rates") {
|
COINGECKO("Coingecko", "Historical rates for the last 365 days") {
|
||||||
@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)))
|
||||||
|
|
@ -164,6 +167,11 @@ 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()) {
|
||||||
|
|
@ -297,6 +305,19 @@ public enum ExchangeSource {
|
||||||
return name;
|
return name;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public SVGImage getSVGImage() {
|
||||||
|
try {
|
||||||
|
URL url = AppServices.class.getResource("/image/exchangesource/" + name.toLowerCase(Locale.ROOT) + "-icon.svg");
|
||||||
|
if(url != null) {
|
||||||
|
return SVGLoader.load(url);
|
||||||
|
}
|
||||||
|
} catch(Exception e) {
|
||||||
|
log.error("Could not load exchange source image for " + name);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
public static class CurrenciesService extends Service<List<Currency>> {
|
public static class CurrenciesService extends Service<List<Currency>> {
|
||||||
private final ExchangeSource exchangeSource;
|
private final ExchangeSource exchangeSource;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -7,9 +7,12 @@ import com.sparrowwallet.drongo.wallet.BlockTransaction;
|
||||||
import com.sparrowwallet.drongo.wallet.BlockTransactionHash;
|
import com.sparrowwallet.drongo.wallet.BlockTransactionHash;
|
||||||
import com.sparrowwallet.sparrow.AppServices;
|
import com.sparrowwallet.sparrow.AppServices;
|
||||||
import com.sparrowwallet.sparrow.BlockSummary;
|
import com.sparrowwallet.sparrow.BlockSummary;
|
||||||
|
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.util.*;
|
import java.util.*;
|
||||||
|
|
||||||
public enum FeeRatesSource {
|
public enum FeeRatesSource {
|
||||||
|
|
@ -31,6 +34,12 @@ public enum FeeRatesSource {
|
||||||
return getThreeTierFeeRates(this, defaultblockTargetFeeRates, url);
|
return getThreeTierFeeRates(this, defaultblockTargetFeeRates, url);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Double getNextBlockMedianFeeRate() throws Exception {
|
||||||
|
String url = getApiUrl() + "v1/fees/mempool-blocks";
|
||||||
|
return requestNextBlockMedianFeeRate(this, url);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public BlockSummary getBlockSummary(Sha256Hash blockId) throws Exception {
|
public BlockSummary getBlockSummary(Sha256Hash blockId) throws Exception {
|
||||||
String url = getApiUrl() + "v1/block/" + Utils.bytesToHex(blockId.getReversedBytes());
|
String url = getApiUrl() + "v1/block/" + Utils.bytesToHex(blockId.getReversedBytes());
|
||||||
|
|
@ -127,6 +136,10 @@ public enum FeeRatesSource {
|
||||||
|
|
||||||
public abstract Map<Integer, Double> getBlockTargetFeeRates(Map<Integer, Double> defaultblockTargetFeeRates);
|
public abstract Map<Integer, Double> getBlockTargetFeeRates(Map<Integer, Double> defaultblockTargetFeeRates);
|
||||||
|
|
||||||
|
public Double getNextBlockMedianFeeRate() throws Exception {
|
||||||
|
throw new UnsupportedOperationException(name + " does not support retrieving the next block median fee rate");
|
||||||
|
}
|
||||||
|
|
||||||
public BlockSummary getBlockSummary(Sha256Hash blockId) throws Exception {
|
public BlockSummary getBlockSummary(Sha256Hash blockId) throws Exception {
|
||||||
throw new UnsupportedOperationException(name + " does not support block summaries");
|
throw new UnsupportedOperationException(name + " does not support block summaries");
|
||||||
}
|
}
|
||||||
|
|
@ -196,6 +209,30 @@ public enum FeeRatesSource {
|
||||||
return httpClientService.requestJson(url, ThreeTierRates.class, null);
|
return httpClientService.requestJson(url, ThreeTierRates.class, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected static Double requestNextBlockMedianFeeRate(FeeRatesSource feeRatesSource, String url) throws Exception {
|
||||||
|
if(log.isInfoEnabled()) {
|
||||||
|
log.info("Requesting next block median fee rate from " + url);
|
||||||
|
}
|
||||||
|
|
||||||
|
HttpClientService httpClientService = AppServices.getHttpClientService();
|
||||||
|
try {
|
||||||
|
MempoolBlock[] mempoolBlocks = feeRatesSource.requestMempoolBlocks(url, httpClientService);
|
||||||
|
return mempoolBlocks.length > 0 ? mempoolBlocks[0].medianFee : null;
|
||||||
|
} catch (Exception e) {
|
||||||
|
if(log.isDebugEnabled()) {
|
||||||
|
log.warn("Error retrieving next block median fee rate from " + url, e);
|
||||||
|
} else {
|
||||||
|
log.warn("Error retrieving next block median fee rate from " + url + " (" + e.getMessage() + ")");
|
||||||
|
}
|
||||||
|
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected MempoolBlock[] requestMempoolBlocks(String url, HttpClientService httpClientService) throws Exception {
|
||||||
|
return httpClientService.requestJson(url, MempoolBlock[].class, null);
|
||||||
|
}
|
||||||
|
|
||||||
protected static BlockSummary requestBlockSummary(FeeRatesSource feeRatesSource, String url) throws Exception {
|
protected static BlockSummary requestBlockSummary(FeeRatesSource feeRatesSource, String url) throws Exception {
|
||||||
if(log.isInfoEnabled()) {
|
if(log.isInfoEnabled()) {
|
||||||
log.info("Requesting block summary from " + url);
|
log.info("Requesting block summary from " + url);
|
||||||
|
|
@ -275,6 +312,27 @@ public enum FeeRatesSource {
|
||||||
return name;
|
return name;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public String getDescription() {
|
||||||
|
return switch(this) {
|
||||||
|
case ELECTRUM_SERVER -> "server";
|
||||||
|
case MINIMUM -> "settings";
|
||||||
|
default -> getName().toLowerCase(Locale.ROOT);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public SVGImage getSVGImage() {
|
||||||
|
try {
|
||||||
|
URL url = AppServices.class.getResource("/image/feeratesource/" + getDescription() + "-icon.svg");
|
||||||
|
if(url != null) {
|
||||||
|
return SVGLoader.load(url);
|
||||||
|
}
|
||||||
|
} catch(Exception e) {
|
||||||
|
log.error("Could not load fee rates source image for " + name);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
protected record ThreeTierRates(Double fastestFee, Double halfHourFee, Double hourFee, Double minimumFee) {}
|
protected record ThreeTierRates(Double fastestFee, Double halfHourFee, Double hourFee, Double minimumFee) {}
|
||||||
|
|
||||||
private record OxtRates(OxtRatesData[] data) {}
|
private record OxtRates(OxtRatesData[] data) {}
|
||||||
|
|
@ -285,6 +343,8 @@ public enum FeeRatesSource {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected record MempoolBlock(Integer nTx, Double medianFee) {}
|
||||||
|
|
||||||
protected record MempoolBlockSummary(String id, Integer height, Long timestamp, Integer tx_count, Integer weight, MempoolBlockSummaryExtras extras) {
|
protected record MempoolBlockSummary(String id, Integer height, Long timestamp, Integer tx_count, Integer weight, MempoolBlockSummaryExtras extras) {
|
||||||
public Double getMedianFee() {
|
public Double getMedianFee() {
|
||||||
return extras == null ? null : extras.medianFee();
|
return extras == null ? null : extras.medianFee();
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,17 @@
|
||||||
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
|
||||||
|
|
@ -111,9 +117,33 @@ 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 (!generatedDir.mkdir())
|
if(!createOwnerOnlyDirectory(generatedDir)) {
|
||||||
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,27 +7,30 @@ public class ServerCapability {
|
||||||
private final int maxTargetBlocks;
|
private final int maxTargetBlocks;
|
||||||
private final boolean supportsRecentMempool;
|
private final boolean supportsRecentMempool;
|
||||||
private final boolean supportsBlockStats;
|
private final boolean supportsBlockStats;
|
||||||
|
private final boolean supportsUnsubscribe;
|
||||||
|
|
||||||
public ServerCapability(boolean supportsBatching) {
|
public ServerCapability(boolean supportsBatching, boolean supportsUnsubscribe) {
|
||||||
this(supportsBatching, AppServices.TARGET_BLOCKS_RANGE.getLast());
|
this(supportsBatching, AppServices.TARGET_BLOCKS_RANGE.getLast(), supportsUnsubscribe);
|
||||||
}
|
}
|
||||||
|
|
||||||
public ServerCapability(boolean supportsBatching, int maxTargetBlocks) {
|
public ServerCapability(boolean supportsBatching, int maxTargetBlocks, boolean supportsUnsubscribe) {
|
||||||
this.supportsBatching = supportsBatching;
|
this.supportsBatching = supportsBatching;
|
||||||
this.maxTargetBlocks = maxTargetBlocks;
|
this.maxTargetBlocks = maxTargetBlocks;
|
||||||
this.supportsRecentMempool = false;
|
this.supportsRecentMempool = false;
|
||||||
this.supportsBlockStats = false;
|
this.supportsBlockStats = false;
|
||||||
|
this.supportsUnsubscribe = supportsUnsubscribe;
|
||||||
}
|
}
|
||||||
|
|
||||||
public ServerCapability(boolean supportsBatching, boolean supportsRecentMempool, boolean supportsBlockStats) {
|
public ServerCapability(boolean supportsBatching, boolean supportsRecentMempool, boolean supportsBlockStats, boolean supportsUnsubscribe) {
|
||||||
this(supportsBatching, AppServices.TARGET_BLOCKS_RANGE.getLast(), supportsRecentMempool, supportsBlockStats);
|
this(supportsBatching, AppServices.TARGET_BLOCKS_RANGE.getLast(), supportsRecentMempool, supportsBlockStats, supportsUnsubscribe);
|
||||||
}
|
}
|
||||||
|
|
||||||
public ServerCapability(boolean supportsBatching, int maxTargetBlocks, boolean supportsRecentMempool, boolean supportsBlockStats) {
|
public ServerCapability(boolean supportsBatching, int maxTargetBlocks, boolean supportsRecentMempool, boolean supportsBlockStats, boolean supportsUnsubscribe) {
|
||||||
this.supportsBatching = supportsBatching;
|
this.supportsBatching = supportsBatching;
|
||||||
this.maxTargetBlocks = maxTargetBlocks;
|
this.maxTargetBlocks = maxTargetBlocks;
|
||||||
this.supportsRecentMempool = supportsRecentMempool;
|
this.supportsRecentMempool = supportsRecentMempool;
|
||||||
this.supportsBlockStats = supportsBlockStats;
|
this.supportsBlockStats = supportsBlockStats;
|
||||||
|
this.supportsUnsubscribe = supportsUnsubscribe;
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean supportsBatching() {
|
public boolean supportsBatching() {
|
||||||
|
|
@ -45,4 +48,8 @@ public class ServerCapability {
|
||||||
public boolean supportsBlockStats() {
|
public boolean supportsBlockStats() {
|
||||||
return supportsBlockStats;
|
return supportsBlockStats;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public boolean supportsUnsubscribe() {
|
||||||
|
return supportsUnsubscribe;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ import com.sparrowwallet.drongo.wallet.Wallet;
|
||||||
import com.sparrowwallet.sparrow.AppServices;
|
import com.sparrowwallet.sparrow.AppServices;
|
||||||
import com.sparrowwallet.sparrow.EventManager;
|
import com.sparrowwallet.sparrow.EventManager;
|
||||||
import com.sparrowwallet.sparrow.event.WalletHistoryStatusEvent;
|
import com.sparrowwallet.sparrow.event.WalletHistoryStatusEvent;
|
||||||
|
import com.sparrowwallet.sparrow.io.Config;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
|
@ -38,16 +39,32 @@ public class SimpleElectrumServerRpc implements ElectrumServerRpc {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public List<String> getServerVersion(Transport transport, String clientName, String[] supportedVersions) {
|
public List<String> getServerVersion(Transport transport, String clientName, String[] supportedVersions) {
|
||||||
|
if(Config.get().getServerType() == ServerType.ELECTRUM_SERVER && Config.get().isLegacyServer()) {
|
||||||
|
return getLegacyServerVersion(transport, clientName);
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
JsonRpcClient client = new JsonRpcClient(transport);
|
JsonRpcClient client = new JsonRpcClient(transport);
|
||||||
//Using 1.4 as the version number as EPS tries to parse this number to a float :(
|
|
||||||
return new RetryLogic<List<String>>(MAX_RETRIES, RETRY_DELAY, IllegalStateException.class).getResult(() ->
|
return new RetryLogic<List<String>>(MAX_RETRIES, RETRY_DELAY, IllegalStateException.class).getResult(() ->
|
||||||
client.createRequest().returnAsList(String.class).method("server.version").id(idCounter.incrementAndGet()).params(clientName, "1.4").execute());
|
client.createRequest().returnAsList(String.class).method("server.version").id(idCounter.incrementAndGet()).params(clientName, supportedVersions).execute());
|
||||||
|
} catch(JsonRpcException e) {
|
||||||
|
return getLegacyServerVersion(transport, clientName);
|
||||||
} catch(Exception e) {
|
} catch(Exception e) {
|
||||||
throw new ElectrumServerRpcException("Error getting server version", e);
|
throw new ElectrumServerRpcException("Error getting server version", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private List<String> getLegacyServerVersion(Transport transport, String clientName) {
|
||||||
|
try {
|
||||||
|
//Fallback to using 1.4 as the version number as EPS tries to parse this number to a float :(
|
||||||
|
JsonRpcClient client = new JsonRpcClient(transport);
|
||||||
|
return new RetryLogic<List<String>>(MAX_RETRIES, RETRY_DELAY, IllegalStateException.class).getResult(() ->
|
||||||
|
client.createRequest().returnAsList(String.class).method("server.version").id(idCounter.incrementAndGet()).params(clientName, "1.4").execute());
|
||||||
|
} catch(Exception ex) {
|
||||||
|
throw new ElectrumServerRpcException("Error getting legacy server version", ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String getServerBanner(Transport transport) {
|
public String getServerBanner(Transport transport) {
|
||||||
try {
|
try {
|
||||||
|
|
|
||||||
|
|
@ -38,6 +38,6 @@ public class SubscriptionService {
|
||||||
existingStatuses.add(status);
|
existingStatuses.add(status);
|
||||||
}
|
}
|
||||||
|
|
||||||
Platform.runLater(() -> EventManager.get().post(new WalletNodeHistoryChangedEvent(scriptHash)));
|
Platform.runLater(() -> EventManager.get().post(new WalletNodeHistoryChangedEvent(scriptHash, status)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,7 @@ import java.security.cert.Certificate;
|
||||||
|
|
||||||
public class TcpOverTlsTransport extends TcpTransport {
|
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;
|
||||||
|
|
||||||
|
|
@ -41,6 +42,24 @@ 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[] {
|
||||||
|
|
|
||||||
|
|
@ -97,7 +97,7 @@ public class TcpTransport implements CloseableTransport, TimeoutCounter {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void writeRequest(String request) throws IOException {
|
protected void writeRequest(String request) throws IOException {
|
||||||
if(log.isTraceEnabled()) {
|
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())));
|
PrintWriter out = new PrintWriter(new BufferedWriter(new OutputStreamWriter(socket.getOutputStream(), StandardCharsets.UTF_8)));
|
||||||
out.println(request);
|
out.println(request);
|
||||||
out.flush();
|
out.flush();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -149,6 +149,9 @@ 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) {
|
||||||
|
|
|
||||||
|
|
@ -35,10 +35,11 @@ public class ElectrumServerService {
|
||||||
}
|
}
|
||||||
|
|
||||||
@JsonRpcMethod("server.version")
|
@JsonRpcMethod("server.version")
|
||||||
public List<String> getServerVersion(@JsonRpcParam("client_name") String clientName, @JsonRpcParam("protocol_version") String protocolVersion) throws UnsupportedVersionException {
|
public List<String> getServerVersion(@JsonRpcParam("client_name") String clientName, @JsonRpcParam("protocol_version") String[] protocolVersion) throws UnsupportedVersionException {
|
||||||
Version clientVersion = new Version(protocolVersion);
|
String version = protocolVersion.length > 1 ? protocolVersion[1] : protocolVersion[0];
|
||||||
|
Version clientVersion = new Version(version);
|
||||||
if(clientVersion.compareTo(VERSION) < 0) {
|
if(clientVersion.compareTo(VERSION) < 0) {
|
||||||
throw new UnsupportedVersionException(protocolVersion);
|
throw new UnsupportedVersionException(version);
|
||||||
}
|
}
|
||||||
|
|
||||||
return List.of(Cormorant.SERVER_NAME + " " + SparrowWallet.APP_VERSION, VERSION.get());
|
return List.of(Cormorant.SERVER_NAME + " " + SparrowWallet.APP_VERSION, VERSION.get());
|
||||||
|
|
|
||||||
|
|
@ -40,6 +40,10 @@ 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");
|
||||||
|
|
@ -104,6 +108,9 @@ 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);
|
||||||
|
|
|
||||||
|
|
@ -616,6 +616,7 @@ 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();
|
||||||
|
|
||||||
|
|
@ -623,7 +624,9 @@ 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));
|
||||||
|
|
||||||
return wallet.createWalletTransaction(utxoSelectors, txoFilters, payments, opReturns, Collections.emptySet(), feeRate, minimumFeeRate, null, AppServices.getCurrentBlockHeight(), groupByAddress, includeMempoolOutputs);
|
TransactionParameters params = new TransactionParameters(utxoSelectors, txoFilters, payments, opReturns, Collections.emptySet(),
|
||||||
|
feeRate, minimumFeeRate, minRelayFeeRate, null, AppServices.getCurrentBlockHeight(), groupByAddress, includeMempoolOutputs, true);
|
||||||
|
return wallet.createWalletTransaction(params);
|
||||||
}
|
}
|
||||||
|
|
||||||
private Map<BlockTransaction, WalletNode> getNotificationTransaction(PaymentCode externalPaymentCode) {
|
private Map<BlockTransaction, WalletNode> getNotificationTransaction(PaymentCode externalPaymentCode) {
|
||||||
|
|
|
||||||
|
|
@ -87,6 +87,8 @@ public class GeneralSettingsController extends SettingsDetailController {
|
||||||
config.setFeeRatesSource(feeRatesSource.getValue());
|
config.setFeeRatesSource(feeRatesSource.getValue());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
feeRatesSource.setCellFactory(_ -> new FeeRatesSourceListCell());
|
||||||
|
feeRatesSource.setButtonCell(feeRatesSource.getCellFactory().call(null));
|
||||||
feeRatesSource.valueProperty().addListener((observable, oldValue, newValue) -> {
|
feeRatesSource.valueProperty().addListener((observable, oldValue, newValue) -> {
|
||||||
config.setFeeRatesSource(newValue);
|
config.setFeeRatesSource(newValue);
|
||||||
EventManager.get().post(new FeeRatesSourceChangedEvent(newValue));
|
EventManager.get().post(new FeeRatesSourceChangedEvent(newValue));
|
||||||
|
|
@ -96,25 +98,8 @@ public class GeneralSettingsController extends SettingsDetailController {
|
||||||
currenciesLoadWarning.setVisible(false);
|
currenciesLoadWarning.setVisible(false);
|
||||||
|
|
||||||
blockExplorers.setItems(getBlockExplorerList());
|
blockExplorers.setItems(getBlockExplorerList());
|
||||||
blockExplorers.setConverter(new StringConverter<>() {
|
blockExplorers.setCellFactory(_ -> new BlockExplorerListCell());
|
||||||
@Override
|
blockExplorers.setButtonCell(blockExplorers.getCellFactory().call(null));
|
||||||
public String toString(Server server) {
|
|
||||||
if(server == null || server == BlockExplorer.NONE.getServer()) {
|
|
||||||
return "None";
|
|
||||||
}
|
|
||||||
|
|
||||||
if(server == CUSTOM_BLOCK_EXPLORER) {
|
|
||||||
return "Custom...";
|
|
||||||
}
|
|
||||||
|
|
||||||
return server.getHost();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Server fromString(String string) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
blockExplorers.valueProperty().addListener((observable, oldValue, newValue) -> {
|
blockExplorers.valueProperty().addListener((observable, oldValue, newValue) -> {
|
||||||
if(newValue != null) {
|
if(newValue != null) {
|
||||||
if(newValue == CUSTOM_BLOCK_EXPLORER) {
|
if(newValue == CUSTOM_BLOCK_EXPLORER) {
|
||||||
|
|
@ -258,14 +243,50 @@ public class GeneralSettingsController extends SettingsDetailController {
|
||||||
fiatCurrency.valueProperty().addListener(fiatCurrencyListener);
|
fiatCurrency.valueProperty().addListener(fiatCurrencyListener);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static class FeeRatesSourceListCell extends ListCell<FeeRatesSource> {
|
||||||
|
@Override
|
||||||
|
protected void updateItem(FeeRatesSource item, boolean empty) {
|
||||||
|
super.updateItem(item, empty);
|
||||||
|
if(empty || item == null) {
|
||||||
|
setText(null);
|
||||||
|
setGraphic(null);
|
||||||
|
} else {
|
||||||
|
setText(item.toString());
|
||||||
|
setGraphic(item.getSVGImage());
|
||||||
|
setGraphicTextGap(8.0d);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class BlockExplorerListCell extends ListCell<Server> {
|
||||||
|
@Override
|
||||||
|
protected void updateItem(Server server, boolean empty) {
|
||||||
|
super.updateItem(server, empty);
|
||||||
|
if(empty || server == null || server == BlockExplorer.NONE.getServer()) {
|
||||||
|
setText("None");
|
||||||
|
setGraphic(null);
|
||||||
|
} else if(server == CUSTOM_BLOCK_EXPLORER) {
|
||||||
|
setText("Custom...");
|
||||||
|
setGraphic(null);
|
||||||
|
} else {
|
||||||
|
setText(server.getHost());
|
||||||
|
setGraphic(BlockExplorer.getSVGImage(server));
|
||||||
|
setGraphicTextGap(8.0d);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private static class ExchangeSourceButtonCell extends ListCell<ExchangeSource> {
|
private static class ExchangeSourceButtonCell extends ListCell<ExchangeSource> {
|
||||||
@Override
|
@Override
|
||||||
protected void updateItem(ExchangeSource exchangeSource, boolean empty) {
|
protected void updateItem(ExchangeSource exchangeSource, boolean empty) {
|
||||||
super.updateItem(exchangeSource, empty);
|
super.updateItem(exchangeSource, empty);
|
||||||
if(exchangeSource == null || empty) {
|
if(exchangeSource == null || empty) {
|
||||||
setText("");
|
setText("");
|
||||||
|
setGraphic(null);
|
||||||
} else {
|
} else {
|
||||||
setText(exchangeSource.getName());
|
setText(exchangeSource.getName());
|
||||||
|
setGraphic(exchangeSource.getSVGImage());
|
||||||
|
setGraphicTextGap(8.0d);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -276,12 +297,15 @@ public class GeneralSettingsController extends SettingsDetailController {
|
||||||
super.updateItem(exchangeSource, empty);
|
super.updateItem(exchangeSource, empty);
|
||||||
if(exchangeSource == null || empty) {
|
if(exchangeSource == null || empty) {
|
||||||
setText("");
|
setText("");
|
||||||
|
setGraphic(null);
|
||||||
} else {
|
} else {
|
||||||
String text = exchangeSource.getName();
|
String text = exchangeSource.getName();
|
||||||
if(exchangeSource.getDescription() != null && !exchangeSource.getDescription().isEmpty()) {
|
if(exchangeSource.getDescription() != null && !exchangeSource.getDescription().isEmpty()) {
|
||||||
text += " (" + exchangeSource.getDescription() + ")";
|
text += " (" + exchangeSource.getDescription() + ")";
|
||||||
}
|
}
|
||||||
setText(text);
|
setText(text);
|
||||||
|
setGraphic(exchangeSource.getSVGImage());
|
||||||
|
setGraphicTextGap(8.0d);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,9 +4,12 @@ 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;
|
||||||
|
|
@ -57,7 +60,6 @@ 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;
|
||||||
|
|
@ -180,6 +182,15 @@ 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;
|
||||||
|
|
||||||
|
|
@ -451,6 +462,7 @@ 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());
|
||||||
|
|
@ -636,24 +648,27 @@ 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 Payment(txOutput.getScript().getToAddress(), ".." + changeNode + " (Fake Mix)", txOutput.getValue(), false, Payment.Type.FAKE_MIX));
|
payments.add(new WalletNodePayment(changeNode, ".." + changeNode + " (Fake Mix)", txOutput.getValue(), false, Payment.Type.FAKE_MIX));
|
||||||
} else {
|
} else {
|
||||||
payments.add(new Payment(txOutput.getScript().getToAddress(), ".." + changeNode + " (Mix)", txOutput.getValue(), false, Payment.Type.MIX));
|
payments.add(new WalletNodePayment(changeNode, ".." + changeNode + " (Mix)", txOutput.getValue(), false, Payment.Type.MIX));
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if(changeMap.containsKey(changeNode)) {
|
if(changeMap.containsKey(changeNode)) {
|
||||||
payments.add(new Payment(txOutput.getScript().getToAddress(), headersForm.getName(), txOutput.getValue(), false, Payment.Type.DEFAULT));
|
payments.add(new WalletNodePayment(changeNode, headersForm.getName(), txOutput.getValue(), false, Payment.Type.DEFAULT));
|
||||||
} else {
|
} 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();
|
||||||
|
|
@ -664,24 +679,44 @@ public class HeadersController extends TransactionFormController implements Init
|
||||||
|
|
||||||
BlockTransactionHashIndex receivedTxo = walletTxos.keySet().stream().filter(txo -> txo.getHash().equals(txOutput.getHash()) && txo.getIndex() == txOutput.getIndex()).findFirst().orElse(null);
|
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();
|
||||||
try {
|
Address address = txOutput.getScript().getToAddress();
|
||||||
Payment payment = new Payment(txOutput.getScript().getToAddresses()[0], receivedTxo != null ? receivedTxo.getLabel() : label, txOutput.getValue(), false, paymentType);
|
WalletNode receiveNode = receiveOutputScripts.get(txOutput.getScript());
|
||||||
|
SilentPaymentAddress silentPaymentAddress = headersForm.getSilentPaymentAddress(txOutput);
|
||||||
|
label = receivedTxo != null ? receivedTxo.getLabel() : label;
|
||||||
|
if(address != null || silentPaymentAddress != null) {
|
||||||
|
Payment payment;
|
||||||
|
if(silentPaymentAddress != null) {
|
||||||
|
payment = new SilentPayment(silentPaymentAddress, address, label, txOutput.getValue(), false);
|
||||||
|
} else if(receiveNode != null) {
|
||||||
|
payment = new WalletNodePayment(receiveNode, label, txOutput.getValue(), false, paymentType);
|
||||||
|
} else {
|
||||||
|
payment = new Payment(address, label, txOutput.getValue(), false, paymentType);
|
||||||
|
}
|
||||||
WalletTransaction createdTx = AppServices.get().getCreatedTransaction(selectedTxos.keySet());
|
WalletTransaction createdTx = AppServices.get().getCreatedTransaction(selectedTxos.keySet());
|
||||||
if(createdTx != null) {
|
if(createdTx != null) {
|
||||||
Optional<String> optLabel = createdTx.getPayments().stream().filter(pymt -> pymt.getAddress().equals(payment.getAddress()) && pymt.getAmount() == payment.getAmount()).map(Payment::getLabel).findFirst();
|
Optional<String> optLabel = createdTx.getPayments().stream()
|
||||||
|
.filter(pymt -> (pymt instanceof SilentPayment silentPayment ? silentPayment.getSilentPaymentAddress().equals(silentPaymentAddress) :
|
||||||
|
pymt.getAddress().equals(payment.getAddress())) && pymt.getAmount() == payment.getAmount()).map(Payment::getLabel).findFirst();
|
||||||
if(optLabel.isPresent()) {
|
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);
|
||||||
} catch(Exception e) {
|
if(payment instanceof SilentPayment silentPayment) {
|
||||||
//ignore
|
outputs.add(new WalletTransaction.SilentPaymentOutput(txOutput, silentPayment));
|
||||||
|
} else if(payment instanceof WalletNodePayment walletNodePayment) {
|
||||||
|
outputs.add(new WalletTransaction.ConsolidationOutput(txOutput, walletNodePayment, walletNodePayment.getAmount()));
|
||||||
|
} else {
|
||||||
|
outputs.add(new WalletTransaction.PaymentOutput(txOutput, payment));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
outputs.add(new WalletTransaction.NonAddressOutput(txOutput));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return new WalletTransaction(wallet, headersForm.getTransaction(), Collections.emptyList(), List.of(selectedTxos), payments, changeMap, fee.getValue(), walletInputTransactions);
|
return new WalletTransaction(wallet, headersForm.getTransaction(), Collections.emptyList(), List.of(selectedTxos), payments, outputs, changeMap, fee.getValue(), walletInputTransactions);
|
||||||
} else {
|
} 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),
|
||||||
|
|
@ -691,16 +726,25 @@ 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()) {
|
||||||
try {
|
Address address = txOutput.getScript().getToAddress();
|
||||||
BlockTransactionHashIndex receivedTxo = getBlockTransactionOutput(txOutput);
|
SilentPaymentAddress silentPaymentAddress = headersForm.getSilentPaymentAddress(txOutput);
|
||||||
payments.add(new Payment(txOutput.getScript().getToAddresses()[0], receivedTxo != null ? receivedTxo.getLabel() : null, txOutput.getValue(), false));
|
BlockTransactionHashIndex receivedTxo = getBlockTransactionOutput(txOutput);
|
||||||
} catch(Exception e) {
|
String label = receivedTxo != null ? receivedTxo.getLabel() : null;
|
||||||
//ignore
|
if(address != null || silentPaymentAddress != null) {
|
||||||
|
Payment payment = (silentPaymentAddress == null ?
|
||||||
|
new Payment(address, label, txOutput.getValue(), false) :
|
||||||
|
new SilentPayment(silentPaymentAddress, address, label, txOutput.getValue(), false));
|
||||||
|
payments.add(payment);
|
||||||
|
outputs.add(payment instanceof SilentPayment silentPayment ? new WalletTransaction.SilentPaymentOutput(txOutput, silentPayment) :
|
||||||
|
new WalletTransaction.PaymentOutput(txOutput, payment));
|
||||||
|
} else {
|
||||||
|
outputs.add(new WalletTransaction.NonAddressOutput(txOutput));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return new WalletTransaction(null, headersForm.getTransaction(), Collections.emptyList(), List.of(selectedTxos), payments, Collections.emptyMap(), fee.getValue(), inputTransactions);
|
return new WalletTransaction(null, headersForm.getTransaction(), Collections.emptyList(), List.of(selectedTxos), payments, outputs, Collections.emptyMap(), fee.getValue(), inputTransactions);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -774,6 +818,7 @@ public class HeadersController extends TransactionFormController implements Init
|
||||||
|
|
||||||
blockHeightField.managedProperty().bind(blockHeightField.visibleProperty());
|
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);
|
||||||
|
|
@ -791,6 +836,19 @@ 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) {
|
||||||
|
|
@ -927,7 +985,7 @@ public class HeadersController extends TransactionFormController implements Init
|
||||||
|
|
||||||
//Don't include non witness utxo fields for segwit wallets when displaying the PSBT as a QR - it can add greatly to the time required for scanning
|
//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().serialize(true, includeNonWitnessUtxos);
|
byte[] psbtBytes = headersForm.getPsbt().getForExport().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;
|
||||||
|
|
@ -1010,7 +1068,7 @@ public class HeadersController extends TransactionFormController implements Init
|
||||||
}
|
}
|
||||||
|
|
||||||
try(FileOutputStream outputStream = new FileOutputStream(file)) {
|
try(FileOutputStream outputStream = new FileOutputStream(file)) {
|
||||||
outputStream.write(headersForm.getPsbt().serialize());
|
outputStream.write(headersForm.getPsbt().getForExport().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());
|
||||||
|
|
@ -1067,7 +1125,12 @@ public class HeadersController extends TransactionFormController implements Init
|
||||||
|
|
||||||
private void signUnencryptedKeystores(Wallet unencryptedWallet) {
|
private void signUnencryptedKeystores(Wallet unencryptedWallet) {
|
||||||
try {
|
try {
|
||||||
unencryptedWallet.sign(headersForm.getPsbt());
|
Map<PSBTInput, WalletNode> signingNodes = unencryptedWallet.getSigningNodes(headersForm.getPsbt());
|
||||||
|
List<SilentPayment> silentPayments = unencryptedWallet.computeSilentPaymentOutputs(headersForm.getPsbt(), signingNodes);
|
||||||
|
if(!silentPayments.isEmpty()) {
|
||||||
|
EventManager.get().post(new TransactionOutputsChangedEvent(headersForm.getTransaction()));
|
||||||
|
}
|
||||||
|
unencryptedWallet.sign(signingNodes);
|
||||||
updateSignedKeystores(headersForm.getSigningWallet());
|
updateSignedKeystores(headersForm.getSigningWallet());
|
||||||
} catch(Exception e) {
|
} catch(Exception e) {
|
||||||
log.warn("Failed to Sign", e);
|
log.warn("Failed to Sign", e);
|
||||||
|
|
@ -1139,7 +1202,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.LONG_FEE_RATES_RANGE.get(AppServices.LONG_FEE_RATES_RANGE.size() - 1)) {
|
if(feeRateAmt > AppServices.getLongFeeRatesRange().getLast()) {
|
||||||
Optional<ButtonType> optType = AppServices.showWarningDialog("Very high fee rate!",
|
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) {
|
||||||
|
|
@ -1225,9 +1288,17 @@ 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")) {
|
||||||
AppServices.showErrorDialog("Error broadcasting transaction", "The fee rate for the signed transaction is below the minimum " + format.getCurrencyFormat().format(AppServices.getMinimumRelayFeeRate()) + " sats/vB. " +
|
if(AppServices.getServerMinimumRelayFeeRate() != null && !AppServices.getServerMinimumRelayFeeRate().equals(AppServices.getMinimumRelayFeeRate())) {
|
||||||
"This usually happens because a keystore has created a signature that is larger than necessary.\n\n" +
|
AppServices.showErrorDialog("Error broadcasting transaction", "The fee rate for the signed transaction is below the minimum configured relay fee rate for the server of " +
|
||||||
"You can solve this by recreating the transaction with a slightly increased fee rate.");
|
format.getCurrencyFormat().format(AppServices.getServerMinimumRelayFeeRate()) + " sats/vB.");
|
||||||
|
} else {
|
||||||
|
Double minRelayFeeRate = AppServices.getServerMinimumRelayFeeRate() != null ? AppServices.getServerMinimumRelayFeeRate() : AppServices.getMinimumRelayFeeRate();
|
||||||
|
AppServices.showErrorDialog("Error broadcasting transaction", "The fee rate for the signed transaction is below the minimum " + format.getCurrencyFormat().format(minRelayFeeRate) + " sats/vB. " +
|
||||||
|
"This usually happens because a keystore has created a signature that is larger than necessary.\n\n" +
|
||||||
|
"You can solve this by recreating the transaction with a slightly increased fee rate.");
|
||||||
|
}
|
||||||
|
} else if(failMessage.startsWith("dust")) {
|
||||||
|
AppServices.showErrorDialog("Error broadcasting transaction", "The server will not accept this transaction for broadcast due to its configured dust limit policy.");
|
||||||
} else if(failMessage.startsWith("bad-txns-inputs-missingorspent")) {
|
} 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")) {
|
||||||
|
|
@ -1428,6 +1499,7 @@ 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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1559,6 +1631,23 @@ public class HeadersController extends TransactionFormController implements Init
|
||||||
|
|
||||||
signButtonBox.setVisible(false);
|
signButtonBox.setVisible(false);
|
||||||
broadcastButtonBox.setVisible(true);
|
broadcastButtonBox.setVisible(true);
|
||||||
|
|
||||||
|
if(Config.get().hasServer() && !AppServices.isConnected() && !AppServices.isConnecting()) {
|
||||||
|
if(Config.get().getConnectToBroadcast() == null) {
|
||||||
|
Platform.runLater(() -> {
|
||||||
|
ConfirmationAlert confirmationAlert = new ConfirmationAlert("Connect to broadcast?", "Connect to the configured server to broadcast the transaction?", ButtonType.NO, ButtonType.YES);
|
||||||
|
Optional<ButtonType> optType = confirmationAlert.showAndWait();
|
||||||
|
if(confirmationAlert.isDontAskAgain() && optType.isPresent()) {
|
||||||
|
Config.get().setConnectToBroadcast(optType.get() == ButtonType.YES);
|
||||||
|
}
|
||||||
|
if(optType.isPresent() && optType.get() == ButtonType.YES) {
|
||||||
|
EventManager.get().post(new RequestConnectEvent());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else if(Config.get().getConnectToBroadcast()) {
|
||||||
|
Platform.runLater(() -> EventManager.get().post(new RequestConnectEvent()));
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1570,6 +1659,13 @@ public class HeadersController extends TransactionFormController implements Init
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Subscribe
|
||||||
|
public void transactionOutputsChanged(TransactionOutputsChangedEvent event) {
|
||||||
|
if(event.getTransaction().equals(headersForm.getTransaction())) {
|
||||||
|
headersForm.setWalletTransaction(getWalletTransaction(headersForm.getInputTransactions()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Subscribe
|
@Subscribe
|
||||||
public void transactionExtracted(TransactionExtractedEvent event) {
|
public void transactionExtracted(TransactionExtractedEvent event) {
|
||||||
if(event.getPsbt().equals(headersForm.getPsbt())) {
|
if(event.getPsbt().equals(headersForm.getPsbt())) {
|
||||||
|
|
|
||||||
|
|
@ -337,7 +337,7 @@ public class InputController extends TransactionFormController implements Initia
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if(txInput.isAbsoluteTimeLocked()) {
|
if(txInput.isAbsoluteTimeLocked()) {
|
||||||
txInput.setSequenceNumber(TransactionInput.SEQUENCE_LOCKTIME_DISABLED - 1);
|
txInput.setSequenceNumber(TransactionInput.SEQUENCE_RBF_DISABLED);
|
||||||
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_LOCKTIME_DISABLED - 1);
|
txInput.setSequenceNumber(TransactionInput.SEQUENCE_RBF_DISABLED);
|
||||||
}
|
}
|
||||||
if(old_toggle != null) {
|
if(old_toggle != null) {
|
||||||
EventManager.get().post(new TransactionChangedEvent(transaction));
|
EventManager.get().post(new TransactionChangedEvent(transaction));
|
||||||
|
|
|
||||||
|
|
@ -5,14 +5,12 @@ import com.sparrowwallet.drongo.address.Address;
|
||||||
import com.sparrowwallet.drongo.protocol.NonStandardScriptException;
|
import com.sparrowwallet.drongo.protocol.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.PSBTReorderedEvent;
|
import com.sparrowwallet.sparrow.event.*;
|
||||||
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;
|
||||||
|
|
@ -70,20 +68,7 @@ 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());
|
||||||
|
|
@ -98,6 +83,32 @@ 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);
|
||||||
}
|
}
|
||||||
|
|
@ -115,11 +126,14 @@ 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);
|
||||||
WalletNode toNode = walletTx.getWallet() != null && !walletTx.getWallet().isBip47() ? walletTx.getAddressNodeMap().get(payment.getAddress()) : null;
|
outputFieldset.setText(baseText + (toWallet == null ? " - Payment" : " - Received to " + toWallet.getFullDisplayName()));
|
||||||
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 {
|
||||||
|
|
@ -206,4 +220,12 @@ 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());
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -89,7 +89,11 @@ 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.getAddress().toString(),
|
return new Label(payment.getLabel() != null && payment.getType() != Payment.Type.FAKE_MIX && payment.getType() != Payment.Type.MIX ? payment.getLabel() : payment.toString(),
|
||||||
|
GlyphUtils.getOutputGlyph(getWalletTransaction(), payment));
|
||||||
|
} else if(output instanceof WalletTransaction.ConsolidationOutput consolidationOutput) {
|
||||||
|
Payment payment = consolidationOutput.getWalletNodePayment();
|
||||||
|
return new Label(payment.getLabel() != null && payment.getType() != Payment.Type.FAKE_MIX && payment.getType() != Payment.Type.MIX ? payment.getLabel() : payment.toString(),
|
||||||
GlyphUtils.getOutputGlyph(getWalletTransaction(), payment));
|
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());
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ 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;
|
||||||
|
|
@ -60,4 +61,11 @@ 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());
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,8 @@ 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;
|
||||||
|
|
@ -193,4 +195,16 @@ 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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,8 +2,10 @@ 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;
|
||||||
|
|
@ -112,6 +114,10 @@ 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;
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ 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;
|
||||||
|
|
@ -33,17 +34,7 @@ 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 = "#" + i;
|
String name = getPieDataName(i, output);
|
||||||
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()));
|
||||||
|
|
@ -52,6 +43,34 @@ 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);
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
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;
|
||||||
|
|
@ -9,15 +10,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.UnitFormat;
|
import com.sparrowwallet.sparrow.*;
|
||||||
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;
|
||||||
|
|
@ -25,6 +26,8 @@ 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;
|
||||||
|
|
@ -36,21 +39,32 @@ 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;
|
||||||
|
|
||||||
|
|
@ -129,8 +143,14 @@ public class PaymentController extends WalletFormController implements Initializ
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
private final ObjectProperty<WalletNode> consolidationNodeProperty = new SimpleObjectProperty<>();
|
||||||
|
|
||||||
private final ObjectProperty<PayNym> payNymProperty = new SimpleObjectProperty<>();
|
private final ObjectProperty<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() {
|
||||||
|
|
@ -145,6 +165,127 @@ public class PaymentController extends WalletFormController implements Initializ
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
private final ChangeListener<String> addressListener = new ChangeListener<>() {
|
||||||
|
@Override
|
||||||
|
public void changed(ObservableValue<? extends String> observable, String oldValue, String newValue) {
|
||||||
|
address.leftProperty().set(null);
|
||||||
|
|
||||||
|
if(consolidationNodeProperty.get() != null && !newValue.equals(consolidationNodeProperty.get().getAddress().toString())) {
|
||||||
|
consolidationNodeProperty.set(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
if(payNymProperty.get() != null && !newValue.equals(payNymProperty.get().nymName())) {
|
||||||
|
payNymProperty.set(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
if(dnsPaymentProperty.get() != null && !newValue.equals(dnsPaymentProperty.get().hrn())) {
|
||||||
|
dnsPaymentProperty.set(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
if(silentPaymentAddressProperty.get() != null && !newValue.equals(silentPaymentAddressProperty.get().getAddress())) {
|
||||||
|
silentPaymentAddressProperty.set(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
BitcoinURI bitcoinURI = new BitcoinURI(newValue);
|
||||||
|
Platform.runLater(() -> updateFromURI(bitcoinURI));
|
||||||
|
return;
|
||||||
|
} catch(Exception e) {
|
||||||
|
//ignore, not a URI
|
||||||
|
}
|
||||||
|
|
||||||
|
Optional<String> optDnsPaymentHrn = DnsPayment.getHrn(newValue);
|
||||||
|
if(optDnsPaymentHrn.isPresent()) {
|
||||||
|
String dnsPaymentHrn = optDnsPaymentHrn.get();
|
||||||
|
DnsPayment cachedDnsPayment = DnsPaymentCache.getDnsPayment(dnsPaymentHrn);
|
||||||
|
if(cachedDnsPayment != null) {
|
||||||
|
setDnsPayment(cachedDnsPayment);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(Config.get().hasServer() && !AppServices.isConnected() && !AppServices.isConnecting()) {
|
||||||
|
if(Config.get().getConnectToResolve() == null || Config.get().getConnectToResolve() == Boolean.FALSE) {
|
||||||
|
Platform.runLater(() -> {
|
||||||
|
ConfirmationAlert confirmationAlert = new ConfirmationAlert("Connect to resolve?", "You are currently offline. Connect to resolve the address?", ButtonType.NO, ButtonType.YES);
|
||||||
|
Optional<ButtonType> optType = confirmationAlert.showAndWait();
|
||||||
|
if(confirmationAlert.isDontAskAgain() && optType.isPresent()) {
|
||||||
|
Config.get().setConnectToResolve(optType.get() == ButtonType.YES);
|
||||||
|
}
|
||||||
|
if(optType.isPresent() && optType.get() == ButtonType.YES) {
|
||||||
|
EventManager.get().post(new RequestConnectEvent());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
Platform.runLater(() -> EventManager.get().post(new RequestConnectEvent()));
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
DnsPaymentService dnsPaymentService = new DnsPaymentService(dnsPaymentHrn);
|
||||||
|
dnsPaymentService.setOnSucceeded(_ -> dnsPaymentService.getValue().ifPresent(dnsPayment -> setDnsPayment(dnsPayment)));
|
||||||
|
dnsPaymentService.setOnFailed(failEvent -> {
|
||||||
|
if(failEvent.getSource().getException() != null && !(failEvent.getSource().getException().getCause() instanceof TimeoutException)) {
|
||||||
|
AppServices.showErrorDialog("Validation failed for " + dnsPaymentHrn, Throwables.getRootCause(failEvent.getSource().getException()).getMessage());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
dnsPaymentService.start();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(sendController.getWalletForm().getWallet().hasPaymentCode()) {
|
||||||
|
try {
|
||||||
|
PaymentCode paymentCode = new PaymentCode(newValue);
|
||||||
|
Wallet recipientBip47Wallet = sendController.getWalletForm().getWallet().getChildWallet(paymentCode, sendController.getWalletForm().getWallet().getScriptType());
|
||||||
|
if(recipientBip47Wallet == null && sendController.getWalletForm().getWallet().getScriptType() != ScriptType.P2PKH) {
|
||||||
|
recipientBip47Wallet = sendController.getWalletForm().getWallet().getChildWallet(paymentCode, ScriptType.P2PKH);
|
||||||
|
}
|
||||||
|
|
||||||
|
if(recipientBip47Wallet != null) {
|
||||||
|
PayNym payNym = PayNym.fromWallet(recipientBip47Wallet);
|
||||||
|
Platform.runLater(() -> setPayNym(payNym));
|
||||||
|
} else if(!paymentCode.equals(sendController.getWalletForm().getWallet().getPaymentCode())) {
|
||||||
|
ButtonType previewType = new ButtonType("Preview Transaction", ButtonBar.ButtonData.YES);
|
||||||
|
Optional<ButtonType> optButton = AppServices.showAlertDialog("Send notification transaction?", "This payment code is not yet linked with a notification transaction. Send a notification transaction?", Alert.AlertType.CONFIRMATION, ButtonType.CANCEL, previewType);
|
||||||
|
if(optButton.isPresent() && optButton.get() == previewType) {
|
||||||
|
Payment payment = new Payment(paymentCode.getNotificationAddress(), "Link " + paymentCode.toAbbreviatedString(), MINIMUM_P2PKH_OUTPUT_SATS, false);
|
||||||
|
Platform.runLater(() -> EventManager.get().post(new SpendUtxoEvent(sendController.getWalletForm().getWallet(), List.of(payment), List.of(new byte[80]), paymentCode)));
|
||||||
|
} else {
|
||||||
|
Platform.runLater(() -> address.setText(""));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch(Exception e) {
|
||||||
|
//ignore, not a payment code
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
SilentPaymentAddress silentPaymentAddress = SilentPaymentAddress.from(newValue);
|
||||||
|
setSilentPaymentAddress(silentPaymentAddress);
|
||||||
|
} catch(Exception e) {
|
||||||
|
//ignore, not a silent payment address
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
Address toAddress = Address.fromString(newValue);
|
||||||
|
WalletNode walletNode = sendController.getWalletNode(toAddress);
|
||||||
|
if(walletNode != null) {
|
||||||
|
consolidationNodeProperty.set(walletNode);
|
||||||
|
}
|
||||||
|
label.requestFocus();
|
||||||
|
} catch(Exception e) {
|
||||||
|
//ignore, not an address
|
||||||
|
}
|
||||||
|
|
||||||
|
revalidateAmount();
|
||||||
|
maxButton.setDisable(!isMaxButtonEnabled());
|
||||||
|
sendController.updateTransaction();
|
||||||
|
|
||||||
|
if(validationSupport != null) {
|
||||||
|
validationSupport.setErrorDecorationEnabled(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void initialize(URL location, ResourceBundle resources) {
|
public void initialize(URL location, ResourceBundle resources) {
|
||||||
EventManager.get().register(this);
|
EventManager.get().register(this);
|
||||||
|
|
@ -210,6 +351,28 @@ 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())) {
|
||||||
|
|
@ -222,55 +385,8 @@ public class PaymentController extends WalletFormController implements Initializ
|
||||||
return change;
|
return change;
|
||||||
}));
|
}));
|
||||||
|
|
||||||
address.textProperty().addListener((observable, oldValue, newValue) -> {
|
address.textProperty().addListener(addressListener);
|
||||||
address.leftProperty().set(null);
|
address.setContextMenu(address.getCustomContextMenu(Collections.emptyList()));
|
||||||
|
|
||||||
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());
|
||||||
|
|
@ -328,6 +444,37 @@ public class PaymentController extends WalletFormController implements Initializ
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void setDnsPayment(DnsPayment dnsPayment) {
|
||||||
|
if(dnsPayment.hasAddress()) {
|
||||||
|
DnsPaymentCache.putDnsPayment(dnsPayment.bitcoinURI().getAddress(), dnsPayment);
|
||||||
|
} else if(dnsPayment.hasSilentPaymentAddress()) {
|
||||||
|
DnsPaymentCache.putDnsPayment(dnsPayment.bitcoinURI().getSilentPaymentAddress(), dnsPayment);
|
||||||
|
setSilentPaymentAddress(dnsPayment.bitcoinURI().getSilentPaymentAddress());
|
||||||
|
} else {
|
||||||
|
AppServices.showWarningDialog("No Address Provided", "The DNS payment instruction for " + dnsPayment.hrn() + " resolved correctly but did not contain a bitcoin address.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
dnsPaymentProperty.set(dnsPayment);
|
||||||
|
address.setText(dnsPayment.hrn());
|
||||||
|
revalidate(address, addressListener);
|
||||||
|
address.leftProperty().set(getBitcoinCharacter());
|
||||||
|
if(label.getText().isEmpty() || (label.getText().startsWith("₿") && !label.getText().contains(" "))) {
|
||||||
|
label.setText(dnsPayment.toString());
|
||||||
|
}
|
||||||
|
label.requestFocus();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void setSilentPaymentAddress(SilentPaymentAddress silentPaymentAddress) {
|
||||||
|
if(!sendController.getWalletForm().getWallet().canSendSilentPayments()) {
|
||||||
|
Platform.runLater(() -> AppServices.showErrorDialog("Silent Payments Unsupported", "This wallet does not support sending silent payments. Use a single signature software wallet."));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
silentPaymentAddressProperty.set(silentPaymentAddress);
|
||||||
|
label.requestFocus();
|
||||||
|
}
|
||||||
|
|
||||||
private void updateOpenWallets() {
|
private void updateOpenWallets() {
|
||||||
updateOpenWallets(AppServices.get().getOpenWallets().keySet());
|
updateOpenWallets(AppServices.get().getOpenWallets().keySet());
|
||||||
}
|
}
|
||||||
|
|
@ -399,6 +546,16 @@ 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());
|
||||||
|
|
@ -516,7 +673,17 @@ 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 = new Payment(recipientAddress, label.getText(), value, sendAll);
|
Payment payment;
|
||||||
|
SilentPaymentAddress silentPaymentAddress = silentPaymentAddressProperty.get();
|
||||||
|
WalletNode consolidationNode = consolidationNodeProperty.get();
|
||||||
|
if(silentPaymentAddress != null) {
|
||||||
|
payment = new SilentPayment(silentPaymentAddress, label.getText(), value, sendAll);
|
||||||
|
} else if(consolidationNode != null) {
|
||||||
|
payment = new WalletNodePayment(consolidationNode, label.getText(), value, sendAll);
|
||||||
|
} else {
|
||||||
|
payment = new Payment(recipientAddress, label.getText(), value, sendAll);
|
||||||
|
}
|
||||||
|
|
||||||
if(address.getUserData() != null) {
|
if(address.getUserData() != null) {
|
||||||
payment.setType((Payment.Type)address.getUserData());
|
payment.setType((Payment.Type)address.getUserData());
|
||||||
}
|
}
|
||||||
|
|
@ -533,7 +700,14 @@ 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) {
|
||||||
address.setText(payment.getAddress().toString());
|
DnsPayment dnsPayment = DnsPaymentCache.getDnsPayment(payment);
|
||||||
|
if(dnsPayment != null) {
|
||||||
|
address.setText(dnsPayment.hrn());
|
||||||
|
} else if(payment instanceof SilentPayment silentPayment) {
|
||||||
|
address.setText(silentPayment.getSilentPaymentAddress().getAddress());
|
||||||
|
} else {
|
||||||
|
address.setText(payment.getAddress().toString());
|
||||||
|
}
|
||||||
address.setUserData(payment.getType());
|
address.setUserData(payment.getType());
|
||||||
}
|
}
|
||||||
if(payment.getLabel() != null && !label.getText().equals(payment.getLabel())) {
|
if(payment.getLabel() != null && !label.getText().equals(payment.getLabel())) {
|
||||||
|
|
@ -564,7 +738,10 @@ 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) {
|
||||||
|
|
@ -572,8 +749,7 @@ 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 && !isValidAddressAndLabel() && sendController.getPaymentTabs().getTabs().size() == 1) {
|
} else if(utxoSelector instanceof PresetUtxoSelector 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;
|
||||||
|
|
@ -625,7 +801,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.getPayjoinUrl() != null) {
|
if(bitcoinURI.getAddress() != null && bitcoinURI.getPayjoinUrl() != null) {
|
||||||
AppServices.addPayjoinURI(bitcoinURI);
|
AppServices.addPayjoinURI(bitcoinURI);
|
||||||
}
|
}
|
||||||
sendController.updateTransaction();
|
sendController.updateTransaction();
|
||||||
|
|
@ -676,10 +852,33 @@ 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(12);
|
payNymGlyph.setFontSize(10);
|
||||||
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");
|
||||||
|
|
@ -723,4 +922,23 @@ 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();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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<Wallet, Map<Address, WalletNode>> addressNodeMap = new HashMap<>();
|
private final Map<Address, WalletNode> walletAddresses = new HashMap<>();
|
||||||
|
|
||||||
private final ChangeListener<String> feeListener = new ChangeListener<>() {
|
private final ChangeListener<String> feeListener = new ChangeListener<>() {
|
||||||
@Override
|
@Override
|
||||||
|
|
@ -326,7 +326,7 @@ public class SendController extends WalletFormController implements Initializabl
|
||||||
recentBlocksView.visibleProperty().bind(Bindings.equal(feeRatesSelectionProperty, FeeRatesSelection.RECENT_BLOCKS));
|
recentBlocksView.visibleProperty().bind(Bindings.equal(feeRatesSelectionProperty, FeeRatesSelection.RECENT_BLOCKS));
|
||||||
List<BlockSummary> blockSummaries = AppServices.getBlockSummaries().values().stream().sorted().toList();
|
List<BlockSummary> blockSummaries = AppServices.getBlockSummaries().values().stream().sorted().toList();
|
||||||
if(!blockSummaries.isEmpty()) {
|
if(!blockSummaries.isEmpty()) {
|
||||||
recentBlocksView.update(blockSummaries, AppServices.getDefaultFeeRate());
|
recentBlocksView.update(blockSummaries, AppServices.getNextBlockMedianFeeRate());
|
||||||
}
|
}
|
||||||
|
|
||||||
feeRatesSelectionProperty.addListener((_, oldValue, newValue) -> {
|
feeRatesSelectionProperty.addListener((_, oldValue, newValue) -> {
|
||||||
|
|
@ -484,18 +484,41 @@ 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())
|
||||||
));
|
));
|
||||||
|
|
||||||
validationSupport.setErrorDecorationEnabled(false);
|
validationSupport.setErrorDecorationEnabled(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
public Tab addPaymentTab() {
|
public void addPaymentTab() {
|
||||||
|
if(Config.get().getSuggestSendToMany() == null && openSendToMany()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
Tab tab = getPaymentTab();
|
Tab tab = getPaymentTab();
|
||||||
paymentTabs.getTabs().add(tab);
|
paymentTabs.getTabs().add(tab);
|
||||||
paymentTabs.getSelectionModel().select(tab);
|
paymentTabs.getSelectionModel().select(tab);
|
||||||
return tab;
|
}
|
||||||
|
|
||||||
|
private boolean openSendToMany() {
|
||||||
|
try {
|
||||||
|
List<Payment> payments = getPayments();
|
||||||
|
if(payments.size() == 3) {
|
||||||
|
ConfirmationAlert confirmationAlert = new ConfirmationAlert("Open Send To Many?", "Open the Tools > Send To Many dialog to add multiple payments?", ButtonType.NO, ButtonType.YES);
|
||||||
|
Optional<ButtonType> optType = confirmationAlert.showAndWait();
|
||||||
|
if(confirmationAlert.isDontAskAgain() && optType.isPresent()) {
|
||||||
|
Config.get().setSuggestSendToMany(optType.get() == ButtonType.YES);
|
||||||
|
}
|
||||||
|
if(optType.isPresent() && optType.get() == ButtonType.YES) {
|
||||||
|
Platform.runLater(() -> EventManager.get().post(new RequestSendToManyEvent(payments)));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch(Exception e) {
|
||||||
|
//ignore
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Tab getPaymentTab() {
|
public Tab getPaymentTab() {
|
||||||
|
|
@ -582,18 +605,25 @@ 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 && getFeeValueSats() > 0)) {
|
if(!userFeeSet.get() || getFeeValueSats() != null) {
|
||||||
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();
|
||||||
|
|
||||||
walletTransactionService = new WalletTransactionService(addressNodeMap, wallet, getUtxoSelectors(payments), getTxoFilters(),
|
//Disable RBF for silent payments, as we can't guarantee RBF won't be attempted on another device without knowledge to recompute the address if necessary
|
||||||
|
boolean allowRbf = (replacedTransaction == null || replacedTransaction.getTransaction().isReplaceByFee())
|
||||||
|
&& payments.stream().noneMatch(payment -> payment instanceof SilentPayment);
|
||||||
|
|
||||||
|
TransactionParameters params = new TransactionParameters(getUtxoSelectors(payments), getTxoFilters(),
|
||||||
payments, opReturnsList, excludedChangeNodes,
|
payments, opReturnsList, excludedChangeNodes,
|
||||||
feeRate, getMinimumFeeRate(), userFee, currentBlockHeight, groupByAddress, includeMempoolOutputs, replacedTransaction);
|
feeRate, getMinimumFeeRate(), minRelayFeeRate, userFee,
|
||||||
|
currentBlockHeight, groupByAddress, includeMempoolOutputs, allowRbf);
|
||||||
|
walletTransactionService = new WalletTransactionService(wallet, params, replacedTransaction);
|
||||||
walletTransactionService.setOnSucceeded(event -> {
|
walletTransactionService.setOnSucceeded(event -> {
|
||||||
if(!walletTransactionService.isIgnoreResult()) {
|
if(!walletTransactionService.isIgnoreResult()) {
|
||||||
walletTransactionProperty.setValue(walletTransactionService.getValue());
|
walletTransactionProperty.setValue(walletTransactionService.getValue());
|
||||||
|
|
@ -628,12 +658,12 @@ public class SendController extends WalletFormController implements Initializabl
|
||||||
|
|
||||||
walletTransactionService.start();
|
walletTransactionService.start();
|
||||||
}
|
}
|
||||||
} catch(InvalidAddressException | IllegalStateException e) {
|
} catch(IllegalStateException e) {
|
||||||
walletTransactionProperty.setValue(null);
|
walletTransactionProperty.setValue(null);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private List<UtxoSelector> getUtxoSelectors(List<Payment> payments) throws InvalidAddressException {
|
private List<UtxoSelector> getUtxoSelectors(List<Payment> payments) {
|
||||||
if(utxoSelectorProperty.get() != null) {
|
if(utxoSelectorProperty.get() != null) {
|
||||||
return List.of(utxoSelectorProperty.get());
|
return List.of(utxoSelectorProperty.get());
|
||||||
}
|
}
|
||||||
|
|
@ -655,39 +685,14 @@ 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 List<UtxoSelector> utxoSelectors;
|
private final TransactionParameters params;
|
||||||
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(Map<Wallet, Map<Address, WalletNode>> addressNodeMap,
|
public WalletTransactionService(Wallet wallet, TransactionParameters params, BlockTransaction replacedTransaction) {
|
||||||
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.utxoSelectors = utxoSelectors;
|
this.params = params;
|
||||||
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -698,16 +703,17 @@ 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 && utxoSelectors.size() == 1 && utxoSelectors.get(0) instanceof PresetUtxoSelector presetUtxoSelector) {
|
if(e.getTargetValue() != null && replacedTransaction != null && wallet.isSafeToAddInputsOrOutputs(replacedTransaction)
|
||||||
|
&& params.utxoSelectors().size() == 1 && params.utxoSelectors().getFirst() instanceof PresetUtxoSelector presetUtxoSelector) {
|
||||||
//Creating RBF transaction - include additional UTXOs if available to pay desired fee
|
//Creating RBF transaction - include additional UTXOs if available to pay desired fee
|
||||||
List<TxoFilter> filters = new ArrayList<>(txoFilters);
|
List<TxoFilter> filters = new ArrayList<>(params.txoFilters());
|
||||||
filters.add(presetUtxoSelector.asExcludeTxoFilter());
|
filters.add(presetUtxoSelector.asExcludeTxoFilter());
|
||||||
List<OutputGroup> outputGroups = wallet.getGroupedUtxos(filters, feeRate, AppServices.getMinimumRelayFeeRate(), Config.get().isGroupByAddress())
|
List<OutputGroup> outputGroups = wallet.getGroupedUtxos(filters, params.feeRate(), AppServices.getMinimumRelayFeeRate(), Config.get().isGroupByAddress())
|
||||||
.stream().filter(outputGroup -> outputGroup.getEffectiveValue() >= 0).collect(Collectors.toList());
|
.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.remove(0);
|
OutputGroup outputGroup = outputGroups.removeFirst();
|
||||||
for(BlockTransactionHashIndex utxo : outputGroup.getUtxos()) {
|
for(BlockTransactionHashIndex utxo : outputGroup.getUtxos()) {
|
||||||
presetUtxoSelector.getPresetUtxos().add(utxo);
|
presetUtxoSelector.getPresetUtxos().add(utxo);
|
||||||
}
|
}
|
||||||
|
|
@ -721,12 +727,12 @@ public class SendController extends WalletFormController implements Initializabl
|
||||||
}
|
}
|
||||||
|
|
||||||
private WalletTransaction getWalletTransaction() throws InsufficientFundsException {
|
private WalletTransaction getWalletTransaction() throws InsufficientFundsException {
|
||||||
updateMessage("Selecting UTXOs...");
|
try {
|
||||||
WalletTransaction walletTransaction = wallet.createWalletTransaction(utxoSelectors, txoFilters, payments, opReturns, excludedChangeNodes,
|
updateMessage("Selecting UTXOs...");
|
||||||
feeRate, longTermFeeRate, fee, currentBlockHeight, groupByAddress, includeMempoolOutputs);
|
return wallet.createWalletTransaction(params);
|
||||||
updateMessage("Deriving keys...");
|
} finally {
|
||||||
walletTransaction.updateAddressNodeMap(addressNodeMap, walletTransaction.getWallet());
|
updateMessage("");
|
||||||
return walletTransaction;
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
@ -854,7 +860,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() ? Transaction.DEFAULT_MIN_RELAY_FEE : getFeeRate());
|
return (userFeeSet.get() ? AppServices.getMinimumRelayFeeRate() : getFeeRate());
|
||||||
}
|
}
|
||||||
|
|
||||||
public Double getFeeRate() {
|
public Double getFeeRate() {
|
||||||
|
|
@ -918,7 +924,6 @@ 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) {
|
||||||
|
|
@ -939,9 +944,10 @@ 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 = FEE_RATES_RANGE.get(FEE_RATES_RANGE.size() - 1).doubleValue();
|
Double maxFeeRate = AppServices.getFeeRatesRange().getLast();
|
||||||
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");
|
||||||
|
|
@ -1091,7 +1097,7 @@ public class SendController extends WalletFormController implements Initializabl
|
||||||
|
|
||||||
paymentCodeProperty.set(null);
|
paymentCodeProperty.set(null);
|
||||||
|
|
||||||
addressNodeMap.clear();
|
walletAddresses.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
public UtxoSelector getUtxoSelector() {
|
public UtxoSelector getUtxoSelector() {
|
||||||
|
|
@ -1169,13 +1175,20 @@ 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());
|
||||||
Map<Address, WalletNode> addressNodeMap = walletTransaction.getAddressNodeMap();
|
nodes.addAll(walletTransaction.getWalletNodePayments().stream().map(WalletNodePayment::getWalletNode).collect(Collectors.toList()));
|
||||||
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);
|
||||||
|
|
@ -1205,7 +1218,7 @@ public class SendController extends WalletFormController implements Initializabl
|
||||||
|
|
||||||
public void broadcastNotification(Wallet decryptedWallet) {
|
public void broadcastNotification(Wallet decryptedWallet) {
|
||||||
try {
|
try {
|
||||||
PaymentCode paymentCode = decryptedWallet.getPaymentCode();
|
PaymentCode paymentCode = decryptedWallet.isMasterWallet() ? decryptedWallet.getPaymentCode() : decryptedWallet.getMasterWallet().getPaymentCode();
|
||||||
PaymentCode externalPaymentCode = paymentCodeProperty.get();
|
PaymentCode externalPaymentCode = paymentCodeProperty.get();
|
||||||
WalletTransaction walletTransaction = walletTransactionProperty.get();
|
WalletTransaction walletTransaction = walletTransactionProperty.get();
|
||||||
WalletNode input0Node = walletTransaction.getSelectedUtxos().entrySet().iterator().next().getValue();
|
WalletNode input0Node = walletTransaction.getSelectedUtxos().entrySet().iterator().next().getValue();
|
||||||
|
|
@ -1219,11 +1232,14 @@ public class SendController extends WalletFormController implements Initializabl
|
||||||
List<UtxoSelector> utxoSelectors = List.of(new PresetUtxoSelector(walletTransaction.getSelectedUtxos().keySet(), true, false));
|
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();
|
||||||
|
|
||||||
WalletTransaction finalWalletTx = decryptedWallet.createWalletTransaction(utxoSelectors, getTxoFilters(), walletTransaction.getPayments(), List.of(blindedPaymentCode), excludedChangeNodes, feeRate, getMinimumFeeRate(), userFee, currentBlockHeight, groupByAddress, includeMempoolOutputs);
|
TransactionParameters params = new TransactionParameters(utxoSelectors, getTxoFilters(), walletTransaction.getPayments(), List.of(blindedPaymentCode),
|
||||||
|
excludedChangeNodes, feeRate, getMinimumFeeRate(), minRelayFeeRate, userFee, currentBlockHeight, groupByAddress, includeMempoolOutputs, true);
|
||||||
|
WalletTransaction finalWalletTx = decryptedWallet.createWalletTransaction(params);
|
||||||
PSBT psbt = finalWalletTx.createPSBT();
|
PSBT psbt = finalWalletTx.createPSBT();
|
||||||
decryptedWallet.sign(psbt);
|
decryptedWallet.sign(psbt);
|
||||||
decryptedWallet.finalise(psbt);
|
decryptedWallet.finalise(psbt);
|
||||||
|
|
@ -1412,6 +1428,12 @@ public class SendController extends WalletFormController implements Initializabl
|
||||||
}
|
}
|
||||||
feeRange.updateTrackHighlight();
|
feeRange.updateTrackHighlight();
|
||||||
|
|
||||||
|
if(event.getNextBlockMedianFeeRate() != null) {
|
||||||
|
recentBlocksView.updateFeeRate(event.getNextBlockMedianFeeRate());
|
||||||
|
} else {
|
||||||
|
recentBlocksView.updateFeeRate(event.getTargetBlockFeeRates());
|
||||||
|
}
|
||||||
|
|
||||||
if(updateDefaultFeeRate) {
|
if(updateDefaultFeeRate) {
|
||||||
if(getFeeRate() != null && Long.valueOf((long)getFallbackFeeRate()).equals(getFeeRate().longValue())) {
|
if(getFeeRate() != null && Long.valueOf((long)getFallbackFeeRate()).equals(getFeeRate().longValue())) {
|
||||||
setDefaultFeeRate();
|
setDefaultFeeRate();
|
||||||
|
|
@ -1434,7 +1456,7 @@ public class SendController extends WalletFormController implements Initializabl
|
||||||
|
|
||||||
@Subscribe
|
@Subscribe
|
||||||
public void blockSummary(BlockSummaryEvent event) {
|
public void blockSummary(BlockSummaryEvent event) {
|
||||||
Platform.runLater(() -> recentBlocksView.update(AppServices.getBlockSummaries().values().stream().sorted().toList(), AppServices.getDefaultFeeRate()));
|
Platform.runLater(() -> recentBlocksView.update(AppServices.getBlockSummaries().values().stream().sorted().toList(), AppServices.getNextBlockMedianFeeRate()));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Subscribe
|
@Subscribe
|
||||||
|
|
@ -1476,7 +1498,7 @@ public class SendController extends WalletFormController implements Initializabl
|
||||||
notificationButton.setVisible(isNotificationTransaction);
|
notificationButton.setVisible(isNotificationTransaction);
|
||||||
notificationButton.setDefaultButton(isNotificationTransaction);
|
notificationButton.setDefaultButton(isNotificationTransaction);
|
||||||
|
|
||||||
setInputFieldsDisabled(isNotificationTransaction, false);
|
setInputFieldsDisabled(!event.allowPaymentChanges(), false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1600,18 +1622,31 @@ public class SendController extends WalletFormController implements Initializabl
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Subscribe
|
||||||
|
public void feeRateSourceChanged(FeeRatesSourceChangedEvent event) {
|
||||||
|
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());
|
||||||
Map<Address, WalletNode> walletAddresses = walletTransaction.getAddressNodeMap();
|
List<WalletNodePayment> walletNodePayments = walletTransaction.getWalletNodePayments();
|
||||||
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 = userPayments.stream().anyMatch(payment -> walletAddresses.get(payment.getAddress()) != null && !walletAddresses.get(payment.getAddress()).getTransactionOutputs().isEmpty());
|
boolean addressReuse = walletNodePayments.stream().anyMatch(walletNodePayment -> !walletNodePayment.getWalletNode().getTransactionOutputs().isEmpty());
|
||||||
boolean payjoinPresent = userPayments.stream().anyMatch(payment -> AppServices.getPayjoinURI(payment.getAddress()) != null);
|
boolean payjoinPresent = userPayments.stream().anyMatch(payment -> AppServices.getPayjoinURI(payment.getAddress()) != null);
|
||||||
|
|
||||||
if(optimizationStrategy == OptimizationStrategy.PRIVACY) {
|
if(optimizationStrategy == OptimizationStrategy.PRIVACY) {
|
||||||
|
|
|
||||||
|
|
@ -41,8 +41,11 @@ 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;
|
||||||
|
|
||||||
|
|
@ -455,6 +458,26 @@ public class SettingsController extends WalletFormController implements Initiali
|
||||||
AppServices.showWarningDialog("Legacy multisig wallet detected", "Sparrow supports BIP67 compatible multisig wallets only.\n\nThe public keys will be lexicographically sorted, and the output descriptor represented with sortedmulti.");
|
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", ""));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -27,13 +27,14 @@ 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 centerdevice.nsmenufx;
|
requires nsmenufx;
|
||||||
requires org.jcommander;
|
requires org.jcommander;
|
||||||
requires jul.to.slf4j;
|
requires jul.to.slf4j;
|
||||||
requires net.sourceforge.javacsv;
|
requires net.sourceforge.javacsv;
|
||||||
|
|
@ -56,4 +57,5 @@ 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;
|
||||||
}
|
}
|
||||||
|
|
@ -1,76 +0,0 @@
|
||||||
/*------------------------------------------------------------------------
|
|
||||||
* Config
|
|
||||||
*
|
|
||||||
* Copyright 2010 (c) Jeff Brown <spadix@users.sourceforge.net>
|
|
||||||
*
|
|
||||||
* This file is part of the ZBar Bar Code Reader.
|
|
||||||
*
|
|
||||||
* The ZBar Bar Code Reader is free software; you can redistribute it
|
|
||||||
* and/or modify it under the terms of the GNU Lesser Public License as
|
|
||||||
* published by the Free Software Foundation; either version 2.1 of
|
|
||||||
* the License, or (at your option) any later version.
|
|
||||||
*
|
|
||||||
* The ZBar Bar Code Reader is distributed in the hope that it will be
|
|
||||||
* useful, but WITHOUT ANY WARRANTY; without even the implied warranty
|
|
||||||
* of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU Lesser Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU Lesser Public License
|
|
||||||
* along with the ZBar Bar Code Reader; if not, write to the Free
|
|
||||||
* Software Foundation, Inc., 51 Franklin St, Fifth Floor,
|
|
||||||
* Boston, MA 02110-1301 USA
|
|
||||||
*
|
|
||||||
* http://sourceforge.net/projects/zbar
|
|
||||||
*------------------------------------------------------------------------*/
|
|
||||||
|
|
||||||
package net.sourceforge.zbar;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Decoder configuration options.
|
|
||||||
*/
|
|
||||||
public class Config {
|
|
||||||
/**
|
|
||||||
* Enable symbology/feature.
|
|
||||||
*/
|
|
||||||
public static final int ENABLE = 0;
|
|
||||||
/**
|
|
||||||
* Enable check digit when optional.
|
|
||||||
*/
|
|
||||||
public static final int ADD_CHECK = 1;
|
|
||||||
/**
|
|
||||||
* Return check digit when present.
|
|
||||||
*/
|
|
||||||
public static final int EMIT_CHECK = 2;
|
|
||||||
/**
|
|
||||||
* Enable full ASCII character set.
|
|
||||||
*/
|
|
||||||
public static final int ASCII = 3;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Minimum data length for valid decode.
|
|
||||||
*/
|
|
||||||
public static final int MIN_LEN = 0x20;
|
|
||||||
/**
|
|
||||||
* Maximum data length for valid decode.
|
|
||||||
*/
|
|
||||||
public static final int MAX_LEN = 0x21;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Required video consistency frames.
|
|
||||||
*/
|
|
||||||
public static final int UNCERTAINTY = 0x40;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Enable scanner to collect position data.
|
|
||||||
*/
|
|
||||||
public static final int POSITION = 0x80;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Image scanner vertical scan density.
|
|
||||||
*/
|
|
||||||
public static final int X_DENSITY = 0x100;
|
|
||||||
/**
|
|
||||||
* Image scanner horizontal scan density.
|
|
||||||
*/
|
|
||||||
public static final int Y_DENSITY = 0x101;
|
|
||||||
}
|
|
||||||
|
|
@ -1,197 +0,0 @@
|
||||||
/*------------------------------------------------------------------------
|
|
||||||
* Image
|
|
||||||
*
|
|
||||||
* Copyright 2007-2010 (c) Jeff Brown <spadix@users.sourceforge.net>
|
|
||||||
*
|
|
||||||
* This file is part of the ZBar Bar Code Reader.
|
|
||||||
*
|
|
||||||
* The ZBar Bar Code Reader is free software; you can redistribute it
|
|
||||||
* and/or modify it under the terms of the GNU Lesser Public License as
|
|
||||||
* published by the Free Software Foundation; either version 2.1 of
|
|
||||||
* the License, or (at your option) any later version.
|
|
||||||
*
|
|
||||||
* The ZBar Bar Code Reader is distributed in the hope that it will be
|
|
||||||
* useful, but WITHOUT ANY WARRANTY; without even the implied warranty
|
|
||||||
* of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU Lesser Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU Lesser Public License
|
|
||||||
* along with the ZBar Bar Code Reader; if not, write to the Free
|
|
||||||
* Software Foundation, Inc., 51 Franklin St, Fifth Floor,
|
|
||||||
* Boston, MA 02110-1301 USA
|
|
||||||
*
|
|
||||||
* http://sourceforge.net/projects/zbar
|
|
||||||
*------------------------------------------------------------------------*/
|
|
||||||
|
|
||||||
package net.sourceforge.zbar;
|
|
||||||
|
|
||||||
import java.io.Closeable;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* stores image data samples along with associated format and size
|
|
||||||
* metadata.
|
|
||||||
*/
|
|
||||||
public class Image implements Closeable {
|
|
||||||
static {
|
|
||||||
init();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* C pointer to a zbar_symbol_t.
|
|
||||||
*/
|
|
||||||
private long peer;
|
|
||||||
private Object data;
|
|
||||||
|
|
||||||
public Image() {
|
|
||||||
peer = create();
|
|
||||||
}
|
|
||||||
|
|
||||||
public Image(int width, int height) {
|
|
||||||
this();
|
|
||||||
setSize(width, height);
|
|
||||||
}
|
|
||||||
|
|
||||||
public Image(int width, int height, String format) {
|
|
||||||
this();
|
|
||||||
setSize(width, height);
|
|
||||||
setFormat(format);
|
|
||||||
}
|
|
||||||
|
|
||||||
public Image(String format) {
|
|
||||||
this();
|
|
||||||
setFormat(format);
|
|
||||||
}
|
|
||||||
|
|
||||||
Image(long peer) {
|
|
||||||
this.peer = peer;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static native void init();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create an associated peer instance.
|
|
||||||
*/
|
|
||||||
private native long create();
|
|
||||||
|
|
||||||
public void close() {
|
|
||||||
destroy();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Clean up native data associated with an instance.
|
|
||||||
*/
|
|
||||||
public synchronized void destroy() {
|
|
||||||
if(peer != 0) {
|
|
||||||
destroy(peer);
|
|
||||||
peer = 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Destroy the associated peer instance.
|
|
||||||
*/
|
|
||||||
private native void destroy(long peer);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Image format conversion.
|
|
||||||
*
|
|
||||||
* @returns a @em new image with the sample data from the original
|
|
||||||
* image converted to the requested format fourcc. the original
|
|
||||||
* image is unaffected.
|
|
||||||
*/
|
|
||||||
public Image convert(String format) {
|
|
||||||
long newpeer = convert(peer, format);
|
|
||||||
if(newpeer == 0) {
|
|
||||||
return (null);
|
|
||||||
}
|
|
||||||
return (new Image(newpeer));
|
|
||||||
}
|
|
||||||
|
|
||||||
private native long convert(long peer, String format);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Retrieve the image format fourcc.
|
|
||||||
*/
|
|
||||||
public native String getFormat();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Specify the fourcc image format code for image sample data.
|
|
||||||
*/
|
|
||||||
public native void setFormat(String format);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Retrieve a "sequence" (page/frame) number associated with this
|
|
||||||
* image.
|
|
||||||
*/
|
|
||||||
public native int getSequence();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Associate a "sequence" (page/frame) number with this image.
|
|
||||||
*/
|
|
||||||
public native void setSequence(int seq);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Retrieve the width of the image.
|
|
||||||
*/
|
|
||||||
public native int getWidth();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Retrieve the height of the image.
|
|
||||||
*/
|
|
||||||
public native int getHeight();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Retrieve the size of the image.
|
|
||||||
*/
|
|
||||||
public native int[] getSize();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Specify the pixel size of the image.
|
|
||||||
*/
|
|
||||||
public native void setSize(int[] size);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Specify the pixel size of the image.
|
|
||||||
*/
|
|
||||||
public native void setSize(int width, int height);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Retrieve the crop region of the image.
|
|
||||||
*/
|
|
||||||
public native int[] getCrop();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Specify the crop region of the image.
|
|
||||||
*/
|
|
||||||
public native void setCrop(int[] crop);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Specify the crop region of the image.
|
|
||||||
*/
|
|
||||||
public native void setCrop(int x, int y, int width, int height);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Retrieve the image sample data.
|
|
||||||
*/
|
|
||||||
public native byte[] getData();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Specify image sample data.
|
|
||||||
*/
|
|
||||||
public native void setData(byte[] data);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Specify image sample data.
|
|
||||||
*/
|
|
||||||
public native void setData(int[] data);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Retrieve the decoded results associated with this image.
|
|
||||||
*/
|
|
||||||
public SymbolSet getSymbols() {
|
|
||||||
return (new SymbolSet(getSymbols(peer)));
|
|
||||||
}
|
|
||||||
|
|
||||||
private native long getSymbols(long peer);
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
@ -1,110 +0,0 @@
|
||||||
/*------------------------------------------------------------------------
|
|
||||||
* ImageScanner
|
|
||||||
*
|
|
||||||
* Copyright 2007-2010 (c) Jeff Brown <spadix@users.sourceforge.net>
|
|
||||||
*
|
|
||||||
* This file is part of the ZBar Bar Code Reader.
|
|
||||||
*
|
|
||||||
* The ZBar Bar Code Reader is free software; you can redistribute it
|
|
||||||
* and/or modify it under the terms of the GNU Lesser Public License as
|
|
||||||
* published by the Free Software Foundation; either version 2.1 of
|
|
||||||
* the License, or (at your option) any later version.
|
|
||||||
*
|
|
||||||
* The ZBar Bar Code Reader is distributed in the hope that it will be
|
|
||||||
* useful, but WITHOUT ANY WARRANTY; without even the implied warranty
|
|
||||||
* of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU Lesser Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU Lesser Public License
|
|
||||||
* along with the ZBar Bar Code Reader; if not, write to the Free
|
|
||||||
* Software Foundation, Inc., 51 Franklin St, Fifth Floor,
|
|
||||||
* Boston, MA 02110-1301 USA
|
|
||||||
*
|
|
||||||
* http://sourceforge.net/projects/zbar
|
|
||||||
*------------------------------------------------------------------------*/
|
|
||||||
|
|
||||||
package net.sourceforge.zbar;
|
|
||||||
|
|
||||||
import java.io.Closeable;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Read barcodes from 2-D images.
|
|
||||||
*/
|
|
||||||
public class ImageScanner implements Closeable {
|
|
||||||
static {
|
|
||||||
init();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* C pointer to a zbar_image_scanner_t.
|
|
||||||
*/
|
|
||||||
private long peer;
|
|
||||||
|
|
||||||
public ImageScanner() {
|
|
||||||
peer = create();
|
|
||||||
}
|
|
||||||
|
|
||||||
private static native void init();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create an associated peer instance.
|
|
||||||
*/
|
|
||||||
private native long create();
|
|
||||||
|
|
||||||
public void close() {
|
|
||||||
destroy();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Clean up native data associated with an instance.
|
|
||||||
*/
|
|
||||||
public synchronized void destroy() {
|
|
||||||
if(peer != 0) {
|
|
||||||
destroy(peer);
|
|
||||||
peer = 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Destroy the associated peer instance.
|
|
||||||
*/
|
|
||||||
private native void destroy(long peer);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Set config for indicated symbology (0 for all) to specified value.
|
|
||||||
*/
|
|
||||||
public native void setConfig(int symbology, int config, int value) throws IllegalArgumentException;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Parse configuration string and apply to image scanner.
|
|
||||||
*/
|
|
||||||
public native void parseConfig(String config);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Enable or disable the inter-image result cache (default disabled).
|
|
||||||
* Mostly useful for scanning video frames, the cache filters duplicate
|
|
||||||
* results from consecutive images, while adding some consistency
|
|
||||||
* checking and hysteresis to the results. Invoking this method also
|
|
||||||
* clears the cache.
|
|
||||||
*/
|
|
||||||
public native void enableCache(boolean enable);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Retrieve decode results for last scanned image.
|
|
||||||
*
|
|
||||||
* @returns the SymbolSet result container
|
|
||||||
*/
|
|
||||||
public SymbolSet getResults() {
|
|
||||||
return (new SymbolSet(getResults(peer)));
|
|
||||||
}
|
|
||||||
|
|
||||||
private native long getResults(long peer);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Scan for symbols in provided Image.
|
|
||||||
* The image format must currently be "Y800" or "GRAY".
|
|
||||||
*
|
|
||||||
* @returns the number of symbols successfully decoded from the image.
|
|
||||||
*/
|
|
||||||
public native int scanImage(Image image);
|
|
||||||
}
|
|
||||||
|
|
@ -1,44 +0,0 @@
|
||||||
/*------------------------------------------------------------------------
|
|
||||||
* Modifier
|
|
||||||
*
|
|
||||||
* Copyright 2010 (c) Jeff Brown <spadix@users.sourceforge.net>
|
|
||||||
*
|
|
||||||
* This file is part of the ZBar Bar Code Reader.
|
|
||||||
*
|
|
||||||
* The ZBar Bar Code Reader is free software; you can redistribute it
|
|
||||||
* and/or modify it under the terms of the GNU Lesser Public License as
|
|
||||||
* published by the Free Software Foundation; either version 2.1 of
|
|
||||||
* the License, or (at your option) any later version.
|
|
||||||
*
|
|
||||||
* The ZBar Bar Code Reader is distributed in the hope that it will be
|
|
||||||
* useful, but WITHOUT ANY WARRANTY; without even the implied warranty
|
|
||||||
* of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU Lesser Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU Lesser Public License
|
|
||||||
* along with the ZBar Bar Code Reader; if not, write to the Free
|
|
||||||
* Software Foundation, Inc., 51 Franklin St, Fifth Floor,
|
|
||||||
* Boston, MA 02110-1301 USA
|
|
||||||
*
|
|
||||||
* http://sourceforge.net/projects/zbar
|
|
||||||
*------------------------------------------------------------------------*/
|
|
||||||
|
|
||||||
package net.sourceforge.zbar;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Decoder symbology modifiers.
|
|
||||||
*/
|
|
||||||
public class Modifier {
|
|
||||||
/**
|
|
||||||
* barcode tagged as GS1 (EAN.UCC) reserved
|
|
||||||
* (eg, FNC1 before first data character).
|
|
||||||
* data may be parsed as a sequence of GS1 AIs
|
|
||||||
*/
|
|
||||||
public static final int GS1 = 0;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* barcode tagged as AIM reserved
|
|
||||||
* (eg, FNC1 after first character or digit pair)
|
|
||||||
*/
|
|
||||||
public static final int AIM = 1;
|
|
||||||
}
|
|
||||||
|
|
@ -1,52 +0,0 @@
|
||||||
/*------------------------------------------------------------------------
|
|
||||||
* Orientation
|
|
||||||
*
|
|
||||||
* Copyright 2010 (c) Jeff Brown <spadix@users.sourceforge.net>
|
|
||||||
*
|
|
||||||
* This file is part of the ZBar Bar Code Reader.
|
|
||||||
*
|
|
||||||
* The ZBar Bar Code Reader is free software; you can redistribute it
|
|
||||||
* and/or modify it under the terms of the GNU Lesser Public License as
|
|
||||||
* published by the Free Software Foundation; either version 2.1 of
|
|
||||||
* the License, or (at your option) any later version.
|
|
||||||
*
|
|
||||||
* The ZBar Bar Code Reader is distributed in the hope that it will be
|
|
||||||
* useful, but WITHOUT ANY WARRANTY; without even the implied warranty
|
|
||||||
* of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU Lesser Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU Lesser Public License
|
|
||||||
* along with the ZBar Bar Code Reader; if not, write to the Free
|
|
||||||
* Software Foundation, Inc., 51 Franklin St, Fifth Floor,
|
|
||||||
* Boston, MA 02110-1301 USA
|
|
||||||
*
|
|
||||||
* http://sourceforge.net/projects/zbar
|
|
||||||
*------------------------------------------------------------------------*/
|
|
||||||
|
|
||||||
package net.sourceforge.zbar;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Decoded symbol coarse orientation.
|
|
||||||
*/
|
|
||||||
public class Orientation {
|
|
||||||
/**
|
|
||||||
* Unable to determine orientation.
|
|
||||||
*/
|
|
||||||
public static final int UNKNOWN = -1;
|
|
||||||
/**
|
|
||||||
* Upright, read left to right.
|
|
||||||
*/
|
|
||||||
public static final int UP = 0;
|
|
||||||
/**
|
|
||||||
* sideways, read top to bottom
|
|
||||||
*/
|
|
||||||
public static final int RIGHT = 1;
|
|
||||||
/**
|
|
||||||
* upside-down, read right to left
|
|
||||||
*/
|
|
||||||
public static final int DOWN = 2;
|
|
||||||
/**
|
|
||||||
* sideways, read bottom to top
|
|
||||||
*/
|
|
||||||
public static final int LEFT = 3;
|
|
||||||
}
|
|
||||||
|
|
@ -1,265 +0,0 @@
|
||||||
/*------------------------------------------------------------------------
|
|
||||||
* Symbol
|
|
||||||
*
|
|
||||||
* Copyright 2007-2010 (c) Jeff Brown <spadix@users.sourceforge.net>
|
|
||||||
*
|
|
||||||
* This file is part of the ZBar Bar Code Reader.
|
|
||||||
*
|
|
||||||
* The ZBar Bar Code Reader is free software; you can redistribute it
|
|
||||||
* and/or modify it under the terms of the GNU Lesser Public License as
|
|
||||||
* published by the Free Software Foundation; either version 2.1 of
|
|
||||||
* the License, or (at your option) any later version.
|
|
||||||
*
|
|
||||||
* The ZBar Bar Code Reader is distributed in the hope that it will be
|
|
||||||
* useful, but WITHOUT ANY WARRANTY; without even the implied warranty
|
|
||||||
* of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU Lesser Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU Lesser Public License
|
|
||||||
* along with the ZBar Bar Code Reader; if not, write to the Free
|
|
||||||
* Software Foundation, Inc., 51 Franklin St, Fifth Floor,
|
|
||||||
* Boston, MA 02110-1301 USA
|
|
||||||
*
|
|
||||||
* http://sourceforge.net/projects/zbar
|
|
||||||
*------------------------------------------------------------------------*/
|
|
||||||
|
|
||||||
package net.sourceforge.zbar;
|
|
||||||
|
|
||||||
import java.io.Closeable;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Immutable container for decoded result symbols associated with an image
|
|
||||||
* or a composite symbol.
|
|
||||||
*/
|
|
||||||
public class Symbol implements Closeable {
|
|
||||||
/**
|
|
||||||
* No symbol decoded.
|
|
||||||
*/
|
|
||||||
public static final int NONE = 0;
|
|
||||||
/**
|
|
||||||
* Symbol detected but not decoded.
|
|
||||||
*/
|
|
||||||
public static final int PARTIAL = 1;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* EAN-8.
|
|
||||||
*/
|
|
||||||
public static final int EAN8 = 8;
|
|
||||||
/**
|
|
||||||
* UPC-E.
|
|
||||||
*/
|
|
||||||
public static final int UPCE = 9;
|
|
||||||
/**
|
|
||||||
* ISBN-10 (from EAN-13).
|
|
||||||
*/
|
|
||||||
public static final int ISBN10 = 10;
|
|
||||||
/**
|
|
||||||
* UPC-A.
|
|
||||||
*/
|
|
||||||
public static final int UPCA = 12;
|
|
||||||
/**
|
|
||||||
* EAN-13.
|
|
||||||
*/
|
|
||||||
public static final int EAN13 = 13;
|
|
||||||
/**
|
|
||||||
* ISBN-13 (from EAN-13).
|
|
||||||
*/
|
|
||||||
public static final int ISBN13 = 14;
|
|
||||||
/**
|
|
||||||
* Interleaved 2 of 5.
|
|
||||||
*/
|
|
||||||
public static final int I25 = 25;
|
|
||||||
/**
|
|
||||||
* DataBar (RSS-14).
|
|
||||||
*/
|
|
||||||
public static final int DATABAR = 34;
|
|
||||||
/**
|
|
||||||
* DataBar Expanded.
|
|
||||||
*/
|
|
||||||
public static final int DATABAR_EXP = 35;
|
|
||||||
/**
|
|
||||||
* Codabar.
|
|
||||||
*/
|
|
||||||
public static final int CODABAR = 38;
|
|
||||||
/**
|
|
||||||
* Code 39.
|
|
||||||
*/
|
|
||||||
public static final int CODE39 = 39;
|
|
||||||
/**
|
|
||||||
* PDF417.
|
|
||||||
*/
|
|
||||||
public static final int PDF417 = 57;
|
|
||||||
/**
|
|
||||||
* QR Code.
|
|
||||||
*/
|
|
||||||
public static final int QRCODE = 64;
|
|
||||||
/**
|
|
||||||
* Code 93.
|
|
||||||
*/
|
|
||||||
public static final int CODE93 = 93;
|
|
||||||
/**
|
|
||||||
* Code 128.
|
|
||||||
*/
|
|
||||||
public static final int CODE128 = 128;
|
|
||||||
|
|
||||||
static {
|
|
||||||
init();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* C pointer to a zbar_symbol_t.
|
|
||||||
*/
|
|
||||||
private long peer;
|
|
||||||
/**
|
|
||||||
* Cached attributes.
|
|
||||||
*/
|
|
||||||
private int type;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Symbols are only created by other package methods.
|
|
||||||
*/
|
|
||||||
Symbol(long peer) {
|
|
||||||
this.peer = peer;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static native void init();
|
|
||||||
|
|
||||||
public void close() {
|
|
||||||
destroy();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Clean up native data associated with an instance.
|
|
||||||
*/
|
|
||||||
public synchronized void destroy() {
|
|
||||||
if(peer != 0) {
|
|
||||||
destroy(peer);
|
|
||||||
peer = 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Release the associated peer instance.
|
|
||||||
*/
|
|
||||||
private native void destroy(long peer);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Retrieve type of decoded symbol.
|
|
||||||
*/
|
|
||||||
public int getType() {
|
|
||||||
if(type == 0) {
|
|
||||||
type = getType(peer);
|
|
||||||
}
|
|
||||||
return (type);
|
|
||||||
}
|
|
||||||
|
|
||||||
private native int getType(long peer);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Retrieve symbology boolean configs settings used during decode.
|
|
||||||
*/
|
|
||||||
public native int getConfigMask();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Retrieve symbology characteristics detected during decode.
|
|
||||||
*/
|
|
||||||
public native int getModifierMask();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Retrieve data decoded from symbol as a String.
|
|
||||||
*/
|
|
||||||
public native String getData();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Retrieve raw data bytes decoded from symbol.
|
|
||||||
*/
|
|
||||||
public native byte[] getDataBytes();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Retrieve a symbol confidence metric. Quality is an unscaled,
|
|
||||||
* relative quantity: larger values are better than smaller
|
|
||||||
* values, where "large" and "small" are application dependent.
|
|
||||||
*/
|
|
||||||
public native int getQuality();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Retrieve current cache count. When the cache is enabled for
|
|
||||||
* the image_scanner this provides inter-frame reliability and
|
|
||||||
* redundancy information for video streams.
|
|
||||||
*
|
|
||||||
* @returns < 0 if symbol is still uncertain
|
|
||||||
* @returns 0 if symbol is newly verified
|
|
||||||
* @returns > 0 for duplicate symbols
|
|
||||||
*/
|
|
||||||
public native int getCount();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Retrieve an approximate, axis-aligned bounding box for the
|
|
||||||
* symbol.
|
|
||||||
*/
|
|
||||||
public int[] getBounds() {
|
|
||||||
int n = getLocationSize(peer);
|
|
||||||
if(n <= 0) {
|
|
||||||
return (null);
|
|
||||||
}
|
|
||||||
|
|
||||||
int[] bounds = new int[4];
|
|
||||||
int xmin = Integer.MAX_VALUE;
|
|
||||||
int xmax = Integer.MIN_VALUE;
|
|
||||||
int ymin = Integer.MAX_VALUE;
|
|
||||||
int ymax = Integer.MIN_VALUE;
|
|
||||||
|
|
||||||
for(int i = 0; i < n; i++) {
|
|
||||||
int x = getLocationX(peer, i);
|
|
||||||
if(xmin > x) {
|
|
||||||
xmin = x;
|
|
||||||
}
|
|
||||||
if(xmax < x) {
|
|
||||||
xmax = x;
|
|
||||||
}
|
|
||||||
|
|
||||||
int y = getLocationY(peer, i);
|
|
||||||
if(ymin > y) {
|
|
||||||
ymin = y;
|
|
||||||
}
|
|
||||||
if(ymax < y) {
|
|
||||||
ymax = y;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
bounds[0] = xmin;
|
|
||||||
bounds[1] = ymin;
|
|
||||||
bounds[2] = xmax - xmin;
|
|
||||||
bounds[3] = ymax - ymin;
|
|
||||||
return (bounds);
|
|
||||||
}
|
|
||||||
|
|
||||||
private native int getLocationSize(long peer);
|
|
||||||
|
|
||||||
private native int getLocationX(long peer, int idx);
|
|
||||||
|
|
||||||
private native int getLocationY(long peer, int idx);
|
|
||||||
|
|
||||||
public int[] getLocationPoint(int idx) {
|
|
||||||
int[] p = new int[2];
|
|
||||||
p[0] = getLocationX(peer, idx);
|
|
||||||
p[1] = getLocationY(peer, idx);
|
|
||||||
return (p);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Retrieve general axis-aligned, orientation of decoded
|
|
||||||
* symbol.
|
|
||||||
*/
|
|
||||||
public native int getOrientation();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Retrieve components of a composite result.
|
|
||||||
*/
|
|
||||||
public SymbolSet getComponents() {
|
|
||||||
return (new SymbolSet(getComponents(peer)));
|
|
||||||
}
|
|
||||||
|
|
||||||
private native long getComponents(long peer);
|
|
||||||
|
|
||||||
native long next();
|
|
||||||
}
|
|
||||||
|
|
@ -1,75 +0,0 @@
|
||||||
/*------------------------------------------------------------------------
|
|
||||||
* SymbolIterator
|
|
||||||
*
|
|
||||||
* Copyright 2007-2010 (c) Jeff Brown <spadix@users.sourceforge.net>
|
|
||||||
*
|
|
||||||
* This file is part of the ZBar Bar Code Reader.
|
|
||||||
*
|
|
||||||
* The ZBar Bar Code Reader is free software; you can redistribute it
|
|
||||||
* and/or modify it under the terms of the GNU Lesser Public License as
|
|
||||||
* published by the Free Software Foundation; either version 2.1 of
|
|
||||||
* the License, or (at your option) any later version.
|
|
||||||
*
|
|
||||||
* The ZBar Bar Code Reader is distributed in the hope that it will be
|
|
||||||
* useful, but WITHOUT ANY WARRANTY; without even the implied warranty
|
|
||||||
* of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU Lesser Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU Lesser Public License
|
|
||||||
* along with the ZBar Bar Code Reader; if not, write to the Free
|
|
||||||
* Software Foundation, Inc., 51 Franklin St, Fifth Floor,
|
|
||||||
* Boston, MA 02110-1301 USA
|
|
||||||
*
|
|
||||||
* http://sourceforge.net/projects/zbar
|
|
||||||
*------------------------------------------------------------------------*/
|
|
||||||
|
|
||||||
package net.sourceforge.zbar;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Iterator over a SymbolSet.
|
|
||||||
*/
|
|
||||||
public class SymbolIterator implements java.util.Iterator<Symbol> {
|
|
||||||
/**
|
|
||||||
* Next symbol to be returned by the iterator.
|
|
||||||
*/
|
|
||||||
private Symbol current;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* SymbolIterators are only created by internal interface methods.
|
|
||||||
*/
|
|
||||||
SymbolIterator(Symbol first) {
|
|
||||||
current = first;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns true if the iteration has more elements.
|
|
||||||
*/
|
|
||||||
public boolean hasNext() {
|
|
||||||
return (current != null);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Retrieves the next element in the iteration.
|
|
||||||
*/
|
|
||||||
public Symbol next() {
|
|
||||||
if(current == null) {
|
|
||||||
throw (new java.util.NoSuchElementException("access past end of SymbolIterator"));
|
|
||||||
}
|
|
||||||
|
|
||||||
Symbol result = current;
|
|
||||||
long sym = current.next();
|
|
||||||
if(sym != 0) {
|
|
||||||
current = new Symbol(sym);
|
|
||||||
} else {
|
|
||||||
current = null;
|
|
||||||
}
|
|
||||||
return (result);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Raises UnsupportedOperationException.
|
|
||||||
*/
|
|
||||||
public void remove() {
|
|
||||||
throw (new UnsupportedOperationException("SymbolIterator is immutable"));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,93 +0,0 @@
|
||||||
/*------------------------------------------------------------------------
|
|
||||||
* SymbolSet
|
|
||||||
*
|
|
||||||
* Copyright 2007-2010 (c) Jeff Brown <spadix@users.sourceforge.net>
|
|
||||||
*
|
|
||||||
* This file is part of the ZBar Bar Code Reader.
|
|
||||||
*
|
|
||||||
* The ZBar Bar Code Reader is free software; you can redistribute it
|
|
||||||
* and/or modify it under the terms of the GNU Lesser Public License as
|
|
||||||
* published by the Free Software Foundation; either version 2.1 of
|
|
||||||
* the License, or (at your option) any later version.
|
|
||||||
*
|
|
||||||
* The ZBar Bar Code Reader is distributed in the hope that it will be
|
|
||||||
* useful, but WITHOUT ANY WARRANTY; without even the implied warranty
|
|
||||||
* of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU Lesser Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU Lesser Public License
|
|
||||||
* along with the ZBar Bar Code Reader; if not, write to the Free
|
|
||||||
* Software Foundation, Inc., 51 Franklin St, Fifth Floor,
|
|
||||||
* Boston, MA 02110-1301 USA
|
|
||||||
*
|
|
||||||
* http://sourceforge.net/projects/zbar
|
|
||||||
*------------------------------------------------------------------------*/
|
|
||||||
|
|
||||||
package net.sourceforge.zbar;
|
|
||||||
|
|
||||||
import java.io.Closeable;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Immutable container for decoded result symbols associated with an image
|
|
||||||
* or a composite symbol.
|
|
||||||
*/
|
|
||||||
public class SymbolSet extends java.util.AbstractCollection<Symbol> implements Closeable {
|
|
||||||
static {
|
|
||||||
init();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* C pointer to a zbar_symbol_set_t.
|
|
||||||
*/
|
|
||||||
private long peer;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* SymbolSets are only created by other package methods.
|
|
||||||
*/
|
|
||||||
SymbolSet(long peer) {
|
|
||||||
this.peer = peer;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static native void init();
|
|
||||||
|
|
||||||
public void close() {
|
|
||||||
destroy();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Clean up native data associated with an instance.
|
|
||||||
*/
|
|
||||||
public synchronized void destroy() {
|
|
||||||
if(peer != 0) {
|
|
||||||
destroy(peer);
|
|
||||||
peer = 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Release the associated peer instance.
|
|
||||||
*/
|
|
||||||
private native void destroy(long peer);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Retrieve an iterator over the Symbol elements in this collection.
|
|
||||||
*/
|
|
||||||
public java.util.Iterator<Symbol> iterator() {
|
|
||||||
long sym = firstSymbol(peer);
|
|
||||||
if(sym == 0) {
|
|
||||||
return (new SymbolIterator(null));
|
|
||||||
}
|
|
||||||
|
|
||||||
return (new SymbolIterator(new Symbol(sym)));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Retrieve the number of elements in the collection.
|
|
||||||
*/
|
|
||||||
public native int size();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Retrieve C pointer to first symbol in the set.
|
|
||||||
*/
|
|
||||||
private native long firstSymbol(long peer);
|
|
||||||
}
|
|
||||||
|
|
@ -329,6 +329,10 @@ HorizontalHeaderColumn > TableColumnHeader.column-header.table-column{
|
||||||
-fx-stroke: #696c77;
|
-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%);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -46,7 +46,7 @@
|
||||||
|
|
||||||
.id, .fixed-width {
|
.id, .fixed-width {
|
||||||
-fx-font-size: 13px;
|
-fx-font-size: 13px;
|
||||||
-fx-font-family: 'Roboto Mono';
|
-fx-font-family: 'Fragment Mono Regular';
|
||||||
}
|
}
|
||||||
|
|
||||||
.form-separator {
|
.form-separator {
|
||||||
|
|
|
||||||
|
|
@ -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: 'Roboto Mono';
|
-fx-font-family: 'Fragment Mono Regular';
|
||||||
-fx-padding: 4;
|
-fx-padding: 4;
|
||||||
-fx-fill: -fx-text-inner-color;
|
-fx-fill: -fx-text-inner-color;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -44,7 +44,7 @@
|
||||||
|
|
||||||
#transactionDiagram .input-label, #transactionDiagram .recipient-label, #transactionDiagram .change-label, #transactionDiagram .fee-tooltip, #transactionDiagram .transaction-tooltip {
|
#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: 'Roboto Mono';
|
-fx-font-family: 'Fragment Mono Regular';
|
||||||
}
|
}
|
||||||
|
|
||||||
#transactionDiagram .fee-warning-icon {
|
#transactionDiagram .fee-warning-icon {
|
||||||
|
|
|
||||||
|
|
@ -40,7 +40,9 @@
|
||||||
<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">
|
||||||
|
|
@ -74,9 +76,11 @@
|
||||||
|
|
||||||
<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">
|
<VBox spacing="8" alignment="CENTER">
|
||||||
<TransactionDiagram fx:id="transactionDiagram" maxWidth="700" final="true"/>
|
<Region VBox.vgrow="SOMETIMES" />
|
||||||
<TransactionDiagramLabel fx:id="transactionDiagramLabel" maxWidth="640" prefWidth="640" />
|
<TransactionDiagram fx:id="transactionDiagram" final="true"/>
|
||||||
|
<TransactionDiagramLabel fx:id="transactionDiagramLabel" />
|
||||||
|
<Region VBox.vgrow="SOMETIMES" />
|
||||||
</VBox>
|
</VBox>
|
||||||
</Tab>
|
</Tab>
|
||||||
<Tab text="Detail" closable="false">
|
<Tab text="Detail" closable="false">
|
||||||
|
|
@ -176,19 +180,44 @@
|
||||||
|
|
||||||
<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"/>
|
||||||
|
|
||||||
<DynamicForm fx:id="blockchainForm" GridPane.columnIndex="0" GridPane.columnSpan="2" GridPane.rowIndex="6">
|
<GridPane GridPane.columnIndex="0" GridPane.columnSpan="2" GridPane.rowIndex="6">
|
||||||
<Fieldset text="Blockchain" inputGrow="SOMETIMES">
|
<columnConstraints>
|
||||||
<Field text="Status:">
|
<ColumnConstraints percentWidth="80" />
|
||||||
<Label fx:id="blockStatus" contentDisplay="RIGHT" graphicTextGap="5" />
|
<ColumnConstraints percentWidth="20" />
|
||||||
</Field>
|
</columnConstraints>
|
||||||
<Field fx:id="blockHeightField" text="Block Height:">
|
<DynamicForm fx:id="blockchainForm" GridPane.columnIndex="0" GridPane.rowIndex="0">
|
||||||
<CopyableLabel fx:id="blockHeight" />
|
<Fieldset text="Blockchain" inputGrow="SOMETIMES">
|
||||||
</Field>
|
<Field text="Status:">
|
||||||
<Field fx:id="blockTimestampField" text="Timestamp:">
|
<Label fx:id="blockStatus" contentDisplay="RIGHT" graphicTextGap="5" />
|
||||||
<CopyableLabel fx:id="blockTimestamp" />
|
</Field>
|
||||||
</Field>
|
<Field fx:id="blockHeightField" text="Block Height:">
|
||||||
</Fieldset>
|
<CopyableLabel fx:id="blockHeight" />
|
||||||
</DynamicForm>
|
</Field>
|
||||||
|
<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">
|
||||||
|
|
@ -236,7 +265,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" prefHeight="50" />
|
<ProgressBar fx:id="broadcastProgressBar" maxWidth="Infinity" minHeight="50" prefHeight="50" />
|
||||||
</VBox>
|
</VBox>
|
||||||
<VBox>
|
<VBox>
|
||||||
<HBox fx:id="signButtonBox" styleClass="signatures-buttons" spacing="20">
|
<HBox fx:id="signButtonBox" styleClass="signatures-buttons" spacing="20">
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,7 @@
|
||||||
|
|
||||||
.chart-legend-item {
|
.chart-legend-item {
|
||||||
-fx-font-size: 13px;
|
-fx-font-size: 13px;
|
||||||
-fx-font-family: 'Roboto Mono';
|
-fx-font-family: 'Fragment Mono Regular';
|
||||||
}
|
}
|
||||||
|
|
||||||
.default-color0.chart-pie { -fx-pie-color: #ca1243 }
|
.default-color0.chart-pie { -fx-pie-color: #ca1243 }
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,7 @@
|
||||||
|
|
||||||
.chart-legend-item {
|
.chart-legend-item {
|
||||||
-fx-font-size: 13;
|
-fx-font-size: 13;
|
||||||
-fx-font-family: 'Roboto Mono';
|
-fx-font-family: 'Fragment Mono Regular';
|
||||||
}
|
}
|
||||||
|
|
||||||
.default-color7.chart-pie { -fx-pie-color: #0184bc }
|
.default-color7.chart-pie { -fx-pie-color: #0184bc }
|
||||||
|
|
|
||||||
|
|
@ -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: 'Roboto Mono';
|
-fx-font-family: 'Fragment Mono Regular';
|
||||||
-fx-padding: 2;
|
-fx-padding: 2;
|
||||||
color-0: #ca1243;
|
color-0: #ca1243;
|
||||||
color-1: #d75f00;
|
color-1: #d75f00;
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,7 @@
|
||||||
|
|
||||||
#fingerprint, #derivation, #xpub {
|
#fingerprint, #derivation, #xpub {
|
||||||
-fx-font-size: 13px;
|
-fx-font-size: 13px;
|
||||||
-fx-font-family: 'Roboto Mono';
|
-fx-font-family: 'Fragment Mono Regular';
|
||||||
}
|
}
|
||||||
|
|
||||||
#type {
|
#type {
|
||||||
|
|
|
||||||
|
|
@ -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" />
|
<ColumnConstraints prefWidth="410" hgrow="SOMETIMES" />
|
||||||
<ColumnConstraints prefWidth="200" />
|
<ColumnConstraints prefWidth="200" />
|
||||||
<ColumnConstraints prefWidth="105" />
|
<ColumnConstraints prefWidth="105" />
|
||||||
</columnConstraints>
|
</columnConstraints>
|
||||||
|
|
|
||||||
|
|
@ -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" />
|
<ColumnConstraints prefWidth="620" hgrow="SOMETIMES" />
|
||||||
<ColumnConstraints prefWidth="140" />
|
<ColumnConstraints prefWidth="140" />
|
||||||
</columnConstraints>
|
</columnConstraints>
|
||||||
<rowConstraints>
|
<rowConstraints>
|
||||||
|
|
|
||||||
|
|
@ -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: 'Roboto Mono';
|
-fx-font-family: 'Fragment Mono Regular';
|
||||||
}
|
}
|
||||||
|
|
||||||
#transactionDiagram .fee-warning-icon {
|
#transactionDiagram .fee-warning-icon {
|
||||||
|
|
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue