Compare commits

...

82 commits

Author SHA1 Message Date
Craig Raw
3a5fa69fb6 fix occasional issue with cell reuse when avoiding updating cells during table size estimation 2025-11-05 12:20:02 +02:00
Craig Raw
4774830ce4 add yu12 to supported pixel formats on linux 2025-11-05 07:51:26 +02:00
Craig Raw
2f62a9e9c8 show signing keystores in transaction blockchain form for spends from multisig wallets 2025-11-04 10:45:31 +02:00
doblon8
75bcfe2253
update jzbar dependency to 0.2.1 2025-10-24 09:38:11 +02:00
Craig Raw
bedf1399ea request display of path when retreiving xpubs on ledger devices for any non-standard path 2025-10-24 08:38:48 +02:00
Craig Raw
58575793ea update openpnp-capture to 0.0.30-1 2025-10-21 15:55:32 +02:00
Craig Raw
6c9b580d4f refactor to use transaction parameters record object when creating a wallet transaction 2025-10-21 12:06:00 +02:00
doblon8
31909b7a15
use language-independent sid for windows users group permission 2025-10-21 09:35:18 +02:00
Craig Raw
092267339a adapt to use declarative style to for consolidation payments 2025-10-17 10:27:20 +02:00
Craig Raw
0974918cff hide confirmations in tooltip when showing inputs and outputs on the transactions table 2025-10-16 08:42:55 +02:00
Craig Raw
0f4c36b3c2 add ctrl+shift+left/right keyboard shortcuts for moving tabs left and right 2025-10-13 14:26:37 +02:00
Craig Raw
e1fe35fb74 update nsmenufx to avoid npe 2025-10-06 14:52:24 +02:00
Craig Raw
d37fd00c4b avoid using deprecated camera device type constants on recent macos versions 2025-10-06 13:27:12 +02:00
Craig Raw
5f54f86df7 bump to v2.3.1 2025-10-03 11:53:27 +02:00
Craig Raw
e2fa3df08d restore pre gradle 9 archive task behaviour for file permissions 2025-10-03 10:09:57 +02:00
Craig Raw
6d6ede9abe bump to v2.3.0 2025-10-03 08:08:25 +02:00
Craig Raw
cca9ab1056 improve implementation of adding dns payment information from psbt 2025-10-02 14:58:02 +02:00
Craig Raw
9e33861110 revert to javafx 23 due to jpackage launcher link bug 2025-10-01 12:10:39 +02:00
Craig Raw
c3d3fd1fda revert to java 22 and javafx 24 due to bug in jpackage launcher linking (jdk-8345810) 2025-10-01 11:52:45 +02:00
Craig Raw
ca8553ecb8 revert continuity camera device change as unsupported on macos 13 2025-09-30 12:33:33 +02:00
Craig Raw
d23ee8c086 upgrade openpnp-capture to iterate over continuity camera devices on mac 2025-09-30 12:24:22 +02:00
Craig Raw
e776a17ad4 upgrade jdbi to remove older caffeine dependency 2025-09-30 09:37:45 +02:00
Craig Raw
480ce1e476 fix deprecation warning 2025-09-29 17:39:48 +02:00
Craig Raw
656cd90b08 upgrade guava and commons-lang3 2025-09-29 14:34:07 +02:00
Craig Raw
8df0777959 upgrade to java 25 and javafx 25 2025-09-29 13:32:56 +02:00
Craig Raw
84566b92e6 remove unnecessary zbar native libraries 2025-09-29 12:39:49 +02:00
Craig Raw
7802510e58 support dns hrns in send to many dialog 2025-09-29 11:53:52 +02:00
Craig Raw
efb1eb1051 add initial sending to silent payments support 2025-09-29 08:37:07 +02:00
Craig Raw
6240667478 improve error dialog on payjoin receiver error 2025-09-02 09:49:39 +02:00
Craig Raw
2c27112dad update drongo 2025-08-16 13:03:03 +02:00
Craig Raw
6d53e1ed1d fix bluewallet spelling 2025-08-12 08:09:01 +02:00
Craig Raw
e8c5660897 allow transaction diagram input and output labels to expand into available width 2025-08-11 14:03:51 +02:00
Craig Raw
bef6c750bd upgrade to gradle 8.14.3 2025-08-07 11:28:27 +02:00
Craig Raw
4ec3603789 fix non bip32 child derivation test 2025-08-07 08:55:19 +02:00
Craig Raw
90c9f9733f display a warning if an output descriptor provided in the wallet settings will be modified for use 2025-08-05 09:28:52 +02:00
Craig Raw
64efcf67d3 display zero byte length witness elements as empty instead of op_0 2025-08-04 13:38:41 +02:00
Craig Raw
385d173948 handle npe connecting to bitcoin core with wallet functionality disabled 2025-08-01 07:44:34 +02:00
Craig Raw
d81b868049 add any dns payment instructions from loaded psbts if not already cached 2025-07-31 11:47:43 +02:00
Craig Raw
2ff7a15d1e add padding to writes when connected over tls 2025-07-31 10:34:56 +02:00
Craig Raw
f48fa7e23c support zero in pin keypad for onekey classic pin entry 2025-07-29 14:45:30 +02:00
Craig Raw
4632850e1e use improved dnssec validation and handle offline state when resolving bip 353 hrns 2025-07-29 12:52:10 +02:00
Craig Raw
5f62523710 support sending to and displaying bip353 human readable names and include dnssec proof in associated psbts 2025-07-24 14:36:11 +02:00
Craig Raw
9dcf210762 support creating transactions with the minimum relay fee rate configured by the user or set by the connected server 2025-07-17 09:15:59 +02:00
Craig Raw
c7e9a0a161 restore coingecko historical rate support by limiting to the last 365 days 2025-07-15 09:06:52 +02:00
Craig Raw
fa10714844 followup 2025-07-10 08:40:31 +02:00
Craig Raw
80105aee62 save webcam device unique id instead of name to config 2025-07-10 08:15:00 +02:00
Craig Raw
3c5fa58a16 suppress warnings for jzbar ffm usage 2025-07-09 12:46:57 +02:00
doblon8
2a2be2617c
replace jni-based zbar wrapper with ffm-based jzbar
* Replace ZBar JNI library implementation with jzbar

* Move ZBar.java to com.sparrowwallet.sparrow.control package

* Move ZBar.java to the com.sparrowwallet.sparrow.io package

* Switch to jzbar from Maven Central and update module/package imports accordingly

* Remove jzbar entry from extraJavaModuleInfo to avoid module patching error
2025-07-09 11:55:51 +02:00
Craig Raw
6c9a0d14cd compare on device unique id when choosing selected camera 2025-07-09 10:58:58 +02:00
Craig Raw
f82fcb58bb fix issue of including parent path elements in deterministic key when deriving child xpub from an output descriptor containing more than two child path elements 2025-07-09 10:26:49 +02:00
Craig Raw
5ec3bff6a4 fix jade configuration for signet and regtest networks 2025-07-02 16:45:10 +02:00
Oleg Koretsky
134dc826ba
do not change coin label unit on right click 2025-07-02 16:32:10 +02:00
Craig Raw
cd2a6623a4 fix restart menu options on linux standalone package 2025-07-01 16:07:45 +02:00
Craig Raw
49ab9e40e3 select first matching webcam by name 2025-06-28 16:10:57 +02:00
Craig Raw
cec7eac9ac fix selection of nearest supported resolution where chosen resolution is not available 2025-06-24 11:26:13 +02:00
Craig Raw
33e043fd9a include child derivations in output descriptor for bip 129 wallet export 2025-06-24 10:52:52 +02:00
Craig Raw
3aae26b196 bump to v2.2.4 2025-06-10 09:01:37 +02:00
Craig Raw
73d4fd5049 prevent double free when closing capture library 2025-06-09 14:43:06 +02:00
Craig Raw
a94380e882 minor specter diy ui tweaks 2025-06-07 11:23:06 +02:00
Craig Raw
e4dd4950bf prevent selection of unsupported bip322 format when signing a message with a connected device 2025-06-06 13:07:36 +02:00
Craig Raw
26ce1b3469 derive to maximum bip32 account level where child path in output descriptor contains more than two elements 2025-06-06 11:45:46 +02:00
Craig Raw
ebce34f3d1 minor tweaks 2025-06-05 14:23:02 +02:00
Craig Raw
f28e00b97e suggest opening the send to many dialog when adding multiple payments on the send tab 2025-06-05 10:31:37 +02:00
Craig Raw
25770c2426 suggest connecting to broadcast a finalized transaction if offlineand a server is configured 2025-06-05 09:40:17 +02:00
Craig Raw
799cac7b1f handle bitkey descriptor export format 2025-06-05 08:28:21 +02:00
Craig Raw
c265fd1969 fix cormorant server.version rpc issue 2025-06-04 17:18:31 +02:00
Craig Raw
890f0476b1 introduce delay before closing capture library 2025-06-04 15:23:48 +02:00
Craig Raw
4d93381124 improve electrum server script hash unsubscribe support 2025-06-04 14:52:33 +02:00
Craig Raw
364909cfa3 support nv12 capture pixel format on linux 2025-06-03 12:48:01 +02:00
Craig Raw
38f0068411 detect if electrum server supports scripthash unsubscribe capability 2025-06-03 12:38:03 +02:00
Craig Raw
8885e48ed9 request rgb3 pixel format on linux where returned format is unsupported 2025-06-02 16:28:44 +02:00
Craig Raw
31ce3ce68a further electrum server optimisations 2025-06-02 15:56:46 +02:00
Craig Raw
b0d0514617 remove possibility of task queueing in webcam service 2025-06-02 11:36:06 +02:00
Craig Raw
d7d23f9b58 always use the master wallet payment code when creating the notification transaction payload on the send tab 2025-06-02 09:41:46 +02:00
Craig Raw
3fdf093a26 use semaphore to ensure last webcam service task has completed before closing stream 2025-05-29 14:17:58 +02:00
Craig Raw
74c298fd93 iterate and remove faulty capture devices on opening qr scan dialog 2025-05-29 13:58:46 +02:00
Craig Raw
4298bfb053 bump to v2.2.3 2025-05-22 14:58:09 +02:00
Craig Raw
231eb13cee retrieve and show next block median fee rate in recent blocks view where available 2025-05-22 13:35:59 +02:00
Craig Raw
52470ee6d8 further electrum server optimization tweaks 2025-05-22 11:59:25 +02:00
Craig Raw
853949675e fix npe configuring recent blocks view on new installs 2025-05-22 08:44:39 +02:00
Craig Raw
098afebbe0 increase recent blocks estimated fee rate update frequency 2025-05-21 15:38:06 +02:00
Craig Raw
63c0a6d6e2 bump to v2.2.2 2025-05-21 13:29:16 +02:00
114 changed files with 2077 additions and 1569 deletions

View file

@ -12,11 +12,11 @@ jobs:
matrix: matrix:
os: [windows-2022, ubuntu-22.04, ubuntu-22.04-arm, macos-13, macos-14] os: [windows-2022, ubuntu-22.04, ubuntu-22.04-arm, macos-13, macos-14]
steps: steps:
- uses: actions/checkout@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'

View file

@ -1,8 +1,8 @@
plugins { plugins {
id 'application' id 'application'
id 'org-openjfx-javafxplugin' id 'org-openjfx-javafxplugin'
id 'org.beryx.jlink' version '3.1.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.3' id 'io.matthewnelson.kmp.tor.resource-filterjar' version '408.16.3'
} }
@ -19,17 +19,16 @@ if(System.getProperty("os.arch") == "aarch64") {
} }
def headless = "true".equals(System.getProperty("java.awt.headless")) def headless = "true".equals(System.getProperty("java.awt.headless"))
group 'com.sparrowwallet' group = 'com.sparrowwallet'
version '2.2.1' 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.3") implementation("io.matthewnelson.kmp-tor:resource-exec-tor-gpl:408.16.3")
implementation('org.jetbrains.kotlinx:kotlinx-coroutines-javafx:1.10.1') { implementation('org.jetbrains.kotlinx:kotlinx-coroutines-javafx:1.10.1') {
exclude group: 'org.jetbrains.kotlin', module: 'kotlin-stdlib-common' exclude group: 'org.jetbrains.kotlin', module: 'kotlin-stdlib-common'
} }
implementation('de.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')

View file

@ -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/")
} }
} }

View file

@ -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);
} }
} }

View file

@ -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");

View file

@ -83,7 +83,7 @@ sudo apt install -y rpm fakeroot binutils
First, assign a temporary variable in your shell for the specific release you want to build. For the current one specify: First, assign a temporary variable in your shell for the specific release you want to build. For the current one specify:
```shell ```shell
GIT_TAG="2.2.0" 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

@ -1 +1 @@
Subproject commit abb598d3b041a9d0b3d0ba41b5fb9785e2100193 Subproject commit e975cbe6f8d8574785124e6db5780d0541e20024

Binary file not shown.

View file

@ -1,6 +1,6 @@
distributionBase=GRADLE_USER_HOME distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-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
View file

@ -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
View file

@ -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

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

View file

@ -21,7 +21,7 @@
<key>CFBundlePackageType</key> <key>CFBundlePackageType</key>
<string>APPL</string> <string>APPL</string>
<key>CFBundleShortVersionString</key> <key>CFBundleShortVersionString</key>
<string>2.2.1</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>

View file

@ -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());

View file

@ -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

View file

@ -113,8 +113,8 @@ public class SparrowDesktop extends Application {
private void initializeFonts() { private void initializeFonts() {
GlyphFontRegistry.register(new FontAwesome5()); GlyphFontRegistry.register(new FontAwesome5());
GlyphFontRegistry.register(new FontAwesome5Brands()); GlyphFontRegistry.register(new FontAwesome5Brands());
Font.loadFont(AppServices.class.getResourceAsStream("/font/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);
} }

View file

@ -18,7 +18,7 @@ import java.util.*;
public class SparrowWallet { public class SparrowWallet {
public static final String APP_ID = "sparrow"; public static final String APP_ID = "sparrow";
public static final String APP_NAME = "Sparrow"; public static final String APP_NAME = "Sparrow";
public static final String APP_VERSION = "2.2.1"; 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";

View file

@ -52,7 +52,10 @@ public class BlockCube extends 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);
this.feeRatesSource.set(Config.get().getFeeRatesSource());
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) {
@ -198,6 +201,8 @@ public class BlockCube extends Group {
} else { } else {
feeRateIcon.getChildren().clear(); feeRateIcon.getChildren().clear();
} }
} else {
feeRateIcon.getChildren().clear();
} }
} }
} }

View file

@ -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() + ")" : ""));
} }

View file

@ -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);
});
}
}
} }

View file

@ -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();
}
}

View file

@ -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();
} }

View file

@ -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);

View file

@ -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")));
}
} }

View file

@ -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);
} }

View file

@ -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();

View file

@ -398,14 +398,14 @@ public class PrivateKeySweepDialog extends Dialog<Transaction> {
double feeRate = feeRange.getFeeRate(); double feeRate = feeRange.getFeeRate();
long fee = (long)Math.ceil(noFeeTransaction.getVirtualSize() * feeRate); long fee = (long)Math.ceil(noFeeTransaction.getVirtualSize() * feeRate);
if(feeRate == 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).");

View file

@ -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();
} }

View file

@ -17,8 +17,10 @@ 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; import static com.sparrowwallet.sparrow.control.BlockCube.CUBE_SIZE;
public class RecentBlocksView extends Pane { public class RecentBlocksView extends Pane {
@ -48,7 +50,9 @@ public class RecentBlocksView extends Pane {
} }
})); }));
updateFeeRatesSource(Config.get().getFeeRatesSource()); FeeRatesSource feeRatesSource = Config.get().getFeeRatesSource();
feeRatesSource = (feeRatesSource == null ? FeeRatesSource.MEMPOOL_SPACE : feeRatesSource);
updateFeeRatesSource(feeRatesSource);
Tooltip.install(this, tooltip); Tooltip.install(this, tooltip);
} }
@ -104,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;
} }
@ -136,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();

View file

@ -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()) {

View file

@ -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) {}
} }

View file

@ -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);
}
} }
} }
} }

View file

@ -90,20 +90,20 @@ public class TransactionDiagramLabel extends HBox {
outputLabels.add(mixOutputLabel); outputLabels.add(mixOutputLabel);
} }
} else if(walletTx.getPayments().size() >= 5 && walletTx.getPayments().stream().mapToLong(Payment::getAmount).distinct().count() <= 1 && walletTx.getWallet() != null } else if(walletTx.getPayments().size() >= 5 && walletTx.getPayments().stream().mapToLong(Payment::getAmount).distinct().count() <= 1 && walletTx.getWallet() != null
&& walletTx.getWallet().getStandardAccountType() == StandardAccount.WHIRLPOOL_POSTMIX && walletTx.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);

View file

@ -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;
} }

View file

@ -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 {

View file

@ -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);
} }
}); });

View file

@ -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;
}
} }

View file

@ -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;
}
} }

View file

@ -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;
}
} }

View file

@ -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;
}
}

View file

@ -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;
}
} }

View file

@ -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);
}
}

View file

@ -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;
}
} }

View file

@ -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");

View file

@ -192,7 +192,7 @@ public class Bip129 implements KeystoreFileExport, KeystoreFileImport, WalletExp
public void exportWallet(Wallet wallet, OutputStream outputStream, String password) throws ExportException { public void exportWallet(Wallet wallet, OutputStream outputStream, String password) throws ExportException {
try { try {
String record = "BSMS 1.0\n" + String record = "BSMS 1.0\n" +
OutputDescriptor.getOutputDescriptor(wallet) + 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));

View file

@ -9,7 +9,7 @@ import java.io.InputStream;
public class BlueWalletMultisig extends ColdcardMultisig { public class BlueWalletMultisig extends ColdcardMultisig {
@Override @Override
public String getName() { public String getName() {
return "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

View file

@ -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;
} }

View file

@ -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());
} }
} }

View file

@ -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);

View file

@ -684,7 +684,7 @@ public class Storage {
public static Executor getSingleThreadedExecutor() { public static Executor getSingleThreadedExecutor() {
if(singleThreadedExecutor == null) { if(singleThreadedExecutor == null) {
BasicThreadFactory factory = 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);
} }

View file

@ -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];

View file

@ -171,7 +171,7 @@ public class DbPersistence implements Persistence {
private synchronized void createUpdateExecutor(Wallet masterWallet) { private synchronized void createUpdateExecutor(Wallet masterWallet) {
if(updateExecutor == null) { if(updateExecutor == null) {
BasicThreadFactory factory = 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);
} }
} }

View file

@ -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;
@ -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;
} }

View file

@ -124,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)))
@ -167,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()) {

View file

@ -34,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());
@ -130,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");
} }
@ -199,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);
@ -309,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();

View file

@ -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;
}
} }

View file

@ -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;
}
} }

View file

@ -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 {

View file

@ -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)));
} }
} }

View file

@ -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[] {

View file

@ -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();
} }

View file

@ -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) {

View file

@ -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());

View file

@ -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);

View file

@ -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) {

View file

@ -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())) {

View file

@ -337,7 +337,7 @@ public class InputController extends TransactionFormController implements Initia
} }
} else { } else {
if(txInput.isAbsoluteTimeLocked()) { if(txInput.isAbsoluteTimeLocked()) {
txInput.setSequenceNumber(TransactionInput.SEQUENCE_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));

View file

@ -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());
}
}
} }

View file

@ -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());

View file

@ -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());
}
}
} }

View file

@ -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;
}
} }

View file

@ -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;

View file

@ -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);

View file

@ -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();
}
};
}
}
} }

View file

@ -6,12 +6,12 @@ import com.sparrowwallet.drongo.KeyPurpose;
import com.sparrowwallet.drongo.Network; import com.sparrowwallet.drongo.Network;
import com.sparrowwallet.drongo.SecureString; import com.sparrowwallet.drongo.SecureString;
import com.sparrowwallet.drongo.address.Address; import com.sparrowwallet.drongo.address.Address;
import com.sparrowwallet.drongo.address.InvalidAddressException;
import com.sparrowwallet.drongo.bip47.PaymentCode; import com.sparrowwallet.drongo.bip47.PaymentCode;
import com.sparrowwallet.drongo.bip47.SecretPoint; import com.sparrowwallet.drongo.bip47.SecretPoint;
import com.sparrowwallet.drongo.crypto.ECKey; import com.sparrowwallet.drongo.crypto.ECKey;
import com.sparrowwallet.drongo.protocol.*; import com.sparrowwallet.drongo.protocol.*;
import com.sparrowwallet.drongo.psbt.PSBT; import com.sparrowwallet.drongo.psbt.PSBT;
import com.sparrowwallet.drongo.silentpayments.SilentPayment;
import com.sparrowwallet.drongo.wallet.*; import com.sparrowwallet.drongo.wallet.*;
import com.sparrowwallet.sparrow.*; import com.sparrowwallet.sparrow.*;
import com.sparrowwallet.sparrow.control.*; import com.sparrowwallet.sparrow.control.*;
@ -172,7 +172,7 @@ public class SendController extends WalletFormController implements Initializabl
private final Set<WalletNode> excludedChangeNodes = new HashSet<>(); private final Set<WalletNode> excludedChangeNodes = new HashSet<>();
private final Map<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);
} }
} }
@ -1605,18 +1627,26 @@ public class SendController extends WalletFormController implements Initializabl
recentBlocksView.updateFeeRatesSource(event.getFeeRateSource()); recentBlocksView.updateFeeRatesSource(event.getFeeRateSource());
} }
@Subscribe
public void connection(ConnectionEvent event) {
if(!Objects.equals(event.getMinimumRelayFeeRate(), event.getPreviousMinimumRelayFeeRate())) {
feeRange.updateFeeRange(event.getMinimumRelayFeeRate(), event.getPreviousMinimumRelayFeeRate());
updateTransaction();
}
}
private class PrivacyAnalysisTooltip extends VBox { private class PrivacyAnalysisTooltip extends VBox {
private final List<Label> analysisLabels = new ArrayList<>(); private final List<Label> analysisLabels = new ArrayList<>();
public PrivacyAnalysisTooltip(WalletTransaction walletTransaction) { public PrivacyAnalysisTooltip(WalletTransaction walletTransaction) {
List<Payment> payments = walletTransaction.getPayments(); List<Payment> payments = walletTransaction.getPayments();
List<Payment> userPayments = payments.stream().filter(payment -> payment.getType() != Payment.Type.FAKE_MIX).collect(Collectors.toList()); List<Payment> userPayments = payments.stream().filter(payment -> payment.getType() != Payment.Type.FAKE_MIX).collect(Collectors.toList());
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) {

View file

@ -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", ""));
} }
} }

View file

@ -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;
} }

View file

@ -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;
}

View file

@ -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);
}

View file

@ -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);
}

View file

@ -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;
}

View file

@ -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;
}

View file

@ -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();
}

View file

@ -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"));
}
}

View file

@ -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);
}

View file

@ -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%);
} }

View file

@ -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 {

View file

@ -29,7 +29,7 @@
.virtualized-scroll-pane .code-area, .uneditable-codearea { .virtualized-scroll-pane .code-area, .uneditable-codearea {
-fx-font-size: 13px; -fx-font-size: 13px;
-fx-font-family: '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;
} }

View file

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

View file

@ -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">

View file

@ -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 }

View file

@ -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 }

View file

@ -1,7 +1,7 @@
#txhex { #txhex {
-fx-background-color: -fx-control-inner-background; -fx-background-color: -fx-control-inner-background;
-fx-font-size: 13px; -fx-font-size: 13px;
-fx-font-family: '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;

View file

@ -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 {

View file

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

View file

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

View file

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

View file

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

View file

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

Binary file not shown.

Some files were not shown because too many files have changed in this diff Show more