mirror of
https://github.com/sparrowwallet/sparrow.git
synced 2025-11-05 11:56:37 +00:00
Compare commits
No commits in common. "master" and "2.1.1" have entirely different histories.
468 changed files with 3429 additions and 7914 deletions
15
.github/workflows/package.yaml
vendored
15
.github/workflows/package.yaml
vendored
|
|
@ -10,13 +10,13 @@ jobs:
|
||||||
runs-on: ${{ matrix.os }}
|
runs-on: ${{ matrix.os }}
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
os: [windows-2022, ubuntu-22.04, ubuntu-22.04-arm, macos-13, macos-14]
|
os: [windows-2022, ubuntu-20.04, ubuntu-22.04-arm, macos-13, macos-14]
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v5
|
- uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
submodules: true
|
submodules: true
|
||||||
- name: Set up JDK 22.0.2
|
- name: Set up JDK 22.0.2
|
||||||
uses: actions/setup-java@v5
|
uses: actions/setup-java@v4
|
||||||
with:
|
with:
|
||||||
distribution: 'temurin'
|
distribution: 'temurin'
|
||||||
java-version: '22.0.2'
|
java-version: '22.0.2'
|
||||||
|
|
@ -30,10 +30,7 @@ jobs:
|
||||||
- name: Package tar distribution
|
- name: Package tar distribution
|
||||||
if: ${{ runner.os == 'Linux' }}
|
if: ${{ runner.os == 'Linux' }}
|
||||||
run: ./gradlew packageTarDistribution
|
run: ./gradlew packageTarDistribution
|
||||||
- name: Repackage deb distribution
|
- name: Upload Artifacts
|
||||||
if: ${{ runner.os == 'Linux' }}
|
|
||||||
run: ./repackage.sh
|
|
||||||
- name: Upload Artifact
|
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: Sparrow Build - ${{ runner.os }} ${{ runner.arch }}
|
name: Sparrow Build - ${{ runner.os }} ${{ runner.arch }}
|
||||||
|
|
@ -46,9 +43,9 @@ jobs:
|
||||||
- name: Package headless tar distribution
|
- name: Package headless tar distribution
|
||||||
if: ${{ runner.os == 'Linux' }}
|
if: ${{ runner.os == 'Linux' }}
|
||||||
run: ./gradlew -Djava.awt.headless=true packageTarDistribution
|
run: ./gradlew -Djava.awt.headless=true packageTarDistribution
|
||||||
- name: Repackage headless deb distribution
|
- name: Rename Headless Artifacts
|
||||||
if: ${{ runner.os == 'Linux' }}
|
if: ${{ runner.os == 'Linux' }}
|
||||||
run: ./repackage.sh
|
run: for f in build/jpackage/sparrow*; do mv -v "$f" "${f/sparrow/sparrow-server}"; done;
|
||||||
- name: Upload Headless Artifact
|
- name: Upload Headless Artifact
|
||||||
if: ${{ runner.os == 'Linux' }}
|
if: ${{ runner.os == 'Linux' }}
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v4
|
||||||
|
|
|
||||||
311
build.gradle
311
build.gradle
|
|
@ -1,34 +1,50 @@
|
||||||
plugins {
|
plugins {
|
||||||
id 'application'
|
id 'application'
|
||||||
id 'org-openjfx-javafxplugin'
|
id 'org-openjfx-javafxplugin'
|
||||||
id 'org.beryx.jlink' version '3.1.3'
|
id 'org.beryx.jlink' version '3.1.1'
|
||||||
id 'org.gradlex.extra-java-module-info' version '1.13'
|
id 'org.gradlex.extra-java-module-info' version '1.9'
|
||||||
id 'io.matthewnelson.kmp.tor.resource-filterjar' version '408.16.3'
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def sparrowVersion = '2.1.1'
|
||||||
def os = org.gradle.internal.os.OperatingSystem.current()
|
def os = org.gradle.internal.os.OperatingSystem.current()
|
||||||
def osName = os.getFamilyName()
|
def osName = os.getFamilyName()
|
||||||
if(os.macOsX) {
|
if(os.macOsX) {
|
||||||
osName = "osx"
|
osName = "osx"
|
||||||
}
|
}
|
||||||
|
def targetName = ""
|
||||||
def osArch = "x64"
|
def osArch = "x64"
|
||||||
def releaseArch = "x86_64"
|
def releaseArch = "x86_64"
|
||||||
if(System.getProperty("os.arch") == "aarch64") {
|
if(System.getProperty("os.arch") == "aarch64") {
|
||||||
osArch = "aarch64"
|
osArch = "aarch64"
|
||||||
releaseArch = "aarch64"
|
releaseArch = "aarch64"
|
||||||
|
targetName = "-" + osArch
|
||||||
}
|
}
|
||||||
def headless = "true".equals(System.getProperty("java.awt.headless"))
|
def headless = "true".equals(System.getProperty("java.awt.headless"))
|
||||||
|
|
||||||
group = 'com.sparrowwallet'
|
def vTor = '4.7.13-4'
|
||||||
version = '2.3.1'
|
def vKmpTor = '1.4.3'
|
||||||
|
def kmpOs = osName
|
||||||
|
if(os.macOsX) {
|
||||||
|
kmpOs = "macos"
|
||||||
|
} else if(os.windows) {
|
||||||
|
kmpOs = "mingw"
|
||||||
|
}
|
||||||
|
def kmpArch = "x64"
|
||||||
|
if(System.getProperty("os.arch") == "aarch64") {
|
||||||
|
kmpArch = "arm64"
|
||||||
|
}
|
||||||
|
|
||||||
|
group "com.sparrowwallet"
|
||||||
|
version "${sparrowVersion}"
|
||||||
|
|
||||||
repositories {
|
repositories {
|
||||||
mavenCentral()
|
mavenCentral()
|
||||||
maven { url = uri('https://code.sparrowwallet.com/api/packages/sparrowwallet/maven') }
|
maven { url 'https://code.sparrowwallet.com/api/packages/sparrowwallet/maven' }
|
||||||
}
|
}
|
||||||
|
|
||||||
tasks.withType(AbstractArchiveTask).configureEach {
|
tasks.withType(AbstractArchiveTask) {
|
||||||
useFileSystemPermissions()
|
preserveFileTimestamps = false
|
||||||
|
reproducibleFileOrder = true
|
||||||
}
|
}
|
||||||
|
|
||||||
javafx {
|
javafx {
|
||||||
|
|
@ -44,20 +60,20 @@ dependencies {
|
||||||
//Any changes to the dependencies must be reflected in the module definitions below!
|
//Any changes to the dependencies must be reflected in the module definitions below!
|
||||||
implementation(project(':drongo'))
|
implementation(project(':drongo'))
|
||||||
implementation(project(':lark'))
|
implementation(project(':lark'))
|
||||||
implementation('com.google.guava:guava:33.5.0-jre')
|
implementation('com.google.guava:guava:33.0.0-jre')
|
||||||
implementation('com.google.code.gson:gson:2.9.1')
|
implementation('com.google.code.gson:gson:2.9.1')
|
||||||
implementation('com.h2database:h2:2.1.214')
|
implementation('com.h2database:h2:2.1.214')
|
||||||
implementation('com.zaxxer:HikariCP:4.0.3') {
|
implementation('com.zaxxer:HikariCP:4.0.3') {
|
||||||
exclude group: 'org.slf4j'
|
exclude group: 'org.slf4j'
|
||||||
}
|
}
|
||||||
implementation('org.jdbi:jdbi3-core:3.49.5') {
|
implementation('org.jdbi:jdbi3-core:3.20.0') {
|
||||||
exclude group: 'org.slf4j'
|
exclude group: 'org.slf4j'
|
||||||
}
|
}
|
||||||
implementation('org.jdbi:jdbi3-sqlobject:3.49.5') {
|
implementation('org.jdbi:jdbi3-sqlobject:3.20.0') {
|
||||||
exclude group: 'org.slf4j'
|
exclude group: 'org.slf4j'
|
||||||
}
|
}
|
||||||
implementation('org.flywaydb:flyway-core:9.22.3')
|
implementation('org.flywaydb:flyway-core:9.1.3')
|
||||||
implementation('org.fxmisc.richtext:richtextfx:0.11.6')
|
implementation('org.fxmisc.richtext:richtextfx:0.10.4')
|
||||||
implementation('no.tornado:tornadofx-controls:1.0.4')
|
implementation('no.tornado:tornadofx-controls:1.0.4')
|
||||||
implementation('com.google.zxing:javase:3.4.0') {
|
implementation('com.google.zxing:javase:3.4.0') {
|
||||||
exclude group: 'com.beust', module: 'jcommander'
|
exclude group: 'com.beust', module: 'jcommander'
|
||||||
|
|
@ -73,15 +89,34 @@ dependencies {
|
||||||
implementation('com.fasterxml.jackson.core:jackson-databind:2.17.2')
|
implementation('com.fasterxml.jackson.core:jackson-databind:2.17.2')
|
||||||
implementation('com.sparrowwallet:hummingbird:1.7.4')
|
implementation('com.sparrowwallet:hummingbird:1.7.4')
|
||||||
implementation('co.nstant.in:cbor:0.9')
|
implementation('co.nstant.in:cbor:0.9')
|
||||||
implementation('org.openpnp:openpnp-capture-java:0.0.30-1')
|
implementation("com.nativelibs4java:bridj${targetName}:0.7-20140918-3") {
|
||||||
implementation("io.matthewnelson.kmp-tor:runtime:2.2.1")
|
exclude group: 'com.google.android.tools', module: 'dx'
|
||||||
implementation("io.matthewnelson.kmp-tor:resource-exec-tor-gpl:408.16.3")
|
}
|
||||||
implementation('org.jetbrains.kotlinx:kotlinx-coroutines-javafx:1.10.1') {
|
implementation("com.github.sarxos:webcam-capture${targetName}:0.3.13-SNAPSHOT") {
|
||||||
|
exclude group: 'com.nativelibs4java', module: 'bridj'
|
||||||
|
}
|
||||||
|
implementation("io.matthewnelson.kotlin-components:kmp-tor:${vTor}-${vKmpTor}") {
|
||||||
exclude group: 'org.jetbrains.kotlin', module: 'kotlin-stdlib-common'
|
exclude group: 'org.jetbrains.kotlin', module: 'kotlin-stdlib-common'
|
||||||
}
|
}
|
||||||
implementation('de.jangassen:nsmenufx:3.1.0') {
|
if(kmpOs == "linux" && kmpArch == "arm64") {
|
||||||
exclude group: 'net.java.dev.jna', module: 'jna'
|
implementation("com.sparrowwallet.kmp-tor-binary:kmp-tor-binary-${kmpOs}${kmpArch}-jvm:${vTor}") {
|
||||||
|
exclude group: 'org.jetbrains.kotlin', module: 'kotlin-stdlib-common'
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
implementation("io.matthewnelson.kotlin-components:kmp-tor-binary-${kmpOs}${kmpArch}:${vTor}") {
|
||||||
|
exclude group: 'org.jetbrains.kotlin', module: 'kotlin-stdlib-common'
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
implementation("io.matthewnelson.kotlin-components:kmp-tor-binary-extract:${vTor}") {
|
||||||
|
exclude group: 'org.jetbrains.kotlin', module: 'kotlin-stdlib-common'
|
||||||
|
}
|
||||||
|
implementation("io.matthewnelson.kotlin-components:kmp-tor-ext-callback-manager:${vKmpTor}") {
|
||||||
|
exclude group: 'org.jetbrains.kotlin', module: 'kotlin-stdlib-common'
|
||||||
|
}
|
||||||
|
implementation('org.jetbrains.kotlinx:kotlinx-coroutines-javafx:1.7.1') {
|
||||||
|
exclude group: 'org.jetbrains.kotlin', module: 'kotlin-stdlib-common'
|
||||||
|
}
|
||||||
|
implementation('de.codecentric.centerdevice:centerdevice-nsmenufx:2.1.7')
|
||||||
implementation('org.controlsfx:controlsfx:11.1.0' ) {
|
implementation('org.controlsfx:controlsfx:11.1.0' ) {
|
||||||
exclude group: 'org.openjfx', module: 'javafx-base'
|
exclude group: 'org.openjfx', module: 'javafx-base'
|
||||||
exclude group: 'org.openjfx', module: 'javafx-graphics'
|
exclude group: 'org.openjfx', module: 'javafx-graphics'
|
||||||
|
|
@ -101,7 +136,7 @@ dependencies {
|
||||||
implementation('com.sparrowwallet:tern:1.0.6')
|
implementation('com.sparrowwallet:tern:1.0.6')
|
||||||
implementation('io.reactivex.rxjava2:rxjava:2.2.15')
|
implementation('io.reactivex.rxjava2:rxjava:2.2.15')
|
||||||
implementation('io.reactivex.rxjava2:rxjavafx:2.2.2')
|
implementation('io.reactivex.rxjava2:rxjavafx:2.2.2')
|
||||||
implementation('org.apache.commons:commons-lang3:3.19.0')
|
implementation('org.apache.commons:commons-lang3:3.7')
|
||||||
implementation('org.apache.commons:commons-compress:1.27.1')
|
implementation('org.apache.commons:commons-compress:1.27.1')
|
||||||
implementation('net.sourceforge.streamsupport:streamsupport:1.7.0')
|
implementation('net.sourceforge.streamsupport:streamsupport:1.7.0')
|
||||||
implementation('com.github.librepdf:openpdf:1.3.30')
|
implementation('com.github.librepdf:openpdf:1.3.30')
|
||||||
|
|
@ -110,7 +145,6 @@ dependencies {
|
||||||
implementation('com.github.hervegirod:fxsvgimage:1.1')
|
implementation('com.github.hervegirod:fxsvgimage:1.1')
|
||||||
implementation('com.sparrowwallet:toucan:0.9.0')
|
implementation('com.sparrowwallet:toucan:0.9.0')
|
||||||
implementation('com.jcraft:jzlib:1.1.3')
|
implementation('com.jcraft:jzlib:1.1.3')
|
||||||
implementation('io.github.doblon8:jzbar:0.2.1')
|
|
||||||
testImplementation('org.junit.jupiter:junit-jupiter-api:5.10.0')
|
testImplementation('org.junit.jupiter:junit-jupiter-api:5.10.0')
|
||||||
testRuntimeOnly('org.junit.jupiter:junit-jupiter-engine:5.10.0')
|
testRuntimeOnly('org.junit.jupiter:junit-jupiter-engine:5.10.0')
|
||||||
testRuntimeOnly('org.junit.platform:junit-platform-launcher')
|
testRuntimeOnly('org.junit.platform:junit-platform-launcher')
|
||||||
|
|
@ -143,12 +177,6 @@ application {
|
||||||
mainClass = 'com.sparrowwallet.sparrow.SparrowWallet'
|
mainClass = 'com.sparrowwallet.sparrow.SparrowWallet'
|
||||||
|
|
||||||
applicationDefaultJvmArgs = ["-XX:+HeapDumpOnOutOfMemoryError",
|
applicationDefaultJvmArgs = ["-XX:+HeapDumpOnOutOfMemoryError",
|
||||||
"--enable-native-access=com.sparrowwallet.drongo",
|
|
||||||
"--enable-native-access=com.sun.jna",
|
|
||||||
"--enable-native-access=javafx.graphics",
|
|
||||||
"--enable-native-access=com.fazecast.jSerialComm",
|
|
||||||
"--enable-native-access=org.usb4java",
|
|
||||||
"--enable-native-access=io.github.doblon8.jzbar",
|
|
||||||
"--add-opens=javafx.graphics/com.sun.javafx.css=org.controlsfx.controls",
|
"--add-opens=javafx.graphics/com.sun.javafx.css=org.controlsfx.controls",
|
||||||
"--add-opens=javafx.graphics/javafx.scene=org.controlsfx.controls",
|
"--add-opens=javafx.graphics/javafx.scene=org.controlsfx.controls",
|
||||||
"--add-opens=javafx.controls/com.sun.javafx.scene.control.behavior=org.controlsfx.controls",
|
"--add-opens=javafx.controls/com.sun.javafx.scene.control.behavior=org.controlsfx.controls",
|
||||||
|
|
@ -158,6 +186,11 @@ application {
|
||||||
"--add-opens=javafx.controls/javafx.scene.control.cell=com.sparrowwallet.sparrow",
|
"--add-opens=javafx.controls/javafx.scene.control.cell=com.sparrowwallet.sparrow",
|
||||||
"--add-opens=org.controlsfx.controls/impl.org.controlsfx.skin=com.sparrowwallet.sparrow",
|
"--add-opens=org.controlsfx.controls/impl.org.controlsfx.skin=com.sparrowwallet.sparrow",
|
||||||
"--add-opens=org.controlsfx.controls/impl.org.controlsfx.skin=javafx.fxml",
|
"--add-opens=org.controlsfx.controls/impl.org.controlsfx.skin=javafx.fxml",
|
||||||
|
"--add-opens=javafx.graphics/com.sun.javafx.tk=centerdevice.nsmenufx",
|
||||||
|
"--add-opens=javafx.graphics/com.sun.javafx.tk.quantum=centerdevice.nsmenufx",
|
||||||
|
"--add-opens=javafx.graphics/com.sun.glass.ui=centerdevice.nsmenufx",
|
||||||
|
"--add-opens=javafx.controls/com.sun.javafx.scene.control=centerdevice.nsmenufx",
|
||||||
|
"--add-opens=javafx.graphics/com.sun.javafx.menu=centerdevice.nsmenufx",
|
||||||
"--add-opens=javafx.graphics/com.sun.glass.ui=com.sparrowwallet.sparrow",
|
"--add-opens=javafx.graphics/com.sun.glass.ui=com.sparrowwallet.sparrow",
|
||||||
"--add-opens=javafx.graphics/com.sun.javafx.application=com.sparrowwallet.sparrow",
|
"--add-opens=javafx.graphics/com.sun.javafx.application=com.sparrowwallet.sparrow",
|
||||||
"--add-opens=javafx.graphics/javafx.scene.input=com.sparrowwallet.sparrow",
|
"--add-opens=javafx.graphics/javafx.scene.input=com.sparrowwallet.sparrow",
|
||||||
|
|
@ -168,7 +201,8 @@ application {
|
||||||
"--add-reads=org.flywaydb.core=java.desktop"]
|
"--add-reads=org.flywaydb.core=java.desktop"]
|
||||||
|
|
||||||
if(os.macOsX) {
|
if(os.macOsX) {
|
||||||
applicationDefaultJvmArgs += ["-Dprism.lcdtext=false", "-Xdock:name=Sparrow"]
|
applicationDefaultJvmArgs += ["-Dprism.lcdtext=false", "-Xdock:name=Sparrow", "-Xdock:icon=/Users/scy/git/sparrow/src/main/resources/sparrow-large.png",
|
||||||
|
"--add-opens=javafx.graphics/com.sun.glass.ui.mac=centerdevice.nsmenufx"]
|
||||||
}
|
}
|
||||||
if(headless) {
|
if(headless) {
|
||||||
applicationDefaultJvmArgs += ["-Dglass.platform=Monocle", "-Dmonocle.platform=Headless", "-Dprism.order=sw"]
|
applicationDefaultJvmArgs += ["-Dglass.platform=Monocle", "-Dmonocle.platform=Headless", "-Dprism.order=sw"]
|
||||||
|
|
@ -191,14 +225,7 @@ jlink {
|
||||||
options = ['--strip-native-commands', '--strip-java-debug-attributes', '--compress', 'zip-6', '--no-header-files', '--no-man-pages', '--ignore-signing-information', '--exclude-files', '**.png', '--exclude-resources', 'glob:/com.sparrowwallet.merged.module/META-INF/*']
|
options = ['--strip-native-commands', '--strip-java-debug-attributes', '--compress', 'zip-6', '--no-header-files', '--no-man-pages', '--ignore-signing-information', '--exclude-files', '**.png', '--exclude-resources', 'glob:/com.sparrowwallet.merged.module/META-INF/*']
|
||||||
launcher {
|
launcher {
|
||||||
name = 'sparrow'
|
name = 'sparrow'
|
||||||
jvmArgs = ["--enable-native-access=com.sparrowwallet.drongo",
|
jvmArgs = ["--add-opens=javafx.graphics/com.sun.javafx.css=org.controlsfx.controls",
|
||||||
"--enable-native-access=com.sun.jna",
|
|
||||||
"--enable-native-access=javafx.graphics",
|
|
||||||
"--enable-native-access=com.sparrowwallet.merged.module",
|
|
||||||
"--enable-native-access=com.fazecast.jSerialComm",
|
|
||||||
"--enable-native-access=org.usb4java",
|
|
||||||
"--enable-native-access=io.github.doblon8.jzbar",
|
|
||||||
"--add-opens=javafx.graphics/com.sun.javafx.css=org.controlsfx.controls",
|
|
||||||
"--add-opens=javafx.graphics/javafx.scene=org.controlsfx.controls",
|
"--add-opens=javafx.graphics/javafx.scene=org.controlsfx.controls",
|
||||||
"--add-opens=javafx.controls/com.sun.javafx.scene.control.behavior=org.controlsfx.controls",
|
"--add-opens=javafx.controls/com.sun.javafx.scene.control.behavior=org.controlsfx.controls",
|
||||||
"--add-opens=javafx.controls/com.sun.javafx.scene.control.inputmap=org.controlsfx.controls",
|
"--add-opens=javafx.controls/com.sun.javafx.scene.control.inputmap=org.controlsfx.controls",
|
||||||
|
|
@ -207,6 +234,11 @@ jlink {
|
||||||
"--add-opens=javafx.controls/javafx.scene.control.cell=com.sparrowwallet.sparrow",
|
"--add-opens=javafx.controls/javafx.scene.control.cell=com.sparrowwallet.sparrow",
|
||||||
"--add-opens=org.controlsfx.controls/impl.org.controlsfx.skin=com.sparrowwallet.sparrow",
|
"--add-opens=org.controlsfx.controls/impl.org.controlsfx.skin=com.sparrowwallet.sparrow",
|
||||||
"--add-opens=org.controlsfx.controls/impl.org.controlsfx.skin=javafx.fxml",
|
"--add-opens=org.controlsfx.controls/impl.org.controlsfx.skin=javafx.fxml",
|
||||||
|
"--add-opens=javafx.graphics/com.sun.javafx.tk=centerdevice.nsmenufx",
|
||||||
|
"--add-opens=javafx.graphics/com.sun.javafx.tk.quantum=centerdevice.nsmenufx",
|
||||||
|
"--add-opens=javafx.graphics/com.sun.glass.ui=centerdevice.nsmenufx",
|
||||||
|
"--add-opens=javafx.controls/com.sun.javafx.scene.control=centerdevice.nsmenufx",
|
||||||
|
"--add-opens=javafx.graphics/com.sun.javafx.menu=centerdevice.nsmenufx",
|
||||||
"--add-opens=javafx.graphics/com.sun.glass.ui=com.sparrowwallet.sparrow",
|
"--add-opens=javafx.graphics/com.sun.glass.ui=com.sparrowwallet.sparrow",
|
||||||
"--add-opens=javafx.graphics/javafx.scene.input=com.sparrowwallet.sparrow",
|
"--add-opens=javafx.graphics/javafx.scene.input=com.sparrowwallet.sparrow",
|
||||||
"--add-opens=javafx.graphics/com.sun.javafx.application=com.sparrowwallet.sparrow",
|
"--add-opens=javafx.graphics/com.sun.javafx.application=com.sparrowwallet.sparrow",
|
||||||
|
|
@ -224,8 +256,6 @@ jlink {
|
||||||
"--add-reads=com.sparrowwallet.merged.module=co.nstant.in.cbor",
|
"--add-reads=com.sparrowwallet.merged.module=co.nstant.in.cbor",
|
||||||
"--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=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"]
|
||||||
|
|
||||||
|
|
@ -243,7 +273,7 @@ jlink {
|
||||||
jpackage {
|
jpackage {
|
||||||
imageName = "Sparrow"
|
imageName = "Sparrow"
|
||||||
installerName = "Sparrow"
|
installerName = "Sparrow"
|
||||||
appVersion = "${version}"
|
appVersion = "${sparrowVersion}"
|
||||||
skipInstaller = os.macOsX || properties.skipInstallers
|
skipInstaller = os.macOsX || properties.skipInstallers
|
||||||
imageOptions = []
|
imageOptions = []
|
||||||
installerOptions = ['--file-associations', 'src/main/deploy/psbt.properties', '--file-associations', 'src/main/deploy/txn.properties', '--file-associations', 'src/main/deploy/asc.properties', '--file-associations', 'src/main/deploy/bitcoin.properties', '--file-associations', 'src/main/deploy/auth47.properties', '--file-associations', 'src/main/deploy/lightning.properties', '--license-file', 'LICENSE']
|
installerOptions = ['--file-associations', 'src/main/deploy/psbt.properties', '--file-associations', 'src/main/deploy/txn.properties', '--file-associations', 'src/main/deploy/asc.properties', '--file-associations', 'src/main/deploy/bitcoin.properties', '--file-associations', 'src/main/deploy/auth47.properties', '--file-associations', 'src/main/deploy/lightning.properties', '--license-file', 'LICENSE']
|
||||||
|
|
@ -254,13 +284,11 @@ jlink {
|
||||||
}
|
}
|
||||||
if(os.linux) {
|
if(os.linux) {
|
||||||
if(headless) {
|
if(headless) {
|
||||||
installerName = "sparrowserver"
|
installerOptions = ['--license-file', 'LICENSE', '--resource-dir', "src/main/deploy/package/linux-headless/${osArch}"]
|
||||||
installerOptions = ['--license-file', 'LICENSE']
|
|
||||||
} else {
|
} else {
|
||||||
installerName = "sparrowwallet"
|
installerOptions += ['--resource-dir', 'src/main/deploy/package/linux/', '--linux-shortcut', '--linux-menu-group', 'Sparrow']
|
||||||
installerOptions += ['--linux-shortcut', '--linux-menu-group', 'Sparrow']
|
|
||||||
}
|
}
|
||||||
installerOptions += ['--resource-dir', layout.buildDirectory.dir('deploy/package').get().asFile.toString(), '--linux-app-category', 'utils', '--linux-app-release', '1', '--linux-rpm-license-type', 'ASL 2.0', '--linux-deb-maintainer', 'mail@sparrowwallet.com']
|
installerOptions += ['--linux-app-category', 'utils', '--linux-app-release', '1', '--linux-rpm-license-type', 'ASL 2.0', '--linux-deb-maintainer', 'mail@sparrowwallet.com']
|
||||||
imageOptions += ['--icon', 'src/main/deploy/package/linux/Sparrow.png', '--resource-dir', 'src/main/deploy/package/linux/']
|
imageOptions += ['--icon', 'src/main/deploy/package/linux/Sparrow.png', '--resource-dir', 'src/main/deploy/package/linux/']
|
||||||
}
|
}
|
||||||
if(os.macOsX) {
|
if(os.macOsX) {
|
||||||
|
|
@ -278,15 +306,13 @@ jlink {
|
||||||
|
|
||||||
if(os.linux) {
|
if(os.linux) {
|
||||||
tasks.jlink.finalizedBy('addUserWritePermission', 'copyUdevRules')
|
tasks.jlink.finalizedBy('addUserWritePermission', 'copyUdevRules')
|
||||||
tasks.jpackageImage.finalizedBy('prepareResourceDir')
|
|
||||||
} else {
|
} else {
|
||||||
tasks.jlink.finalizedBy('addUserWritePermission')
|
tasks.jlink.finalizedBy('addUserWritePermission')
|
||||||
}
|
}
|
||||||
|
|
||||||
tasks.register('addUserWritePermission', Exec) {
|
tasks.register('addUserWritePermission', Exec) {
|
||||||
if(os.windows) {
|
if(os.windows) {
|
||||||
def usersGroup = '*S-1-5-32-545' // Windows "Users" group SID (language-independent)
|
commandLine 'icacls', "$buildDir\\image\\legal", '/grant', 'Users:(OI)(CI)F', '/T'
|
||||||
commandLine 'icacls', "$buildDir\\image\\legal", '/grant', "${usersGroup}:(OI)(CI)F", '/T'
|
|
||||||
} else {
|
} else {
|
||||||
commandLine 'chmod', '-R', 'u+w', "$buildDir/image/legal"
|
commandLine 'chmod', '-R', 'u+w', "$buildDir/image/legal"
|
||||||
}
|
}
|
||||||
|
|
@ -298,42 +324,12 @@ tasks.register('copyUdevRules', Copy) {
|
||||||
include('*')
|
include('*')
|
||||||
}
|
}
|
||||||
|
|
||||||
tasks.register('prepareResourceDir', Copy) {
|
|
||||||
from("src/main/deploy/package/linux${headless ? '-headless' : ''}")
|
|
||||||
into(layout.buildDirectory.dir('deploy/package'))
|
|
||||||
include('*')
|
|
||||||
eachFile { file ->
|
|
||||||
if(file.name.equals('control') || file.name.endsWith('.spec')) {
|
|
||||||
filter { line ->
|
|
||||||
if(line.contains('${size}')) {
|
|
||||||
line = line.replace('${size}', getDirectorySize(layout.buildDirectory.dir('jpackage/Sparrow').get().asFile))
|
|
||||||
}
|
|
||||||
return line.replace('${version}', "${version}").replace('${arch}', osArch == 'aarch64' ? 'arm64' : 'amd64')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static def getDirectorySize(File directory) {
|
|
||||||
long size = 0
|
|
||||||
if(directory.isFile()) {
|
|
||||||
size = directory.length()
|
|
||||||
} else if(directory.isDirectory()) {
|
|
||||||
directory.eachFileRecurse { file ->
|
|
||||||
if(file.isFile()) {
|
|
||||||
size += file.length()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return Long.toString(size/1024 as long)
|
|
||||||
}
|
|
||||||
|
|
||||||
tasks.register('removeGroupWritePermission', Exec) {
|
tasks.register('removeGroupWritePermission', Exec) {
|
||||||
commandLine 'chmod', '-R', 'g-w', "$buildDir/jpackage/Sparrow"
|
commandLine 'chmod', '-R', 'g-w', "$buildDir/jpackage/Sparrow"
|
||||||
}
|
}
|
||||||
|
|
||||||
tasks.register('packageZipDistribution', Zip) {
|
tasks.register('packageZipDistribution', Zip) {
|
||||||
archiveFileName = "Sparrow-${version}.zip"
|
archiveFileName = "Sparrow-${sparrowVersion}.zip"
|
||||||
destinationDirectory = file("$buildDir/jpackage")
|
destinationDirectory = file("$buildDir/jpackage")
|
||||||
preserveFileTimestamps = os.macOsX
|
preserveFileTimestamps = os.macOsX
|
||||||
from("$buildDir/jpackage/") {
|
from("$buildDir/jpackage/") {
|
||||||
|
|
@ -344,7 +340,7 @@ tasks.register('packageZipDistribution', Zip) {
|
||||||
|
|
||||||
tasks.register('packageTarDistribution', Tar) {
|
tasks.register('packageTarDistribution', Tar) {
|
||||||
dependsOn removeGroupWritePermission
|
dependsOn removeGroupWritePermission
|
||||||
archiveFileName = "sparrow${headless ? 'server': 'wallet'}-${version}-${releaseArch}.tar.gz"
|
archiveFileName = "sparrow-${sparrowVersion}-${releaseArch}.tar.gz"
|
||||||
destinationDirectory = file("$buildDir/jpackage")
|
destinationDirectory = file("$buildDir/jpackage")
|
||||||
compression = Compression.GZIP
|
compression = Compression.GZIP
|
||||||
from("$buildDir/jpackage/") {
|
from("$buildDir/jpackage/") {
|
||||||
|
|
@ -380,11 +376,24 @@ extraJavaModuleInfo {
|
||||||
requires('org.slf4j')
|
requires('org.slf4j')
|
||||||
requires('com.fasterxml.jackson.databind')
|
requires('com.fasterxml.jackson.databind')
|
||||||
}
|
}
|
||||||
module('org.openpnp:openpnp-capture-java', 'openpnp.capture.java') {
|
module("com.nativelibs4java:bridj${targetName}", 'com.nativelibs4java.bridj') {
|
||||||
exports('org.openpnp.capture')
|
exports('org.bridj')
|
||||||
exports('org.openpnp.capture.library')
|
exports('org.bridj.cpp')
|
||||||
|
requires('java.logging')
|
||||||
|
}
|
||||||
|
module("com.github.sarxos:webcam-capture${targetName}", 'com.github.sarxos.webcam.capture') {
|
||||||
|
exports('com.github.sarxos.webcam')
|
||||||
|
exports('com.github.sarxos.webcam.ds.buildin')
|
||||||
|
exports('com.github.sarxos.webcam.ds.buildin.natives')
|
||||||
requires('java.desktop')
|
requires('java.desktop')
|
||||||
requires('com.sun.jna')
|
requires('com.nativelibs4java.bridj')
|
||||||
|
requires('org.slf4j')
|
||||||
|
}
|
||||||
|
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')
|
||||||
|
|
@ -392,6 +401,21 @@ extraJavaModuleInfo {
|
||||||
module('com.google.guava:listenablefuture|empty-to-avoid-conflict-with-guava', 'com.google.guava.listenablefuture')
|
module('com.google.guava:listenablefuture|empty-to-avoid-conflict-with-guava', 'com.google.guava.listenablefuture')
|
||||||
module('com.google.code.findbugs:jsr305', 'com.google.code.findbugs.jsr305')
|
module('com.google.code.findbugs:jsr305', 'com.google.code.findbugs.jsr305')
|
||||||
module('j2objc-annotations-2.8.jar', 'com.google.j2objc.j2objc.annotations', '2.8')
|
module('j2objc-annotations-2.8.jar', 'com.google.j2objc.j2objc.annotations', '2.8')
|
||||||
|
module('org.jdbi:jdbi3-core', 'org.jdbi.v3.core') {
|
||||||
|
exports('org.jdbi.v3.core')
|
||||||
|
exports('org.jdbi.v3.core.mapper')
|
||||||
|
exports('org.jdbi.v3.core.statement')
|
||||||
|
exports('org.jdbi.v3.core.result')
|
||||||
|
exports('org.jdbi.v3.core.h2')
|
||||||
|
exports('org.jdbi.v3.core.spi')
|
||||||
|
requires('io.leangen.geantyref')
|
||||||
|
requires('java.sql')
|
||||||
|
requires('org.slf4j')
|
||||||
|
requires('com.github.benmanes.caffeine')
|
||||||
|
}
|
||||||
|
module('io.leangen.geantyref:geantyref', 'io.leangen.geantyref') {
|
||||||
|
exports('io.leangen.geantyref')
|
||||||
|
}
|
||||||
module('org.fxmisc.richtext:richtextfx', 'org.fxmisc.richtext') {
|
module('org.fxmisc.richtext:richtextfx', 'org.fxmisc.richtext') {
|
||||||
exports('org.fxmisc.richtext')
|
exports('org.fxmisc.richtext')
|
||||||
exports('org.fxmisc.richtext.event')
|
exports('org.fxmisc.richtext.event')
|
||||||
|
|
@ -401,10 +425,10 @@ extraJavaModuleInfo {
|
||||||
requires('javafx.graphics')
|
requires('javafx.graphics')
|
||||||
requires('org.fxmisc.flowless')
|
requires('org.fxmisc.flowless')
|
||||||
requires('org.reactfx.reactfx')
|
requires('org.reactfx.reactfx')
|
||||||
requires('org.fxmisc.undo')
|
requires('org.fxmisc.undo.undofx')
|
||||||
requires('org.fxmisc.wellbehaved')
|
requires('org.fxmisc.wellbehaved')
|
||||||
}
|
}
|
||||||
module('org.fxmisc.undo:undofx', 'org.fxmisc.undo') {
|
module('org.fxmisc.undo:undofx', 'org.fxmisc.undo.undofx') {
|
||||||
requires('javafx.base')
|
requires('javafx.base')
|
||||||
requires('javafx.controls')
|
requires('javafx.controls')
|
||||||
requires('javafx.graphics')
|
requires('javafx.graphics')
|
||||||
|
|
@ -461,6 +485,119 @@ extraJavaModuleInfo {
|
||||||
exports('net.coobird.thumbnailator')
|
exports('net.coobird.thumbnailator')
|
||||||
requires('java.desktop')
|
requires('java.desktop')
|
||||||
}
|
}
|
||||||
|
module("io.matthewnelson.kotlin-components:kmp-tor-jvm", 'kmp.tor.jvm') {
|
||||||
|
exports('io.matthewnelson.kmp.tor')
|
||||||
|
requires('kmp.tor.binary.extract.jvm')
|
||||||
|
requires('kmp.tor.manager.jvm')
|
||||||
|
requires('kmp.tor.manager.common.jvm')
|
||||||
|
requires('kmp.tor.controller.common.jvm')
|
||||||
|
requires('kotlin.stdlib')
|
||||||
|
requires('kotlinx.coroutines.core')
|
||||||
|
requires('java.management')
|
||||||
|
}
|
||||||
|
if(kmpOs == "linux" && kmpArch == "arm64") {
|
||||||
|
module("com.sparrowwallet.kmp-tor-binary:kmp-tor-binary-${kmpOs}${kmpArch}-jvm", "kmp.tor.binary.${kmpOs}${kmpArch}") {
|
||||||
|
exports("io.matthewnelson.kmp.tor.resource.${kmpOs}.${kmpArch}")
|
||||||
|
exports("kmptor.${kmpOs}.${kmpArch}")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
module("io.matthewnelson.kotlin-components:kmp-tor-binary-${kmpOs}${kmpArch}-jvm", "kmp.tor.binary.${kmpOs}${kmpArch}") {
|
||||||
|
exports("io.matthewnelson.kmp.tor.binary.${kmpOs}.${kmpArch}")
|
||||||
|
exports("kmptor.${kmpOs}.${kmpArch}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
module("io.matthewnelson.kotlin-components:kmp-tor-binary-extract-jvm", 'kmp.tor.binary.extract.jvm') {
|
||||||
|
exports('io.matthewnelson.kmp.tor.binary.extract')
|
||||||
|
exports('io.matthewnelson.kmp.tor.binary.extract.internal')
|
||||||
|
requires('kotlin.stdlib')
|
||||||
|
requires("kmp.tor.binary.${kmpOs}${kmpArch}")
|
||||||
|
requires('kmp.tor.binary.geoip.jvm')
|
||||||
|
}
|
||||||
|
module("io.matthewnelson.kotlin-components:kmp-tor-manager-jvm", 'kmp.tor.manager.jvm') {
|
||||||
|
exports('io.matthewnelson.kmp.tor.manager')
|
||||||
|
exports('io.matthewnelson.kmp.tor.manager.util')
|
||||||
|
requires('kmp.tor.controller.common.jvm')
|
||||||
|
requires('kmp.tor.manager.common.jvm')
|
||||||
|
requires('kotlin.stdlib')
|
||||||
|
requires('kotlinx.coroutines.core')
|
||||||
|
requires('kotlinx.atomicfu')
|
||||||
|
requires('kmp.tor.controller.jvm')
|
||||||
|
requires('kmp.tor.common.jvm')
|
||||||
|
}
|
||||||
|
module("io.matthewnelson.kotlin-components:kmp-tor-manager-common-jvm", 'kmp.tor.manager.common.jvm') {
|
||||||
|
exports('io.matthewnelson.kmp.tor.manager.common')
|
||||||
|
exports('io.matthewnelson.kmp.tor.manager.common.event')
|
||||||
|
exports('io.matthewnelson.kmp.tor.manager.common.state')
|
||||||
|
requires('kmp.tor.controller.common.jvm')
|
||||||
|
requires('kmp.tor.common.jvm')
|
||||||
|
requires('kotlin.stdlib')
|
||||||
|
}
|
||||||
|
module("io.matthewnelson.kotlin-components:kmp-tor-controller-common-jvm", 'kmp.tor.controller.common.jvm') {
|
||||||
|
exports('io.matthewnelson.kmp.tor.controller.common.config')
|
||||||
|
exports('io.matthewnelson.kmp.tor.controller.common.file')
|
||||||
|
exports('io.matthewnelson.kmp.tor.controller.common.control')
|
||||||
|
exports('io.matthewnelson.kmp.tor.controller.common.control.usecase')
|
||||||
|
exports('io.matthewnelson.kmp.tor.controller.common.events')
|
||||||
|
exports('io.matthewnelson.kmp.tor.controller.common.exceptions')
|
||||||
|
requires('kmp.tor.common.jvm')
|
||||||
|
requires('kotlin.stdlib')
|
||||||
|
requires('kotlinx.atomicfu')
|
||||||
|
}
|
||||||
|
module("io.matthewnelson.kotlin-components:kmp-tor-common-jvm", 'kmp.tor.common.jvm') {
|
||||||
|
exports('io.matthewnelson.kmp.tor.common.address')
|
||||||
|
requires('parcelize.jvm')
|
||||||
|
requires('kotlin.stdlib')
|
||||||
|
}
|
||||||
|
module("io.matthewnelson.kotlin-components:kmp-tor-controller-jvm", 'kmp.tor.controller.jvm') {
|
||||||
|
exports('io.matthewnelson.kmp.tor.controller.internal.controller')
|
||||||
|
requires('kmp.tor.common.jvm')
|
||||||
|
requires('kmp.tor.controller.common.jvm')
|
||||||
|
requires('kotlinx.coroutines.core')
|
||||||
|
requires('kotlin.stdlib')
|
||||||
|
requires('kotlinx.atomicfu')
|
||||||
|
requires('encoding.core.jvm')
|
||||||
|
requires('encoding.base16.jvm')
|
||||||
|
}
|
||||||
|
module("io.matthewnelson.kotlin-components:kmp-tor-ext-callback-common-jvm", 'kmp.tor.ext.callback.common.jvm') {
|
||||||
|
exports('io.matthewnelson.kmp.tor.ext.callback.common')
|
||||||
|
}
|
||||||
|
module("io.matthewnelson.kotlin-components:kmp-tor-ext-callback-manager-jvm", 'kmp.tor.ext.callback.manager.jvm') {
|
||||||
|
exports('io.matthewnelson.kmp.tor.ext.callback.manager')
|
||||||
|
requires('kmp.tor.manager.jvm')
|
||||||
|
requires('kmp.tor.ext.callback.common.jvm')
|
||||||
|
requires('kmp.tor.ext.callback.manager.common.jvm')
|
||||||
|
requires('kmp.tor.ext.callback.controller.common.jvm')
|
||||||
|
requires('kmp.tor.manager.common.jvm')
|
||||||
|
requires('kmp.tor.controller.common.jvm')
|
||||||
|
requires('kotlin.stdlib')
|
||||||
|
requires('kotlinx.coroutines.core')
|
||||||
|
}
|
||||||
|
module("io.matthewnelson.kotlin-components:kmp-tor-ext-callback-manager-common-jvm", 'kmp.tor.ext.callback.manager.common.jvm') {
|
||||||
|
exports('io.matthewnelson.kmp.tor.ext.callback.manager.common')
|
||||||
|
requires('kmp.tor.ext.callback.controller.common.jvm')
|
||||||
|
}
|
||||||
|
module("io.matthewnelson.kotlin-components:kmp-tor-ext-callback-controller-common-jvm", 'kmp.tor.ext.callback.controller.common.jvm') {
|
||||||
|
exports('io.matthewnelson.kmp.tor.ext.callback.controller.common.control')
|
||||||
|
exports('io.matthewnelson.kmp.tor.ext.callback.controller.common.control.usecase')
|
||||||
|
}
|
||||||
|
module("io.matthewnelson.kotlin-components:kmp-tor-binary-geoip-jvm", 'kmp.tor.binary.geoip.jvm') {
|
||||||
|
exports('io.matthewnelson.kmp.tor.binary.geoip')
|
||||||
|
exports('kmptor')
|
||||||
|
}
|
||||||
|
module("base16-jvm-2.0.0.jar", 'encoding.base16.jvm', "2.0.0") {
|
||||||
|
exports('io.matthewnelson.encoding.base16')
|
||||||
|
requires('encoding.core.jvm')
|
||||||
|
requires('kotlin.stdlib')
|
||||||
|
}
|
||||||
|
module("base32-jvm-2.0.0.jar", 'encoding.base32.jvm', "2.0.0")
|
||||||
|
module("base64-jvm-2.0.0.jar", 'encoding.base64.jvm', "2.0.0")
|
||||||
|
module("core-jvm-2.0.0.jar", 'encoding.core.jvm', "2.0.0") {
|
||||||
|
exports('io.matthewnelson.encoding.core')
|
||||||
|
requires('kotlin.stdlib')
|
||||||
|
}
|
||||||
|
module("parcelize-jvm-0.1.2.jar", 'parcelize.jvm', "0.1.2") {
|
||||||
|
exports('io.matthewnelson.component.parcelize')
|
||||||
|
}
|
||||||
module('org.jcommander:jcommander', 'org.jcommander') {
|
module('org.jcommander:jcommander', 'org.jcommander') {
|
||||||
exports('com.beust.jcommander')
|
exports('com.beust.jcommander')
|
||||||
}
|
}
|
||||||
|
|
@ -475,8 +612,4 @@ extraJavaModuleInfo {
|
||||||
module('com.jcraft:jzlib', 'com.jcraft.jzlib') {
|
module('com.jcraft:jzlib', 'com.jcraft.jzlib') {
|
||||||
exports('com.jcraft.jzlib')
|
exports('com.jcraft.jzlib')
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
kmpTorResourceFilterJar {
|
|
||||||
keepTorCompilation("current","current")
|
|
||||||
}
|
}
|
||||||
|
|
@ -4,12 +4,13 @@ plugins {
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation 'com.google.gradle:osdetector-gradle-plugin:1.7.3'
|
implementation 'com.google.gradle:osdetector-gradle-plugin:1.7.3'
|
||||||
|
implementation 'org.javamodularity:moduleplugin:1.8.14'
|
||||||
}
|
}
|
||||||
|
|
||||||
repositories {
|
repositories {
|
||||||
mavenCentral()
|
mavenCentral()
|
||||||
maven {
|
maven {
|
||||||
url = uri("https://plugins.gradle.org/m2/")
|
url "https://plugins.gradle.org/m2/"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -32,6 +32,7 @@ package org.openjfx.gradle;
|
||||||
import com.google.gradle.osdetector.OsDetectorPlugin;
|
import com.google.gradle.osdetector.OsDetectorPlugin;
|
||||||
import org.gradle.api.Plugin;
|
import org.gradle.api.Plugin;
|
||||||
import org.gradle.api.Project;
|
import org.gradle.api.Project;
|
||||||
|
import org.javamodularity.moduleplugin.ModuleSystemPlugin;
|
||||||
import org.openjfx.gradle.tasks.ExecTask;
|
import org.openjfx.gradle.tasks.ExecTask;
|
||||||
|
|
||||||
public class JavaFXPlugin implements Plugin<Project> {
|
public class JavaFXPlugin implements Plugin<Project> {
|
||||||
|
|
@ -39,9 +40,10 @@ public class JavaFXPlugin implements Plugin<Project> {
|
||||||
@Override
|
@Override
|
||||||
public void apply(Project project) {
|
public void apply(Project project) {
|
||||||
project.getPlugins().apply(OsDetectorPlugin.class);
|
project.getPlugins().apply(OsDetectorPlugin.class);
|
||||||
|
project.getPlugins().apply(ModuleSystemPlugin.class);
|
||||||
|
|
||||||
project.getExtensions().create("javafx", JavaFXOptions.class, project);
|
project.getExtensions().create("javafx", JavaFXOptions.class, project);
|
||||||
|
|
||||||
project.getTasks().register("configJavafxRun", ExecTask.class, project);
|
project.getTasks().create("configJavafxRun", ExecTask.class, project);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -33,19 +33,27 @@ import org.gradle.api.DefaultTask;
|
||||||
import org.gradle.api.GradleException;
|
import org.gradle.api.GradleException;
|
||||||
import org.gradle.api.Project;
|
import org.gradle.api.Project;
|
||||||
import org.gradle.api.file.FileCollection;
|
import org.gradle.api.file.FileCollection;
|
||||||
|
import org.gradle.api.logging.Logger;
|
||||||
|
import org.gradle.api.logging.Logging;
|
||||||
import org.gradle.api.plugins.ApplicationPlugin;
|
import org.gradle.api.plugins.ApplicationPlugin;
|
||||||
import org.gradle.api.tasks.JavaExec;
|
import org.gradle.api.tasks.JavaExec;
|
||||||
import org.gradle.api.tasks.TaskAction;
|
import org.gradle.api.tasks.TaskAction;
|
||||||
|
import org.javamodularity.moduleplugin.extensions.RunModuleOptions;
|
||||||
import org.openjfx.gradle.JavaFXModule;
|
import org.openjfx.gradle.JavaFXModule;
|
||||||
import org.openjfx.gradle.JavaFXOptions;
|
import org.openjfx.gradle.JavaFXOptions;
|
||||||
import org.openjfx.gradle.JavaFXPlatform;
|
import org.openjfx.gradle.JavaFXPlatform;
|
||||||
|
|
||||||
import javax.inject.Inject;
|
import javax.inject.Inject;
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
|
import java.util.ArrayList;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
|
import java.util.List;
|
||||||
import java.util.TreeSet;
|
import java.util.TreeSet;
|
||||||
|
|
||||||
public class ExecTask extends DefaultTask {
|
public class ExecTask extends DefaultTask {
|
||||||
|
|
||||||
|
private static final Logger LOGGER = Logging.getLogger(ExecTask.class);
|
||||||
|
|
||||||
private final Project project;
|
private final Project project;
|
||||||
private JavaExec execTask;
|
private JavaExec execTask;
|
||||||
|
|
||||||
|
|
@ -70,11 +78,37 @@ public class ExecTask extends DefaultTask {
|
||||||
|
|
||||||
var definedJavaFXModuleNames = new TreeSet<>(javaFXOptions.getModules());
|
var definedJavaFXModuleNames = new TreeSet<>(javaFXOptions.getModules());
|
||||||
if (!definedJavaFXModuleNames.isEmpty()) {
|
if (!definedJavaFXModuleNames.isEmpty()) {
|
||||||
|
RunModuleOptions moduleOptions = execTask.getExtensions().findByType(RunModuleOptions.class);
|
||||||
|
|
||||||
final FileCollection classpathWithoutJavaFXJars = execTask.getClasspath().filter(
|
final FileCollection classpathWithoutJavaFXJars = execTask.getClasspath().filter(
|
||||||
jar -> Arrays.stream(JavaFXModule.values()).noneMatch(javaFXModule -> jar.getName().contains(javaFXModule.getArtifactName()))
|
jar -> Arrays.stream(JavaFXModule.values()).noneMatch(javaFXModule -> jar.getName().contains(javaFXModule.getArtifactName()))
|
||||||
);
|
);
|
||||||
final FileCollection javaFXPlatformJars = execTask.getClasspath().filter(jar -> isJavaFXJar(jar, javaFXOptions.getPlatform()));
|
final FileCollection javaFXPlatformJars = execTask.getClasspath().filter(jar -> isJavaFXJar(jar, javaFXOptions.getPlatform()));
|
||||||
execTask.setClasspath(classpathWithoutJavaFXJars.plus(javaFXPlatformJars));
|
|
||||||
|
if (moduleOptions != null) {
|
||||||
|
LOGGER.info("Modular JavaFX application found");
|
||||||
|
// Remove empty JavaFX jars from classpath
|
||||||
|
execTask.setClasspath(classpathWithoutJavaFXJars.plus(javaFXPlatformJars));
|
||||||
|
definedJavaFXModuleNames.forEach(javaFXModule -> moduleOptions.getAddModules().add(javaFXModule));
|
||||||
|
} else {
|
||||||
|
LOGGER.info("Non-modular JavaFX application found");
|
||||||
|
// Remove all JavaFX jars from classpath
|
||||||
|
execTask.setClasspath(classpathWithoutJavaFXJars);
|
||||||
|
|
||||||
|
var javaFXModuleJvmArgs = List.of("--module-path", javaFXPlatformJars.getAsPath());
|
||||||
|
|
||||||
|
var jvmArgs = new ArrayList<String>();
|
||||||
|
jvmArgs.add("--add-modules");
|
||||||
|
jvmArgs.add(String.join(",", definedJavaFXModuleNames));
|
||||||
|
|
||||||
|
List<String> execJvmArgs = execTask.getJvmArgs();
|
||||||
|
if (execJvmArgs != null) {
|
||||||
|
jvmArgs.addAll(execJvmArgs);
|
||||||
|
}
|
||||||
|
jvmArgs.addAll(javaFXModuleJvmArgs);
|
||||||
|
|
||||||
|
execTask.setJvmArgs(jvmArgs);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
throw new GradleException("Run task not found. Please, make sure the Application plugin is applied");
|
throw new GradleException("Run task not found. Please, make sure the Application plugin is applied");
|
||||||
|
|
|
||||||
|
|
@ -83,7 +83,7 @@ sudo apt install -y rpm fakeroot binutils
|
||||||
First, assign a temporary variable in your shell for the specific release you want to build. For the current one specify:
|
First, assign a temporary variable in your shell for the specific release you want to build. For the current one specify:
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
GIT_TAG="2.3.0"
|
GIT_TAG="2.0.0"
|
||||||
```
|
```
|
||||||
|
|
||||||
The project can then be initially cloned as follows:
|
The project can then be initially cloned as follows:
|
||||||
|
|
|
||||||
2
drongo
2
drongo
|
|
@ -1 +1 @@
|
||||||
Subproject commit e975cbe6f8d8574785124e6db5780d0541e20024
|
Subproject commit ca758e128876470f673b5955d75d5311b47c6938
|
||||||
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
Binary file not shown.
2
gradle/wrapper/gradle-wrapper.properties
vendored
2
gradle/wrapper/gradle-wrapper.properties
vendored
|
|
@ -1,6 +1,6 @@
|
||||||
distributionBase=GRADLE_USER_HOME
|
distributionBase=GRADLE_USER_HOME
|
||||||
distributionPath=wrapper/dists
|
distributionPath=wrapper/dists
|
||||||
distributionUrl=https\://services.gradle.org/distributions/gradle-9.1.0-bin.zip
|
distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-bin.zip
|
||||||
networkTimeout=10000
|
networkTimeout=10000
|
||||||
validateDistributionUrl=true
|
validateDistributionUrl=true
|
||||||
zipStoreBase=GRADLE_USER_HOME
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
|
|
|
||||||
15
gradlew
vendored
15
gradlew
vendored
|
|
@ -1,7 +1,7 @@
|
||||||
#!/bin/sh
|
#!/bin/sh
|
||||||
|
|
||||||
#
|
#
|
||||||
# Copyright © 2015 the original authors.
|
# Copyright © 2015-2021 the original authors.
|
||||||
#
|
#
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
# you may not use this file except in compliance with the License.
|
# you may not use this file except in compliance with the License.
|
||||||
|
|
@ -15,8 +15,6 @@
|
||||||
# See the License for the specific language governing permissions and
|
# See the License for the specific language governing permissions and
|
||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
#
|
#
|
||||||
# SPDX-License-Identifier: Apache-2.0
|
|
||||||
#
|
|
||||||
|
|
||||||
##############################################################################
|
##############################################################################
|
||||||
#
|
#
|
||||||
|
|
@ -57,7 +55,7 @@
|
||||||
# Darwin, MinGW, and NonStop.
|
# Darwin, MinGW, and NonStop.
|
||||||
#
|
#
|
||||||
# (3) This script is generated from the Groovy template
|
# (3) This script is generated from the Groovy template
|
||||||
# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
|
# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
|
||||||
# within the Gradle project.
|
# within the Gradle project.
|
||||||
#
|
#
|
||||||
# You can find Gradle at https://github.com/gradle/gradle/.
|
# You can find Gradle at https://github.com/gradle/gradle/.
|
||||||
|
|
@ -86,7 +84,7 @@ done
|
||||||
# shellcheck disable=SC2034
|
# shellcheck disable=SC2034
|
||||||
APP_BASE_NAME=${0##*/}
|
APP_BASE_NAME=${0##*/}
|
||||||
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
|
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
|
||||||
APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit
|
APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit
|
||||||
|
|
||||||
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
||||||
MAX_FD=maximum
|
MAX_FD=maximum
|
||||||
|
|
@ -114,6 +112,7 @@ case "$( uname )" in #(
|
||||||
NONSTOP* ) nonstop=true ;;
|
NONSTOP* ) nonstop=true ;;
|
||||||
esac
|
esac
|
||||||
|
|
||||||
|
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
|
||||||
|
|
||||||
|
|
||||||
# Determine the Java command to use to start the JVM.
|
# Determine the Java command to use to start the JVM.
|
||||||
|
|
@ -171,6 +170,7 @@ fi
|
||||||
# For Cygwin or MSYS, switch paths to Windows format before running java
|
# For Cygwin or MSYS, switch paths to Windows format before running java
|
||||||
if "$cygwin" || "$msys" ; then
|
if "$cygwin" || "$msys" ; then
|
||||||
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
|
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
|
||||||
|
CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
|
||||||
|
|
||||||
JAVACMD=$( cygpath --unix "$JAVACMD" )
|
JAVACMD=$( cygpath --unix "$JAVACMD" )
|
||||||
|
|
||||||
|
|
@ -203,14 +203,15 @@ fi
|
||||||
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
|
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
|
||||||
|
|
||||||
# Collect all arguments for the java command:
|
# Collect all arguments for the java command:
|
||||||
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
|
# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
|
||||||
# and any embedded shellness will be escaped.
|
# and any embedded shellness will be escaped.
|
||||||
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
|
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
|
||||||
# treated as '${Hostname}' itself on the command line.
|
# treated as '${Hostname}' itself on the command line.
|
||||||
|
|
||||||
set -- \
|
set -- \
|
||||||
"-Dorg.gradle.appname=$APP_BASE_NAME" \
|
"-Dorg.gradle.appname=$APP_BASE_NAME" \
|
||||||
-jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \
|
-classpath "$CLASSPATH" \
|
||||||
|
org.gradle.wrapper.GradleWrapperMain \
|
||||||
"$@"
|
"$@"
|
||||||
|
|
||||||
# Stop when "xargs" is not available.
|
# Stop when "xargs" is not available.
|
||||||
|
|
|
||||||
25
gradlew.bat
vendored
25
gradlew.bat
vendored
|
|
@ -13,8 +13,6 @@
|
||||||
@rem See the License for the specific language governing permissions and
|
@rem See the License for the specific language governing permissions and
|
||||||
@rem limitations under the License.
|
@rem limitations under the License.
|
||||||
@rem
|
@rem
|
||||||
@rem SPDX-License-Identifier: Apache-2.0
|
|
||||||
@rem
|
|
||||||
|
|
||||||
@if "%DEBUG%"=="" @echo off
|
@if "%DEBUG%"=="" @echo off
|
||||||
@rem ##########################################################################
|
@rem ##########################################################################
|
||||||
|
|
@ -45,11 +43,11 @@ set JAVA_EXE=java.exe
|
||||||
%JAVA_EXE% -version >NUL 2>&1
|
%JAVA_EXE% -version >NUL 2>&1
|
||||||
if %ERRORLEVEL% equ 0 goto execute
|
if %ERRORLEVEL% equ 0 goto execute
|
||||||
|
|
||||||
echo. 1>&2
|
echo.
|
||||||
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
|
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||||
echo. 1>&2
|
echo.
|
||||||
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
|
echo Please set the JAVA_HOME variable in your environment to match the
|
||||||
echo location of your Java installation. 1>&2
|
echo location of your Java installation.
|
||||||
|
|
||||||
goto fail
|
goto fail
|
||||||
|
|
||||||
|
|
@ -59,21 +57,22 @@ set JAVA_EXE=%JAVA_HOME%/bin/java.exe
|
||||||
|
|
||||||
if exist "%JAVA_EXE%" goto execute
|
if exist "%JAVA_EXE%" goto execute
|
||||||
|
|
||||||
echo. 1>&2
|
echo.
|
||||||
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
|
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
|
||||||
echo. 1>&2
|
echo.
|
||||||
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
|
echo Please set the JAVA_HOME variable in your environment to match the
|
||||||
echo location of your Java installation. 1>&2
|
echo location of your Java installation.
|
||||||
|
|
||||||
goto fail
|
goto fail
|
||||||
|
|
||||||
:execute
|
:execute
|
||||||
@rem Setup the command line
|
@rem Setup the command line
|
||||||
|
|
||||||
|
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
|
||||||
|
|
||||||
|
|
||||||
@rem Execute Gradle
|
@rem Execute Gradle
|
||||||
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %*
|
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
|
||||||
|
|
||||||
:end
|
:end
|
||||||
@rem End local scope for the variables with windows NT shell
|
@rem End local scope for the variables with windows NT shell
|
||||||
|
|
|
||||||
2
lark
2
lark
|
|
@ -1 +1 @@
|
||||||
Subproject commit 10e8d9cd4bbe9fde4dd93c059e2a9faeec6be3e0
|
Subproject commit b80cbbbc57939284cf270a7b450a0059d5f0ec15
|
||||||
48
repackage.sh
48
repackage.sh
|
|
@ -1,48 +0,0 @@
|
||||||
#!/bin/bash
|
|
||||||
set -e # Exit on any error
|
|
||||||
|
|
||||||
# Define paths
|
|
||||||
BUILD_DIR="build"
|
|
||||||
JPACKAGE_DIR="$BUILD_DIR/jpackage"
|
|
||||||
TEMP_DIR="$BUILD_DIR/repackage"
|
|
||||||
|
|
||||||
# Find the .deb file in build/jpackage (assuming there is only one)
|
|
||||||
DEB_FILE=$(find "$JPACKAGE_DIR" -type f -name "*.deb" -print -quit)
|
|
||||||
|
|
||||||
# Check if a .deb file was found
|
|
||||||
if [ -z "$DEB_FILE" ]; then
|
|
||||||
echo "Error: No .deb file found in $JPACKAGE_DIR"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Extract the filename from the path for later use
|
|
||||||
DEB_FILENAME=$(basename "$DEB_FILE")
|
|
||||||
|
|
||||||
echo "Found .deb file: $DEB_FILENAME"
|
|
||||||
|
|
||||||
# Create a temp directory inside build to avoid file conflicts
|
|
||||||
mkdir -p "$TEMP_DIR"
|
|
||||||
cd "$TEMP_DIR"
|
|
||||||
|
|
||||||
# Extract the .deb file contents
|
|
||||||
ar x "../../$DEB_FILE"
|
|
||||||
|
|
||||||
# Decompress zst files to tar
|
|
||||||
unzstd control.tar.zst
|
|
||||||
unzstd data.tar.zst
|
|
||||||
|
|
||||||
# Compress tar files to xz
|
|
||||||
xz -c control.tar > control.tar.xz
|
|
||||||
xz -c data.tar > data.tar.xz
|
|
||||||
|
|
||||||
# Remove the original .deb file
|
|
||||||
rm "../../$DEB_FILE"
|
|
||||||
|
|
||||||
# Create the new .deb file with xz compression in the original location
|
|
||||||
ar cr "../../$DEB_FILE" debian-binary control.tar.xz data.tar.xz
|
|
||||||
|
|
||||||
# Clean up temp files
|
|
||||||
cd ../..
|
|
||||||
rm -rf "$TEMP_DIR"
|
|
||||||
|
|
||||||
echo "Repackaging complete: $DEB_FILENAME"
|
|
||||||
9
src/main/deploy/package/linux-headless/aarch64/control
Normal file
9
src/main/deploy/package/linux-headless/aarch64/control
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
Package: sparrow
|
||||||
|
Version: 2.1.1-1
|
||||||
|
Section: utils
|
||||||
|
Maintainer: Craig Raw <mail@sparrowwallet.com>
|
||||||
|
Priority: optional
|
||||||
|
Architecture: arm64
|
||||||
|
Provides: sparrow
|
||||||
|
Description: Sparrow
|
||||||
|
Depends: libc6, zlib1g
|
||||||
|
|
@ -1,12 +0,0 @@
|
||||||
Package: sparrowserver
|
|
||||||
Version: ${version}-1
|
|
||||||
Section: utils
|
|
||||||
Maintainer: Craig Raw <mail@sparrowwallet.com>
|
|
||||||
Priority: optional
|
|
||||||
Architecture: ${arch}
|
|
||||||
Conflicts: sparrow (<= 2.1.4)
|
|
||||||
Replaces: sparrow (<= 2.1.4)
|
|
||||||
Provides: sparrowserver
|
|
||||||
Description: Sparrow Server
|
|
||||||
Depends: libc6, zlib1g
|
|
||||||
Installed-Size: ${size}
|
|
||||||
|
|
@ -1,85 +0,0 @@
|
||||||
Summary: Sparrow Server
|
|
||||||
Name: sparrowserver
|
|
||||||
Version: ${version}
|
|
||||||
Release: 1
|
|
||||||
License: ASL 2.0
|
|
||||||
Vendor: Unknown
|
|
||||||
|
|
||||||
%if "x" != "x"
|
|
||||||
URL: https://sparrowwallet.com
|
|
||||||
%endif
|
|
||||||
|
|
||||||
%if "x/opt" != "x"
|
|
||||||
Prefix: /opt
|
|
||||||
%endif
|
|
||||||
|
|
||||||
Provides: sparrowserver
|
|
||||||
Obsoletes: sparrow <= 2.1.4
|
|
||||||
|
|
||||||
%if "xutils" != "x"
|
|
||||||
Group: utils
|
|
||||||
%endif
|
|
||||||
|
|
||||||
Autoprov: 0
|
|
||||||
Autoreq: 0
|
|
||||||
|
|
||||||
#comment line below to enable effective jar compression
|
|
||||||
#it could easily get your package size from 40 to 15Mb but
|
|
||||||
#build time will substantially increase and it may require unpack200/system java to install
|
|
||||||
%define __jar_repack %{nil}
|
|
||||||
|
|
||||||
# on RHEL we got unwanted improved debugging enhancements
|
|
||||||
%define _build_id_links none
|
|
||||||
|
|
||||||
%define package_filelist %{_builddir}/%{name}.files
|
|
||||||
%define app_filelist %{_builddir}/%{name}.app.files
|
|
||||||
%define filesystem_filelist %{_builddir}/%{name}.filesystem.files
|
|
||||||
|
|
||||||
%define default_filesystem / /opt /usr /usr/bin /usr/lib /usr/local /usr/local/bin /usr/local/lib
|
|
||||||
|
|
||||||
%description
|
|
||||||
Sparrow Server
|
|
||||||
|
|
||||||
%global __os_install_post %{nil}
|
|
||||||
|
|
||||||
%prep
|
|
||||||
|
|
||||||
%build
|
|
||||||
|
|
||||||
%install
|
|
||||||
rm -rf %{buildroot}
|
|
||||||
install -d -m 755 %{buildroot}/opt/sparrowserver
|
|
||||||
cp -r %{_sourcedir}/opt/sparrowserver/* %{buildroot}/opt/sparrowserver
|
|
||||||
if [ "$(echo %{_sourcedir}/lib/systemd/system/*.service)" != '%{_sourcedir}/lib/systemd/system/*.service' ]; then
|
|
||||||
install -d -m 755 %{buildroot}/lib/systemd/system
|
|
||||||
cp %{_sourcedir}/lib/systemd/system/*.service %{buildroot}/lib/systemd/system
|
|
||||||
fi
|
|
||||||
%if "x%{_rpmdir}/../../LICENSE" != "x"
|
|
||||||
%define license_install_file %{_defaultlicensedir}/%{name}-%{version}/%{basename:%{_rpmdir}/../../LICENSE}
|
|
||||||
install -d -m 755 "%{buildroot}%{dirname:%{license_install_file}}"
|
|
||||||
install -m 644 "%{_rpmdir}/../../LICENSE" "%{buildroot}%{license_install_file}"
|
|
||||||
%endif
|
|
||||||
(cd %{buildroot} && find . -path ./lib/systemd -prune -o -type d -print) | sed -e 's/^\.//' -e '/^$/d' | sort > %{app_filelist}
|
|
||||||
{ rpm -ql filesystem || echo %{default_filesystem}; } | sort > %{filesystem_filelist}
|
|
||||||
comm -23 %{app_filelist} %{filesystem_filelist} > %{package_filelist}
|
|
||||||
sed -i -e 's/.*/%dir "&"/' %{package_filelist}
|
|
||||||
(cd %{buildroot} && find . -not -type d) | sed -e 's/^\.//' -e 's/.*/"&"/' >> %{package_filelist}
|
|
||||||
%if "x%{_rpmdir}/../../LICENSE" != "x"
|
|
||||||
sed -i -e 's|"%{license_install_file}"||' -e '/^$/d' %{package_filelist}
|
|
||||||
%endif
|
|
||||||
|
|
||||||
%files -f %{package_filelist}
|
|
||||||
%if "x%{_rpmdir}/../../LICENSE" != "x"
|
|
||||||
%license "%{license_install_file}"
|
|
||||||
%endif
|
|
||||||
|
|
||||||
%post
|
|
||||||
package_type=rpm
|
|
||||||
|
|
||||||
%pre
|
|
||||||
package_type=rpm
|
|
||||||
|
|
||||||
%preun
|
|
||||||
package_type=rpm
|
|
||||||
|
|
||||||
%clean
|
|
||||||
9
src/main/deploy/package/linux-headless/x64/control
Normal file
9
src/main/deploy/package/linux-headless/x64/control
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
Package: sparrow
|
||||||
|
Version: 2.1.1-1
|
||||||
|
Section: utils
|
||||||
|
Maintainer: Craig Raw <mail@sparrowwallet.com>
|
||||||
|
Priority: optional
|
||||||
|
Architecture: amd64
|
||||||
|
Provides: sparrow
|
||||||
|
Description: Sparrow
|
||||||
|
Depends: libc6, zlib1g
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
[Desktop Entry]
|
[Desktop Entry]
|
||||||
Name=Sparrow
|
Name=Sparrow
|
||||||
Comment=Sparrow
|
Comment=Sparrow
|
||||||
Exec=/opt/sparrowwallet/bin/Sparrow %U
|
Exec=/opt/sparrow/bin/Sparrow %U
|
||||||
Icon=/opt/sparrowwallet/lib/Sparrow.png
|
Icon=/opt/sparrow/lib/Sparrow.png
|
||||||
Terminal=false
|
Terminal=false
|
||||||
Type=Application
|
Type=Application
|
||||||
Categories=Finance;Network;
|
Categories=Finance;Network;
|
||||||
|
|
|
||||||
|
|
@ -1,12 +0,0 @@
|
||||||
Package: sparrowwallet
|
|
||||||
Version: ${version}-1
|
|
||||||
Section: utils
|
|
||||||
Maintainer: Craig Raw <mail@sparrowwallet.com>
|
|
||||||
Priority: optional
|
|
||||||
Architecture: ${arch}
|
|
||||||
Provides: sparrowwallet
|
|
||||||
Conflicts: sparrow (<= 2.1.4)
|
|
||||||
Replaces: sparrow (<= 2.1.4)
|
|
||||||
Description: Sparrow Wallet
|
|
||||||
Depends: libasound2, libbsd0, libc6, libmd0, libx11-6, libxau6, libxcb1, libxdmcp6, libxext6, libxi6, libxrender1, libxtst6, xdg-utils
|
|
||||||
Installed-Size: ${size}
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
#!/bin/sh
|
#!/bin/sh
|
||||||
# postinst script for sparrowwallet
|
# postinst script for sparrow
|
||||||
#
|
#
|
||||||
# see: dh_installdeb(1)
|
# see: dh_installdeb(1)
|
||||||
|
|
||||||
|
|
@ -22,9 +22,9 @@ package_type=deb
|
||||||
|
|
||||||
case "$1" in
|
case "$1" in
|
||||||
configure)
|
configure)
|
||||||
xdg-desktop-menu install /opt/sparrowwallet/lib/sparrowwallet-Sparrow.desktop
|
xdg-desktop-menu install /opt/sparrow/lib/sparrow-Sparrow.desktop
|
||||||
xdg-mime install /opt/sparrowwallet/lib/sparrowwallet-Sparrow-MimeInfo.xml
|
xdg-mime install /opt/sparrow/lib/sparrow-Sparrow-MimeInfo.xml
|
||||||
install -D -m 644 /opt/sparrowwallet/lib/runtime/conf/udev/*.rules /etc/udev/rules.d
|
install -D -m 644 /opt/sparrow/lib/runtime/conf/udev/*.rules /etc/udev/rules.d
|
||||||
if ! getent group plugdev > /dev/null; then
|
if ! getent group plugdev > /dev/null; then
|
||||||
groupadd plugdev
|
groupadd plugdev
|
||||||
fi
|
fi
|
||||||
|
|
|
||||||
|
|
@ -1,20 +1,19 @@
|
||||||
Summary: Sparrow
|
Summary: Sparrow
|
||||||
Name: sparrowwallet
|
Name: sparrow
|
||||||
Version: ${version}
|
Version: 2.1.1
|
||||||
Release: 1
|
Release: 1
|
||||||
License: ASL 2.0
|
License: ASL 2.0
|
||||||
Vendor: Unknown
|
Vendor: Unknown
|
||||||
|
|
||||||
%if "x" != "x"
|
%if "x" != "x"
|
||||||
URL: https://sparrowwallet.com
|
URL:
|
||||||
%endif
|
%endif
|
||||||
|
|
||||||
%if "x/opt" != "x"
|
%if "x/opt" != "x"
|
||||||
Prefix: /opt
|
Prefix: /opt
|
||||||
%endif
|
%endif
|
||||||
|
|
||||||
Provides: sparrowwallet
|
Provides: sparrow
|
||||||
Obsoletes: sparrow <= 2.1.4
|
|
||||||
|
|
||||||
%if "xutils" != "x"
|
%if "xutils" != "x"
|
||||||
Group: utils
|
Group: utils
|
||||||
|
|
@ -41,7 +40,7 @@ Requires: xdg-utils
|
||||||
%define default_filesystem / /opt /usr /usr/bin /usr/lib /usr/local /usr/local/bin /usr/local/lib
|
%define default_filesystem / /opt /usr /usr/bin /usr/lib /usr/local /usr/local/bin /usr/local/lib
|
||||||
|
|
||||||
%description
|
%description
|
||||||
Sparrow Wallet
|
Sparrow
|
||||||
|
|
||||||
%global __os_install_post %{nil}
|
%global __os_install_post %{nil}
|
||||||
|
|
||||||
|
|
@ -51,8 +50,8 @@ Sparrow Wallet
|
||||||
|
|
||||||
%install
|
%install
|
||||||
rm -rf %{buildroot}
|
rm -rf %{buildroot}
|
||||||
install -d -m 755 %{buildroot}/opt/sparrowwallet
|
install -d -m 755 %{buildroot}/opt/sparrow
|
||||||
cp -r %{_sourcedir}/opt/sparrowwallet/* %{buildroot}/opt/sparrowwallet
|
cp -r %{_sourcedir}/opt/sparrow/* %{buildroot}/opt/sparrow
|
||||||
if [ "$(echo %{_sourcedir}/lib/systemd/system/*.service)" != '%{_sourcedir}/lib/systemd/system/*.service' ]; then
|
if [ "$(echo %{_sourcedir}/lib/systemd/system/*.service)" != '%{_sourcedir}/lib/systemd/system/*.service' ]; then
|
||||||
install -d -m 755 %{buildroot}/lib/systemd/system
|
install -d -m 755 %{buildroot}/lib/systemd/system
|
||||||
cp %{_sourcedir}/lib/systemd/system/*.service %{buildroot}/lib/systemd/system
|
cp %{_sourcedir}/lib/systemd/system/*.service %{buildroot}/lib/systemd/system
|
||||||
|
|
@ -78,9 +77,9 @@ sed -i -e 's/.*/%dir "&"/' %{package_filelist}
|
||||||
|
|
||||||
%post
|
%post
|
||||||
package_type=rpm
|
package_type=rpm
|
||||||
xdg-desktop-menu install /opt/sparrowwallet/lib/sparrowwallet-Sparrow.desktop
|
xdg-desktop-menu install /opt/sparrow/lib/sparrow-Sparrow.desktop
|
||||||
xdg-mime install /opt/sparrowwallet/lib/sparrowwallet-Sparrow-MimeInfo.xml
|
xdg-mime install /opt/sparrow/lib/sparrow-Sparrow-MimeInfo.xml
|
||||||
install -D -m 644 /opt/sparrowwallet/lib/runtime/conf/udev/*.rules /etc/udev/rules.d
|
install -D -m 644 /opt/sparrow/lib/runtime/conf/udev/*.rules /etc/udev/rules.d
|
||||||
if ! getent group plugdev > /dev/null; then
|
if ! getent group plugdev > /dev/null; then
|
||||||
groupadd plugdev
|
groupadd plugdev
|
||||||
fi
|
fi
|
||||||
|
|
@ -252,9 +251,9 @@ desktop_trace ()
|
||||||
echo "$@"
|
echo "$@"
|
||||||
}
|
}
|
||||||
|
|
||||||
do_if_file_belongs_to_single_package /opt/sparrowwallet/lib/sparrowwallet-Sparrow.desktop xdg-desktop-menu uninstall /opt/sparrowwallet/lib/sparrowwallet-Sparrow.desktop
|
do_if_file_belongs_to_single_package /opt/sparrow/lib/sparrow-Sparrow.desktop xdg-desktop-menu uninstall /opt/sparrow/lib/sparrow-Sparrow.desktop
|
||||||
do_if_file_belongs_to_single_package /opt/sparrowwallet/lib/sparrowwallet-Sparrow-MimeInfo.xml xdg-mime uninstall /opt/sparrowwallet/lib/sparrowwallet-Sparrow-MimeInfo.xml
|
do_if_file_belongs_to_single_package /opt/sparrow/lib/sparrow-Sparrow-MimeInfo.xml xdg-mime uninstall /opt/sparrow/lib/sparrow-Sparrow-MimeInfo.xml
|
||||||
do_if_file_belongs_to_single_package /opt/sparrowwallet/lib/sparrowwallet-Sparrow.desktop desktop_uninstall_default_mime_handler sparrowwallet-Sparrow.desktop application/psbt application/bitcoin-transaction application/pgp-signature x-scheme-handler/bitcoin x-scheme-handler/auth47 x-scheme-handler/lightning
|
do_if_file_belongs_to_single_package /opt/sparrow/lib/sparrow-Sparrow.desktop desktop_uninstall_default_mime_handler sparrow-Sparrow.desktop application/psbt application/bitcoin-transaction application/pgp-signature x-scheme-handler/bitcoin x-scheme-handler/auth47 x-scheme-handler/lightning
|
||||||
|
|
||||||
|
|
||||||
%clean
|
%clean
|
||||||
|
|
@ -21,7 +21,7 @@
|
||||||
<key>CFBundlePackageType</key>
|
<key>CFBundlePackageType</key>
|
||||||
<string>APPL</string>
|
<string>APPL</string>
|
||||||
<key>CFBundleShortVersionString</key>
|
<key>CFBundleShortVersionString</key>
|
||||||
<string>2.3.1</string>
|
<string>2.1.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,12 +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>
|
|
||||||
<string>Sparrow requires access to the local network in order to connect to your configured server</string>
|
|
||||||
<key>CFBundleURLTypes</key>
|
<key>CFBundleURLTypes</key>
|
||||||
<array>
|
<array>
|
||||||
<dict>
|
<dict>
|
||||||
|
|
|
||||||
|
|
@ -1,16 +1,17 @@
|
||||||
package com.sparrowwallet.sparrow;
|
package com.sparrowwallet.sparrow;
|
||||||
|
|
||||||
import com.beust.jcommander.JCommander;
|
import com.beust.jcommander.JCommander;
|
||||||
|
import com.google.common.base.Charsets;
|
||||||
import com.google.common.eventbus.Subscribe;
|
import com.google.common.eventbus.Subscribe;
|
||||||
|
import com.google.common.io.ByteSource;
|
||||||
import com.sparrowwallet.drongo.*;
|
import com.sparrowwallet.drongo.*;
|
||||||
import com.sparrowwallet.drongo.address.Address;
|
|
||||||
import com.sparrowwallet.drongo.crypto.*;
|
import com.sparrowwallet.drongo.crypto.*;
|
||||||
import com.sparrowwallet.drongo.dns.DnsPayment;
|
|
||||||
import com.sparrowwallet.drongo.dns.DnsPaymentCache;
|
|
||||||
import com.sparrowwallet.drongo.policy.PolicyType;
|
import com.sparrowwallet.drongo.policy.PolicyType;
|
||||||
import com.sparrowwallet.drongo.protocol.*;
|
import com.sparrowwallet.drongo.protocol.*;
|
||||||
import com.sparrowwallet.drongo.psbt.*;
|
import com.sparrowwallet.drongo.psbt.PSBT;
|
||||||
import com.sparrowwallet.drongo.silentpayments.SilentPaymentAddress;
|
import com.sparrowwallet.drongo.psbt.PSBTInput;
|
||||||
|
import com.sparrowwallet.drongo.psbt.PSBTParseException;
|
||||||
|
import com.sparrowwallet.drongo.psbt.PSBTSignatureException;
|
||||||
import com.sparrowwallet.drongo.wallet.*;
|
import com.sparrowwallet.drongo.wallet.*;
|
||||||
import com.sparrowwallet.hummingbird.UR;
|
import com.sparrowwallet.hummingbird.UR;
|
||||||
import com.sparrowwallet.hummingbird.registry.CryptoPSBT;
|
import com.sparrowwallet.hummingbird.registry.CryptoPSBT;
|
||||||
|
|
@ -31,7 +32,7 @@ import com.sparrowwallet.sparrow.transaction.TransactionView;
|
||||||
import com.sparrowwallet.sparrow.wallet.Entry;
|
import com.sparrowwallet.sparrow.wallet.Entry;
|
||||||
import com.sparrowwallet.sparrow.wallet.WalletController;
|
import com.sparrowwallet.sparrow.wallet.WalletController;
|
||||||
import com.sparrowwallet.sparrow.wallet.WalletForm;
|
import com.sparrowwallet.sparrow.wallet.WalletForm;
|
||||||
import de.jangassen.MenuToolkit;
|
import de.codecentric.centerdevice.MenuToolkit;
|
||||||
import javafx.animation.*;
|
import javafx.animation.*;
|
||||||
import javafx.application.Platform;
|
import javafx.application.Platform;
|
||||||
import javafx.beans.binding.Bindings;
|
import javafx.beans.binding.Bindings;
|
||||||
|
|
@ -50,14 +51,12 @@ import javafx.geometry.Side;
|
||||||
import javafx.scene.Node;
|
import javafx.scene.Node;
|
||||||
import javafx.scene.Scene;
|
import javafx.scene.Scene;
|
||||||
import javafx.scene.control.*;
|
import javafx.scene.control.*;
|
||||||
import javafx.scene.control.Label;
|
import javafx.scene.image.Image;
|
||||||
import javafx.scene.control.Menu;
|
import javafx.scene.image.ImageView;
|
||||||
import javafx.scene.control.MenuItem;
|
|
||||||
import javafx.scene.input.*;
|
import javafx.scene.input.*;
|
||||||
import javafx.scene.layout.*;
|
import javafx.scene.layout.*;
|
||||||
import javafx.scene.paint.Color;
|
import javafx.scene.paint.Color;
|
||||||
import javafx.stage.*;
|
import javafx.stage.*;
|
||||||
import javafx.stage.Window;
|
|
||||||
import javafx.util.Duration;
|
import javafx.util.Duration;
|
||||||
import org.controlsfx.control.Notifications;
|
import org.controlsfx.control.Notifications;
|
||||||
import org.controlsfx.control.StatusBar;
|
import org.controlsfx.control.StatusBar;
|
||||||
|
|
@ -72,7 +71,6 @@ import java.nio.charset.StandardCharsets;
|
||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
import java.text.ParseException;
|
import java.text.ParseException;
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
import java.util.List;
|
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
import static com.sparrowwallet.sparrow.AppServices.*;
|
import static com.sparrowwallet.sparrow.AppServices.*;
|
||||||
|
|
@ -82,7 +80,6 @@ public class AppController implements Initializable {
|
||||||
private static final Logger log = LoggerFactory.getLogger(AppController.class);
|
private static final Logger log = LoggerFactory.getLogger(AppController.class);
|
||||||
|
|
||||||
public static final String DRAG_OVER_CLASS = "drag-over";
|
public static final String DRAG_OVER_CLASS = "drag-over";
|
||||||
public static final int TAB_LABEL_MAX_WIDTH = 300;
|
|
||||||
public static final double TAB_LABEL_GRAPHIC_OPACITY_INACTIVE = 0.8;
|
public static final double TAB_LABEL_GRAPHIC_OPACITY_INACTIVE = 0.8;
|
||||||
public static final double TAB_LABEL_GRAPHIC_OPACITY_ACTIVE = 0.95;
|
public static final double TAB_LABEL_GRAPHIC_OPACITY_ACTIVE = 0.95;
|
||||||
public static final String LOADING_TRANSACTIONS_MESSAGE = "Loading wallet, select Transactions tab to view...";
|
public static final String LOADING_TRANSACTIONS_MESSAGE = "Loading wallet, select Transactions tab to view...";
|
||||||
|
|
@ -384,7 +381,7 @@ public class AppController implements Initializable {
|
||||||
openWalletsInNewWindows.selectedProperty().bindBidirectional(openWalletsInNewWindowsProperty);
|
openWalletsInNewWindows.selectedProperty().bindBidirectional(openWalletsInNewWindowsProperty);
|
||||||
hideEmptyUsedAddressesProperty.set(Config.get().isHideEmptyUsedAddresses());
|
hideEmptyUsedAddressesProperty.set(Config.get().isHideEmptyUsedAddresses());
|
||||||
hideEmptyUsedAddresses.selectedProperty().bindBidirectional(hideEmptyUsedAddressesProperty);
|
hideEmptyUsedAddresses.selectedProperty().bindBidirectional(hideEmptyUsedAddressesProperty);
|
||||||
useHdCameraResolutionProperty.set(Config.get().getWebcamResolution() == null || Config.get().getWebcamResolution().isWidescreenAspect());
|
useHdCameraResolutionProperty.set(Config.get().isHdCapture());
|
||||||
useHdCameraResolution.selectedProperty().bindBidirectional(useHdCameraResolutionProperty);
|
useHdCameraResolution.selectedProperty().bindBidirectional(useHdCameraResolutionProperty);
|
||||||
mirrorCameraImageProperty.set(Config.get().isMirrorCapture());
|
mirrorCameraImageProperty.set(Config.get().isMirrorCapture());
|
||||||
mirrorCameraImage.selectedProperty().bindBidirectional(mirrorCameraImageProperty);
|
mirrorCameraImage.selectedProperty().bindBidirectional(mirrorCameraImageProperty);
|
||||||
|
|
@ -576,16 +573,16 @@ public class AppController implements Initializable {
|
||||||
|
|
||||||
public void installUdevRules(ActionEvent event) {
|
public void installUdevRules(ActionEvent event) {
|
||||||
String commands = """
|
String commands = """
|
||||||
sudo install -m 644 /opt/sparrowwallet/lib/runtime/conf/udev/*.rules /etc/udev/rules.d
|
sudo install -m 644 /opt/sparrow/lib/runtime/conf/udev/*.rules /etc/udev/rules.d
|
||||||
sudo udevadm control --reload
|
sudo udevadm control --reload
|
||||||
sudo udevadm trigger
|
sudo udevadm trigger
|
||||||
sudo groupadd -f plugdev
|
sudo groupadd -f plugdev
|
||||||
sudo usermod -aG plugdev `whoami`
|
sudo usermod -aG plugdev `whoami`
|
||||||
""";
|
""";
|
||||||
String home = System.getProperty(JPACKAGE_APP_PATH);
|
String home = System.getProperty(JPACKAGE_APP_PATH);
|
||||||
if(home != null && !home.startsWith("/opt/sparrowwallet") && home.endsWith("bin/Sparrow")) {
|
if(home != null && !home.startsWith("/opt/sparrow") && home.endsWith("bin/Sparrow")) {
|
||||||
home = home.replace("bin/Sparrow", "");
|
home = home.replace("bin/Sparrow", "");
|
||||||
commands = commands.replace("/opt/sparrowwallet/", home);
|
commands = commands.replace("/opt/sparrow/", home);
|
||||||
}
|
}
|
||||||
|
|
||||||
TextAreaDialog dialog = new TextAreaDialog(commands, false);
|
TextAreaDialog dialog = new TextAreaDialog(commands, false);
|
||||||
|
|
@ -636,10 +633,19 @@ public class AppController implements Initializable {
|
||||||
byte[] bytes = Files.readAllBytes(file.toPath());
|
byte[] bytes = Files.readAllBytes(file.toPath());
|
||||||
String name = file.getName();
|
String name = file.getName();
|
||||||
|
|
||||||
if(Utils.isHex(bytes) || Utils.isBase64(bytes)) {
|
try {
|
||||||
addTransactionTab(name, file, new String(bytes, StandardCharsets.UTF_8).trim());
|
|
||||||
} else {
|
|
||||||
addTransactionTab(name, file, bytes);
|
addTransactionTab(name, file, bytes);
|
||||||
|
} catch(ParseException e) {
|
||||||
|
ByteArrayInputStream inputStream = new ByteArrayInputStream(bytes);
|
||||||
|
ByteSource byteSource = new ByteSource() {
|
||||||
|
@Override
|
||||||
|
public InputStream openStream() {
|
||||||
|
return inputStream;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
String text = byteSource.asCharSource(Charsets.UTF_8).read().trim();
|
||||||
|
addTransactionTab(name, file, text);
|
||||||
}
|
}
|
||||||
} catch(IOException e) {
|
} catch(IOException e) {
|
||||||
showErrorDialog("Error opening file", e.getMessage());
|
showErrorDialog("Error opening file", e.getMessage());
|
||||||
|
|
@ -826,10 +832,10 @@ public class AppController implements Initializable {
|
||||||
try(FileOutputStream outputStream = new FileOutputStream(file)) {
|
try(FileOutputStream outputStream = new FileOutputStream(file)) {
|
||||||
if(asText) {
|
if(asText) {
|
||||||
PrintWriter writer = new PrintWriter(new OutputStreamWriter(outputStream, StandardCharsets.UTF_8));
|
PrintWriter writer = new PrintWriter(new OutputStreamWriter(outputStream, StandardCharsets.UTF_8));
|
||||||
writer.print(transactionTabData.getPsbt().getForExport().toBase64String(includeXpubs));
|
writer.print(transactionTabData.getPsbt().toBase64String(includeXpubs));
|
||||||
writer.flush();
|
writer.flush();
|
||||||
} else {
|
} else {
|
||||||
outputStream.write(transactionTabData.getPsbt().getForExport().serialize(includeXpubs, true));
|
outputStream.write(transactionTabData.getPsbt().serialize(includeXpubs, true));
|
||||||
}
|
}
|
||||||
} catch(IOException e) {
|
} catch(IOException e) {
|
||||||
log.error("Error saving PSBT", e);
|
log.error("Error saving PSBT", e);
|
||||||
|
|
@ -852,7 +858,7 @@ public class AppController implements Initializable {
|
||||||
TabData tabData = (TabData)selectedTab.getUserData();
|
TabData tabData = (TabData)selectedTab.getUserData();
|
||||||
if(tabData.getType() == TabData.TabType.TRANSACTION) {
|
if(tabData.getType() == TabData.TabType.TRANSACTION) {
|
||||||
TransactionTabData transactionTabData = (TransactionTabData)tabData;
|
TransactionTabData transactionTabData = (TransactionTabData)tabData;
|
||||||
String data = asBase64 ? transactionTabData.getPsbt().getForExport().toBase64String() : transactionTabData.getPsbt().getForExport().toString();
|
String data = asBase64 ? transactionTabData.getPsbt().toBase64String() : transactionTabData.getPsbt().toString();
|
||||||
|
|
||||||
ClipboardContent content = new ClipboardContent();
|
ClipboardContent content = new ClipboardContent();
|
||||||
content.putString(data);
|
content.putString(data);
|
||||||
|
|
@ -866,7 +872,7 @@ public class AppController implements Initializable {
|
||||||
if(tabData.getType() == TabData.TabType.TRANSACTION) {
|
if(tabData.getType() == TabData.TabType.TRANSACTION) {
|
||||||
TransactionTabData transactionTabData = (TransactionTabData)tabData;
|
TransactionTabData transactionTabData = (TransactionTabData)tabData;
|
||||||
|
|
||||||
byte[] psbtBytes = transactionTabData.getPsbt().getForExport().serialize();
|
byte[] psbtBytes = transactionTabData.getPsbt().serialize();
|
||||||
CryptoPSBT cryptoPSBT = new CryptoPSBT(psbtBytes);
|
CryptoPSBT cryptoPSBT = new CryptoPSBT(psbtBytes);
|
||||||
BBQR bbqr = new BBQR(BBQRType.PSBT, psbtBytes);
|
BBQR bbqr = new BBQR(BBQRType.PSBT, psbtBytes);
|
||||||
QRDisplayDialog qrDisplayDialog = new QRDisplayDialog(cryptoPSBT.toUR(), bbqr, false, true, false);
|
QRDisplayDialog qrDisplayDialog = new QRDisplayDialog(cryptoPSBT.toUR(), bbqr, false, true, false);
|
||||||
|
|
@ -949,11 +955,7 @@ public class AppController implements Initializable {
|
||||||
|
|
||||||
public void useHdCameraResolution(ActionEvent event) {
|
public void useHdCameraResolution(ActionEvent event) {
|
||||||
CheckMenuItem item = (CheckMenuItem)event.getSource();
|
CheckMenuItem item = (CheckMenuItem)event.getSource();
|
||||||
if(Config.get().getWebcamResolution().isStandardAspect() && item.isSelected()) {
|
Config.get().setHdCapture(item.isSelected());
|
||||||
Config.get().setWebcamResolution(WebcamResolution.HD);
|
|
||||||
} else if(Config.get().getWebcamResolution().isWidescreenAspect() && !item.isSelected()) {
|
|
||||||
Config.get().setWebcamResolution(WebcamResolution.VGA);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void mirrorCameraImage(ActionEvent event) {
|
public void mirrorCameraImage(ActionEvent event) {
|
||||||
|
|
@ -1038,10 +1040,6 @@ public class AppController implements Initializable {
|
||||||
cmd.add(System.getProperty(JPACKAGE_APP_PATH));
|
cmd.add(System.getProperty(JPACKAGE_APP_PATH));
|
||||||
cmd.addAll(args.toParams());
|
cmd.addAll(args.toParams());
|
||||||
final ProcessBuilder builder = new ProcessBuilder(cmd);
|
final ProcessBuilder builder = new ProcessBuilder(cmd);
|
||||||
if(OsType.getCurrent() == OsType.UNIX) {
|
|
||||||
Map<String, String> env = builder.environment();
|
|
||||||
env.remove("LD_LIBRARY_PATH");
|
|
||||||
}
|
|
||||||
builder.start();
|
builder.start();
|
||||||
quit(event);
|
quit(event);
|
||||||
} catch(Exception e) {
|
} catch(Exception e) {
|
||||||
|
|
@ -1265,10 +1263,6 @@ public class AppController implements Initializable {
|
||||||
}
|
}
|
||||||
|
|
||||||
private void addImportedWallet(Wallet wallet) {
|
private void addImportedWallet(Wallet wallet) {
|
||||||
if(AppServices.disallowAnyInvalidDerivationPaths(wallet)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
WalletNameDialog nameDlg = new WalletNameDialog(wallet.getName(), true, wallet.getBirthDate());
|
WalletNameDialog nameDlg = new WalletNameDialog(wallet.getName(), true, wallet.getBirthDate());
|
||||||
nameDlg.initOwner(rootStack.getScene().getWindow());
|
nameDlg.initOwner(rootStack.getScene().getWindow());
|
||||||
Optional<WalletNameDialog.NameAndBirthDate> optNameAndBirthDate = nameDlg.showAndWait();
|
Optional<WalletNameDialog.NameAndBirthDate> optNameAndBirthDate = nameDlg.showAndWait();
|
||||||
|
|
@ -1384,7 +1378,7 @@ public class AppController implements Initializable {
|
||||||
public void exportWallet(ActionEvent event) {
|
public void exportWallet(ActionEvent event) {
|
||||||
WalletForm selectedWalletForm = getSelectedWalletForm();
|
WalletForm selectedWalletForm = getSelectedWalletForm();
|
||||||
if(selectedWalletForm != null) {
|
if(selectedWalletForm != null) {
|
||||||
WalletExportDialog dlg = new WalletExportDialog(selectedWalletForm, getSelectedWalletForms());
|
WalletExportDialog dlg = new WalletExportDialog(selectedWalletForm);
|
||||||
dlg.initOwner(rootStack.getScene().getWindow());
|
dlg.initOwner(rootStack.getScene().getWindow());
|
||||||
Optional<Wallet> wallet = dlg.showAndWait();
|
Optional<Wallet> wallet = dlg.showAndWait();
|
||||||
if(wallet.isPresent()) {
|
if(wallet.isPresent()) {
|
||||||
|
|
@ -1430,10 +1424,6 @@ 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);
|
||||||
|
|
@ -1449,7 +1439,7 @@ public class AppController implements Initializable {
|
||||||
bitcoinUnit = wallet.getAutoUnit();
|
bitcoinUnit = wallet.getAutoUnit();
|
||||||
}
|
}
|
||||||
|
|
||||||
sendToManyDialog = new SendToManyDialog(bitcoinUnit, initialPayments);
|
sendToManyDialog = new SendToManyDialog(bitcoinUnit);
|
||||||
sendToManyDialog.initModality(Modality.NONE);
|
sendToManyDialog.initModality(Modality.NONE);
|
||||||
Optional<List<Payment>> optPayments = sendToManyDialog.showAndWait();
|
Optional<List<Payment>> optPayments = sendToManyDialog.showAndWait();
|
||||||
sendToManyDialog = null;
|
sendToManyDialog = null;
|
||||||
|
|
@ -1490,7 +1480,6 @@ public class AppController implements Initializable {
|
||||||
stage.setAlwaysOnTop(true);
|
stage.setAlwaysOnTop(true);
|
||||||
stage.setAlwaysOnTop(false);
|
stage.setAlwaysOnTop(false);
|
||||||
if(event.getSource() instanceof File file) {
|
if(event.getSource() instanceof File file) {
|
||||||
downloadVerifierDialog.setInitialFile(file);
|
|
||||||
downloadVerifierDialog.setSignatureFile(file);
|
downloadVerifierDialog.setSignatureFile(file);
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
|
|
@ -1901,11 +1890,6 @@ public class AppController implements Initializable {
|
||||||
}
|
}
|
||||||
|
|
||||||
private void addTransactionTab(String name, File file, PSBT psbt) {
|
private void addTransactionTab(String name, File file, PSBT psbt) {
|
||||||
//Convert to PSBTv0 first
|
|
||||||
if(psbt.getVersion() != null && psbt.getVersion() >= 2) {
|
|
||||||
psbt.convertVersion(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
//Add any missing previous outputs if available in open wallets
|
//Add any missing previous outputs if available in open wallets
|
||||||
for(PSBTInput psbtInput : psbt.getPsbtInputs()) {
|
for(PSBTInput psbtInput : psbt.getPsbtInputs()) {
|
||||||
if(psbtInput.getUtxo() == null) {
|
if(psbtInput.getUtxo() == null) {
|
||||||
|
|
@ -1925,39 +1909,6 @@ public class AppController implements Initializable {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
//Add DNS payment information if not already cached
|
|
||||||
for(PSBTOutput psbtOutput : psbt.getPsbtOutputs()) {
|
|
||||||
if(psbtOutput.getDnssecProof() != null && !psbtOutput.getDnssecProof().isEmpty()) {
|
|
||||||
Address address = psbtOutput.getScript() != null ? psbtOutput.getScript().getToAddress() : null;
|
|
||||||
if(address != null && DnsPaymentCache.getDnsPayment(address) == null) {
|
|
||||||
try {
|
|
||||||
Optional<DnsPayment> optDnsPayment = psbtOutput.getDnsPayment();
|
|
||||||
if(optDnsPayment.isPresent() && address.equals(optDnsPayment.get().bitcoinURI().getAddress())) {
|
|
||||||
DnsPaymentCache.putDnsPayment(address, optDnsPayment.get());
|
|
||||||
}
|
|
||||||
} catch(Exception e) {
|
|
||||||
log.debug("Error resolving DNS payment", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
SilentPaymentAddress silentPaymentAddress = psbtOutput.getSilentPaymentAddress();
|
|
||||||
if(address != null && silentPaymentAddress == null) {
|
|
||||||
silentPaymentAddress = AppServices.get().getOpenWallets().keySet().stream()
|
|
||||||
.map(wallet -> wallet.getSilentPaymentAddress(address)).filter(Objects::nonNull).findFirst().orElse(null);
|
|
||||||
}
|
|
||||||
if(silentPaymentAddress != null && DnsPaymentCache.getDnsPayment(silentPaymentAddress) == null) {
|
|
||||||
try {
|
|
||||||
Optional<DnsPayment> optDnsPayment = psbtOutput.getDnsPayment();
|
|
||||||
if(optDnsPayment.isPresent() && silentPaymentAddress.equals(optDnsPayment.get().bitcoinURI().getSilentPaymentAddress())) {
|
|
||||||
DnsPaymentCache.putDnsPayment(silentPaymentAddress, optDnsPayment.get());
|
|
||||||
}
|
|
||||||
} catch(Exception e) {
|
|
||||||
log.debug("Error resolving DNS payment", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Window psbtWalletWindow = AppServices.get().getWindowForPSBT(psbt);
|
Window psbtWalletWindow = AppServices.get().getWindowForPSBT(psbt);
|
||||||
if(psbtWalletWindow != null && !tabs.getScene().getWindow().equals(psbtWalletWindow)) {
|
if(psbtWalletWindow != null && !tabs.getScene().getWindow().equals(psbtWalletWindow)) {
|
||||||
EventManager.get().post(new ViewPSBTEvent(psbtWalletWindow, name, file, psbt));
|
EventManager.get().post(new ViewPSBTEvent(psbtWalletWindow, name, file, psbt));
|
||||||
|
|
@ -2041,13 +1992,8 @@ public class AppController implements Initializable {
|
||||||
glyph.setFontSize(10.0);
|
glyph.setFontSize(10.0);
|
||||||
glyph.setOpacity(TAB_LABEL_GRAPHIC_OPACITY_ACTIVE);
|
glyph.setOpacity(TAB_LABEL_GRAPHIC_OPACITY_ACTIVE);
|
||||||
Label tabLabel = new Label(tabName);
|
Label tabLabel = new Label(tabName);
|
||||||
tabLabel.setMaxWidth(TAB_LABEL_MAX_WIDTH);
|
|
||||||
tabLabel.setGraphic(glyph);
|
tabLabel.setGraphic(glyph);
|
||||||
tabLabel.setGraphicTextGap(5.0);
|
tabLabel.setGraphicTextGap(5.0);
|
||||||
if(TextUtils.computeTextWidth(tabLabel.getFont(), tabName, 0.0D) > TAB_LABEL_MAX_WIDTH) {
|
|
||||||
Tooltip tooltip = new Tooltip(tabName);
|
|
||||||
tabLabel.setTooltip(tooltip);
|
|
||||||
}
|
|
||||||
tab.setGraphic(tabLabel);
|
tab.setGraphic(tabLabel);
|
||||||
tab.setContextMenu(getTabContextMenu(tab));
|
tab.setContextMenu(getTabContextMenu(tab));
|
||||||
tab.setClosable(true);
|
tab.setClosable(true);
|
||||||
|
|
@ -2096,33 +2042,23 @@ public class AppController implements Initializable {
|
||||||
}
|
}
|
||||||
|
|
||||||
MenuItem moveRight = new MenuItem("Move Right");
|
MenuItem moveRight = new MenuItem("Move Right");
|
||||||
moveRight.setAccelerator(new KeyCodeCombination(KeyCode.RIGHT, KeyCombination.CONTROL_DOWN, KeyCombination.SHIFT_DOWN));
|
|
||||||
moveRight.setOnAction(event -> {
|
moveRight.setOnAction(event -> {
|
||||||
int currentIndex = tabs.getSelectionModel().getSelectedIndex();
|
int index = tabs.getTabs().indexOf(tab);
|
||||||
if(currentIndex + 1 >= tabs.getTabs().size()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
Tab selectedTab = tabs.getSelectionModel().getSelectedItem();
|
|
||||||
tabs.getTabs().removeListener(tabsChangeListener);
|
tabs.getTabs().removeListener(tabsChangeListener);
|
||||||
tabs.getTabs().remove(selectedTab);
|
tabs.getTabs().remove(tab);
|
||||||
tabs.getTabs().add(currentIndex + 1, selectedTab);
|
tabs.getTabs().add(index + 1, tab);
|
||||||
tabs.getTabs().addListener(tabsChangeListener);
|
tabs.getTabs().addListener(tabsChangeListener);
|
||||||
tabs.getSelectionModel().select(selectedTab);
|
tabs.getSelectionModel().select(tab);
|
||||||
EventManager.get().post(new RequestOpenWalletsEvent()); //Rearrange recent files list
|
EventManager.get().post(new RequestOpenWalletsEvent()); //Rearrange recent files list
|
||||||
});
|
});
|
||||||
MenuItem moveLeft = new MenuItem("Move Left");
|
MenuItem moveLeft = new MenuItem("Move Left");
|
||||||
moveLeft.setAccelerator(new KeyCodeCombination(KeyCode.LEFT, KeyCombination.CONTROL_DOWN, KeyCombination.SHIFT_DOWN));
|
|
||||||
moveLeft.setOnAction(event -> {
|
moveLeft.setOnAction(event -> {
|
||||||
int currentIndex = tabs.getSelectionModel().getSelectedIndex();
|
int index = tabs.getTabs().indexOf(tab);
|
||||||
if(currentIndex == 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
Tab selectedTab = tabs.getSelectionModel().getSelectedItem();
|
|
||||||
tabs.getTabs().removeListener(tabsChangeListener);
|
tabs.getTabs().removeListener(tabsChangeListener);
|
||||||
tabs.getTabs().remove(selectedTab);
|
tabs.getTabs().remove(tab);
|
||||||
tabs.getTabs().add(currentIndex - 1, selectedTab);
|
tabs.getTabs().add(index - 1, tab);
|
||||||
tabs.getTabs().addListener(tabsChangeListener);
|
tabs.getTabs().addListener(tabsChangeListener);
|
||||||
tabs.getSelectionModel().select(selectedTab);
|
tabs.getSelectionModel().select(tab);
|
||||||
EventManager.get().post(new RequestOpenWalletsEvent()); //Rearrange recent files list
|
EventManager.get().post(new RequestOpenWalletsEvent()); //Rearrange recent files list
|
||||||
});
|
});
|
||||||
contextMenu.getItems().addAll(moveRight, moveLeft);
|
contextMenu.getItems().addAll(moveRight, moveLeft);
|
||||||
|
|
@ -2694,6 +2630,7 @@ public class AppController implements Initializable {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
Image image = new Image("image/sparrow-small.png", 50, 50, false, false);
|
||||||
String walletName = event.getWallet().getFullDisplayName();
|
String walletName = event.getWallet().getFullDisplayName();
|
||||||
if(walletName.length() > 40) {
|
if(walletName.length() > 40) {
|
||||||
walletName = walletName.substring(0, 40) + "...";
|
walletName = walletName.substring(0, 40) + "...";
|
||||||
|
|
@ -2702,10 +2639,10 @@ public class AppController implements Initializable {
|
||||||
Notifications notificationBuilder = Notifications.create()
|
Notifications notificationBuilder = Notifications.create()
|
||||||
.title("Sparrow - " + walletName)
|
.title("Sparrow - " + walletName)
|
||||||
.text(text)
|
.text(text)
|
||||||
.graphic(new DialogImage(DialogImage.Type.SPARROW))
|
.graphic(new ImageView(image))
|
||||||
.hideAfter(Duration.seconds(15))
|
.hideAfter(Duration.seconds(15))
|
||||||
.position(Pos.TOP_RIGHT)
|
.position(Pos.TOP_RIGHT)
|
||||||
.threshold(5, Notifications.create().title("Sparrow").text("Multiple new wallet transactions").graphic(new DialogImage(DialogImage.Type.SPARROW)))
|
.threshold(5, Notifications.create().title("Sparrow").text("Multiple new wallet transactions").graphic(new ImageView(image)))
|
||||||
.onAction(e -> selectTab(event.getWallet()));
|
.onAction(e -> selectTab(event.getWallet()));
|
||||||
|
|
||||||
//If controlsfx can't find our window, we must set the window ourselves (unfortunately notification is then shown within this window)
|
//If controlsfx can't find our window, we must set the window ourselves (unfortunately notification is then shown within this window)
|
||||||
|
|
@ -2946,7 +2883,6 @@ public class AppController implements Initializable {
|
||||||
}
|
}
|
||||||
} else if(event.isCompleted()) {
|
} else if(event.isCompleted()) {
|
||||||
serverToggle.setDisable(false);
|
serverToggle.setDisable(false);
|
||||||
statusBar.setProgress(0);
|
|
||||||
if(statusBar.getText().startsWith("Scanning...")) {
|
if(statusBar.getText().startsWith("Scanning...")) {
|
||||||
statusBar.setText("");
|
statusBar.setText("");
|
||||||
}
|
}
|
||||||
|
|
@ -3167,11 +3103,6 @@ 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());
|
||||||
|
|
@ -3225,7 +3156,7 @@ public class AppController implements Initializable {
|
||||||
|
|
||||||
@Subscribe
|
@Subscribe
|
||||||
public void webcamResolutionChanged(WebcamResolutionChangedEvent event) {
|
public void webcamResolutionChanged(WebcamResolutionChangedEvent event) {
|
||||||
useHdCameraResolutionProperty.set(event.getResolution().isWidescreenAspect());
|
useHdCameraResolutionProperty.set(event.isHdResolution());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Subscribe
|
@Subscribe
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,6 @@ import com.sparrowwallet.drongo.crypto.InvalidPasswordException;
|
||||||
import com.sparrowwallet.drongo.crypto.Key;
|
import com.sparrowwallet.drongo.crypto.Key;
|
||||||
import com.sparrowwallet.drongo.policy.PolicyType;
|
import com.sparrowwallet.drongo.policy.PolicyType;
|
||||||
import com.sparrowwallet.drongo.wallet.*;
|
import com.sparrowwallet.drongo.wallet.*;
|
||||||
import com.sparrowwallet.sparrow.control.DialogImage;
|
|
||||||
import com.sparrowwallet.sparrow.control.WalletPasswordDialog;
|
import com.sparrowwallet.sparrow.control.WalletPasswordDialog;
|
||||||
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
|
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
|
||||||
import com.sparrowwallet.sparrow.net.Auth47;
|
import com.sparrowwallet.sparrow.net.Auth47;
|
||||||
|
|
@ -26,8 +25,6 @@ import com.sparrowwallet.sparrow.control.TrayManager;
|
||||||
import com.sparrowwallet.sparrow.event.*;
|
import com.sparrowwallet.sparrow.event.*;
|
||||||
import com.sparrowwallet.sparrow.io.*;
|
import com.sparrowwallet.sparrow.io.*;
|
||||||
import com.sparrowwallet.sparrow.net.*;
|
import com.sparrowwallet.sparrow.net.*;
|
||||||
import io.reactivex.rxjavafx.schedulers.JavaFxScheduler;
|
|
||||||
import io.reactivex.subjects.PublishSubject;
|
|
||||||
import javafx.application.Application;
|
import javafx.application.Application;
|
||||||
import javafx.application.Platform;
|
import javafx.application.Platform;
|
||||||
import javafx.beans.property.BooleanProperty;
|
import javafx.beans.property.BooleanProperty;
|
||||||
|
|
@ -45,6 +42,7 @@ import javafx.scene.Scene;
|
||||||
import javafx.scene.control.*;
|
import javafx.scene.control.*;
|
||||||
import javafx.scene.control.Dialog;
|
import javafx.scene.control.Dialog;
|
||||||
import javafx.scene.image.Image;
|
import javafx.scene.image.Image;
|
||||||
|
import javafx.scene.image.ImageView;
|
||||||
import javafx.scene.input.KeyCode;
|
import javafx.scene.input.KeyCode;
|
||||||
import javafx.scene.text.Font;
|
import javafx.scene.text.Font;
|
||||||
import javafx.stage.Screen;
|
import javafx.stage.Screen;
|
||||||
|
|
@ -68,8 +66,6 @@ import java.time.ZonedDateTime;
|
||||||
import java.time.temporal.ChronoUnit;
|
import java.time.temporal.ChronoUnit;
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.concurrent.ConcurrentHashMap;
|
|
||||||
import java.util.concurrent.TimeUnit;
|
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
import static com.sparrowwallet.sparrow.control.DownloadVerifierDialog.*;
|
import static com.sparrowwallet.sparrow.control.DownloadVerifierDialog.*;
|
||||||
|
|
@ -91,7 +87,8 @@ public class AppServices {
|
||||||
private static final String TOR_DEFAULT_PROXY_CIRCUIT_ID = "default";
|
private static final String TOR_DEFAULT_PROXY_CIRCUIT_ID = "default";
|
||||||
|
|
||||||
public static final List<Integer> TARGET_BLOCKS_RANGE = List.of(1, 2, 3, 4, 5, 10, 25, 50);
|
public static final List<Integer> TARGET_BLOCKS_RANGE = List.of(1, 2, 3, 4, 5, 10, 25, 50);
|
||||||
private static final List<Double> LONG_FEE_RATES_RANGE = List.of(1d, 2d, 4d, 8d, 16d, 32d, 64d, 128d, 256d, 512d, 1024d, 2048d, 4096d, 8192d);
|
public static final List<Long> LONG_FEE_RATES_RANGE = List.of(1L, 2L, 4L, 8L, 16L, 32L, 64L, 128L, 256L, 512L, 1024L, 2048L, 4096L, 8192L);
|
||||||
|
public static final List<Long> FEE_RATES_RANGE = LONG_FEE_RATES_RANGE.subList(0, LONG_FEE_RATES_RANGE.size() - 3);
|
||||||
public static final double FALLBACK_FEE_RATE = 20000d / 1000;
|
public static final double FALLBACK_FEE_RATE = 20000d / 1000;
|
||||||
public static final double TESTNET_FALLBACK_FEE_RATE = 1000d / 1000;
|
public static final double TESTNET_FALLBACK_FEE_RATE = 1000d / 1000;
|
||||||
|
|
||||||
|
|
@ -107,8 +104,6 @@ public class AppServices {
|
||||||
|
|
||||||
private TrayManager trayManager;
|
private TrayManager trayManager;
|
||||||
|
|
||||||
private final PublishSubject<NewBlockEvent> newBlockSubject = PublishSubject.create();
|
|
||||||
|
|
||||||
private static Image windowIcon;
|
private static Image windowIcon;
|
||||||
|
|
||||||
private static final BooleanProperty onlineProperty = new SimpleBooleanProperty(false);
|
private static final BooleanProperty onlineProperty = new SimpleBooleanProperty(false);
|
||||||
|
|
@ -131,18 +126,12 @@ public class AppServices {
|
||||||
|
|
||||||
private static BlockHeader latestBlockHeader;
|
private static BlockHeader latestBlockHeader;
|
||||||
|
|
||||||
private static final Map<Integer, BlockSummary> blockSummaries = new ConcurrentHashMap<>();
|
|
||||||
|
|
||||||
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;
|
||||||
|
|
@ -193,12 +182,6 @@ public class AppServices {
|
||||||
private AppServices(Application application, InteractionServices interactionServices) {
|
private AppServices(Application application, InteractionServices interactionServices) {
|
||||||
this.application = application;
|
this.application = application;
|
||||||
this.interactionServices = interactionServices;
|
this.interactionServices = interactionServices;
|
||||||
|
|
||||||
newBlockSubject.buffer(4, TimeUnit.SECONDS)
|
|
||||||
.filter(newBlockEvents -> !newBlockEvents.isEmpty())
|
|
||||||
.observeOn(JavaFxScheduler.platform())
|
|
||||||
.subscribe(this::fetchBlockSummaries, exception -> log.error("Error fetching block summaries", exception));
|
|
||||||
|
|
||||||
EventManager.get().register(this);
|
EventManager.get().register(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -212,7 +195,6 @@ public class AppServices {
|
||||||
preventSleepService = createPreventSleepService();
|
preventSleepService = createPreventSleepService();
|
||||||
|
|
||||||
onlineProperty.addListener(onlineServicesListener);
|
onlineProperty.addListener(onlineServicesListener);
|
||||||
minimumRelayFeeRate = getConfiguredMinimumRelayFeeRate(config);
|
|
||||||
|
|
||||||
if(config.getMode() == Mode.ONLINE) {
|
if(config.getMode() == Mode.ONLINE) {
|
||||||
if(config.requiresInternalTor()) {
|
if(config.requiresInternalTor()) {
|
||||||
|
|
@ -279,7 +261,7 @@ public class AppServices {
|
||||||
}
|
}
|
||||||
|
|
||||||
if(Tor.getDefault() != null) {
|
if(Tor.getDefault() != null) {
|
||||||
Tor.getDefault().close();
|
Tor.getDefault().getTorManager().destroy(true, success -> {});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -309,6 +291,12 @@ public class AppServices {
|
||||||
if(event != null) {
|
if(event != null) {
|
||||||
EventManager.get().post(event);
|
EventManager.get().post(event);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
FeeRatesSource feeRatesSource = Config.get().getFeeRatesSource();
|
||||||
|
feeRatesSource = (feeRatesSource == null ? FeeRatesSource.MEMPOOL_SPACE : feeRatesSource);
|
||||||
|
if(event instanceof ConnectionEvent && feeRatesSource.supportsNetwork(Network.get()) && feeRatesSource.isExternal()) {
|
||||||
|
EventManager.get().post(new FeeRatesSourceChangedEvent(feeRatesSource));
|
||||||
|
}
|
||||||
});
|
});
|
||||||
connectionService.setOnFailed(failEvent -> {
|
connectionService.setOnFailed(failEvent -> {
|
||||||
//Close connection here to create a new transport next time we try
|
//Close connection here to create a new transport next time we try
|
||||||
|
|
@ -492,26 +480,6 @@ public class AppServices {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void fetchFeeRates() {
|
|
||||||
if(feeRatesService != null && !feeRatesService.isRunning() && Config.get().getMode() != Mode.OFFLINE) {
|
|
||||||
feeRatesService = createFeeRatesService();
|
|
||||||
feeRatesService.start();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void fetchBlockSummaries(List<NewBlockEvent> newBlockEvents) {
|
|
||||||
if(isConnected()) {
|
|
||||||
ElectrumServer.BlockSummaryService blockSummaryService = new ElectrumServer.BlockSummaryService(newBlockEvents);
|
|
||||||
blockSummaryService.setOnSucceeded(_ -> {
|
|
||||||
EventManager.get().post(blockSummaryService.getValue());
|
|
||||||
});
|
|
||||||
blockSummaryService.setOnFailed(failedState -> {
|
|
||||||
log.error("Error fetching block summaries", failedState.getSource().getException());
|
|
||||||
});
|
|
||||||
blockSummaryService.start();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static boolean isTorRunning() {
|
public static boolean isTorRunning() {
|
||||||
return Tor.getDefault() != null;
|
return Tor.getDefault() != null;
|
||||||
}
|
}
|
||||||
|
|
@ -737,10 +705,6 @@ public class AppServices {
|
||||||
return latestBlockHeader;
|
return latestBlockHeader;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static Map<Integer, BlockSummary> getBlockSummaries() {
|
|
||||||
return blockSummaries;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static Double getDefaultFeeRate() {
|
public static Double getDefaultFeeRate() {
|
||||||
int defaultTarget = TARGET_BLOCKS_RANGE.get((TARGET_BLOCKS_RANGE.size() / 2) - 1);
|
int defaultTarget = TARGET_BLOCKS_RANGE.get((TARGET_BLOCKS_RANGE.size() / 2) - 1);
|
||||||
return getTargetBlockFeeRates() == null ? getFallbackFeeRate() : getTargetBlockFeeRates().get(defaultTarget);
|
return getTargetBlockFeeRates() == null ? getFallbackFeeRate() : getTargetBlockFeeRates().get(defaultTarget);
|
||||||
|
|
@ -752,30 +716,6 @@ public class AppServices {
|
||||||
return Math.max(minRate, Transaction.DUST_RELAY_TX_FEE);
|
return Math.max(minRate, Transaction.DUST_RELAY_TX_FEE);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static List<Double> getLongFeeRatesRange() {
|
|
||||||
if(minimumRelayFeeRate == null || minimumRelayFeeRate >= Transaction.DEFAULT_MIN_RELAY_FEE) {
|
|
||||||
return LONG_FEE_RATES_RANGE;
|
|
||||||
} else {
|
|
||||||
List<Double> longFeeRatesRange = new ArrayList<>();
|
|
||||||
longFeeRatesRange.add(minimumRelayFeeRate);
|
|
||||||
longFeeRatesRange.addAll(LONG_FEE_RATES_RANGE);
|
|
||||||
return longFeeRatesRange;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static List<Double> getFeeRatesRange() {
|
|
||||||
if(minimumRelayFeeRate == null || minimumRelayFeeRate >= Transaction.DEFAULT_MIN_RELAY_FEE) {
|
|
||||||
return LONG_FEE_RATES_RANGE.subList(0, LONG_FEE_RATES_RANGE.size() - 3);
|
|
||||||
} else {
|
|
||||||
List<Double> longFeeRatesRange = getLongFeeRatesRange();
|
|
||||||
return longFeeRatesRange.subList(0, longFeeRatesRange.size() - 4);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static Double getNextBlockMedianFeeRate() {
|
|
||||||
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;
|
||||||
}
|
}
|
||||||
|
|
@ -810,18 +750,10 @@ public class AppServices {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public static Double getConfiguredMinimumRelayFeeRate(Config config) {
|
|
||||||
return config.getMinRelayFeeRate() >= 0d && config.getMinRelayFeeRate() < Transaction.DEFAULT_MIN_RELAY_FEE ? config.getMinRelayFeeRate() : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static Double getMinimumRelayFeeRate() {
|
public static Double getMinimumRelayFeeRate() {
|
||||||
return minimumRelayFeeRate == null ? Transaction.DEFAULT_MIN_RELAY_FEE : minimumRelayFeeRate;
|
return minimumRelayFeeRate == null ? Transaction.DEFAULT_MIN_RELAY_FEE : minimumRelayFeeRate;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static Double getServerMinimumRelayFeeRate() {
|
|
||||||
return serverMinimumRelayFeeRate;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static CurrencyRate getFiatCurrencyExchangeRate() {
|
public static CurrencyRate getFiatCurrencyExchangeRate() {
|
||||||
return fiatCurrencyExchangeRate;
|
return fiatCurrencyExchangeRate;
|
||||||
}
|
}
|
||||||
|
|
@ -835,8 +767,8 @@ public class AppServices {
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void addPayjoinURI(BitcoinURI bitcoinURI) {
|
public static void addPayjoinURI(BitcoinURI bitcoinURI) {
|
||||||
if(bitcoinURI.getPayjoinUrl() == null || bitcoinURI.getAddress() == null) {
|
if(bitcoinURI.getPayjoinUrl() == null) {
|
||||||
throw new IllegalArgumentException("Not a valid payjoin URI");
|
throw new IllegalArgumentException("Not a payjoin URI");
|
||||||
}
|
}
|
||||||
payjoinURIs.put(bitcoinURI.getAddress(), bitcoinURI);
|
payjoinURIs.put(bitcoinURI.getAddress(), bitcoinURI);
|
||||||
}
|
}
|
||||||
|
|
@ -1163,7 +1095,8 @@ public class AppServices {
|
||||||
walletChoiceDialog.initOwner(getActiveWindow());
|
walletChoiceDialog.initOwner(getActiveWindow());
|
||||||
walletChoiceDialog.setTitle("Choose Wallet");
|
walletChoiceDialog.setTitle("Choose Wallet");
|
||||||
walletChoiceDialog.setHeaderText("Choose a wallet to " + actionDescription);
|
walletChoiceDialog.setHeaderText("Choose a wallet to " + actionDescription);
|
||||||
walletChoiceDialog.getDialogPane().setGraphic(new DialogImage(DialogImage.Type.SPARROW));
|
Image image = new Image("/image/sparrow-small.png");
|
||||||
|
walletChoiceDialog.getDialogPane().setGraphic(new ImageView(image));
|
||||||
setStageIcon(walletChoiceDialog.getDialogPane().getScene().getWindow());
|
setStageIcon(walletChoiceDialog.getDialogPane().getScene().getWindow());
|
||||||
moveToActiveWindowScreen(walletChoiceDialog);
|
moveToActiveWindowScreen(walletChoiceDialog);
|
||||||
Optional<Wallet> optWallet = walletChoiceDialog.showAndWait();
|
Optional<Wallet> optWallet = walletChoiceDialog.showAndWait();
|
||||||
|
|
@ -1175,31 +1108,6 @@ public class AppServices {
|
||||||
return wallet;
|
return wallet;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static boolean disallowAnyInvalidDerivationPaths(Wallet wallet) {
|
|
||||||
Optional<ScriptType> optInvalidScriptType = wallet.getKeystores().stream()
|
|
||||||
.filter(keystore -> keystore.getKeyDerivation() != null)
|
|
||||||
.map(keystore -> wallet.getOtherScriptTypeMatchingDerivation(keystore.getKeyDerivation().getDerivationPath()))
|
|
||||||
.filter(Optional::isPresent).map(Optional::get).findFirst();
|
|
||||||
if(optInvalidScriptType.isPresent()) {
|
|
||||||
ScriptType invalidScriptType = optInvalidScriptType.get();
|
|
||||||
boolean includePolicyType = !wallet.getScriptType().getAllowedPolicyTypes().getFirst().equals(invalidScriptType.getAllowedPolicyTypes().getFirst());
|
|
||||||
Optional<ButtonType> optType = AppServices.showWarningDialog("Invalid derivation path", "This wallet is using the derivation path for " +
|
|
||||||
invalidScriptType.getDescription(includePolicyType) + ", instead of the derivation path for its defined script type of " + wallet.getScriptType().getDescription(includePolicyType) +
|
|
||||||
". \n\nDisable derivation path validation to import this wallet?", ButtonType.NO, ButtonType.YES);
|
|
||||||
if(optType.isPresent()) {
|
|
||||||
if(optType.get() == ButtonType.YES) {
|
|
||||||
Config.get().setValidateDerivationPaths(false);
|
|
||||||
System.setProperty(Wallet.ALLOW_DERIVATIONS_MATCHING_OTHER_SCRIPT_TYPES_PROPERTY, Boolean.toString(true));
|
|
||||||
System.setProperty(Wallet.ALLOW_DERIVATIONS_MATCHING_OTHER_NETWORKS_PROPERTY, Boolean.toString(true));
|
|
||||||
} else {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static final List<Network> WHIRLPOOL_NETWORKS = List.of(Network.MAINNET, Network.TESTNET);
|
public static final List<Network> WHIRLPOOL_NETWORKS = List.of(Network.MAINNET, Network.TESTNET);
|
||||||
|
|
||||||
public static boolean isWhirlpoolCompatible(Wallet wallet) {
|
public static boolean isWhirlpoolCompatible(Wallet wallet) {
|
||||||
|
|
@ -1215,8 +1123,7 @@ public class AppServices {
|
||||||
public static boolean isWhirlpoolPostmixCompatible(Wallet wallet) {
|
public static boolean isWhirlpoolPostmixCompatible(Wallet wallet) {
|
||||||
return WHIRLPOOL_NETWORKS.contains(Network.get())
|
return WHIRLPOOL_NETWORKS.contains(Network.get())
|
||||||
&& wallet.getScriptType() != ScriptType.P2TR //Taproot not yet supported
|
&& wallet.getScriptType() != ScriptType.P2TR //Taproot not yet supported
|
||||||
&& wallet.getKeystores().size() == 1
|
&& wallet.getKeystores().size() == 1;
|
||||||
&& wallet.getKeystores().getFirst().getWalletModel() != WalletModel.BITBOX_02; //BitBox02 does not support high account numbers
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static List<Wallet> addWhirlpoolWallets(Wallet decryptedWallet, String walletId, Storage storage) {
|
public static List<Wallet> addWhirlpoolWallets(Wallet decryptedWallet, String walletId, Storage storage) {
|
||||||
|
|
@ -1233,7 +1140,7 @@ public class AppServices {
|
||||||
}
|
}
|
||||||
|
|
||||||
public static Font getMonospaceFont() {
|
public static Font getMonospaceFont() {
|
||||||
return Font.font("Fragment Mono Regular", 13);
|
return Font.font("Roboto Mono", 13);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static boolean isOnWayland() {
|
public static boolean isOnWayland() {
|
||||||
|
|
@ -1249,22 +1156,9 @@ public class AppServices {
|
||||||
public void newConnection(ConnectionEvent event) {
|
public void newConnection(ConnectionEvent event) {
|
||||||
currentBlockHeight = event.getBlockHeight();
|
currentBlockHeight = event.getBlockHeight();
|
||||||
System.setProperty(Network.BLOCK_HEIGHT_PROPERTY, Integer.toString(currentBlockHeight));
|
System.setProperty(Network.BLOCK_HEIGHT_PROPERTY, Integer.toString(currentBlockHeight));
|
||||||
if(getConfiguredMinimumRelayFeeRate(Config.get()) == null) {
|
minimumRelayFeeRate = Math.max(event.getMinimumRelayFeeRate(), Transaction.DEFAULT_MIN_RELAY_FEE);
|
||||||
minimumRelayFeeRate = event.getMinimumRelayFeeRate() == null ? Transaction.DEFAULT_MIN_RELAY_FEE : event.getMinimumRelayFeeRate();
|
|
||||||
}
|
|
||||||
serverMinimumRelayFeeRate = event.getMinimumRelayFeeRate();
|
|
||||||
latestBlockHeader = event.getBlockHeader();
|
latestBlockHeader = event.getBlockHeader();
|
||||||
Config.get().addRecentServer();
|
Config.get().addRecentServer();
|
||||||
|
|
||||||
FeeRatesSource feeRatesSource = Config.get().getFeeRatesSource();
|
|
||||||
feeRatesSource = (feeRatesSource == null ? FeeRatesSource.MEMPOOL_SPACE : feeRatesSource);
|
|
||||||
if(feeRatesSource.supportsNetwork(Network.get()) && feeRatesSource.isExternal()) {
|
|
||||||
fetchFeeRates();
|
|
||||||
}
|
|
||||||
|
|
||||||
if(!blockSummaries.containsKey(currentBlockHeight)) {
|
|
||||||
fetchBlockSummaries(Collections.emptyList());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Subscribe
|
@Subscribe
|
||||||
|
|
@ -1279,22 +1173,11 @@ public class AppServices {
|
||||||
latestBlockHeader = event.getBlockHeader();
|
latestBlockHeader = event.getBlockHeader();
|
||||||
String status = "Updating to new block height " + event.getHeight();
|
String status = "Updating to new block height " + event.getHeight();
|
||||||
EventManager.get().post(new StatusEvent(status));
|
EventManager.get().post(new StatusEvent(status));
|
||||||
newBlockSubject.onNext(event);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Subscribe
|
|
||||||
public void blockSummary(BlockSummaryEvent event) {
|
|
||||||
blockSummaries.putAll(event.getBlockSummaryMap());
|
|
||||||
if(AppServices.currentBlockHeight != null) {
|
|
||||||
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
|
||||||
|
|
@ -1307,8 +1190,10 @@ public class AppServices {
|
||||||
@Subscribe
|
@Subscribe
|
||||||
public void feeRateSourceChanged(FeeRatesSourceChangedEvent event) {
|
public void feeRateSourceChanged(FeeRatesSourceChangedEvent event) {
|
||||||
//Perform once-off fee rates retrieval to immediately change displayed rates
|
//Perform once-off fee rates retrieval to immediately change displayed rates
|
||||||
fetchFeeRates();
|
if(feeRatesService != null && !feeRatesService.isRunning() && Config.get().getMode() != Mode.OFFLINE) {
|
||||||
fetchBlockSummaries(Collections.emptyList());
|
feeRatesService = createFeeRatesService();
|
||||||
|
feeRatesService.start();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Subscribe
|
@Subscribe
|
||||||
|
|
|
||||||
|
|
@ -1,76 +0,0 @@
|
||||||
package com.sparrowwallet.sparrow;
|
|
||||||
|
|
||||||
import java.time.Instant;
|
|
||||||
import java.time.temporal.ChronoUnit;
|
|
||||||
import java.util.Date;
|
|
||||||
import java.util.Optional;
|
|
||||||
|
|
||||||
public class BlockSummary implements Comparable<BlockSummary> {
|
|
||||||
private final Integer height;
|
|
||||||
private final Date timestamp;
|
|
||||||
private final Double medianFee;
|
|
||||||
private final Integer transactionCount;
|
|
||||||
private final Integer weight;
|
|
||||||
|
|
||||||
public BlockSummary(Integer height, Date timestamp) {
|
|
||||||
this(height, timestamp, null, null, null);
|
|
||||||
}
|
|
||||||
|
|
||||||
public BlockSummary(Integer height, Date timestamp, Double medianFee, Integer transactionCount, Integer weight) {
|
|
||||||
this.height = height;
|
|
||||||
this.timestamp = timestamp;
|
|
||||||
this.medianFee = medianFee;
|
|
||||||
this.transactionCount = transactionCount;
|
|
||||||
this.weight = weight;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Integer getHeight() {
|
|
||||||
return height;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Date getTimestamp() {
|
|
||||||
return timestamp;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Optional<Double> getMedianFee() {
|
|
||||||
return medianFee == null ? Optional.empty() : Optional.of(medianFee);
|
|
||||||
}
|
|
||||||
|
|
||||||
public Optional<Integer> getTransactionCount() {
|
|
||||||
return transactionCount == null ? Optional.empty() : Optional.of(transactionCount);
|
|
||||||
}
|
|
||||||
|
|
||||||
public Optional<Integer> getWeight() {
|
|
||||||
return weight == null ? Optional.empty() : Optional.of(weight);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static long calculateElapsedSeconds(long timestampUtc) {
|
|
||||||
Instant timestampInstant = Instant.ofEpochMilli(timestampUtc);
|
|
||||||
Instant nowInstant = Instant.now();
|
|
||||||
return ChronoUnit.SECONDS.between(timestampInstant, nowInstant);
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getElapsed() {
|
|
||||||
long elapsed = calculateElapsedSeconds(getTimestamp().getTime());
|
|
||||||
if(elapsed < 0) {
|
|
||||||
return "now";
|
|
||||||
} else if(elapsed < 60) {
|
|
||||||
return elapsed + "s";
|
|
||||||
} else if(elapsed < 3600) {
|
|
||||||
return elapsed / 60 + "m";
|
|
||||||
} else if(elapsed < 86400) {
|
|
||||||
return elapsed / 3600 + "h";
|
|
||||||
} else {
|
|
||||||
return elapsed / 86400 + "d";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public String toString() {
|
|
||||||
return getElapsed() + ":" + getMedianFee();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int compareTo(BlockSummary o) {
|
|
||||||
return o.height - height;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -72,6 +72,10 @@ public class SparrowDesktop extends Application {
|
||||||
Config.get().setServerType(ServerType.ELECTRUM_SERVER);
|
Config.get().setServerType(ServerType.ELECTRUM_SERVER);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if(Config.get().getHdCapture() == null && OsType.getCurrent() == OsType.MACOS) {
|
||||||
|
Config.get().setHdCapture(Boolean.TRUE);
|
||||||
|
}
|
||||||
|
|
||||||
System.setProperty(Wallet.ALLOW_DERIVATIONS_MATCHING_OTHER_SCRIPT_TYPES_PROPERTY, Boolean.toString(!Config.get().isValidateDerivationPaths()));
|
System.setProperty(Wallet.ALLOW_DERIVATIONS_MATCHING_OTHER_SCRIPT_TYPES_PROPERTY, Boolean.toString(!Config.get().isValidateDerivationPaths()));
|
||||||
System.setProperty(Wallet.ALLOW_DERIVATIONS_MATCHING_OTHER_NETWORKS_PROPERTY, Boolean.toString(!Config.get().isValidateDerivationPaths()));
|
System.setProperty(Wallet.ALLOW_DERIVATIONS_MATCHING_OTHER_NETWORKS_PROPERTY, Boolean.toString(!Config.get().isValidateDerivationPaths()));
|
||||||
|
|
||||||
|
|
@ -113,8 +117,8 @@ public class SparrowDesktop extends Application {
|
||||||
private void initializeFonts() {
|
private void initializeFonts() {
|
||||||
GlyphFontRegistry.register(new FontAwesome5());
|
GlyphFontRegistry.register(new FontAwesome5());
|
||||||
GlyphFontRegistry.register(new FontAwesome5Brands());
|
GlyphFontRegistry.register(new FontAwesome5Brands());
|
||||||
Font.loadFont(AppServices.class.getResourceAsStream("/font/FragmentMono-Regular.ttf"), 13);
|
Font.loadFont(AppServices.class.getResourceAsStream("/font/RobotoMono-Regular.ttf"), 13);
|
||||||
Font.loadFont(AppServices.class.getResourceAsStream("/font/FragmentMono-Italic.ttf"), 11);
|
Font.loadFont(AppServices.class.getResourceAsStream("/font/RobotoMono-Italic.ttf"), 11);
|
||||||
if(OsType.getCurrent() == OsType.MACOS) {
|
if(OsType.getCurrent() == OsType.MACOS) {
|
||||||
Font.loadFont(AppServices.class.getResourceAsStream("/font/LiberationSans-Regular.ttf"), 13);
|
Font.loadFont(AppServices.class.getResourceAsStream("/font/LiberationSans-Regular.ttf"), 13);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,7 @@ import java.util.*;
|
||||||
public class SparrowWallet {
|
public class SparrowWallet {
|
||||||
public static final String APP_ID = "sparrow";
|
public static final String APP_ID = "sparrow";
|
||||||
public static final String APP_NAME = "Sparrow";
|
public static final String APP_NAME = "Sparrow";
|
||||||
public static final String APP_VERSION = "2.3.1";
|
public static final String APP_VERSION = "2.1.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";
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,7 @@ public class WelcomeDialog extends Dialog<Mode> {
|
||||||
welcomeController.initializeView();
|
welcomeController.initializeView();
|
||||||
|
|
||||||
dialogPane.setPrefWidth(600);
|
dialogPane.setPrefWidth(600);
|
||||||
dialogPane.setPrefHeight(540);
|
dialogPane.setPrefHeight(520);
|
||||||
dialogPane.setMinHeight(dialogPane.getPrefHeight());
|
dialogPane.setMinHeight(dialogPane.getPrefHeight());
|
||||||
AppServices.moveToActiveWindowScreen(this);
|
AppServices.moveToActiveWindowScreen(this);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,372 +0,0 @@
|
||||||
package com.sparrowwallet.sparrow.control;
|
|
||||||
|
|
||||||
import com.sparrowwallet.drongo.Network;
|
|
||||||
import com.sparrowwallet.drongo.protocol.Transaction;
|
|
||||||
import com.sparrowwallet.sparrow.BlockSummary;
|
|
||||||
import com.sparrowwallet.sparrow.io.Config;
|
|
||||||
import com.sparrowwallet.sparrow.net.FeeRatesSource;
|
|
||||||
import javafx.animation.KeyFrame;
|
|
||||||
import javafx.animation.KeyValue;
|
|
||||||
import javafx.animation.Timeline;
|
|
||||||
import javafx.beans.property.*;
|
|
||||||
import javafx.scene.Group;
|
|
||||||
import javafx.scene.shape.Polygon;
|
|
||||||
import javafx.scene.shape.Rectangle;
|
|
||||||
import javafx.scene.text.Font;
|
|
||||||
import javafx.scene.text.FontWeight;
|
|
||||||
import javafx.scene.text.Text;
|
|
||||||
import javafx.scene.text.TextFlow;
|
|
||||||
import javafx.util.Duration;
|
|
||||||
import org.girod.javafx.svgimage.SVGImage;
|
|
||||||
|
|
||||||
import java.time.Instant;
|
|
||||||
import java.time.temporal.ChronoUnit;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
public class BlockCube extends Group {
|
|
||||||
public static final List<Integer> MEMPOOL_FEE_RATES_INTERVALS = List.of(1, 2, 3, 4, 5, 6, 8, 10, 12, 15, 20, 30, 40, 50, 60, 70, 80, 90, 100, 125, 150, 175, 200, 250, 300, 350, 400, 500, 600, 700, 800, 900, 1000, 1200, 1400, 1600, 1800, 2000);
|
|
||||||
|
|
||||||
public static final double CUBE_SIZE = 60;
|
|
||||||
|
|
||||||
private final IntegerProperty weightProperty = new SimpleIntegerProperty(0);
|
|
||||||
private final DoubleProperty medianFeeProperty = new SimpleDoubleProperty(-Double.MAX_VALUE);
|
|
||||||
private final IntegerProperty heightProperty = new SimpleIntegerProperty(0);
|
|
||||||
private final IntegerProperty txCountProperty = new SimpleIntegerProperty(0);
|
|
||||||
private final LongProperty timestampProperty = new SimpleLongProperty(System.currentTimeMillis());
|
|
||||||
private final StringProperty elapsedProperty = new SimpleStringProperty("");
|
|
||||||
private final BooleanProperty confirmedProperty = new SimpleBooleanProperty(false);
|
|
||||||
private final ObjectProperty<FeeRatesSource> feeRatesSource = new SimpleObjectProperty<>(null);
|
|
||||||
|
|
||||||
private Polygon front;
|
|
||||||
private Rectangle unusedArea;
|
|
||||||
private Rectangle usedArea;
|
|
||||||
|
|
||||||
private final Text heightText = new Text();
|
|
||||||
private final Text medianFeeText = new Text();
|
|
||||||
private final Text unitsText = new Text();
|
|
||||||
private final TextFlow medianFeeTextFlow = new TextFlow();
|
|
||||||
private final Text txCountText = new Text();
|
|
||||||
private final Text elapsedText = new Text();
|
|
||||||
private final Group feeRateIcon = new Group();
|
|
||||||
|
|
||||||
public BlockCube(Integer weight, Double medianFee, Integer height, Integer txCount, Long timestamp, boolean confirmed) {
|
|
||||||
getStyleClass().addAll("block-" + Network.getCanonical().getName(), "block-cube");
|
|
||||||
this.confirmedProperty.set(confirmed);
|
|
||||||
|
|
||||||
FeeRatesSource feeRatesSource = Config.get().getFeeRatesSource();
|
|
||||||
feeRatesSource = (feeRatesSource == null ? FeeRatesSource.MEMPOOL_SPACE : feeRatesSource);
|
|
||||||
this.feeRatesSource.set(feeRatesSource);
|
|
||||||
|
|
||||||
this.weightProperty.addListener((_, _, _) -> {
|
|
||||||
if(front != null) {
|
|
||||||
updateFill();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
this.medianFeeProperty.addListener((_, _, newValue) -> {
|
|
||||||
medianFeeText.setText(newValue.doubleValue() < 0.0d ? "" : "~" + Math.round(Math.max(newValue.doubleValue(), 1.0d)));
|
|
||||||
unitsText.setText(newValue.doubleValue() < 0.0d ? "" : " s/vb");
|
|
||||||
double medianFeeWidth = TextUtils.computeTextWidth(medianFeeText.getFont(), medianFeeText.getText(), 0.0d);
|
|
||||||
double unitsWidth = TextUtils.computeTextWidth(unitsText.getFont(), unitsText.getText(), 0.0d);
|
|
||||||
medianFeeTextFlow.setTranslateX((CUBE_SIZE - (medianFeeWidth + unitsWidth)) / 2);
|
|
||||||
});
|
|
||||||
this.txCountProperty.addListener((_, _, newValue) -> {
|
|
||||||
txCountText.setText(newValue.intValue() == 0 ? "" : newValue + " txes");
|
|
||||||
txCountText.setX((CUBE_SIZE - txCountText.getLayoutBounds().getWidth()) / 2);
|
|
||||||
});
|
|
||||||
this.timestampProperty.addListener((_, _, newValue) -> {
|
|
||||||
elapsedProperty.set(getElapsed(newValue.longValue()));
|
|
||||||
});
|
|
||||||
this.elapsedProperty.addListener((_, _, newValue) -> {
|
|
||||||
elapsedText.setText(isConfirmed() ? newValue : "In ~10m");
|
|
||||||
elapsedText.setX((CUBE_SIZE - elapsedText.getLayoutBounds().getWidth()) / 2);
|
|
||||||
});
|
|
||||||
this.heightProperty.addListener((_, _, newValue) -> {
|
|
||||||
heightText.setText(newValue.intValue() == 0 ? "" : String.valueOf(newValue));
|
|
||||||
heightText.setX(((CUBE_SIZE * 0.7) - heightText.getLayoutBounds().getWidth()) / 2);
|
|
||||||
});
|
|
||||||
this.confirmedProperty.addListener((_, _, _) -> {
|
|
||||||
if(front != null) {
|
|
||||||
updateFill();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
this.feeRatesSource.addListener((_, _, _) -> {
|
|
||||||
if(front != null) {
|
|
||||||
updateFill();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
this.medianFeeText.textProperty().addListener((_, _, _) -> {
|
|
||||||
pulse();
|
|
||||||
});
|
|
||||||
|
|
||||||
if(weight != null) {
|
|
||||||
this.weightProperty.set(weight);
|
|
||||||
}
|
|
||||||
if(medianFee != null) {
|
|
||||||
this.medianFeeProperty.set(medianFee);
|
|
||||||
}
|
|
||||||
if(height != null) {
|
|
||||||
this.heightProperty.set(height);
|
|
||||||
}
|
|
||||||
if(txCount != null) {
|
|
||||||
this.txCountProperty.set(txCount);
|
|
||||||
}
|
|
||||||
if(timestamp != null) {
|
|
||||||
this.timestampProperty.set(timestamp);
|
|
||||||
}
|
|
||||||
|
|
||||||
drawCube();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void drawCube() {
|
|
||||||
double depth = CUBE_SIZE * 0.2;
|
|
||||||
double perspective = CUBE_SIZE * 0.04;
|
|
||||||
|
|
||||||
front = new Polygon(0, 0, CUBE_SIZE, 0, CUBE_SIZE, CUBE_SIZE, 0, CUBE_SIZE);
|
|
||||||
front.getStyleClass().add("block-front");
|
|
||||||
front.setFill(null);
|
|
||||||
unusedArea = new Rectangle(0, 0, CUBE_SIZE, CUBE_SIZE);
|
|
||||||
unusedArea.getStyleClass().add("block-unused");
|
|
||||||
usedArea = new Rectangle(0, 0, CUBE_SIZE, CUBE_SIZE);
|
|
||||||
usedArea.getStyleClass().add("block-used");
|
|
||||||
|
|
||||||
Group frontFaceGroup = new Group(front, unusedArea, usedArea);
|
|
||||||
|
|
||||||
Polygon top = new Polygon(0, 0, CUBE_SIZE, 0, CUBE_SIZE - depth - perspective, -depth, -depth, -depth);
|
|
||||||
top.getStyleClass().add("block-top");
|
|
||||||
top.setStroke(null);
|
|
||||||
|
|
||||||
Polygon left = new Polygon(0, 0, -depth, -depth, -depth, CUBE_SIZE - depth - perspective, 0, CUBE_SIZE);
|
|
||||||
left.getStyleClass().add("block-left");
|
|
||||||
left.setStroke(null);
|
|
||||||
|
|
||||||
updateFill();
|
|
||||||
|
|
||||||
heightText.getStyleClass().add("block-height");
|
|
||||||
heightText.setFont(new Font(11));
|
|
||||||
heightText.setX(((CUBE_SIZE * 0.7) - heightText.getLayoutBounds().getWidth()) / 2);
|
|
||||||
heightText.setY(-24);
|
|
||||||
|
|
||||||
medianFeeText.getStyleClass().add("block-text");
|
|
||||||
medianFeeText.setFont(Font.font(null, FontWeight.BOLD, 11));
|
|
||||||
unitsText.getStyleClass().add("block-text");
|
|
||||||
unitsText.setFont(new Font(10));
|
|
||||||
medianFeeTextFlow.getChildren().addAll(medianFeeText, unitsText);
|
|
||||||
medianFeeTextFlow.setTranslateX((CUBE_SIZE - (medianFeeText.getLayoutBounds().getWidth() + unitsText.getLayoutBounds().getWidth())) / 2);
|
|
||||||
medianFeeTextFlow.setTranslateY(7);
|
|
||||||
|
|
||||||
txCountText.getStyleClass().add("block-text");
|
|
||||||
txCountText.setFont(new Font(10));
|
|
||||||
txCountText.setOpacity(0.7);
|
|
||||||
txCountText.setX((CUBE_SIZE - txCountText.getLayoutBounds().getWidth()) / 2);
|
|
||||||
txCountText.setY(34);
|
|
||||||
|
|
||||||
feeRateIcon.setTranslateX(((CUBE_SIZE * 0.7) - 14) / 2);
|
|
||||||
feeRateIcon.setTranslateY(-36);
|
|
||||||
|
|
||||||
elapsedText.getStyleClass().add("block-text");
|
|
||||||
elapsedText.setFont(new Font(10));
|
|
||||||
elapsedText.setX((CUBE_SIZE - elapsedText.getLayoutBounds().getWidth()) / 2);
|
|
||||||
elapsedText.setY(50);
|
|
||||||
|
|
||||||
getChildren().addAll(frontFaceGroup, top, left, heightText, medianFeeTextFlow, txCountText, feeRateIcon, elapsedText);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void updateFill() {
|
|
||||||
if(isConfirmed()) {
|
|
||||||
getStyleClass().removeAll("block-unconfirmed");
|
|
||||||
if(!getStyleClass().contains("block-confirmed")) {
|
|
||||||
getStyleClass().add("block-confirmed");
|
|
||||||
}
|
|
||||||
double startY = 1 - weightProperty.doubleValue() / (Transaction.MAX_BLOCK_SIZE_VBYTES * Transaction.WITNESS_SCALE_FACTOR);
|
|
||||||
double startYAbsolute = startY * BlockCube.CUBE_SIZE;
|
|
||||||
unusedArea.setHeight(startYAbsolute);
|
|
||||||
unusedArea.setStyle(null);
|
|
||||||
usedArea.setY(startYAbsolute);
|
|
||||||
usedArea.setHeight(CUBE_SIZE - startYAbsolute);
|
|
||||||
usedArea.setVisible(true);
|
|
||||||
heightText.setVisible(true);
|
|
||||||
feeRateIcon.getChildren().clear();
|
|
||||||
} else {
|
|
||||||
getStyleClass().removeAll("block-confirmed");
|
|
||||||
if(!getStyleClass().contains("block-unconfirmed")) {
|
|
||||||
getStyleClass().add("block-unconfirmed");
|
|
||||||
}
|
|
||||||
usedArea.setVisible(false);
|
|
||||||
unusedArea.setStyle("-fx-fill: " + getFeeRateStyleName() + ";");
|
|
||||||
heightText.setVisible(false);
|
|
||||||
if(feeRatesSource.get() != null) {
|
|
||||||
SVGImage svgImage = feeRatesSource.get().getSVGImage();
|
|
||||||
if(svgImage != null) {
|
|
||||||
feeRateIcon.getChildren().setAll(feeRatesSource.get().getSVGImage());
|
|
||||||
} else {
|
|
||||||
feeRateIcon.getChildren().clear();
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
feeRateIcon.getChildren().clear();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void pulse() {
|
|
||||||
if(isConfirmed()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if(unusedArea != null) {
|
|
||||||
unusedArea.setStyle("-fx-fill: " + getFeeRateStyleName() + ";");
|
|
||||||
}
|
|
||||||
|
|
||||||
Timeline timeline = new Timeline(
|
|
||||||
new KeyFrame(Duration.ZERO, new KeyValue(opacityProperty(), 1.0)),
|
|
||||||
new KeyFrame(Duration.millis(500), new KeyValue(opacityProperty(), 0.7)),
|
|
||||||
new KeyFrame(Duration.millis(1000), new KeyValue(opacityProperty(), 1.0))
|
|
||||||
);
|
|
||||||
|
|
||||||
timeline.setCycleCount(1);
|
|
||||||
timeline.play();
|
|
||||||
}
|
|
||||||
|
|
||||||
private static long calculateElapsedSeconds(long timestampUtc) {
|
|
||||||
Instant timestampInstant = Instant.ofEpochMilli(timestampUtc);
|
|
||||||
Instant nowInstant = Instant.now();
|
|
||||||
return ChronoUnit.SECONDS.between(timestampInstant, nowInstant);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static String getElapsed(long timestampUtc) {
|
|
||||||
long elapsed = calculateElapsedSeconds(timestampUtc);
|
|
||||||
if(elapsed < 60) {
|
|
||||||
return "Just now";
|
|
||||||
} else if(elapsed < 3600) {
|
|
||||||
return Math.round(elapsed / 60f) + "m ago";
|
|
||||||
} else if(elapsed < 86400) {
|
|
||||||
return Math.round(elapsed / 3600f) + "h ago";
|
|
||||||
} else {
|
|
||||||
return Math.round(elapsed / 86400d) + "d ago";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private String getFeeRateStyleName() {
|
|
||||||
double rate = getMedianFee();
|
|
||||||
int[] feeRateInterval = getFeeRateInterval(rate);
|
|
||||||
if(feeRateInterval[1] == Integer.MAX_VALUE) {
|
|
||||||
return "VSIZE2000-2200_COLOR";
|
|
||||||
}
|
|
||||||
int[] nextRateInterval = getFeeRateInterval(rate * 2);
|
|
||||||
String from = "VSIZE" + feeRateInterval[0] + "-" + feeRateInterval[1] + "_COLOR";
|
|
||||||
String to = "VSIZE" + nextRateInterval[0] + "-" + (nextRateInterval[1] == Integer.MAX_VALUE ? "2200" : nextRateInterval[1]) + "_COLOR";
|
|
||||||
return "linear-gradient(from 75% 0% to 100% 0%, " + from + " 0%, " + to + " 100%, " + from +")";
|
|
||||||
}
|
|
||||||
|
|
||||||
private int[] getFeeRateInterval(double medianFee) {
|
|
||||||
for(int i = 0; i < MEMPOOL_FEE_RATES_INTERVALS.size(); i++) {
|
|
||||||
int feeRate = MEMPOOL_FEE_RATES_INTERVALS.get(i);
|
|
||||||
int nextFeeRate = (i == MEMPOOL_FEE_RATES_INTERVALS.size() - 1 ? Integer.MAX_VALUE : MEMPOOL_FEE_RATES_INTERVALS.get(i + 1));
|
|
||||||
if(feeRate <= medianFee && nextFeeRate > medianFee) {
|
|
||||||
return new int[] { feeRate, nextFeeRate };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return new int[] { 1, 2 };
|
|
||||||
}
|
|
||||||
|
|
||||||
public int getWeight() {
|
|
||||||
return weightProperty.get();
|
|
||||||
}
|
|
||||||
|
|
||||||
public IntegerProperty weightProperty() {
|
|
||||||
return weightProperty;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setWeight(int weight) {
|
|
||||||
weightProperty.set(weight);
|
|
||||||
}
|
|
||||||
|
|
||||||
public double getMedianFee() {
|
|
||||||
return medianFeeProperty.get();
|
|
||||||
}
|
|
||||||
|
|
||||||
public DoubleProperty medianFee() {
|
|
||||||
return medianFeeProperty;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setMedianFee(double medianFee) {
|
|
||||||
medianFeeProperty.set(medianFee);
|
|
||||||
}
|
|
||||||
|
|
||||||
public int getHeight() {
|
|
||||||
return heightProperty.get();
|
|
||||||
}
|
|
||||||
|
|
||||||
public IntegerProperty heightProperty() {
|
|
||||||
return heightProperty;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setHeight(int height) {
|
|
||||||
heightProperty.set(height);
|
|
||||||
}
|
|
||||||
|
|
||||||
public int getTxCount() {
|
|
||||||
return txCountProperty.get();
|
|
||||||
}
|
|
||||||
|
|
||||||
public IntegerProperty txCountProperty() {
|
|
||||||
return txCountProperty;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setTxCount(int txCount) {
|
|
||||||
txCountProperty.set(txCount);
|
|
||||||
}
|
|
||||||
|
|
||||||
public long getTimestamp() {
|
|
||||||
return timestampProperty.get();
|
|
||||||
}
|
|
||||||
|
|
||||||
public LongProperty timestampProperty() {
|
|
||||||
return timestampProperty;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setTimestamp(long timestamp) {
|
|
||||||
timestampProperty.set(timestamp);
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getElapsed() {
|
|
||||||
return elapsedProperty.get();
|
|
||||||
}
|
|
||||||
|
|
||||||
public StringProperty elapsedProperty() {
|
|
||||||
return elapsedProperty;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setElapsed(String elapsed) {
|
|
||||||
elapsedProperty.set(elapsed);
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean isConfirmed() {
|
|
||||||
return confirmedProperty.get();
|
|
||||||
}
|
|
||||||
|
|
||||||
public BooleanProperty confirmedProperty() {
|
|
||||||
return confirmedProperty;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setConfirmed(boolean confirmed) {
|
|
||||||
confirmedProperty.set(confirmed);
|
|
||||||
}
|
|
||||||
|
|
||||||
public FeeRatesSource getFeeRatesSource() {
|
|
||||||
return feeRatesSource.get();
|
|
||||||
}
|
|
||||||
|
|
||||||
public ObjectProperty<FeeRatesSource> feeRatesSourceProperty() {
|
|
||||||
return feeRatesSource;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setFeeRatesSource(FeeRatesSource feeRatesSource) {
|
|
||||||
this.feeRatesSource.set(feeRatesSource);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static BlockCube fromBlockSummary(BlockSummary blockSummary) {
|
|
||||||
return new BlockCube(blockSummary.getWeight().orElse(0), blockSummary.getMedianFee().orElse(-1.0d), blockSummary.getHeight(),
|
|
||||||
blockSummary.getTransactionCount().orElse(0), blockSummary.getTimestamp().getTime(), true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -48,7 +48,7 @@ public class CardImportPane extends TitledDescriptionPane {
|
||||||
private final SimpleStringProperty pin = new SimpleStringProperty("");
|
private final SimpleStringProperty pin = new SimpleStringProperty("");
|
||||||
|
|
||||||
public CardImportPane(Wallet wallet, KeystoreCardImport importer, KeyDerivation requiredDerivation) {
|
public CardImportPane(Wallet wallet, KeystoreCardImport importer, KeyDerivation requiredDerivation) {
|
||||||
super(importer.getName(), "Place card on reader", importer.getKeystoreImportDescription(getAccount(wallet, requiredDerivation)), importer.getWalletModel());
|
super(importer.getName(), "Place card on reader", importer.getKeystoreImportDescription(getAccount(wallet, requiredDerivation)), "image/" + importer.getWalletModel().getType() + ".png");
|
||||||
this.importer = importer;
|
this.importer = importer;
|
||||||
this.derivation = requiredDerivation == null ? wallet.getScriptType().getDefaultDerivation() : requiredDerivation.getDerivation();
|
this.derivation = requiredDerivation == null ? wallet.getScriptType().getDefaultDerivation() : requiredDerivation.getDerivation();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -87,8 +87,6 @@ class CoinCell extends TreeTableCell<Entry, Number> implements ConfirmationsList
|
||||||
} else if(entry instanceof UtxoEntry) {
|
} else if(entry instanceof UtxoEntry) {
|
||||||
setGraphic(null);
|
setGraphic(null);
|
||||||
} else if(entry instanceof HashIndexEntry) {
|
} else if(entry instanceof HashIndexEntry) {
|
||||||
tooltip.hideConfirmations();
|
|
||||||
|
|
||||||
Region node = new Region();
|
Region node = new Region();
|
||||||
node.setPrefWidth(10);
|
node.setPrefWidth(10);
|
||||||
setGraphic(node);
|
setGraphic(node);
|
||||||
|
|
@ -150,14 +148,6 @@ class CoinCell extends TreeTableCell<Entry, Number> implements ConfirmationsList
|
||||||
setTooltipText();
|
setTooltipText();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void hideConfirmations() {
|
|
||||||
showConfirmations = false;
|
|
||||||
isCoinbase = false;
|
|
||||||
confirmationsProperty.unbind();
|
|
||||||
|
|
||||||
setTooltipText();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void setTooltipText() {
|
private void setTooltipText() {
|
||||||
setText(value + (showConfirmations ? " (" + getConfirmationsDescription() + ")" : ""));
|
setText(value + (showConfirmations ? " (" + getConfirmationsDescription() + ")" : ""));
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -225,13 +225,6 @@ public class CoinTreeTable extends TreeTableView<Entry> {
|
||||||
walletTableEvents.skip(3, TimeUnit.SECONDS).subscribe(event -> {
|
walletTableEvents.skip(3, TimeUnit.SECONDS).subscribe(event -> {
|
||||||
event.getWallet().getWalletTables().put(event.getTableType(), event.getWalletTable());
|
event.getWallet().getWalletTables().put(event.getTableType(), event.getWalletTable());
|
||||||
EventManager.get().post(event);
|
EventManager.get().post(event);
|
||||||
|
|
||||||
//Reset pref widths here so window resizes don't cause reversion to previously set pref widths
|
|
||||||
Double[] widths = event.getWalletTable().getWidths();
|
|
||||||
for(int i = 0; i < getColumns().size(); i++) {
|
|
||||||
TreeTableColumn<Entry, ?> column = getColumns().get(i);
|
|
||||||
column.setPrefWidth(widths != null && getColumns().size() == widths.length ? widths[i] : STANDARD_WIDTH);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -6,16 +6,10 @@ import javafx.beans.property.SimpleObjectProperty;
|
||||||
import javafx.scene.Cursor;
|
import javafx.scene.Cursor;
|
||||||
import javafx.scene.Node;
|
import javafx.scene.Node;
|
||||||
import javafx.scene.control.ComboBox;
|
import javafx.scene.control.ComboBox;
|
||||||
import javafx.scene.control.ContextMenu;
|
|
||||||
import javafx.scene.control.MenuItem;
|
|
||||||
import javafx.scene.control.SeparatorMenuItem;
|
|
||||||
import javafx.scene.input.Clipboard;
|
|
||||||
import javafx.scene.layout.Region;
|
import javafx.scene.layout.Region;
|
||||||
import javafx.scene.layout.StackPane;
|
import javafx.scene.layout.StackPane;
|
||||||
import org.controlsfx.control.textfield.CustomTextField;
|
import org.controlsfx.control.textfield.CustomTextField;
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
public class ComboBoxTextField extends CustomTextField {
|
public class ComboBoxTextField extends CustomTextField {
|
||||||
private final ObjectProperty<ComboBox<?>> comboProperty = new SimpleObjectProperty<>();
|
private final ObjectProperty<ComboBox<?>> comboProperty = new SimpleObjectProperty<>();
|
||||||
|
|
||||||
|
|
@ -74,53 +68,4 @@ public class ComboBoxTextField extends CustomTextField {
|
||||||
public void setComboProperty(ComboBox<?> comboProperty) {
|
public void setComboProperty(ComboBox<?> comboProperty) {
|
||||||
this.comboProperty.set(comboProperty);
|
this.comboProperty.set(comboProperty);
|
||||||
}
|
}
|
||||||
|
|
||||||
public ContextMenu getCustomContextMenu(List<MenuItem> customItems) {
|
|
||||||
return new CustomContextMenu(customItems);
|
|
||||||
}
|
|
||||||
|
|
||||||
public class CustomContextMenu extends ContextMenu {
|
|
||||||
public CustomContextMenu(List<MenuItem> customItems) {
|
|
||||||
super();
|
|
||||||
setFont(null);
|
|
||||||
|
|
||||||
MenuItem undo = new MenuItem("Undo");
|
|
||||||
undo.setOnAction(_ -> undo());
|
|
||||||
|
|
||||||
MenuItem redo = new MenuItem("Redo");
|
|
||||||
redo.setOnAction(_ -> redo());
|
|
||||||
|
|
||||||
MenuItem cut = new MenuItem("Cut");
|
|
||||||
cut.setOnAction(_ -> cut());
|
|
||||||
|
|
||||||
MenuItem copy = new MenuItem("Copy");
|
|
||||||
copy.setOnAction(_ -> copy());
|
|
||||||
|
|
||||||
MenuItem paste = new MenuItem("Paste");
|
|
||||||
paste.setOnAction(_ -> paste());
|
|
||||||
|
|
||||||
MenuItem delete = new MenuItem("Delete");
|
|
||||||
delete.setOnAction(_ -> deleteText(getSelection()));
|
|
||||||
|
|
||||||
MenuItem selectAll = new MenuItem("Select All");
|
|
||||||
selectAll.setOnAction(_ -> selectAll());
|
|
||||||
|
|
||||||
getItems().addAll(undo, redo, new SeparatorMenuItem(), cut, copy, paste, delete, new SeparatorMenuItem(), selectAll);
|
|
||||||
getItems().addAll(customItems);
|
|
||||||
|
|
||||||
setOnShowing(_ -> {
|
|
||||||
boolean hasSelection = getSelection().getLength() > 0;
|
|
||||||
boolean hasText = getText() != null && !getText().isEmpty();
|
|
||||||
boolean clipboardHasContent = Clipboard.getSystemClipboard().hasString();
|
|
||||||
|
|
||||||
undo.setDisable(!isUndoable());
|
|
||||||
redo.setDisable(!isRedoable());
|
|
||||||
cut.setDisable(!isEditable() || !hasSelection);
|
|
||||||
copy.setDisable(!hasSelection);
|
|
||||||
paste.setDisable(!isEditable() || !clipboardHasContent);
|
|
||||||
delete.setDisable(!hasSelection);
|
|
||||||
selectAll.setDisable(!hasText);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,39 +0,0 @@
|
||||||
package com.sparrowwallet.sparrow.control;
|
|
||||||
|
|
||||||
import com.sparrowwallet.sparrow.AppServices;
|
|
||||||
import javafx.geometry.Insets;
|
|
||||||
import javafx.scene.control.Alert;
|
|
||||||
import javafx.scene.control.ButtonType;
|
|
||||||
import javafx.scene.control.CheckBox;
|
|
||||||
import javafx.scene.control.Label;
|
|
||||||
import javafx.scene.layout.VBox;
|
|
||||||
|
|
||||||
import static com.sparrowwallet.sparrow.AppServices.getActiveWindow;
|
|
||||||
import static com.sparrowwallet.sparrow.AppServices.setStageIcon;
|
|
||||||
|
|
||||||
public class ConfirmationAlert extends Alert {
|
|
||||||
private final CheckBox dontAskAgain;
|
|
||||||
|
|
||||||
public ConfirmationAlert(String title, String contentText, ButtonType... buttons) {
|
|
||||||
super(AlertType.CONFIRMATION, contentText, buttons);
|
|
||||||
|
|
||||||
initOwner(getActiveWindow());
|
|
||||||
setStageIcon(getDialogPane().getScene().getWindow());
|
|
||||||
getDialogPane().getScene().getStylesheets().add(AppServices.class.getResource("general.css").toExternalForm());
|
|
||||||
setTitle(title);
|
|
||||||
setHeaderText(title);
|
|
||||||
|
|
||||||
VBox contentBox = new VBox(20);
|
|
||||||
contentBox.setPadding(new Insets(10, 20, 10, 20));
|
|
||||||
Label contentLabel = new Label(contentText);
|
|
||||||
contentLabel.setWrapText(true);
|
|
||||||
dontAskAgain = new CheckBox("Don't ask again");
|
|
||||||
contentBox.getChildren().addAll(contentLabel, dontAskAgain);
|
|
||||||
|
|
||||||
getDialogPane().setContent(contentBox);
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean isDontAskAgain() {
|
|
||||||
return dontAskAgain.isSelected();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -11,7 +11,6 @@ import javafx.scene.control.Tooltip;
|
||||||
import javafx.scene.input.Clipboard;
|
import javafx.scene.input.Clipboard;
|
||||||
import javafx.scene.input.ClipboardContent;
|
import javafx.scene.input.ClipboardContent;
|
||||||
import javafx.event.EventHandler;
|
import javafx.event.EventHandler;
|
||||||
import javafx.scene.input.MouseButton;
|
|
||||||
import javafx.scene.input.MouseEvent;
|
import javafx.scene.input.MouseEvent;
|
||||||
|
|
||||||
public class CopyableCoinLabel extends CopyableLabel {
|
public class CopyableCoinLabel extends CopyableLabel {
|
||||||
|
|
@ -30,10 +29,6 @@ public class CopyableCoinLabel extends CopyableLabel {
|
||||||
valueProperty().addListener((observable, oldValue, newValue) -> setValueAsText((Long)newValue, Config.get().getUnitFormat(), Config.get().getBitcoinUnit()));
|
valueProperty().addListener((observable, oldValue, newValue) -> setValueAsText((Long)newValue, Config.get().getUnitFormat(), Config.get().getBitcoinUnit()));
|
||||||
|
|
||||||
setOnMouseClicked(event -> {
|
setOnMouseClicked(event -> {
|
||||||
if(!event.getButton().equals(MouseButton.PRIMARY)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if(bitcoinUnit == null) {
|
if(bitcoinUnit == null) {
|
||||||
bitcoinUnit = Config.get().getBitcoinUnit();
|
bitcoinUnit = Config.get().getBitcoinUnit();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -75,7 +75,7 @@ public class DevicePane extends TitledDescriptionPane {
|
||||||
private boolean defaultDevice;
|
private boolean defaultDevice;
|
||||||
|
|
||||||
public DevicePane(Wallet wallet, Device device, boolean defaultDevice, KeyDerivation requiredDerivation) {
|
public DevicePane(Wallet wallet, Device device, boolean defaultDevice, KeyDerivation requiredDerivation) {
|
||||||
super(device.getModel().toDisplayString(), "", "", device.getModel());
|
super(device.getModel().toDisplayString(), "", "", "image/" + device.getType() + ".png");
|
||||||
this.deviceOperation = DeviceOperation.IMPORT;
|
this.deviceOperation = DeviceOperation.IMPORT;
|
||||||
this.wallet = wallet;
|
this.wallet = wallet;
|
||||||
this.psbt = null;
|
this.psbt = null;
|
||||||
|
|
@ -102,7 +102,7 @@ public class DevicePane extends TitledDescriptionPane {
|
||||||
}
|
}
|
||||||
|
|
||||||
public DevicePane(Wallet wallet, PSBT psbt, Device device, boolean defaultDevice) {
|
public DevicePane(Wallet wallet, PSBT psbt, Device device, boolean defaultDevice) {
|
||||||
super(device.getModel().toDisplayString(), "", "", device.getModel());
|
super(device.getModel().toDisplayString(), "", "", "image/" + device.getType() + ".png");
|
||||||
this.deviceOperation = DeviceOperation.SIGN;
|
this.deviceOperation = DeviceOperation.SIGN;
|
||||||
this.wallet = wallet;
|
this.wallet = wallet;
|
||||||
this.psbt = psbt;
|
this.psbt = psbt;
|
||||||
|
|
@ -129,7 +129,7 @@ public class DevicePane extends TitledDescriptionPane {
|
||||||
}
|
}
|
||||||
|
|
||||||
public DevicePane(Wallet wallet, OutputDescriptor outputDescriptor, Device device, boolean defaultDevice) {
|
public DevicePane(Wallet wallet, OutputDescriptor outputDescriptor, Device device, boolean defaultDevice) {
|
||||||
super(device.getModel().toDisplayString(), "", "", device.getModel());
|
super(device.getModel().toDisplayString(), "", "", "image/" + device.getType() + ".png");
|
||||||
this.deviceOperation = DeviceOperation.DISPLAY_ADDRESS;
|
this.deviceOperation = DeviceOperation.DISPLAY_ADDRESS;
|
||||||
this.wallet = wallet;
|
this.wallet = wallet;
|
||||||
this.psbt = null;
|
this.psbt = null;
|
||||||
|
|
@ -152,7 +152,7 @@ public class DevicePane extends TitledDescriptionPane {
|
||||||
}
|
}
|
||||||
|
|
||||||
public DevicePane(Wallet wallet, String message, KeyDerivation keyDerivation, Device device, boolean defaultDevice) {
|
public DevicePane(Wallet wallet, String message, KeyDerivation keyDerivation, Device device, boolean defaultDevice) {
|
||||||
super(device.getModel().toDisplayString(), "", "", device.getModel());
|
super(device.getModel().toDisplayString(), "", "", "image/" + device.getType() + ".png");
|
||||||
this.deviceOperation = DeviceOperation.SIGN_MESSAGE;
|
this.deviceOperation = DeviceOperation.SIGN_MESSAGE;
|
||||||
this.wallet = wallet;
|
this.wallet = wallet;
|
||||||
this.psbt = null;
|
this.psbt = null;
|
||||||
|
|
@ -179,7 +179,7 @@ public class DevicePane extends TitledDescriptionPane {
|
||||||
}
|
}
|
||||||
|
|
||||||
public DevicePane(Wallet wallet, List<StandardAccount> availableAccounts, Device device, boolean defaultDevice) {
|
public DevicePane(Wallet wallet, List<StandardAccount> availableAccounts, Device device, boolean defaultDevice) {
|
||||||
super(device.getModel().toDisplayString(), "", "", device.getModel());
|
super(device.getModel().toDisplayString(), "", "", "image/" + device.getType() + ".png");
|
||||||
this.deviceOperation = DeviceOperation.DISCOVER_KEYSTORES;
|
this.deviceOperation = DeviceOperation.DISCOVER_KEYSTORES;
|
||||||
this.wallet = wallet;
|
this.wallet = wallet;
|
||||||
this.psbt = null;
|
this.psbt = null;
|
||||||
|
|
@ -202,7 +202,7 @@ public class DevicePane extends TitledDescriptionPane {
|
||||||
}
|
}
|
||||||
|
|
||||||
public DevicePane(DeviceOperation deviceOperation, Device device, boolean defaultDevice) {
|
public DevicePane(DeviceOperation deviceOperation, Device device, boolean defaultDevice) {
|
||||||
super(device.getModel().toDisplayString(), "", "", device.getModel());
|
super(device.getModel().toDisplayString(), "", "", "image/" + device.getType() + ".png");
|
||||||
this.deviceOperation = deviceOperation;
|
this.deviceOperation = deviceOperation;
|
||||||
this.wallet = null;
|
this.wallet = null;
|
||||||
this.psbt = null;
|
this.psbt = null;
|
||||||
|
|
@ -453,26 +453,20 @@ public class DevicePane extends TitledDescriptionPane {
|
||||||
});
|
});
|
||||||
vBox.getChildren().addAll(pinField, enterPinButton);
|
vBox.getChildren().addAll(pinField, enterPinButton);
|
||||||
|
|
||||||
GridPane gridPane = new GridPane();
|
TilePane tilePane = new TilePane();
|
||||||
gridPane.setHgap(10);
|
tilePane.setPrefColumns(3);
|
||||||
gridPane.setVgap(10);
|
tilePane.setHgap(10);
|
||||||
gridPane.setMaxWidth(150);
|
tilePane.setVgap(10);
|
||||||
gridPane.setMaxHeight(device.getModel().hasZeroInPin() ? 160 : 120);
|
tilePane.setMaxWidth(150);
|
||||||
|
tilePane.setMaxHeight(120);
|
||||||
|
|
||||||
int[] digits = device.getModel().hasZeroInPin() ? new int[] {7, 8, 9, 4, 5, 6, 1, 2, 3, 0} : new int[] {7, 8, 9, 4, 5, 6, 1, 2, 3};
|
int[] digits = new int[] {7, 8, 9, 4, 5, 6, 1, 2, 3};
|
||||||
for(int i = 0; i < digits.length; i++) {
|
for(int i = 0; i < digits.length; i++) {
|
||||||
Button pinButton = new Button();
|
Button pinButton = new Button();
|
||||||
Glyph circle = new Glyph(FontAwesome5.FONT_NAME, "CIRCLE");
|
Glyph circle = new Glyph(FontAwesome5.FONT_NAME, "CIRCLE");
|
||||||
pinButton.setGraphic(circle);
|
pinButton.setGraphic(circle);
|
||||||
pinButton.setUserData(digits[i]);
|
pinButton.setUserData(digits[i]);
|
||||||
GridPane.setRowIndex(pinButton, i / 3);
|
tilePane.getChildren().add(pinButton);
|
||||||
GridPane.setColumnIndex(pinButton, i % 3);
|
|
||||||
if((i / 3) == 3) {
|
|
||||||
GridPane.setHgrow(pinButton, Priority.ALWAYS);
|
|
||||||
GridPane.setColumnSpan(pinButton, 3);
|
|
||||||
pinButton.setMaxWidth(Double.MAX_VALUE);
|
|
||||||
}
|
|
||||||
gridPane.getChildren().add(pinButton);
|
|
||||||
pinButton.setOnAction(event -> {
|
pinButton.setOnAction(event -> {
|
||||||
pinField.setText(pinField.getText() + pinButton.getUserData());
|
pinField.setText(pinField.getText() + pinButton.getUserData());
|
||||||
});
|
});
|
||||||
|
|
@ -480,7 +474,7 @@ public class DevicePane extends TitledDescriptionPane {
|
||||||
|
|
||||||
HBox contentBox = new HBox();
|
HBox contentBox = new HBox();
|
||||||
contentBox.setSpacing(50);
|
contentBox.setSpacing(50);
|
||||||
contentBox.getChildren().add(gridPane);
|
contentBox.getChildren().add(tilePane);
|
||||||
contentBox.getChildren().add(vBox);
|
contentBox.getChildren().add(vBox);
|
||||||
contentBox.setPadding(new Insets(10, 0, 10, 0));
|
contentBox.setPadding(new Insets(10, 0, 10, 0));
|
||||||
contentBox.setAlignment(Pos.TOP_CENTER);
|
contentBox.setAlignment(Pos.TOP_CENTER);
|
||||||
|
|
|
||||||
|
|
@ -1,85 +0,0 @@
|
||||||
package com.sparrowwallet.sparrow.control;
|
|
||||||
|
|
||||||
import com.sparrowwallet.sparrow.AppServices;
|
|
||||||
import com.sparrowwallet.sparrow.Theme;
|
|
||||||
import com.sparrowwallet.sparrow.io.Config;
|
|
||||||
import javafx.beans.NamedArg;
|
|
||||||
import javafx.beans.property.ObjectProperty;
|
|
||||||
import javafx.beans.property.SimpleObjectProperty;
|
|
||||||
import javafx.scene.layout.StackPane;
|
|
||||||
import org.girod.javafx.svgimage.SVGImage;
|
|
||||||
import org.girod.javafx.svgimage.SVGLoader;
|
|
||||||
import org.slf4j.Logger;
|
|
||||||
import org.slf4j.LoggerFactory;
|
|
||||||
|
|
||||||
import java.net.URL;
|
|
||||||
import java.util.Locale;
|
|
||||||
|
|
||||||
public class DialogImage extends StackPane {
|
|
||||||
private static final Logger log = LoggerFactory.getLogger(DialogImage.class);
|
|
||||||
|
|
||||||
public static final int WIDTH = 50;
|
|
||||||
public static final int HEIGHT = 50;
|
|
||||||
|
|
||||||
public ObjectProperty<DialogImage.Type> typeProperty = new SimpleObjectProperty<>();
|
|
||||||
|
|
||||||
public DialogImage() {
|
|
||||||
setPrefSize(WIDTH, HEIGHT);
|
|
||||||
this.typeProperty.addListener((observable, oldValue, type) -> {
|
|
||||||
refresh(type);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public DialogImage(@NamedArg("type") Type type) {
|
|
||||||
this();
|
|
||||||
this.typeProperty.set(type);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void refresh() {
|
|
||||||
Type type = getType();
|
|
||||||
refresh(type);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected void refresh(Type type) {
|
|
||||||
SVGImage svgImage;
|
|
||||||
if(Config.get().getTheme() == Theme.DARK) {
|
|
||||||
svgImage = loadSVGImage("/image/dialog/" + type.name().toLowerCase(Locale.ROOT) + "-invert.svg");
|
|
||||||
} else {
|
|
||||||
svgImage = loadSVGImage("/image/dialog/" + type.name().toLowerCase(Locale.ROOT) + ".svg");
|
|
||||||
}
|
|
||||||
|
|
||||||
if(svgImage != null) {
|
|
||||||
getChildren().clear();
|
|
||||||
getChildren().add(svgImage);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public Type getType() {
|
|
||||||
return typeProperty.get();
|
|
||||||
}
|
|
||||||
|
|
||||||
public ObjectProperty<Type> typeProperty() {
|
|
||||||
return typeProperty;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setType(Type type) {
|
|
||||||
this.typeProperty.set(type);
|
|
||||||
}
|
|
||||||
|
|
||||||
private SVGImage loadSVGImage(String imageName) {
|
|
||||||
try {
|
|
||||||
URL url = AppServices.class.getResource(imageName);
|
|
||||||
if(url != null) {
|
|
||||||
return SVGLoader.load(url);
|
|
||||||
}
|
|
||||||
} catch(Exception e) {
|
|
||||||
log.error("Could not find image " + imageName);
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
public enum Type {
|
|
||||||
SPARROW, SEED, PAYNYM, BORDERWALLETS, USERADD, WHIRLPOOL;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -56,15 +56,13 @@ public class DownloadVerifierDialog extends Dialog<ButtonBar.ButtonData> {
|
||||||
private static final List<String> MANIFEST_EXTENSIONS = List.of("txt");
|
private static final List<String> MANIFEST_EXTENSIONS = List.of("txt");
|
||||||
private static final List<String> PUBLIC_KEY_EXTENSIONS = List.of("asc");
|
private static final List<String> PUBLIC_KEY_EXTENSIONS = List.of("asc");
|
||||||
private static final List<String> MACOS_RELEASE_EXTENSIONS = List.of("dmg");
|
private static final List<String> MACOS_RELEASE_EXTENSIONS = List.of("dmg");
|
||||||
private static final List<String> WINDOWS_RELEASE_EXTENSIONS = List.of("exe", "msi", "zip");
|
private static final List<String> WINDOWS_RELEASE_EXTENSIONS = List.of("exe", "zip");
|
||||||
private static final List<String> LINUX_RELEASE_EXTENSIONS = List.of("deb", "rpm", "tar.gz");
|
private static final List<String> LINUX_RELEASE_EXTENSIONS = List.of("deb", "rpm", "tar.gz");
|
||||||
private static final List<String> DISK_IMAGE_EXTENSIONS = List.of("img", "bin", "dfu");
|
private static final List<String> DISK_IMAGE_EXTENSIONS = List.of("img", "bin", "dfu");
|
||||||
private static final List<String> ARCHIVE_EXTENSIONS = List.of("zip", "tar.gz", "tar.bz2", "tar.xz", "rar", "7z");
|
private static final List<String> ARCHIVE_EXTENSIONS = List.of("zip", "tar.gz", "tar.bz2", "tar.xz", "rar", "7z");
|
||||||
|
|
||||||
private static final String SPARROW_RELEASE_PREFIX = "sparrow-";
|
private static final String SPARROW_RELEASE_PREFIX = "sparrow-";
|
||||||
private static final String SPARROW_RELEASE_ALT_PREFIX = "sparrow_";
|
private static final String SPARROW_SIGNATURE_SUFFIX = "-manifest.txt.asc";
|
||||||
private static final String SPARROW_MANIFEST_SUFFIX = "-manifest.txt";
|
|
||||||
private static final String SPARROW_SIGNATURE_SUFFIX = SPARROW_MANIFEST_SUFFIX + ".asc";
|
|
||||||
private static final Pattern SPARROW_RELEASE_VERSION = Pattern.compile("[0-9]+(\\.[0-9]+)*");
|
private static final Pattern SPARROW_RELEASE_VERSION = Pattern.compile("[0-9]+(\\.[0-9]+)*");
|
||||||
private static final long MIN_VALID_SPARROW_RELEASE_SIZE = 10 * 1024 * 1024;
|
private static final long MIN_VALID_SPARROW_RELEASE_SIZE = 10 * 1024 * 1024;
|
||||||
|
|
||||||
|
|
@ -72,7 +70,6 @@ public class DownloadVerifierDialog extends Dialog<ButtonBar.ButtonData> {
|
||||||
private final ObjectProperty<File> manifest = new SimpleObjectProperty<>();
|
private final ObjectProperty<File> manifest = new SimpleObjectProperty<>();
|
||||||
private final ObjectProperty<File> publicKey = new SimpleObjectProperty<>();
|
private final ObjectProperty<File> publicKey = new SimpleObjectProperty<>();
|
||||||
private final ObjectProperty<File> release = new SimpleObjectProperty<>();
|
private final ObjectProperty<File> release = new SimpleObjectProperty<>();
|
||||||
private final ObjectProperty<File> initial = new SimpleObjectProperty<>();
|
|
||||||
|
|
||||||
private final BooleanProperty manifestDisabled = new SimpleBooleanProperty();
|
private final BooleanProperty manifestDisabled = new SimpleBooleanProperty();
|
||||||
private final BooleanProperty publicKeyDisabled = new SimpleBooleanProperty();
|
private final BooleanProperty publicKeyDisabled = new SimpleBooleanProperty();
|
||||||
|
|
@ -84,7 +81,7 @@ public class DownloadVerifierDialog extends Dialog<ButtonBar.ButtonData> {
|
||||||
|
|
||||||
private static File lastFileParent;
|
private static File lastFileParent;
|
||||||
|
|
||||||
public DownloadVerifierDialog(File initialFile) {
|
public DownloadVerifierDialog(File initialSignatureFile) {
|
||||||
final DialogPane dialogPane = getDialogPane();
|
final DialogPane dialogPane = getDialogPane();
|
||||||
dialogPane.getStylesheets().add(AppServices.class.getResource("general.css").toExternalForm());
|
dialogPane.getStylesheets().add(AppServices.class.getResource("general.css").toExternalForm());
|
||||||
dialogPane.getStylesheets().add(AppServices.class.getResource("dialog.css").toExternalForm());
|
dialogPane.getStylesheets().add(AppServices.class.getResource("dialog.css").toExternalForm());
|
||||||
|
|
@ -226,17 +223,11 @@ public class DownloadVerifierDialog extends Dialog<ButtonBar.ButtonData> {
|
||||||
});
|
});
|
||||||
|
|
||||||
release.addListener((observable, oldValue, releaseFile) -> {
|
release.addListener((observable, oldValue, releaseFile) -> {
|
||||||
if(releaseFile != null) {
|
|
||||||
initial.set(null);
|
|
||||||
}
|
|
||||||
verify();
|
verify();
|
||||||
});
|
});
|
||||||
|
|
||||||
if(initialFile != null) {
|
if(initialSignatureFile != null) {
|
||||||
javafx.application.Platform.runLater(() -> {
|
javafx.application.Platform.runLater(() -> signature.set(initialSignatureFile));
|
||||||
initial.set(initialFile);
|
|
||||||
signature.set(initialFile);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -301,7 +292,7 @@ public class DownloadVerifierDialog extends Dialog<ButtonBar.ButtonData> {
|
||||||
publicKeyDisabled.set(true);
|
publicKeyDisabled.set(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
if(manifest.get().equals(release.get()) && !isSparrowManifest(manifest.get())) {
|
if(manifest.get().equals(release.get())) {
|
||||||
manifestDisabled.set(true);
|
manifestDisabled.set(true);
|
||||||
releaseHash.setText("No hash required, signature signs release file directly");
|
releaseHash.setText("No hash required, signature signs release file directly");
|
||||||
releaseHash.setGraphic(GlyphUtils.getSuccessGlyph());
|
releaseHash.setGraphic(GlyphUtils.getSuccessGlyph());
|
||||||
|
|
@ -464,8 +455,7 @@ public class DownloadVerifierDialog extends Dialog<ButtonBar.ButtonData> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
String providedName = providedFile.getName().toLowerCase(Locale.ROOT);
|
if(providedFile.getName().toLowerCase(Locale.ROOT).startsWith(SPARROW_RELEASE_PREFIX)) {
|
||||||
if(providedName.startsWith(SPARROW_RELEASE_PREFIX) || providedName.startsWith(SPARROW_RELEASE_ALT_PREFIX)) {
|
|
||||||
Matcher matcher = SPARROW_RELEASE_VERSION.matcher(providedFile.getName());
|
Matcher matcher = SPARROW_RELEASE_VERSION.matcher(providedFile.getName());
|
||||||
if(matcher.find()) {
|
if(matcher.find()) {
|
||||||
String version = matcher.group();
|
String version = matcher.group();
|
||||||
|
|
@ -492,22 +482,6 @@ public class DownloadVerifierDialog extends Dialog<ButtonBar.ButtonData> {
|
||||||
}
|
}
|
||||||
|
|
||||||
private File findReleaseFile(File manifestFile, Map<File, String> manifestMap) {
|
private File findReleaseFile(File manifestFile, Map<File, String> manifestMap) {
|
||||||
File initialFile = initial.get();
|
|
||||||
if(initialFile != null && initialFile.exists()) {
|
|
||||||
for(File file : manifestMap.keySet()) {
|
|
||||||
if(initialFile.getName().equals(file.getName())) {
|
|
||||||
return initialFile;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
List<List<String>> allExtensionLists = List.of(MACOS_RELEASE_EXTENSIONS, WINDOWS_RELEASE_EXTENSIONS, LINUX_RELEASE_EXTENSIONS, DISK_IMAGE_EXTENSIONS, ARCHIVE_EXTENSIONS);
|
|
||||||
for(List<String> extensions : allExtensionLists) {
|
|
||||||
if(extensions.stream().anyMatch(ext -> initialFile.getName().toLowerCase(Locale.ROOT).endsWith(ext))) {
|
|
||||||
return initialFile;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
List<String> releaseExtensions = getReleaseFileExtensions();
|
List<String> releaseExtensions = getReleaseFileExtensions();
|
||||||
List<List<String>> extensionLists = List.of(releaseExtensions, DISK_IMAGE_EXTENSIONS, ARCHIVE_EXTENSIONS, List.of(""));
|
List<List<String>> extensionLists = List.of(releaseExtensions, DISK_IMAGE_EXTENSIONS, ARCHIVE_EXTENSIONS, List.of(""));
|
||||||
|
|
||||||
|
|
@ -591,7 +565,7 @@ public class DownloadVerifierDialog extends Dialog<ButtonBar.ButtonData> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if((name.startsWith(SPARROW_RELEASE_PREFIX) || name.startsWith(SPARROW_RELEASE_ALT_PREFIX)) && file.length() >= MIN_VALID_SPARROW_RELEASE_SIZE) {
|
if(name.startsWith(SPARROW_RELEASE_PREFIX) && file.length() >= MIN_VALID_SPARROW_RELEASE_SIZE) {
|
||||||
Matcher matcher = SPARROW_RELEASE_VERSION.matcher(name);
|
Matcher matcher = SPARROW_RELEASE_VERSION.matcher(name);
|
||||||
return matcher.find();
|
return matcher.find();
|
||||||
}
|
}
|
||||||
|
|
@ -600,18 +574,10 @@ public class DownloadVerifierDialog extends Dialog<ButtonBar.ButtonData> {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static boolean isSparrowManifest(File manifestFile) {
|
|
||||||
return manifestFile.getName().startsWith(SPARROW_RELEASE_PREFIX) && manifestFile.getName().endsWith(SPARROW_MANIFEST_SUFFIX);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setSignatureFile(File signatureFile) {
|
public void setSignatureFile(File signatureFile) {
|
||||||
signature.set(signatureFile);
|
signature.set(signatureFile);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setInitialFile(File initialFile) {
|
|
||||||
initial.set(initialFile);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static class Header extends GridPane {
|
private static class Header extends GridPane {
|
||||||
public Header() {
|
public Header() {
|
||||||
setMaxWidth(Double.MAX_VALUE);
|
setMaxWidth(Double.MAX_VALUE);
|
||||||
|
|
@ -632,8 +598,15 @@ public class DownloadVerifierDialog extends Dialog<ButtonBar.ButtonData> {
|
||||||
vBox.getChildren().addAll(headerLabel, descriptionLabel);
|
vBox.getChildren().addAll(headerLabel, descriptionLabel);
|
||||||
add(vBox, 0, 0);
|
add(vBox, 0, 0);
|
||||||
|
|
||||||
StackPane graphicContainer = new DialogImage(DialogImage.Type.SPARROW);
|
StackPane graphicContainer = new StackPane();
|
||||||
graphicContainer.getStyleClass().add("graphic-container");
|
graphicContainer.getStyleClass().add("graphic-container");
|
||||||
|
Image image = new Image("image/sparrow-small.png", 50, 50, false, false);
|
||||||
|
if (!image.isError()) {
|
||||||
|
ImageView imageView = new ImageView();
|
||||||
|
imageView.setSmooth(false);
|
||||||
|
imageView.setImage(image);
|
||||||
|
graphicContainer.getChildren().add(imageView);
|
||||||
|
}
|
||||||
add(graphicContainer, 1, 0);
|
add(graphicContainer, 1, 0);
|
||||||
|
|
||||||
ColumnConstraints textColumn = new ColumnConstraints();
|
ColumnConstraints textColumn = new ColumnConstraints();
|
||||||
|
|
|
||||||
|
|
@ -5,8 +5,6 @@ import com.sparrowwallet.drongo.OsType;
|
||||||
import com.sparrowwallet.drongo.Utils;
|
import com.sparrowwallet.drongo.Utils;
|
||||||
import com.sparrowwallet.drongo.address.Address;
|
import com.sparrowwallet.drongo.address.Address;
|
||||||
import com.sparrowwallet.drongo.protocol.*;
|
import com.sparrowwallet.drongo.protocol.*;
|
||||||
import com.sparrowwallet.drongo.silentpayments.SilentPayment;
|
|
||||||
import com.sparrowwallet.drongo.silentpayments.SilentPaymentAddress;
|
|
||||||
import com.sparrowwallet.drongo.wallet.*;
|
import com.sparrowwallet.drongo.wallet.*;
|
||||||
import com.sparrowwallet.sparrow.AppServices;
|
import com.sparrowwallet.sparrow.AppServices;
|
||||||
import com.sparrowwallet.sparrow.EventManager;
|
import com.sparrowwallet.sparrow.EventManager;
|
||||||
|
|
@ -57,7 +55,7 @@ public class EntryCell extends TreeTableCell<Entry, Entry> implements Confirmati
|
||||||
super.updateItem(entry, empty);
|
super.updateItem(entry, empty);
|
||||||
|
|
||||||
//Return immediately to avoid CPU usage when updating the same invisible cell to determine tableview size (see https://bugs.openjdk.org/browse/JDK-8280442)
|
//Return immediately to avoid CPU usage when updating the same invisible cell to determine tableview size (see https://bugs.openjdk.org/browse/JDK-8280442)
|
||||||
if(this == lastCell && !getTableRow().isVisible() && isTableSizeRecalculation()) {
|
if(this == lastCell && !getTableRow().isVisible()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
lastCell = this;
|
lastCell = this;
|
||||||
|
|
@ -68,7 +66,8 @@ public class EntryCell extends TreeTableCell<Entry, Entry> implements Confirmati
|
||||||
setText(null);
|
setText(null);
|
||||||
setGraphic(null);
|
setGraphic(null);
|
||||||
} else {
|
} else {
|
||||||
if(entry instanceof TransactionEntry transactionEntry) {
|
if(entry instanceof TransactionEntry) {
|
||||||
|
TransactionEntry transactionEntry = (TransactionEntry)entry;
|
||||||
if(transactionEntry.getBlockTransaction().getHeight() == -1) {
|
if(transactionEntry.getBlockTransaction().getHeight() == -1) {
|
||||||
setText("Unconfirmed Parent");
|
setText("Unconfirmed Parent");
|
||||||
setContextMenu(new UnconfirmedTransactionContextMenu(transactionEntry));
|
setContextMenu(new UnconfirmedTransactionContextMenu(transactionEntry));
|
||||||
|
|
@ -102,7 +101,7 @@ public class EntryCell extends TreeTableCell<Entry, Entry> implements Confirmati
|
||||||
actionBox.getChildren().add(viewTransactionButton);
|
actionBox.getChildren().add(viewTransactionButton);
|
||||||
|
|
||||||
BlockTransaction blockTransaction = transactionEntry.getBlockTransaction();
|
BlockTransaction blockTransaction = transactionEntry.getBlockTransaction();
|
||||||
if(blockTransaction.getHeight() <= 0 && canRBF(blockTransaction, transactionEntry.getWallet()) &&
|
if(blockTransaction.getHeight() <= 0 && canRBF(blockTransaction) &&
|
||||||
Config.get().isIncludeMempoolOutputs() && transactionEntry.getWallet().allInputsFromWallet(blockTransaction.getHash())) {
|
Config.get().isIncludeMempoolOutputs() && transactionEntry.getWallet().allInputsFromWallet(blockTransaction.getHash())) {
|
||||||
Button increaseFeeButton = new Button("");
|
Button increaseFeeButton = new Button("");
|
||||||
increaseFeeButton.setGraphic(getIncreaseFeeRBFGlyph());
|
increaseFeeButton.setGraphic(getIncreaseFeeRBFGlyph());
|
||||||
|
|
@ -122,7 +121,8 @@ public class EntryCell extends TreeTableCell<Entry, Entry> implements Confirmati
|
||||||
}
|
}
|
||||||
|
|
||||||
setGraphic(actionBox);
|
setGraphic(actionBox);
|
||||||
} else if(entry instanceof NodeEntry nodeEntry) {
|
} else if(entry instanceof NodeEntry) {
|
||||||
|
NodeEntry nodeEntry = (NodeEntry)entry;
|
||||||
Address address = nodeEntry.getAddress();
|
Address address = nodeEntry.getAddress();
|
||||||
setText(address.toString());
|
setText(address.toString());
|
||||||
setContextMenu(new AddressContextMenu(address, nodeEntry.getOutputDescriptor(), nodeEntry, true, getTreeTableView()));
|
setContextMenu(new AddressContextMenu(address, nodeEntry.getOutputDescriptor(), nodeEntry, true, getTreeTableView()));
|
||||||
|
|
@ -163,7 +163,8 @@ public class EntryCell extends TreeTableCell<Entry, Entry> implements Confirmati
|
||||||
setContextMenu(null);
|
setContextMenu(null);
|
||||||
setGraphic(new HBox());
|
setGraphic(new HBox());
|
||||||
}
|
}
|
||||||
} else if(entry instanceof HashIndexEntry hashIndexEntry) {
|
} else if(entry instanceof HashIndexEntry) {
|
||||||
|
HashIndexEntry hashIndexEntry = (HashIndexEntry)entry;
|
||||||
setText(hashIndexEntry.getDescription());
|
setText(hashIndexEntry.getDescription());
|
||||||
setContextMenu(getTreeTableView().getStyleClass().contains("bip47") ? null : new HashIndexEntryContextMenu(getTreeTableView(), hashIndexEntry));
|
setContextMenu(getTreeTableView().getStyleClass().contains("bip47") ? null : new HashIndexEntryContextMenu(getTreeTableView(), hashIndexEntry));
|
||||||
Tooltip tooltip = new Tooltip();
|
Tooltip tooltip = new Tooltip();
|
||||||
|
|
@ -211,14 +212,13 @@ public class EntryCell extends TreeTableCell<Entry, Entry> implements Confirmati
|
||||||
|
|
||||||
private static void increaseFee(TransactionEntry transactionEntry, boolean cancelTransaction) {
|
private static void increaseFee(TransactionEntry transactionEntry, boolean cancelTransaction) {
|
||||||
BlockTransaction blockTransaction = transactionEntry.getBlockTransaction();
|
BlockTransaction blockTransaction = transactionEntry.getBlockTransaction();
|
||||||
boolean silentPaymentTransaction = transactionEntry.getWallet().isSilentPaymentsTransaction(blockTransaction);
|
|
||||||
Map<BlockTransactionHashIndex, WalletNode> walletTxos = transactionEntry.getWallet().getWalletTxos();
|
Map<BlockTransactionHashIndex, WalletNode> walletTxos = transactionEntry.getWallet().getWalletTxos();
|
||||||
List<BlockTransactionHashIndex> utxos = transactionEntry.getChildren().stream()
|
List<BlockTransactionHashIndex> utxos = transactionEntry.getChildren().stream()
|
||||||
.filter(e -> e instanceof HashIndexEntry)
|
.filter(e -> e instanceof HashIndexEntry)
|
||||||
.map(e -> (HashIndexEntry)e)
|
.map(e -> (HashIndexEntry)e)
|
||||||
.filter(e -> e.getType().equals(HashIndexEntry.Type.INPUT) && e.isSpendable())
|
.filter(e -> e.getType().equals(HashIndexEntry.Type.INPUT) && e.isSpendable())
|
||||||
.map(e -> blockTransaction.getTransaction().getInputs().get((int)e.getHashIndex().getIndex()))
|
.map(e -> blockTransaction.getTransaction().getInputs().get((int)e.getHashIndex().getIndex()))
|
||||||
.filter(i -> Config.get().isMempoolFullRbf() || i.isReplaceByFeeEnabled() || silentPaymentTransaction)
|
.filter(i -> Config.get().isMempoolFullRbf() || i.isReplaceByFeeEnabled())
|
||||||
.map(txInput -> walletTxos.keySet().stream().filter(txo -> txo.getHash().equals(txInput.getOutpoint().getHash()) && txo.getIndex() == txInput.getOutpoint().getIndex()).findFirst().get())
|
.map(txInput -> walletTxos.keySet().stream().filter(txo -> txo.getHash().equals(txInput.getOutpoint().getHash()) && txo.getIndex() == txInput.getOutpoint().getIndex()).findFirst().get())
|
||||||
.collect(Collectors.toList());
|
.collect(Collectors.toList());
|
||||||
|
|
||||||
|
|
@ -243,7 +243,6 @@ public class EntryCell extends TreeTableCell<Entry, Entry> implements Confirmati
|
||||||
.collect(Collectors.toList());
|
.collect(Collectors.toList());
|
||||||
|
|
||||||
boolean consolidationTransaction = consolidationOutputs.size() == blockTransaction.getTransaction().getOutputs().size() && consolidationOutputs.size() == 1;
|
boolean consolidationTransaction = consolidationOutputs.size() == blockTransaction.getTransaction().getOutputs().size() && consolidationOutputs.size() == 1;
|
||||||
boolean safeToAddInputsOrOutputs = transactionEntry.getWallet().isSafeToAddInputsOrOutputs(blockTransaction);
|
|
||||||
long changeTotal = ourOutputs.stream().mapToLong(TransactionOutput::getValue).sum() - consolidationOutputs.stream().mapToLong(TransactionOutput::getValue).sum();
|
long changeTotal = ourOutputs.stream().mapToLong(TransactionOutput::getValue).sum() - consolidationOutputs.stream().mapToLong(TransactionOutput::getValue).sum();
|
||||||
Transaction tx = blockTransaction.getTransaction();
|
Transaction tx = blockTransaction.getTransaction();
|
||||||
double vSize = tx.getVirtualSize();
|
double vSize = tx.getVirtualSize();
|
||||||
|
|
@ -258,7 +257,7 @@ public class EntryCell extends TreeTableCell<Entry, Entry> implements Confirmati
|
||||||
List<OutputGroup> outputGroups = transactionEntry.getWallet().getGroupedUtxos(txoFilters, feeRate, AppServices.getMinimumRelayFeeRate(), Config.get().isGroupByAddress())
|
List<OutputGroup> outputGroups = transactionEntry.getWallet().getGroupedUtxos(txoFilters, feeRate, AppServices.getMinimumRelayFeeRate(), Config.get().isGroupByAddress())
|
||||||
.stream().filter(outputGroup -> outputGroup.getEffectiveValue() >= 0).collect(Collectors.toList());
|
.stream().filter(outputGroup -> outputGroup.getEffectiveValue() >= 0).collect(Collectors.toList());
|
||||||
Collections.shuffle(outputGroups);
|
Collections.shuffle(outputGroups);
|
||||||
while((double)changeTotal / vSize < getMaxFeeRate() && !outputGroups.isEmpty() && !cancelTransaction && !consolidationTransaction && safeToAddInputsOrOutputs) {
|
while((double)changeTotal / vSize < getMaxFeeRate() && !outputGroups.isEmpty() && !cancelTransaction && !consolidationTransaction) {
|
||||||
//If there is insufficient change output, include another random output group so the fee can be increased
|
//If there is insufficient change output, include another random output group so the fee can be increased
|
||||||
OutputGroup outputGroup = outputGroups.remove(0);
|
OutputGroup outputGroup = outputGroups.remove(0);
|
||||||
for(BlockTransactionHashIndex utxo : outputGroup.getUtxos()) {
|
for(BlockTransactionHashIndex utxo : outputGroup.getUtxos()) {
|
||||||
|
|
@ -299,13 +298,9 @@ public class EntryCell extends TreeTableCell<Entry, Entry> implements Confirmati
|
||||||
label += " (Replaced By Fee)";
|
label += " (Replaced By Fee)";
|
||||||
}
|
}
|
||||||
|
|
||||||
Address address = txOutput.getScript().getToAddress();
|
if(txOutput.getScript().getToAddress() != null) {
|
||||||
if(address != null) {
|
|
||||||
long value = txOutput.getValue();
|
|
||||||
//Disable change creation by enabling max payment when there is only one output and no additional UTXOs included
|
//Disable change creation by enabling max payment when there is only one output and no additional UTXOs included
|
||||||
boolean sendMax = blockTransaction.getTransaction().getOutputs().size() == 1 && rbfChange == 0;
|
return new Payment(txOutput.getScript().getToAddress(), label, txOutput.getValue(), blockTransaction.getTransaction().getOutputs().size() == 1 && rbfChange == 0);
|
||||||
SilentPaymentAddress silentPaymentAddress = transactionEntry.getWallet().getSilentPaymentAddress(address);
|
|
||||||
return silentPaymentAddress == null ? new Payment(address, label, value, sendMax) : new SilentPayment(silentPaymentAddress, label, value, sendMax);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
|
|
@ -342,7 +337,7 @@ public class EntryCell extends TreeTableCell<Entry, Entry> implements Confirmati
|
||||||
}
|
}
|
||||||
|
|
||||||
EventManager.get().post(new SendActionEvent(transactionEntry.getWallet(), utxos));
|
EventManager.get().post(new SendActionEvent(transactionEntry.getWallet(), utxos));
|
||||||
Platform.runLater(() -> EventManager.get().post(new SpendUtxoEvent(transactionEntry.getWallet(), utxos, payments, opReturns.isEmpty() ? null : opReturns, rbfFee, true, blockTransaction, safeToAddInputsOrOutputs)));
|
Platform.runLater(() -> EventManager.get().post(new SpendUtxoEvent(transactionEntry.getWallet(), utxos, payments, opReturns.isEmpty() ? null : opReturns, rbfFee, true, blockTransaction)));
|
||||||
}
|
}
|
||||||
|
|
||||||
private static Double getMaxFeeRate() {
|
private static Double getMaxFeeRate() {
|
||||||
|
|
@ -399,11 +394,11 @@ public class EntryCell extends TreeTableCell<Entry, Entry> implements Confirmati
|
||||||
Payment payment = new Payment(freshAddress, label, inputTotal, true);
|
Payment payment = new Payment(freshAddress, label, inputTotal, true);
|
||||||
|
|
||||||
EventManager.get().post(new SendActionEvent(transactionEntry.getWallet(), utxos));
|
EventManager.get().post(new SendActionEvent(transactionEntry.getWallet(), utxos));
|
||||||
Platform.runLater(() -> EventManager.get().post(new SpendUtxoEvent(transactionEntry.getWallet(), utxos, List.of(payment), null, blockTransaction.getFee(), true, null, true)));
|
Platform.runLater(() -> EventManager.get().post(new SpendUtxoEvent(transactionEntry.getWallet(), utxos, List.of(payment), null, blockTransaction.getFee(), true, null)));
|
||||||
}
|
}
|
||||||
|
|
||||||
private static boolean canRBF(BlockTransaction blockTransaction, Wallet wallet) {
|
private static boolean canRBF(BlockTransaction blockTransaction) {
|
||||||
return Config.get().isMempoolFullRbf() || blockTransaction.getTransaction().isReplaceByFee() || wallet.isSilentPaymentsTransaction(blockTransaction);
|
return Config.get().isMempoolFullRbf() || blockTransaction.getTransaction().isReplaceByFee();
|
||||||
}
|
}
|
||||||
|
|
||||||
private static boolean canSignMessage(WalletNode walletNode) {
|
private static boolean canSignMessage(WalletNode walletNode) {
|
||||||
|
|
@ -481,7 +476,7 @@ public class EntryCell extends TreeTableCell<Entry, Entry> implements Confirmati
|
||||||
tooltip += "\nFee rate: " + String.format("%.2f", feeRate) + " sats/vB";
|
tooltip += "\nFee rate: " + String.format("%.2f", feeRate) + " sats/vB";
|
||||||
}
|
}
|
||||||
|
|
||||||
tooltip += "\nRBF: " + (canRBF(transactionEntry.getBlockTransaction(), transactionEntry.getWallet()) ? "Enabled" : "Disabled");
|
tooltip += "\nRBF: " + (canRBF(transactionEntry.getBlockTransaction()) ? "Enabled" : "Disabled");
|
||||||
}
|
}
|
||||||
|
|
||||||
return tooltip;
|
return tooltip;
|
||||||
|
|
@ -549,7 +544,6 @@ public class EntryCell extends TreeTableCell<Entry, Entry> implements Confirmati
|
||||||
|
|
||||||
private static class UnconfirmedTransactionContextMenu extends ContextMenu {
|
private static class UnconfirmedTransactionContextMenu extends ContextMenu {
|
||||||
public UnconfirmedTransactionContextMenu(TransactionEntry transactionEntry) {
|
public UnconfirmedTransactionContextMenu(TransactionEntry transactionEntry) {
|
||||||
Wallet wallet = transactionEntry.getWallet();
|
|
||||||
BlockTransaction blockTransaction = transactionEntry.getBlockTransaction();
|
BlockTransaction blockTransaction = transactionEntry.getBlockTransaction();
|
||||||
MenuItem viewTransaction = new MenuItem("View Transaction");
|
MenuItem viewTransaction = new MenuItem("View Transaction");
|
||||||
viewTransaction.setGraphic(getViewTransactionGlyph());
|
viewTransaction.setGraphic(getViewTransactionGlyph());
|
||||||
|
|
@ -559,7 +553,7 @@ public class EntryCell extends TreeTableCell<Entry, Entry> implements Confirmati
|
||||||
});
|
});
|
||||||
getItems().add(viewTransaction);
|
getItems().add(viewTransaction);
|
||||||
|
|
||||||
if(canRBF(blockTransaction, wallet) && Config.get().isIncludeMempoolOutputs() && transactionEntry.getWallet().allInputsFromWallet(blockTransaction.getHash())) {
|
if(canRBF(blockTransaction) && Config.get().isIncludeMempoolOutputs() && transactionEntry.getWallet().allInputsFromWallet(blockTransaction.getHash())) {
|
||||||
MenuItem increaseFee = new MenuItem("Increase Fee (RBF)");
|
MenuItem increaseFee = new MenuItem("Increase Fee (RBF)");
|
||||||
increaseFee.setGraphic(getIncreaseFeeRBFGlyph());
|
increaseFee.setGraphic(getIncreaseFeeRBFGlyph());
|
||||||
increaseFee.setOnAction(AE -> {
|
increaseFee.setOnAction(AE -> {
|
||||||
|
|
@ -570,7 +564,7 @@ public class EntryCell extends TreeTableCell<Entry, Entry> implements Confirmati
|
||||||
getItems().add(increaseFee);
|
getItems().add(increaseFee);
|
||||||
}
|
}
|
||||||
|
|
||||||
if(canRBF(blockTransaction, wallet) && Config.get().isIncludeMempoolOutputs() && transactionEntry.getWallet().allInputsFromWallet(blockTransaction.getHash())) {
|
if(canRBF(blockTransaction) && Config.get().isIncludeMempoolOutputs() && transactionEntry.getWallet().allInputsFromWallet(blockTransaction.getHash())) {
|
||||||
MenuItem cancelTx = new MenuItem("Cancel Transaction (RBF)");
|
MenuItem cancelTx = new MenuItem("Cancel Transaction (RBF)");
|
||||||
cancelTx.setGraphic(getCancelTransactionRBFGlyph());
|
cancelTx.setGraphic(getCancelTransactionRBFGlyph());
|
||||||
cancelTx.setOnAction(AE -> {
|
cancelTx.setOnAction(AE -> {
|
||||||
|
|
@ -856,11 +850,4 @@ public class EntryCell extends TreeTableCell<Entry, Entry> implements Confirmati
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean isTableSizeRecalculation() {
|
|
||||||
//As per https://bugs.openjdk.org/browse/JDK-8265669 we check for cell visibility to avoid unnecessary recalculation, but this can result in false positives
|
|
||||||
//The method releaseCell in VirtualFlow is responsible for setting accumCell visibility to false after use, so check this method is calling updateItem
|
|
||||||
return StackWalker.getInstance().walk(frames -> frames.anyMatch(frame -> frame.getClassName().equals("javafx.scene.control.skin.VirtualFlow")
|
|
||||||
&& frame.getMethodName().equals("releaseCell")));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
package com.sparrowwallet.sparrow.control;
|
package com.sparrowwallet.sparrow.control;
|
||||||
|
|
||||||
import com.sparrowwallet.drongo.protocol.Transaction;
|
|
||||||
import com.sparrowwallet.sparrow.AppServices;
|
import com.sparrowwallet.sparrow.AppServices;
|
||||||
import com.sparrowwallet.sparrow.net.FeeRatesSource;
|
import com.sparrowwallet.sparrow.net.FeeRatesSource;
|
||||||
import javafx.application.Platform;
|
import javafx.application.Platform;
|
||||||
|
|
@ -8,7 +7,6 @@ import javafx.scene.Node;
|
||||||
import javafx.scene.control.Slider;
|
import javafx.scene.control.Slider;
|
||||||
import javafx.util.StringConverter;
|
import javafx.util.StringConverter;
|
||||||
|
|
||||||
import java.text.DecimalFormat;
|
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
|
@ -16,11 +14,9 @@ import static com.sparrowwallet.sparrow.AppServices.*;
|
||||||
|
|
||||||
public class FeeRangeSlider extends Slider {
|
public class FeeRangeSlider extends Slider {
|
||||||
private static final double FEE_RATE_SCROLL_INCREMENT = 0.01;
|
private static final double FEE_RATE_SCROLL_INCREMENT = 0.01;
|
||||||
private static final DecimalFormat INTEGER_FEE_RATE_FORMAT = new DecimalFormat("0");
|
|
||||||
private static final DecimalFormat FRACTIONAL_FEE_RATE_FORMAT = new DecimalFormat("0.###");
|
|
||||||
|
|
||||||
public FeeRangeSlider() {
|
public FeeRangeSlider() {
|
||||||
super(0, AppServices.getFeeRatesRange().size() - 1, 0);
|
super(0, FEE_RATES_RANGE.size() - 1, 0);
|
||||||
setMajorTickUnit(1);
|
setMajorTickUnit(1);
|
||||||
setMinorTickCount(0);
|
setMinorTickCount(0);
|
||||||
setSnapToTicks(false);
|
setSnapToTicks(false);
|
||||||
|
|
@ -31,11 +27,11 @@ public class FeeRangeSlider extends Slider {
|
||||||
setLabelFormatter(new StringConverter<>() {
|
setLabelFormatter(new StringConverter<>() {
|
||||||
@Override
|
@Override
|
||||||
public String toString(Double object) {
|
public String toString(Double object) {
|
||||||
Double feeRate = AppServices.getLongFeeRatesRange().get(object.intValue());
|
Long feeRate = LONG_FEE_RATES_RANGE.get(object.intValue());
|
||||||
if(isLongFeeRange() && feeRate >= 1000) {
|
if(isLongFeeRange() && feeRate >= 1000) {
|
||||||
return INTEGER_FEE_RATE_FORMAT.format(feeRate / 1000) + "k";
|
return feeRate / 1000 + "k";
|
||||||
}
|
}
|
||||||
return feeRate > 0d && feeRate < Transaction.DEFAULT_MIN_RELAY_FEE ? FRACTIONAL_FEE_RATE_FORMAT.format(feeRate) : INTEGER_FEE_RATE_FORMAT.format(feeRate);
|
return Long.toString(feeRate);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
@ -55,10 +51,10 @@ public class FeeRangeSlider extends Slider {
|
||||||
setOnScroll(event -> {
|
setOnScroll(event -> {
|
||||||
if(event.getDeltaY() != 0) {
|
if(event.getDeltaY() != 0) {
|
||||||
double newFeeRate = getFeeRate() + (event.getDeltaY() > 0 ? FEE_RATE_SCROLL_INCREMENT : -FEE_RATE_SCROLL_INCREMENT);
|
double newFeeRate = getFeeRate() + (event.getDeltaY() > 0 ? FEE_RATE_SCROLL_INCREMENT : -FEE_RATE_SCROLL_INCREMENT);
|
||||||
if(newFeeRate < AppServices.getLongFeeRatesRange().getFirst()) {
|
if(newFeeRate < LONG_FEE_RATES_RANGE.get(0)) {
|
||||||
newFeeRate = AppServices.getLongFeeRatesRange().getFirst();
|
newFeeRate = LONG_FEE_RATES_RANGE.get(0);
|
||||||
} else if(newFeeRate > AppServices.getLongFeeRatesRange().getLast()) {
|
} else if(newFeeRate > LONG_FEE_RATES_RANGE.get(LONG_FEE_RATES_RANGE.size() - 1)) {
|
||||||
newFeeRate = AppServices.getLongFeeRatesRange().getLast();
|
newFeeRate = LONG_FEE_RATES_RANGE.get(LONG_FEE_RATES_RANGE.size() - 1);
|
||||||
}
|
}
|
||||||
setFeeRate(newFeeRate);
|
setFeeRate(newFeeRate);
|
||||||
}
|
}
|
||||||
|
|
@ -66,79 +62,27 @@ public class FeeRangeSlider extends Slider {
|
||||||
}
|
}
|
||||||
|
|
||||||
public double getFeeRate() {
|
public double getFeeRate() {
|
||||||
return getFeeRate(AppServices.getMinimumRelayFeeRate());
|
return Math.pow(2.0, getValue());
|
||||||
}
|
|
||||||
|
|
||||||
public double getFeeRate(Double minRelayFeeRate) {
|
|
||||||
if(minRelayFeeRate >= Transaction.DEFAULT_MIN_RELAY_FEE) {
|
|
||||||
return Math.pow(2.0, getValue());
|
|
||||||
}
|
|
||||||
|
|
||||||
if(getValue() < 1.0d) {
|
|
||||||
if(minRelayFeeRate == 0.0d) {
|
|
||||||
return getValue();
|
|
||||||
}
|
|
||||||
return Math.pow(minRelayFeeRate, 1.0d - getValue());
|
|
||||||
}
|
|
||||||
|
|
||||||
return Math.pow(2.0, getValue() - 1.0d);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setFeeRate(double feeRate) {
|
public void setFeeRate(double feeRate) {
|
||||||
setFeeRate(feeRate, AppServices.getMinimumRelayFeeRate());
|
double value = Math.log(feeRate) / Math.log(2);
|
||||||
}
|
|
||||||
|
|
||||||
public void setFeeRate(double feeRate, Double minRelayFeeRate) {
|
|
||||||
double value = getValue(feeRate, minRelayFeeRate);
|
|
||||||
updateMaxFeeRange(value);
|
updateMaxFeeRange(value);
|
||||||
setValue(value);
|
setValue(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
private double getValue(double feeRate, Double minRelayFeeRate) {
|
|
||||||
double value;
|
|
||||||
|
|
||||||
if(minRelayFeeRate >= Transaction.DEFAULT_MIN_RELAY_FEE) {
|
|
||||||
value = Math.log(feeRate) / Math.log(2);
|
|
||||||
} else {
|
|
||||||
if(feeRate < Transaction.DEFAULT_MIN_RELAY_FEE) {
|
|
||||||
if(minRelayFeeRate == 0.0d) {
|
|
||||||
return feeRate;
|
|
||||||
}
|
|
||||||
value = 1.0d - (Math.log(feeRate) / Math.log(minRelayFeeRate));
|
|
||||||
} else {
|
|
||||||
value = (Math.log(feeRate) / Math.log(2.0)) + 1.0d;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void updateFeeRange(Double minRelayFeeRate, Double previousMinRelayFeeRate) {
|
|
||||||
if(minRelayFeeRate != null && previousMinRelayFeeRate != null) {
|
|
||||||
setFeeRate(getFeeRate(previousMinRelayFeeRate), minRelayFeeRate);
|
|
||||||
}
|
|
||||||
setMinorTickCount(1);
|
|
||||||
setMinorTickCount(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void updateMaxFeeRange(double value) {
|
private void updateMaxFeeRange(double value) {
|
||||||
if(value >= getMax() && !isLongFeeRange()) {
|
if(value >= getMax() && !isLongFeeRange()) {
|
||||||
if(AppServices.getMinimumRelayFeeRate() < Transaction.DEFAULT_MIN_RELAY_FEE) {
|
setMax(LONG_FEE_RATES_RANGE.size() - 1);
|
||||||
setMin(1.0d);
|
|
||||||
}
|
|
||||||
setMax(AppServices.getLongFeeRatesRange().size() - 1);
|
|
||||||
updateTrackHighlight();
|
updateTrackHighlight();
|
||||||
} else if(value == getMin() && isLongFeeRange()) {
|
} else if(value == getMin() && isLongFeeRange()) {
|
||||||
if(AppServices.getMinimumRelayFeeRate() < Transaction.DEFAULT_MIN_RELAY_FEE) {
|
setMax(FEE_RATES_RANGE.size() - 1);
|
||||||
setMin(0.0d);
|
|
||||||
}
|
|
||||||
setMax(AppServices.getFeeRatesRange().size() - 1);
|
|
||||||
updateTrackHighlight();
|
updateTrackHighlight();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean isLongFeeRange() {
|
private boolean isLongFeeRange() {
|
||||||
return getMax() > AppServices.getFeeRatesRange().size() - 1;
|
return getMax() > FEE_RATES_RANGE.size() - 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void updateTrackHighlight() {
|
public void updateTrackHighlight() {
|
||||||
|
|
@ -193,9 +137,9 @@ public class FeeRangeSlider extends Slider {
|
||||||
}
|
}
|
||||||
|
|
||||||
private int getPercentageOfFeeRange(Double feeRate) {
|
private int getPercentageOfFeeRange(Double feeRate) {
|
||||||
double index = getValue(feeRate, AppServices.getMinimumRelayFeeRate());
|
double index = Math.log(feeRate) / Math.log(2);
|
||||||
if(isLongFeeRange()) {
|
if(isLongFeeRange()) {
|
||||||
index *= ((double)AppServices.getFeeRatesRange().size() / (AppServices.getLongFeeRatesRange().size())) * 0.99;
|
index *= ((double)FEE_RATES_RANGE.size() / (LONG_FEE_RATES_RANGE.size())) * 0.99;
|
||||||
}
|
}
|
||||||
return (int)Math.round(index * 10.0);
|
return (int)Math.round(index * 10.0);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,6 @@ import com.sparrowwallet.drongo.protocol.ScriptType;
|
||||||
import com.sparrowwallet.drongo.wallet.Keystore;
|
import com.sparrowwallet.drongo.wallet.Keystore;
|
||||||
import com.sparrowwallet.drongo.wallet.KeystoreSource;
|
import com.sparrowwallet.drongo.wallet.KeystoreSource;
|
||||||
import com.sparrowwallet.drongo.wallet.Wallet;
|
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||||
import com.sparrowwallet.drongo.wallet.WalletModel;
|
|
||||||
import com.sparrowwallet.sparrow.AppServices;
|
import com.sparrowwallet.sparrow.AppServices;
|
||||||
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
|
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
|
||||||
import com.sparrowwallet.sparrow.io.FileImport;
|
import com.sparrowwallet.sparrow.io.FileImport;
|
||||||
|
|
@ -45,8 +44,8 @@ public abstract class FileImportPane extends TitledDescriptionPane {
|
||||||
private final boolean fileFormatAvailable;
|
private final boolean fileFormatAvailable;
|
||||||
protected List<Wallet> wallets;
|
protected List<Wallet> wallets;
|
||||||
|
|
||||||
public FileImportPane(FileImport importer, String title, String description, String content, WalletModel walletModel, boolean scannable, boolean fileFormatAvailable) {
|
public FileImportPane(FileImport importer, String title, String description, String content, String imageUrl, boolean scannable, boolean fileFormatAvailable) {
|
||||||
super(title, description, content, walletModel);
|
super(title, description, content, imageUrl);
|
||||||
this.importer = importer;
|
this.importer = importer;
|
||||||
this.scannable = scannable;
|
this.scannable = scannable;
|
||||||
this.fileFormatAvailable = fileFormatAvailable;
|
this.fileFormatAvailable = fileFormatAvailable;
|
||||||
|
|
|
||||||
|
|
@ -37,7 +37,7 @@ public class FileKeystoreExportPane extends TitledDescriptionPane {
|
||||||
private final boolean file;
|
private final boolean file;
|
||||||
|
|
||||||
public FileKeystoreExportPane(Keystore keystore, KeystoreFileExport exporter) {
|
public FileKeystoreExportPane(Keystore keystore, KeystoreFileExport exporter) {
|
||||||
super(exporter.getName(), "Keystore export", exporter.getKeystoreExportDescription(), exporter.getWalletModel());
|
super(exporter.getName(), "Keystore export", exporter.getKeystoreExportDescription(), "image/" + exporter.getWalletModel().getType() + ".png");
|
||||||
this.keystore = keystore;
|
this.keystore = keystore;
|
||||||
this.exporter = exporter;
|
this.exporter = exporter;
|
||||||
this.scannable = exporter.isKeystoreExportScannable();
|
this.scannable = exporter.isKeystoreExportScannable();
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,7 @@ public class FileKeystoreImportPane extends FileImportPane {
|
||||||
private final KeyDerivation requiredDerivation;
|
private final KeyDerivation requiredDerivation;
|
||||||
|
|
||||||
public FileKeystoreImportPane(Wallet wallet, KeystoreFileImport importer, KeyDerivation requiredDerivation) {
|
public FileKeystoreImportPane(Wallet wallet, KeystoreFileImport importer, KeyDerivation requiredDerivation) {
|
||||||
super(importer, importer.getName(), "Key import", importer.getKeystoreImportDescription(getAccount(wallet, requiredDerivation)), importer.getWalletModel(), importer.isKeystoreImportScannable(), importer.isFileFormatAvailable());
|
super(importer, importer.getName(), "Key import", importer.getKeystoreImportDescription(getAccount(wallet, requiredDerivation)), "image/" + importer.getWalletModel().getType() + ".png", importer.isKeystoreImportScannable(), importer.isFileFormatAvailable());
|
||||||
this.wallet = wallet;
|
this.wallet = wallet;
|
||||||
this.importer = importer;
|
this.importer = importer;
|
||||||
this.requiredDerivation = requiredDerivation;
|
this.requiredDerivation = requiredDerivation;
|
||||||
|
|
|
||||||
|
|
@ -41,7 +41,7 @@ public class FileWalletExportPane extends TitledDescriptionPane {
|
||||||
private final boolean file;
|
private final boolean file;
|
||||||
|
|
||||||
public FileWalletExportPane(Wallet wallet, WalletExport exporter) {
|
public FileWalletExportPane(Wallet wallet, WalletExport exporter) {
|
||||||
super(exporter.getName(), "Wallet export", exporter.getWalletExportDescription(), exporter.getWalletModel());
|
super(exporter.getName(), "Wallet export", exporter.getWalletExportDescription(), "image/" + exporter.getWalletModel().getType() + ".png");
|
||||||
this.wallet = wallet;
|
this.wallet = wallet;
|
||||||
this.exporter = exporter;
|
this.exporter = exporter;
|
||||||
this.scannable = exporter.isWalletExportScannable();
|
this.scannable = exporter.isWalletExportScannable();
|
||||||
|
|
@ -168,7 +168,7 @@ public class FileWalletExportPane extends TitledDescriptionPane {
|
||||||
qrDisplayDialog = new QRDisplayDialog(RegistryType.BYTES.toString(), outputStream.toByteArray(), true);
|
qrDisplayDialog = new QRDisplayDialog(RegistryType.BYTES.toString(), outputStream.toByteArray(), true);
|
||||||
} else if(exporter instanceof PassportMultisig || exporter instanceof KeystoneMultisig || exporter instanceof JadeMultisig) {
|
} else if(exporter instanceof PassportMultisig || exporter instanceof KeystoneMultisig || exporter instanceof JadeMultisig) {
|
||||||
qrDisplayDialog = new QRDisplayDialog(RegistryType.BYTES.toString(), outputStream.toByteArray(), false);
|
qrDisplayDialog = new QRDisplayDialog(RegistryType.BYTES.toString(), outputStream.toByteArray(), false);
|
||||||
} else if(exporter instanceof Bip129 || exporter instanceof WalletLabels) {
|
} else if(exporter instanceof Bip129) {
|
||||||
UR ur = UR.fromBytes(outputStream.toByteArray());
|
UR ur = UR.fromBytes(outputStream.toByteArray());
|
||||||
BBQR bbqr = new BBQR(BBQRType.UNICODE, outputStream.toByteArray());
|
BBQR bbqr = new BBQR(BBQRType.UNICODE, outputStream.toByteArray());
|
||||||
qrDisplayDialog = new QRDisplayDialog(ur, bbqr, false, false, false);
|
qrDisplayDialog = new QRDisplayDialog(ur, bbqr, false, false, false);
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@ public class FileWalletImportPane extends FileImportPane {
|
||||||
private final WalletImport importer;
|
private final WalletImport importer;
|
||||||
|
|
||||||
public FileWalletImportPane(WalletImport importer) {
|
public FileWalletImportPane(WalletImport importer) {
|
||||||
super(importer, importer.getName(), "Wallet import", importer.getWalletImportDescription(), importer.getWalletModel(), importer.isWalletImportScannable(), importer.isWalletImportFileFormatAvailable());
|
super(importer, importer.getName(), "Wallet import", importer.getWalletImportDescription(), "image/" + importer.getWalletModel().getType() + ".png", importer.isWalletImportScannable(), importer.isWalletImportFileFormatAvailable());
|
||||||
this.importer = importer;
|
this.importer = importer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -42,7 +42,7 @@ public class FileWalletKeystoreImportPane extends FileImportPane {
|
||||||
private String password;
|
private String password;
|
||||||
|
|
||||||
public FileWalletKeystoreImportPane(KeystoreFileImport importer) {
|
public FileWalletKeystoreImportPane(KeystoreFileImport importer) {
|
||||||
super(importer, importer.getName(), "Wallet import", importer.getKeystoreImportDescription(), importer.getWalletModel(), importer.isKeystoreImportScannable(), importer.isFileFormatAvailable());
|
super(importer, importer.getName(), "Wallet import", importer.getKeystoreImportDescription(), "image/" + importer.getWalletModel().getType() + ".png", importer.isKeystoreImportScannable(), importer.isFileFormatAvailable());
|
||||||
this.importer = importer;
|
this.importer = importer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -38,23 +38,12 @@ class LabelCell extends TextFieldTreeTableCell<Entry, String> implements Confirm
|
||||||
if(empty) {
|
if(empty) {
|
||||||
setText(null);
|
setText(null);
|
||||||
setGraphic(null);
|
setGraphic(null);
|
||||||
setTooltip(null);
|
|
||||||
} else {
|
} else {
|
||||||
Entry entry = getTreeTableView().getTreeItem(getIndex()).getValue();
|
Entry entry = getTreeTableView().getTreeItem(getIndex()).getValue();
|
||||||
EntryCell.applyRowStyles(this, entry);
|
EntryCell.applyRowStyles(this, entry);
|
||||||
|
|
||||||
setText(label);
|
setText(label);
|
||||||
setContextMenu(new LabelContextMenu(entry, label));
|
setContextMenu(new LabelContextMenu(entry, label));
|
||||||
|
|
||||||
double width = label == null || label.length() < 20 ? 0.0 : TextUtils.computeTextWidth(getFont(), label, 0.0D);
|
|
||||||
if(width > getTableColumn().getWidth()) {
|
|
||||||
Tooltip tooltip = new Tooltip(label);
|
|
||||||
tooltip.setMaxWidth(getTreeTableView().getWidth());
|
|
||||||
tooltip.setWrapText(true);
|
|
||||||
setTooltip(tooltip);
|
|
||||||
} else {
|
|
||||||
setTooltip(null);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -132,7 +121,7 @@ class LabelCell extends TextFieldTreeTableCell<Entry, String> implements Confirm
|
||||||
return confirmationsProperty;
|
return confirmationsProperty;
|
||||||
}
|
}
|
||||||
|
|
||||||
private class LabelContextMenu extends ContextMenu {
|
private static class LabelContextMenu extends ContextMenu {
|
||||||
public LabelContextMenu(Entry entry, String label) {
|
public LabelContextMenu(Entry entry, String label) {
|
||||||
MenuItem copyLabel = new MenuItem("Copy Label");
|
MenuItem copyLabel = new MenuItem("Copy Label");
|
||||||
copyLabel.setOnAction(AE -> {
|
copyLabel.setOnAction(AE -> {
|
||||||
|
|
@ -152,13 +141,6 @@ class LabelCell extends TextFieldTreeTableCell<Entry, String> implements Confirm
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
getItems().add(pasteLabel);
|
getItems().add(pasteLabel);
|
||||||
|
|
||||||
MenuItem editLabel = new MenuItem("Edit Label...");
|
|
||||||
editLabel.setOnAction(AE -> {
|
|
||||||
hide();
|
|
||||||
startEdit();
|
|
||||||
});
|
|
||||||
getItems().add(editLabel);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -118,7 +118,14 @@ public class MessageSignDialog extends Dialog<ButtonBar.ButtonData> {
|
||||||
dialogPane.getStylesheets().add(AppServices.class.getResource("dialog.css").toExternalForm());
|
dialogPane.getStylesheets().add(AppServices.class.getResource("dialog.css").toExternalForm());
|
||||||
AppServices.setStageIcon(dialogPane.getScene().getWindow());
|
AppServices.setStageIcon(dialogPane.getScene().getWindow());
|
||||||
dialogPane.setHeaderText(title == null ? (wallet == null ? "Verify Message" : "Sign/Verify Message") : title);
|
dialogPane.setHeaderText(title == null ? (wallet == null ? "Verify Message" : "Sign/Verify Message") : title);
|
||||||
dialogPane.setGraphic(new WalletModelImage(WalletModel.SEED));
|
|
||||||
|
Image image = new Image("image/seed.png", 50, 50, false, false);
|
||||||
|
if (!image.isError()) {
|
||||||
|
ImageView imageView = new ImageView();
|
||||||
|
imageView.setSmooth(false);
|
||||||
|
imageView.setImage(image);
|
||||||
|
dialogPane.setGraphic(imageView);
|
||||||
|
}
|
||||||
|
|
||||||
VBox vBox = new VBox();
|
VBox vBox = new VBox();
|
||||||
vBox.setSpacing(20);
|
vBox.setSpacing(20);
|
||||||
|
|
@ -240,9 +247,6 @@ 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
|
||||||
|
|
@ -276,7 +280,7 @@ public class MessageSignDialog extends Dialog<ButtonBar.ButtonData> {
|
||||||
}
|
}
|
||||||
|
|
||||||
if(wallet != null && walletNode != null) {
|
if(wallet != null && walletNode != null) {
|
||||||
setFormatFromScriptType(getSigningScriptType(walletNode));
|
setFormatFromScriptType(wallet.getScriptType());
|
||||||
} else {
|
} else {
|
||||||
formatGroup.selectToggle(formatElectrum);
|
formatGroup.selectToggle(formatElectrum);
|
||||||
}
|
}
|
||||||
|
|
@ -290,13 +294,9 @@ public class MessageSignDialog extends Dialog<ButtonBar.ButtonData> {
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean canSign(Wallet wallet) {
|
private boolean canSign(Wallet wallet) {
|
||||||
return wallet.getKeystores().getFirst().hasPrivateKey()
|
return wallet.getKeystores().get(0).hasPrivateKey()
|
||||||
|| wallet.getKeystores().getFirst().getSource() == KeystoreSource.HW_USB
|
|| wallet.getKeystores().get(0).getSource() == KeystoreSource.HW_USB
|
||||||
|| wallet.getKeystores().getFirst().getWalletModel().isCard();
|
|| wallet.getKeystores().get(0).getWalletModel().isCard();
|
||||||
}
|
|
||||||
|
|
||||||
private boolean canSignBip322(Wallet wallet) {
|
|
||||||
return wallet.getKeystores().getFirst().hasPrivateKey();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private Address getAddress()throws InvalidAddressException {
|
private Address getAddress()throws InvalidAddressException {
|
||||||
|
|
@ -320,11 +320,6 @@ 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);
|
||||||
|
|
@ -357,7 +352,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().getFirst().hasPrivateKey()) {
|
if(signingWallet.getKeystores().get(0).hasPrivateKey()) {
|
||||||
if(signingWallet.isEncrypted()) {
|
if(signingWallet.isEncrypted()) {
|
||||||
EventManager.get().post(new RequestOpenWalletsEvent());
|
EventManager.get().post(new RequestOpenWalletsEvent());
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -370,7 +365,7 @@ public class MessageSignDialog extends Dialog<ButtonBar.ButtonData> {
|
||||||
|
|
||||||
private void signUnencryptedKeystore(Wallet decryptedWallet) {
|
private void signUnencryptedKeystore(Wallet decryptedWallet) {
|
||||||
try {
|
try {
|
||||||
Keystore keystore = decryptedWallet.getKeystores().getFirst();
|
Keystore keystore = decryptedWallet.getKeystores().get(0);
|
||||||
ECKey privKey = keystore.getKey(walletNode);
|
ECKey privKey = keystore.getKey(walletNode);
|
||||||
String signatureText;
|
String signatureText;
|
||||||
if(isBip322()) {
|
if(isBip322()) {
|
||||||
|
|
@ -390,8 +385,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().getFirst().getKeyDerivation().getMasterFingerprint());
|
List<String> fingerprints = List.of(deviceWallet.getKeystores().get(0).getKeyDerivation().getMasterFingerprint());
|
||||||
KeyDerivation fullDerivation = deviceWallet.getKeystores().getFirst().getKeyDerivation().extend(walletNode.getDerivation());
|
KeyDerivation fullDerivation = deviceWallet.getKeystores().get(0).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();
|
||||||
|
|
|
||||||
|
|
@ -50,7 +50,8 @@ public class MnemonicGridDialog extends Dialog<List<String>> {
|
||||||
dialogPane.getStylesheets().add(AppServices.class.getResource("general.css").toExternalForm());
|
dialogPane.getStylesheets().add(AppServices.class.getResource("general.css").toExternalForm());
|
||||||
dialogPane.getStylesheets().add(AppServices.class.getResource("grid.css").toExternalForm());
|
dialogPane.getStylesheets().add(AppServices.class.getResource("grid.css").toExternalForm());
|
||||||
dialogPane.setHeaderText("Load a Border Wallets PDF, or generate a grid from a BIP39 seed.\nThen select 11 or 23 words in a pattern on the grid.\nThe order of selection is important!");
|
dialogPane.setHeaderText("Load a Border Wallets PDF, or generate a grid from a BIP39 seed.\nThen select 11 or 23 words in a pattern on the grid.\nThe order of selection is important!");
|
||||||
dialogPane.setGraphic(new DialogImage(DialogImage.Type.BORDERWALLETS));
|
javafx.scene.image.Image image = new Image("/image/border-wallets.png");
|
||||||
|
dialogPane.setGraphic(new ImageView(image));
|
||||||
|
|
||||||
String[][] emptyWordGrid = new String[128][GRID_COLUMN_COUNT];
|
String[][] emptyWordGrid = new String[128][GRID_COLUMN_COUNT];
|
||||||
Grid grid = getGrid(emptyWordGrid);
|
Grid grid = getGrid(emptyWordGrid);
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,7 @@ public class MnemonicKeystoreDisplayPane extends MnemonicKeystorePane {
|
||||||
private final DeterministicSeed.Type type;
|
private final DeterministicSeed.Type type;
|
||||||
|
|
||||||
public MnemonicKeystoreDisplayPane(Keystore keystore) {
|
public MnemonicKeystoreDisplayPane(Keystore keystore) {
|
||||||
super(keystore.getSeed().getType().getName(), keystore.getSeed().needsPassphrase() && (keystore.getSeed().getPassphrase() == null || keystore.getSeed().getPassphrase().length() > 0) ? "Passphrase entered" : "No passphrase", "", WalletModel.SEED);
|
super(keystore.getSeed().getType().getName(), keystore.getSeed().needsPassphrase() && (keystore.getSeed().getPassphrase() == null || keystore.getSeed().getPassphrase().length() > 0) ? "Passphrase entered" : "No passphrase", "", "image/" + WalletModel.SEED.getType() + ".png");
|
||||||
showHideLink.setVisible(false);
|
showHideLink.setVisible(false);
|
||||||
buttonBox.getChildren().clear();
|
buttonBox.getChildren().clear();
|
||||||
this.type = keystore.getSeed().getType();
|
this.type = keystore.getSeed().getType();
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,7 @@ public class MnemonicKeystoreEntryPane extends MnemonicKeystorePane {
|
||||||
private boolean generated;
|
private boolean generated;
|
||||||
|
|
||||||
public MnemonicKeystoreEntryPane(String name, int numWords) {
|
public MnemonicKeystoreEntryPane(String name, int numWords) {
|
||||||
super(name, "Enter seed words", "", WalletModel.SEED);
|
super(name, "Enter seed words", "", "image/" + WalletModel.SEED.getType() + ".png");
|
||||||
showHideLink.setVisible(false);
|
showHideLink.setVisible(false);
|
||||||
buttonBox.getChildren().clear();
|
buttonBox.getChildren().clear();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -45,7 +45,7 @@ public class MnemonicKeystoreImportPane extends MnemonicKeystorePane {
|
||||||
private List<String> generatedMnemonicCode;
|
private List<String> generatedMnemonicCode;
|
||||||
|
|
||||||
public MnemonicKeystoreImportPane(Wallet wallet, KeystoreMnemonicImport importer, KeyDerivation defaultDerivation) {
|
public MnemonicKeystoreImportPane(Wallet wallet, KeystoreMnemonicImport importer, KeyDerivation defaultDerivation) {
|
||||||
super(importer.getName(), "Create or enter seed", importer.getKeystoreImportDescription(), importer.getWalletModel());
|
super(importer.getName(), "Create or enter seed", importer.getKeystoreImportDescription(), "image/" + importer.getWalletModel().getType() + ".png");
|
||||||
this.wallet = wallet;
|
this.wallet = wallet;
|
||||||
this.importer = importer;
|
this.importer = importer;
|
||||||
this.defaultDerivation = defaultDerivation;
|
this.defaultDerivation = defaultDerivation;
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,6 @@ package com.sparrowwallet.sparrow.control;
|
||||||
import com.sparrowwallet.drongo.wallet.Bip39MnemonicCode;
|
import com.sparrowwallet.drongo.wallet.Bip39MnemonicCode;
|
||||||
import com.sparrowwallet.drongo.wallet.DeterministicSeed;
|
import com.sparrowwallet.drongo.wallet.DeterministicSeed;
|
||||||
import com.sparrowwallet.drongo.wallet.MnemonicException;
|
import com.sparrowwallet.drongo.wallet.MnemonicException;
|
||||||
import com.sparrowwallet.drongo.wallet.WalletModel;
|
|
||||||
import com.sparrowwallet.drongo.wallet.slip39.Slip39MnemonicCode;
|
import com.sparrowwallet.drongo.wallet.slip39.Slip39MnemonicCode;
|
||||||
import com.sparrowwallet.sparrow.AppServices;
|
import com.sparrowwallet.sparrow.AppServices;
|
||||||
import com.sparrowwallet.sparrow.glyphfont.GlyphUtils;
|
import com.sparrowwallet.sparrow.glyphfont.GlyphUtils;
|
||||||
|
|
@ -52,8 +51,8 @@ public class MnemonicKeystorePane extends TitledDescriptionPane {
|
||||||
protected final SimpleStringProperty passphraseProperty = new SimpleStringProperty("");
|
protected final SimpleStringProperty passphraseProperty = new SimpleStringProperty("");
|
||||||
protected IntegerProperty defaultWordSizeProperty;
|
protected IntegerProperty defaultWordSizeProperty;
|
||||||
|
|
||||||
public MnemonicKeystorePane(String title, String description, String content, WalletModel walletModel) {
|
public MnemonicKeystorePane(String title, String description, String content, String imageUrl) {
|
||||||
super(title, description, content, walletModel);
|
super(title, description, content, imageUrl);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
@ -321,7 +320,6 @@ public class MnemonicKeystorePane extends TitledDescriptionPane {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
wordField.setMaxWidth(100);
|
wordField.setMaxWidth(100);
|
||||||
wordField.setAccessibleText("Word " + (wordNumber + 1));
|
|
||||||
TextFormatter<?> formatter = new TextFormatter<>((TextFormatter.Change change) -> {
|
TextFormatter<?> formatter = new TextFormatter<>((TextFormatter.Change change) -> {
|
||||||
String text = change.getText();
|
String text = change.getText();
|
||||||
// if text was added, fix the text to fit the requirements
|
// if text was added, fix the text to fit the requirements
|
||||||
|
|
|
||||||
|
|
@ -44,7 +44,7 @@ public class MnemonicShareKeystoreImportPane extends MnemonicKeystorePane {
|
||||||
private int currentShare;
|
private int currentShare;
|
||||||
|
|
||||||
public MnemonicShareKeystoreImportPane(Wallet wallet, KeystoreMnemonicShareImport importer, KeyDerivation defaultDerivation) {
|
public MnemonicShareKeystoreImportPane(Wallet wallet, KeystoreMnemonicShareImport importer, KeyDerivation defaultDerivation) {
|
||||||
super(importer.getName(), "Enter seed share", importer.getKeystoreImportDescription(), importer.getWalletModel());
|
super(importer.getName(), "Enter seed share", importer.getKeystoreImportDescription(), "image/" + importer.getWalletModel().getType() + ".png");
|
||||||
this.wallet = wallet;
|
this.wallet = wallet;
|
||||||
this.importer = importer;
|
this.importer = importer;
|
||||||
this.defaultDerivation = defaultDerivation;
|
this.defaultDerivation = defaultDerivation;
|
||||||
|
|
|
||||||
|
|
@ -41,7 +41,7 @@ public class MnemonicWalletKeystoreImportPane extends MnemonicKeystorePane {
|
||||||
private Button importButton;
|
private Button importButton;
|
||||||
|
|
||||||
public MnemonicWalletKeystoreImportPane(KeystoreMnemonicImport importer) {
|
public MnemonicWalletKeystoreImportPane(KeystoreMnemonicImport importer) {
|
||||||
super(importer.getName(), "Seed import", importer.getKeystoreImportDescription(), importer.getWalletModel());
|
super(importer.getName(), "Seed import", importer.getKeystoreImportDescription(), "image/" + importer.getWalletModel().getType() + ".png");
|
||||||
this.importer = importer;
|
this.importer = importer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -55,7 +55,7 @@ public class MnemonicWalletKeystoreImportPane extends MnemonicKeystorePane {
|
||||||
protected List<Node> createRightButtons() {
|
protected List<Node> createRightButtons() {
|
||||||
discoverButton = new Button("Discover Wallet");
|
discoverButton = new Button("Discover Wallet");
|
||||||
discoverButton.setDisable(true);
|
discoverButton.setDisable(true);
|
||||||
discoverButton.setDefaultButton(AppServices.onlineProperty().get());
|
discoverButton.setDefaultButton(true);
|
||||||
discoverButton.managedProperty().bind(discoverButton.visibleProperty());
|
discoverButton.managedProperty().bind(discoverButton.visibleProperty());
|
||||||
discoverButton.setOnAction(event -> {
|
discoverButton.setOnAction(event -> {
|
||||||
discoverWallet();
|
discoverWallet();
|
||||||
|
|
@ -66,7 +66,6 @@ public class MnemonicWalletKeystoreImportPane extends MnemonicKeystorePane {
|
||||||
|
|
||||||
importButton = new Button("Import Wallet");
|
importButton = new Button("Import Wallet");
|
||||||
importButton.setDisable(true);
|
importButton.setDisable(true);
|
||||||
importButton.setDefaultButton(!AppServices.onlineProperty().get());
|
|
||||||
importButton.managedProperty().bind(importButton.visibleProperty());
|
importButton.managedProperty().bind(importButton.visibleProperty());
|
||||||
importButton.visibleProperty().bind(discoverButton.visibleProperty().not());
|
importButton.visibleProperty().bind(discoverButton.visibleProperty().not());
|
||||||
importButton.setOnAction(event -> {
|
importButton.setOnAction(event -> {
|
||||||
|
|
@ -197,7 +196,6 @@ public class MnemonicWalletKeystoreImportPane extends MnemonicKeystorePane {
|
||||||
HBox.setHgrow(region, Priority.SOMETIMES);
|
HBox.setHgrow(region, Priority.SOMETIMES);
|
||||||
|
|
||||||
Button importMnemonicButton = new Button("Import");
|
Button importMnemonicButton = new Button("Import");
|
||||||
importMnemonicButton.setDefaultButton(true);
|
|
||||||
importMnemonicButton.setOnAction(event -> {
|
importMnemonicButton.setOnAction(event -> {
|
||||||
showHideLink.setVisible(true);
|
showHideLink.setVisible(true);
|
||||||
setExpanded(false);
|
setExpanded(false);
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,6 @@ 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.wallet.Wallet;
|
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||||
import com.sparrowwallet.drongo.wallet.WalletModel;
|
|
||||||
import com.sparrowwallet.sparrow.AppServices;
|
import com.sparrowwallet.sparrow.AppServices;
|
||||||
import com.sparrowwallet.sparrow.EventManager;
|
import com.sparrowwallet.sparrow.EventManager;
|
||||||
import com.sparrowwallet.sparrow.UnitFormat;
|
import com.sparrowwallet.sparrow.UnitFormat;
|
||||||
|
|
@ -62,7 +61,6 @@ public class PrivateKeySweepDialog extends Dialog<Transaction> {
|
||||||
private final TextArea key;
|
private final TextArea key;
|
||||||
private final ComboBox<ScriptType> keyScriptType;
|
private final ComboBox<ScriptType> keyScriptType;
|
||||||
private final CopyableLabel keyAddress;
|
private final CopyableLabel keyAddress;
|
||||||
private final CopyableLabel keyUtxos;
|
|
||||||
private final ComboBoxTextField toAddress;
|
private final ComboBoxTextField toAddress;
|
||||||
private final ComboBox<Wallet> toWallet;
|
private final ComboBox<Wallet> toWallet;
|
||||||
private final FeeRangeSlider feeRange;
|
private final FeeRangeSlider feeRange;
|
||||||
|
|
@ -74,7 +72,14 @@ public class PrivateKeySweepDialog extends Dialog<Transaction> {
|
||||||
dialogPane.getStylesheets().add(AppServices.class.getResource("dialog.css").toExternalForm());
|
dialogPane.getStylesheets().add(AppServices.class.getResource("dialog.css").toExternalForm());
|
||||||
AppServices.setStageIcon(dialogPane.getScene().getWindow());
|
AppServices.setStageIcon(dialogPane.getScene().getWindow());
|
||||||
dialogPane.setHeaderText("Sweep Private Key");
|
dialogPane.setHeaderText("Sweep Private Key");
|
||||||
dialogPane.setGraphic(new WalletModelImage(WalletModel.SEED));
|
|
||||||
|
Image image = new Image("image/seed.png", 50, 50, false, false);
|
||||||
|
if(!image.isError()) {
|
||||||
|
ImageView imageView = new ImageView();
|
||||||
|
imageView.setSmooth(false);
|
||||||
|
imageView.setImage(image);
|
||||||
|
dialogPane.setGraphic(imageView);
|
||||||
|
}
|
||||||
|
|
||||||
Form form = new Form();
|
Form form = new Form();
|
||||||
Fieldset fieldset = new Fieldset();
|
Fieldset fieldset = new Fieldset();
|
||||||
|
|
@ -131,12 +136,6 @@ public class PrivateKeySweepDialog extends Dialog<Transaction> {
|
||||||
keyAddress.getStyleClass().add("fixed-width");
|
keyAddress.getStyleClass().add("fixed-width");
|
||||||
addressField.getInputs().add(keyAddress);
|
addressField.getInputs().add(keyAddress);
|
||||||
|
|
||||||
Field utxosField = new Field();
|
|
||||||
utxosField.setText("UTXOs:");
|
|
||||||
keyUtxos = new CopyableLabel();
|
|
||||||
utxosField.getInputs().add(keyUtxos);
|
|
||||||
|
|
||||||
|
|
||||||
Field toAddressField = new Field();
|
Field toAddressField = new Field();
|
||||||
toAddressField.setText("Sweep to:");
|
toAddressField.setText("Sweep to:");
|
||||||
toAddress = new ComboBoxTextField();
|
toAddress = new ComboBoxTextField();
|
||||||
|
|
@ -356,8 +355,6 @@ public class PrivateKeySweepDialog extends Dialog<Transaction> {
|
||||||
Optional<Date> optSince = addressScanDateDialog.showAndWait();
|
Optional<Date> optSince = addressScanDateDialog.showAndWait();
|
||||||
if(optSince.isPresent()) {
|
if(optSince.isPresent()) {
|
||||||
since = optSince.get();
|
since = optSince.get();
|
||||||
} else {
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -372,7 +369,7 @@ public class PrivateKeySweepDialog extends Dialog<Transaction> {
|
||||||
});
|
});
|
||||||
|
|
||||||
if(Config.get().getServerType() == ServerType.BITCOIN_CORE) {
|
if(Config.get().getServerType() == ServerType.BITCOIN_CORE) {
|
||||||
ServiceProgressDialog serviceProgressDialog = new ServiceProgressDialog("Address Scan", "Scanning address for transactions...", new DialogImage(DialogImage.Type.SPARROW), addressUtxosService);
|
ServiceProgressDialog serviceProgressDialog = new ServiceProgressDialog("Address Scan", "Scanning address for transactions...", "/image/sparrow.png", addressUtxosService);
|
||||||
serviceProgressDialog.initOwner(getDialogPane().getScene().getWindow());
|
serviceProgressDialog.initOwner(getDialogPane().getScene().getWindow());
|
||||||
AppServices.moveToActiveWindowScreen(serviceProgressDialog);
|
AppServices.moveToActiveWindowScreen(serviceProgressDialog);
|
||||||
}
|
}
|
||||||
|
|
@ -398,14 +395,14 @@ public class PrivateKeySweepDialog extends Dialog<Transaction> {
|
||||||
|
|
||||||
double feeRate = feeRange.getFeeRate();
|
double feeRate = feeRange.getFeeRate();
|
||||||
long fee = (long)Math.ceil(noFeeTransaction.getVirtualSize() * feeRate);
|
long fee = (long)Math.ceil(noFeeTransaction.getVirtualSize() * feeRate);
|
||||||
if(feeRate == AppServices.getMinimumRelayFeeRate() && feeRate > 0d) {
|
if(feeRate == Transaction.DEFAULT_MIN_RELAY_FEE) {
|
||||||
fee++;
|
fee++;
|
||||||
}
|
}
|
||||||
|
|
||||||
long dustThreshold = destAddress.getScriptType().getDustThreshold(sweepOutput, Transaction.DUST_RELAY_TX_FEE);
|
long dustThreshold = destAddress.getScriptType().getDustThreshold(sweepOutput, Transaction.DUST_RELAY_TX_FEE);
|
||||||
if(total - fee <= dustThreshold) {
|
if(total - fee <= dustThreshold) {
|
||||||
feeRate = AppServices.getMinimumRelayFeeRate();
|
feeRate = Transaction.DEFAULT_MIN_RELAY_FEE;
|
||||||
fee = (long)Math.ceil(noFeeTransaction.getVirtualSize() * feeRate) + (feeRate > 0d ? 1 : 0);
|
fee = (long)Math.ceil(noFeeTransaction.getVirtualSize() * feeRate) + 1;
|
||||||
|
|
||||||
if(total - fee <= dustThreshold) {
|
if(total - fee <= dustThreshold) {
|
||||||
AppServices.showErrorDialog("Insufficient funds", "The unspent outputs for this private key contain insufficient funds to spend (" + total + " sats).");
|
AppServices.showErrorDialog("Insufficient funds", "The unspent outputs for this private key contain insufficient funds to spend (" + total + " sats).");
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
package com.sparrowwallet.sparrow.control;
|
package com.sparrowwallet.sparrow.control;
|
||||||
|
|
||||||
import com.google.common.base.Throwables;
|
import com.github.sarxos.webcam.*;
|
||||||
import com.sparrowwallet.drongo.*;
|
import com.sparrowwallet.drongo.*;
|
||||||
import com.sparrowwallet.drongo.address.Address;
|
import com.sparrowwallet.drongo.address.Address;
|
||||||
import com.sparrowwallet.drongo.address.P2PKHAddress;
|
import com.sparrowwallet.drongo.address.P2PKHAddress;
|
||||||
|
|
@ -27,6 +27,7 @@ import com.sparrowwallet.hummingbird.registry.pathcomponent.PathComponent;
|
||||||
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.WebcamResolutionChangedEvent;
|
import com.sparrowwallet.sparrow.event.WebcamResolutionChangedEvent;
|
||||||
|
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
|
||||||
import com.sparrowwallet.sparrow.io.Config;
|
import com.sparrowwallet.sparrow.io.Config;
|
||||||
import com.sparrowwallet.sparrow.io.bbqr.BBQRDecoder;
|
import com.sparrowwallet.sparrow.io.bbqr.BBQRDecoder;
|
||||||
import com.sparrowwallet.sparrow.io.bbqr.BBQRException;
|
import com.sparrowwallet.sparrow.io.bbqr.BBQRException;
|
||||||
|
|
@ -38,16 +39,14 @@ import javafx.beans.property.SimpleDoubleProperty;
|
||||||
import javafx.beans.property.SimpleObjectProperty;
|
import javafx.beans.property.SimpleObjectProperty;
|
||||||
import javafx.beans.value.ChangeListener;
|
import javafx.beans.value.ChangeListener;
|
||||||
import javafx.beans.value.ObservableValue;
|
import javafx.beans.value.ObservableValue;
|
||||||
import javafx.collections.FXCollections;
|
|
||||||
import javafx.collections.ObservableList;
|
|
||||||
import javafx.geometry.Insets;
|
import javafx.geometry.Insets;
|
||||||
import javafx.scene.Node;
|
import javafx.scene.Node;
|
||||||
import javafx.scene.control.*;
|
import javafx.scene.control.*;
|
||||||
import javafx.scene.layout.*;
|
import javafx.scene.layout.*;
|
||||||
import javafx.util.Duration;
|
import javafx.util.Duration;
|
||||||
import javafx.util.StringConverter;
|
import javafx.util.StringConverter;
|
||||||
|
import org.controlsfx.glyphfont.Glyph;
|
||||||
import org.controlsfx.tools.Borders;
|
import org.controlsfx.tools.Borders;
|
||||||
import org.openpnp.capture.CaptureDevice;
|
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
|
@ -77,141 +76,108 @@ public class QRScanDialog extends Dialog<QRScanDialog.Result> {
|
||||||
private static final Pattern PART_PATTERN = Pattern.compile("p(\\d+)of(\\d+) (.+)");
|
private static final Pattern PART_PATTERN = Pattern.compile("p(\\d+)of(\\d+) (.+)");
|
||||||
|
|
||||||
private static final int SCAN_PERIOD_MILLIS = 100;
|
private static final int SCAN_PERIOD_MILLIS = 100;
|
||||||
private final ObjectProperty<CaptureDevice> webcamDeviceProperty = new SimpleObjectProperty<>();
|
private final ObjectProperty<WebcamResolution> webcamResolutionProperty = new SimpleObjectProperty<>(WebcamResolution.VGA);
|
||||||
private final ObjectProperty<WebcamResolution> webcamResolutionProperty = new SimpleObjectProperty<>(WebcamResolution.HD);
|
|
||||||
|
|
||||||
private final DoubleProperty percentComplete = new SimpleDoubleProperty(0.0);
|
private final DoubleProperty percentComplete = new SimpleDoubleProperty(0.0);
|
||||||
|
|
||||||
private final ObservableList<CaptureDevice> foundDevices = FXCollections.observableList(new ArrayList<>());
|
private final ObjectProperty<WebcamDevice> webcamDeviceProperty = new SimpleObjectProperty<>();
|
||||||
private final ObservableList<WebcamResolution> availableResolutions = FXCollections.observableList(new ArrayList<>());
|
|
||||||
private boolean postOpenUpdate;
|
|
||||||
|
|
||||||
public QRScanDialog() {
|
public QRScanDialog() {
|
||||||
this.urDecoder = new URDecoder();
|
this.urDecoder = new URDecoder();
|
||||||
this.legacyUrDecoder = new LegacyURDecoder();
|
this.legacyUrDecoder = new LegacyURDecoder();
|
||||||
this.bbqrDecoder = new BBQRDecoder();
|
this.bbqrDecoder = new BBQRDecoder();
|
||||||
|
|
||||||
if(Config.get().getWebcamResolution() != null) {
|
if(Config.get().isHdCapture()) {
|
||||||
webcamResolutionProperty.set(Config.get().getWebcamResolution());
|
webcamResolutionProperty.set(WebcamResolution.HD);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.webcamService = new WebcamService(webcamResolutionProperty.get(), null);
|
this.webcamService = new WebcamService(webcamResolutionProperty.get(), null, new QRScanListener(), new ScanDelayCalculator());
|
||||||
webcamService.setPeriod(Duration.millis(SCAN_PERIOD_MILLIS));
|
webcamService.setPeriod(Duration.millis(SCAN_PERIOD_MILLIS));
|
||||||
webcamService.setRestartOnFailure(false);
|
webcamService.setRestartOnFailure(false);
|
||||||
|
WebcamView webcamView = new WebcamView(webcamService, Config.get().isMirrorCapture());
|
||||||
|
|
||||||
final DialogPane dialogPane = new QRScanDialogPane();
|
final DialogPane dialogPane = new QRScanDialogPane();
|
||||||
setDialogPane(dialogPane);
|
setDialogPane(dialogPane);
|
||||||
AppServices.setStageIcon(dialogPane.getScene().getWindow());
|
AppServices.setStageIcon(dialogPane.getScene().getWindow());
|
||||||
|
|
||||||
WebcamView webcamView = new WebcamView(webcamService, Config.get().isMirrorCapture());
|
StackPane stackPane = new StackPane();
|
||||||
|
stackPane.getChildren().add(webcamView.getView());
|
||||||
|
Node wrappedView = Borders.wrap(stackPane).lineBorder().buildAll();
|
||||||
|
|
||||||
ProgressBar progressBar = new ProgressBar();
|
ProgressBar progressBar = new ProgressBar();
|
||||||
progressBar.setMinHeight(20);
|
progressBar.setMinHeight(20);
|
||||||
progressBar.setPadding(new Insets(0, 10, 0, 10));
|
progressBar.setPadding(new Insets(0, 10, 0, 10));
|
||||||
progressBar.setPrefWidth(Integer.MAX_VALUE);
|
progressBar.setPrefWidth(Integer.MAX_VALUE);
|
||||||
progressBar.progressProperty().bind(percentComplete);
|
progressBar.progressProperty().bind(percentComplete);
|
||||||
|
webcamService.openingProperty().addListener((observable, oldValue, newValue) -> {
|
||||||
|
if(percentComplete.get() <= 0.0) {
|
||||||
|
Platform.runLater(() -> percentComplete.set(newValue ? 0.0 : -1.0));
|
||||||
|
}
|
||||||
|
Platform.runLater(() -> {
|
||||||
|
if(Config.get().getWebcamDevice() != null && webcamDeviceProperty.get() == null) {
|
||||||
|
for(WebcamDevice device : WebcamScanDriver.getFoundDevices()) {
|
||||||
|
if(device.getName().equals(Config.get().getWebcamDevice())) {
|
||||||
|
webcamDeviceProperty.set(device);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
VBox vBox = new VBox(20);
|
VBox vBox = new VBox(20);
|
||||||
StackPane stackPane = new StackPane();
|
|
||||||
stackPane.getChildren().add(webcamView.getView());
|
|
||||||
Node wrappedView = Borders.wrap(stackPane).lineBorder().buildAll();
|
|
||||||
vBox.getChildren().addAll(wrappedView, progressBar);
|
vBox.getChildren().addAll(wrappedView, progressBar);
|
||||||
|
|
||||||
dialogPane.setContent(vBox);
|
dialogPane.setContent(vBox);
|
||||||
|
|
||||||
webcamService.openingProperty().addListener((_, _, opening) -> {
|
|
||||||
if(percentComplete.get() <= 0.0) {
|
|
||||||
Platform.runLater(() -> percentComplete.set(opening ? 0.0 : -1.0));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
webcamService.openedProperty().addListener((_, _, opened) -> {
|
|
||||||
if(opened) {
|
|
||||||
Platform.runLater(() -> {
|
|
||||||
try {
|
|
||||||
postOpenUpdate = true;
|
|
||||||
List<CaptureDevice> newDevices = new ArrayList<>(webcamService.getAvailableDevices());
|
|
||||||
newDevices.removeAll(foundDevices);
|
|
||||||
foundDevices.addAll(newDevices);
|
|
||||||
foundDevices.removeIf(device -> !webcamService.getDevices().contains(device));
|
|
||||||
|
|
||||||
if(webcamService.getDevice() != null) {
|
|
||||||
for(CaptureDevice device : foundDevices) {
|
|
||||||
if(device.equals(webcamService.getDevice())) {
|
|
||||||
webcamDeviceProperty.set(device);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
updateList(availableResolutions, webcamService.getResolutions());
|
|
||||||
webcamResolutionProperty.set(webcamService.getResolution());
|
|
||||||
} finally {
|
|
||||||
postOpenUpdate = false;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} else if(webcamResolutionProperty.get() != null) {
|
|
||||||
webcamService.setResolution(webcamResolutionProperty.get());
|
|
||||||
webcamService.setDevice(webcamDeviceProperty.get());
|
|
||||||
Platform.runLater(() -> {
|
|
||||||
if(!webcamService.isRunning()) {
|
|
||||||
webcamService.reset();
|
|
||||||
webcamService.start();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
webcamService.resultProperty().addListener(new QRResultListener());
|
webcamService.resultProperty().addListener(new QRResultListener());
|
||||||
webcamService.setOnFailed(failedEvent -> {
|
webcamService.setOnFailed(failedEvent -> {
|
||||||
Throwable exception = Throwables.getRootCause(failedEvent.getSource().getException());
|
Throwable exception = failedEvent.getSource().getException();
|
||||||
Platform.runLater(() -> setResult(new Result(exception)));
|
|
||||||
|
Throwable nested = exception;
|
||||||
|
while(nested.getCause() != null) {
|
||||||
|
nested = nested.getCause();
|
||||||
|
}
|
||||||
|
if(OsType.getCurrent() == OsType.WINDOWS &&
|
||||||
|
nested.getMessage().startsWith("Library 'OpenIMAJGrabber' was not loaded successfully from file")) {
|
||||||
|
exception = new WebcamDependencyException("Your system is missing a dependency required for the webcam. Follow the link below for more details.\n\n[https://sparrowwallet.com/docs/faq.html#your-system-is-missing-a-dependency-for-the-webcam]", exception);
|
||||||
|
} else if(nested.getMessage().startsWith("Cannot start native grabber") && Config.get().getWebcamDevice() != null) {
|
||||||
|
exception = new WebcamOpenException("Cannot open configured webcam " + Config.get().getWebcamDevice() + ", reverting to the default webcam");
|
||||||
|
Config.get().setWebcamDevice(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
final Throwable result = exception;
|
||||||
|
Platform.runLater(() -> setResult(new Result(result)));
|
||||||
});
|
});
|
||||||
webcamService.start();
|
webcamService.start();
|
||||||
|
webcamResolutionProperty.addListener((observable, oldValue, newResolution) -> {
|
||||||
webcamResolutionProperty.addListener((_, oldResolution, newResolution) -> {
|
|
||||||
if(newResolution != null) {
|
if(newResolution != null) {
|
||||||
if(newResolution.isStandardAspect() && oldResolution.isWidescreenAspect()) {
|
setHeight(newResolution == WebcamResolution.HD ? (getHeight() - 100) : (getHeight() + 100));
|
||||||
setWidth(getWidth());
|
EventManager.get().post(new WebcamResolutionChangedEvent(newResolution == WebcamResolution.HD));
|
||||||
setHeight(getHeight() + 100);
|
|
||||||
dialogPane.setMaxHeight(dialogPane.getPrefHeight() + 100);
|
|
||||||
dialogPane.setPrefHeight(dialogPane.getMaxHeight());
|
|
||||||
dialogPane.setMinHeight(dialogPane.getMaxHeight());
|
|
||||||
} else if(newResolution.isWidescreenAspect() && oldResolution.isStandardAspect()) {
|
|
||||||
setWidth(getWidth());
|
|
||||||
setHeight(getHeight() - 100);
|
|
||||||
dialogPane.setMaxHeight(dialogPane.getPrefHeight() - 100);
|
|
||||||
dialogPane.setPrefHeight(dialogPane.getMaxHeight());
|
|
||||||
dialogPane.setMinHeight(dialogPane.getMaxHeight());
|
|
||||||
}
|
|
||||||
EventManager.get().post(new WebcamResolutionChangedEvent(newResolution));
|
|
||||||
}
|
|
||||||
if(newResolution == null || !postOpenUpdate) {
|
|
||||||
webcamService.cancel();
|
|
||||||
}
|
}
|
||||||
|
webcamService.cancel();
|
||||||
});
|
});
|
||||||
webcamDeviceProperty.addListener((_, _, newValue) -> {
|
webcamDeviceProperty.addListener((observable, oldValue, 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();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
setOnCloseRequest(_ -> {
|
setOnCloseRequest(event -> {
|
||||||
if(webcamResolutionProperty.get() != null) {
|
boolean isHdCapture = (webcamResolutionProperty.get() == WebcamResolution.HD);
|
||||||
Config.get().setWebcamResolution(webcamResolutionProperty.get());
|
if(Config.get().isHdCapture() != isHdCapture) {
|
||||||
|
Config.get().setHdCapture(isHdCapture);
|
||||||
}
|
}
|
||||||
|
|
||||||
Platform.runLater(() -> {
|
Platform.runLater(() -> webcamResolutionProperty.set(null));
|
||||||
webcamResolutionProperty.set(null);
|
|
||||||
webcamService.close();
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
final ButtonType cancelButtonType = new javafx.scene.control.ButtonType("Close", ButtonBar.ButtonData.CANCEL_CLOSE);
|
final ButtonType cancelButtonType = new javafx.scene.control.ButtonType("Close", ButtonBar.ButtonData.CANCEL_CLOSE);
|
||||||
final ButtonType deviceButtonType = new javafx.scene.control.ButtonType("Default Camera", ButtonBar.ButtonData.LEFT);
|
final ButtonType hdButtonType = new javafx.scene.control.ButtonType("Use HD Capture", ButtonBar.ButtonData.LEFT);
|
||||||
final ButtonType resolutionButtonType = new javafx.scene.control.ButtonType("Resolution", ButtonBar.ButtonData.HELP_2);
|
final ButtonType camButtonType = new javafx.scene.control.ButtonType("Default Camera", ButtonBar.ButtonData.HELP_2);
|
||||||
dialogPane.getButtonTypes().addAll(deviceButtonType, resolutionButtonType, cancelButtonType);
|
dialogPane.getButtonTypes().addAll(hdButtonType, camButtonType, cancelButtonType);
|
||||||
dialogPane.setPrefWidth(646);
|
dialogPane.setPrefWidth(646);
|
||||||
dialogPane.setPrefHeight(webcamResolutionProperty.get().isWidescreenAspect() ? 490 : 590);
|
dialogPane.setPrefHeight(webcamResolutionProperty.get() == WebcamResolution.HD ? 490 : 590);
|
||||||
dialogPane.setMinHeight(dialogPane.getPrefHeight());
|
dialogPane.setMinHeight(dialogPane.getPrefHeight());
|
||||||
AppServices.moveToActiveWindowScreen(this);
|
AppServices.moveToActiveWindowScreen(this);
|
||||||
|
|
||||||
|
|
@ -719,32 +685,72 @@ public class QRScanDialog extends Dialog<QRScanDialog.Result> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private class QRScanListener implements WebcamListener {
|
||||||
|
@Override
|
||||||
|
public void webcamOpen(WebcamEvent webcamEvent) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void webcamClosed(WebcamEvent webcamEvent) {
|
||||||
|
if(webcamResolutionProperty.get() != null) {
|
||||||
|
webcamService.setResolution(webcamResolutionProperty.get());
|
||||||
|
webcamService.setDevice(webcamDeviceProperty.get());
|
||||||
|
Platform.runLater(() -> {
|
||||||
|
if(!webcamService.isRunning()) {
|
||||||
|
webcamService.reset();
|
||||||
|
webcamService.start();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void webcamDisposed(WebcamEvent webcamEvent) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void webcamImageObtained(WebcamEvent webcamEvent) {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private class QRScanDialogPane extends DialogPane {
|
private class QRScanDialogPane extends DialogPane {
|
||||||
@Override
|
@Override
|
||||||
protected Node createButton(ButtonType buttonType) {
|
protected Node createButton(ButtonType buttonType) {
|
||||||
Node button;
|
Node button = null;
|
||||||
if(buttonType.getButtonData() == ButtonBar.ButtonData.LEFT) {
|
if(buttonType.getButtonData() == ButtonBar.ButtonData.LEFT) {
|
||||||
ComboBox<CaptureDevice> devicesCombo = new ComboBox<>(foundDevices);
|
ToggleButton hd = new ToggleButton(buttonType.getText());
|
||||||
|
hd.setSelected(webcamResolutionProperty.get() == WebcamResolution.HD);
|
||||||
|
hd.setGraphicTextGap(5);
|
||||||
|
setHdGraphic(hd, hd.isSelected());
|
||||||
|
|
||||||
|
final ButtonBar.ButtonData buttonData = buttonType.getButtonData();
|
||||||
|
ButtonBar.setButtonData(hd, buttonData);
|
||||||
|
hd.selectedProperty().addListener((observable, oldValue, newValue) -> {
|
||||||
|
webcamResolutionProperty.set(newValue ? WebcamResolution.HD : WebcamResolution.VGA);
|
||||||
|
setHdGraphic(hd, newValue);
|
||||||
|
});
|
||||||
|
|
||||||
|
button = hd;
|
||||||
|
} else if(buttonType.getButtonData() == ButtonBar.ButtonData.HELP_2) {
|
||||||
|
ComboBox<WebcamDevice> devicesCombo = new ComboBox<>(WebcamScanDriver.getFoundDevices());
|
||||||
devicesCombo.setConverter(new StringConverter<>() {
|
devicesCombo.setConverter(new StringConverter<>() {
|
||||||
@Override
|
@Override
|
||||||
public String toString(CaptureDevice device) {
|
public String toString(WebcamDevice device) {
|
||||||
return device != null && device.getName() != null ? device.getName().replaceAll(" \\(.*\\)", "") : "Default Camera";
|
return device instanceof WebcamScanDevice ? ((WebcamScanDevice)device).getDeviceName() : "Default Camera";
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public CaptureDevice fromString(String string) {
|
public WebcamDevice fromString(String string) {
|
||||||
throw new UnsupportedOperationException();
|
throw new UnsupportedOperationException();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
devicesCombo.valueProperty().bindBidirectional(webcamDeviceProperty);
|
devicesCombo.valueProperty().bindBidirectional(webcamDeviceProperty);
|
||||||
final ButtonBar.ButtonData buttonData = buttonType.getButtonData();
|
ButtonBar.setButtonData(devicesCombo, ButtonBar.ButtonData.LEFT);
|
||||||
ButtonBar.setButtonData(devicesCombo, buttonData);
|
|
||||||
button = devicesCombo;
|
button = devicesCombo;
|
||||||
} else if(buttonType.getButtonData() == ButtonBar.ButtonData.HELP_2) {
|
|
||||||
ComboBox<WebcamResolution> resolutionsCombo = new ComboBox<>(availableResolutions);
|
|
||||||
resolutionsCombo.valueProperty().bindBidirectional(webcamResolutionProperty);
|
|
||||||
ButtonBar.setButtonData(resolutionsCombo, ButtonBar.ButtonData.LEFT);
|
|
||||||
button = resolutionsCombo;
|
|
||||||
} else {
|
} else {
|
||||||
button = super.createButton(buttonType);
|
button = super.createButton(buttonType);
|
||||||
}
|
}
|
||||||
|
|
@ -757,39 +763,19 @@ public class QRScanDialog extends Dialog<QRScanDialog.Result> {
|
||||||
button.disableProperty().bind(webcamService.openingProperty());
|
button.disableProperty().bind(webcamService.openingProperty());
|
||||||
return button;
|
return button;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
public static <T extends Comparable<T>> void updateList(List<T> targetList, Collection<T> sourceList) {
|
private void setHdGraphic(ToggleButton hd, boolean isHd) {
|
||||||
List<T> sortedSource = new ArrayList<>(sourceList);
|
if(isHd) {
|
||||||
Collections.sort(sortedSource);
|
hd.setGraphic(getGlyph(FontAwesome5.Glyph.CHECK_CIRCLE));
|
||||||
|
|
||||||
ListIterator<T> targetIter = targetList.listIterator();
|
|
||||||
int sourceIndex = 0;
|
|
||||||
|
|
||||||
while (sourceIndex < sortedSource.size() && targetIter.hasNext()) {
|
|
||||||
T sourceItem = sortedSource.get(sourceIndex);
|
|
||||||
T targetItem = targetIter.next();
|
|
||||||
int comparison = sourceItem.compareTo(targetItem);
|
|
||||||
|
|
||||||
if (comparison < 0) {
|
|
||||||
targetIter.previous(); // Back up to insert before
|
|
||||||
targetIter.add(sourceItem);
|
|
||||||
sourceIndex++;
|
|
||||||
} else if (comparison > 0) {
|
|
||||||
targetIter.remove();
|
|
||||||
} else {
|
} else {
|
||||||
sourceIndex++;
|
hd.setGraphic(getGlyph(FontAwesome5.Glyph.BAN));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
while (sourceIndex < sortedSource.size()) {
|
private Glyph getGlyph(FontAwesome5.Glyph glyphName) {
|
||||||
targetIter.add(sortedSource.get(sourceIndex));
|
Glyph glyph = new Glyph(FontAwesome5.FONT_NAME, glyphName);
|
||||||
sourceIndex++;
|
glyph.setFontSize(11);
|
||||||
}
|
return glyph;
|
||||||
|
|
||||||
while (targetIter.hasNext()) {
|
|
||||||
targetIter.next();
|
|
||||||
targetIter.remove();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1007,4 +993,10 @@ public class QRScanDialog extends Dialog<QRScanDialog.Result> {
|
||||||
super(message, cause);
|
super(message, cause);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static class ScanDelayCalculator implements WebcamUpdater.DelayCalculator {
|
||||||
|
public long calculateDelay(long snapshotDuration, double deviceFps) {
|
||||||
|
return Math.max(SCAN_PERIOD_MILLIS - snapshotDuration, 0L);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,178 +0,0 @@
|
||||||
package com.sparrowwallet.sparrow.control;
|
|
||||||
|
|
||||||
import com.sparrowwallet.sparrow.BlockSummary;
|
|
||||||
import com.sparrowwallet.sparrow.io.Config;
|
|
||||||
import com.sparrowwallet.sparrow.net.FeeRatesSource;
|
|
||||||
import io.reactivex.Observable;
|
|
||||||
import io.reactivex.disposables.CompositeDisposable;
|
|
||||||
import io.reactivex.rxjavafx.schedulers.JavaFxScheduler;
|
|
||||||
import javafx.animation.TranslateTransition;
|
|
||||||
import javafx.beans.property.ObjectProperty;
|
|
||||||
import javafx.beans.property.SimpleObjectProperty;
|
|
||||||
import javafx.scene.control.Tooltip;
|
|
||||||
import javafx.scene.layout.Pane;
|
|
||||||
import javafx.scene.shape.Line;
|
|
||||||
import javafx.scene.shape.Rectangle;
|
|
||||||
import javafx.util.Duration;
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Map;
|
|
||||||
import java.util.concurrent.TimeUnit;
|
|
||||||
|
|
||||||
import static com.sparrowwallet.sparrow.AppServices.TARGET_BLOCKS_RANGE;
|
|
||||||
import static com.sparrowwallet.sparrow.control.BlockCube.CUBE_SIZE;
|
|
||||||
|
|
||||||
public class RecentBlocksView extends Pane {
|
|
||||||
private static final double CUBE_SPACING = 100;
|
|
||||||
private static final double ANIMATION_DURATION_MILLIS = 1000;
|
|
||||||
private static final double SEPARATOR_X = 74;
|
|
||||||
|
|
||||||
private final CompositeDisposable disposables = new CompositeDisposable();
|
|
||||||
|
|
||||||
private final ObjectProperty<List<BlockCube>> cubesProperty = new SimpleObjectProperty<>(new ArrayList<>());
|
|
||||||
private final Tooltip tooltip = new Tooltip();
|
|
||||||
|
|
||||||
public RecentBlocksView() {
|
|
||||||
cubesProperty.addListener((_, _, newValue) -> {
|
|
||||||
if(newValue != null && newValue.size() == 3) {
|
|
||||||
drawView();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
Rectangle clip = new Rectangle(-20, -40, CUBE_SPACING * 3 - 20, 100);
|
|
||||||
setClip(clip);
|
|
||||||
|
|
||||||
Observable<Long> intervalObservable = Observable.interval(1, TimeUnit.MINUTES);
|
|
||||||
disposables.add(intervalObservable.observeOn(JavaFxScheduler.platform()).subscribe(_ -> {
|
|
||||||
for(BlockCube cube : getCubes()) {
|
|
||||||
cube.setElapsed(BlockCube.getElapsed(cube.getTimestamp()));
|
|
||||||
}
|
|
||||||
}));
|
|
||||||
|
|
||||||
FeeRatesSource feeRatesSource = Config.get().getFeeRatesSource();
|
|
||||||
feeRatesSource = (feeRatesSource == null ? FeeRatesSource.MEMPOOL_SPACE : feeRatesSource);
|
|
||||||
updateFeeRatesSource(feeRatesSource);
|
|
||||||
Tooltip.install(this, tooltip);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void updateFeeRatesSource(FeeRatesSource feeRatesSource) {
|
|
||||||
tooltip.setText("Fee rate estimate from " + feeRatesSource.getDescription());
|
|
||||||
if(getCubes() != null && !getCubes().isEmpty()) {
|
|
||||||
getCubes().getFirst().setFeeRatesSource(feeRatesSource);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void drawView() {
|
|
||||||
createSeparator();
|
|
||||||
|
|
||||||
for(int i = 0; i < 3; i++) {
|
|
||||||
BlockCube cube = getCubes().get(i);
|
|
||||||
cube.setTranslateX(i * CUBE_SPACING);
|
|
||||||
getChildren().add(cube);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void createSeparator() {
|
|
||||||
Line separator = new Line(SEPARATOR_X, -9, SEPARATOR_X, CUBE_SIZE);
|
|
||||||
separator.getStyleClass().add("blocks-separator");
|
|
||||||
separator.getStrokeDashArray().addAll(5.0, 5.0); // Create dotted line pattern
|
|
||||||
separator.setStrokeWidth(1.0);
|
|
||||||
getChildren().add(separator);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void update(List<BlockSummary> latestBlocks, Double currentFeeRate) {
|
|
||||||
if(getCubes().isEmpty()) {
|
|
||||||
List<BlockCube> cubes = new ArrayList<>();
|
|
||||||
cubes.add(new BlockCube(null, currentFeeRate, null, null, 0L, false));
|
|
||||||
cubes.addAll(latestBlocks.stream().map(BlockCube::fromBlockSummary).limit(2).toList());
|
|
||||||
setCubes(cubes);
|
|
||||||
} else {
|
|
||||||
int knownTip = getCubes().stream().mapToInt(BlockCube::getHeight).max().orElse(0);
|
|
||||||
int latestTip = latestBlocks.stream().mapToInt(BlockSummary::getHeight).max().orElse(0);
|
|
||||||
if(latestTip > knownTip) {
|
|
||||||
addNewBlock(latestBlocks, currentFeeRate);
|
|
||||||
} else {
|
|
||||||
for(int i = 1; i < getCubes().size() && i <= latestBlocks.size(); i++) {
|
|
||||||
BlockCube blockCube = getCubes().get(i);
|
|
||||||
BlockSummary latestBlock = latestBlocks.get(i - 1);
|
|
||||||
blockCube.setConfirmed(true);
|
|
||||||
blockCube.setHeight(latestBlock.getHeight());
|
|
||||||
blockCube.setTimestamp(latestBlock.getTimestamp().getTime());
|
|
||||||
blockCube.setWeight(latestBlock.getWeight().orElse(0));
|
|
||||||
blockCube.setMedianFee(latestBlock.getMedianFee().orElse(-1.0d));
|
|
||||||
blockCube.setTxCount(latestBlock.getTransactionCount().orElse(0));
|
|
||||||
}
|
|
||||||
updateFeeRate(currentFeeRate);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void addNewBlock(List<BlockSummary> latestBlocks, Double currentFeeRate) {
|
|
||||||
if(getCubes().isEmpty()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
for(int i = 0; i < getCubes().size() && i < latestBlocks.size(); i++) {
|
|
||||||
BlockCube blockCube = getCubes().get(i);
|
|
||||||
BlockSummary latestBlock = latestBlocks.get(i);
|
|
||||||
blockCube.setConfirmed(true);
|
|
||||||
blockCube.setHeight(latestBlock.getHeight());
|
|
||||||
blockCube.setTimestamp(latestBlock.getTimestamp().getTime());
|
|
||||||
blockCube.setWeight(latestBlock.getWeight().orElse(0));
|
|
||||||
blockCube.setMedianFee(latestBlock.getMedianFee().orElse(-1.0d));
|
|
||||||
blockCube.setTxCount(latestBlock.getTransactionCount().orElse(0));
|
|
||||||
}
|
|
||||||
|
|
||||||
add(new BlockCube(null, currentFeeRate, null, null, 0L, false));
|
|
||||||
}
|
|
||||||
|
|
||||||
public void add(BlockCube newCube) {
|
|
||||||
newCube.setTranslateX(-CUBE_SPACING);
|
|
||||||
getChildren().add(newCube);
|
|
||||||
getCubes().getFirst().setConfirmed(true);
|
|
||||||
getCubes().addFirst(newCube);
|
|
||||||
animateCubes();
|
|
||||||
if(getCubes().size() > 4) {
|
|
||||||
BlockCube lastCube = getCubes().getLast();
|
|
||||||
getChildren().remove(lastCube);
|
|
||||||
getCubes().remove(lastCube);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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) {
|
|
||||||
if(!getCubes().isEmpty()) {
|
|
||||||
BlockCube firstCube = getCubes().getFirst();
|
|
||||||
firstCube.setMedianFee(currentFeeRate);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void animateCubes() {
|
|
||||||
for(int i = 0; i < getCubes().size(); i++) {
|
|
||||||
BlockCube cube = getCubes().get(i);
|
|
||||||
TranslateTransition transition = new TranslateTransition(Duration.millis(ANIMATION_DURATION_MILLIS), cube);
|
|
||||||
transition.setToX(i * CUBE_SPACING);
|
|
||||||
transition.play();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public List<BlockCube> getCubes() {
|
|
||||||
return cubesProperty.get();
|
|
||||||
}
|
|
||||||
|
|
||||||
public ObjectProperty<List<BlockCube>> cubesProperty() {
|
|
||||||
return cubesProperty;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setCubes(List<BlockCube> cubes) {
|
|
||||||
this.cubesProperty.set(cubes);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -2,7 +2,6 @@ package com.sparrowwallet.sparrow.control;
|
||||||
|
|
||||||
import com.sparrowwallet.drongo.protocol.Script;
|
import com.sparrowwallet.drongo.protocol.Script;
|
||||||
import com.sparrowwallet.drongo.protocol.ScriptChunk;
|
import com.sparrowwallet.drongo.protocol.ScriptChunk;
|
||||||
import com.sparrowwallet.drongo.protocol.ScriptOpCodes;
|
|
||||||
import javafx.geometry.Pos;
|
import javafx.geometry.Pos;
|
||||||
import org.controlsfx.control.decoration.Decorator;
|
import org.controlsfx.control.decoration.Decorator;
|
||||||
import org.controlsfx.control.decoration.GraphicDecoration;
|
import org.controlsfx.control.decoration.GraphicDecoration;
|
||||||
|
|
@ -54,11 +53,7 @@ public class ScriptArea extends CodeArea {
|
||||||
for (int i = 0; i < script.getChunks().size(); i++) {
|
for (int i = 0; i < script.getChunks().size(); i++) {
|
||||||
ScriptChunk chunk = script.getChunks().get(i);
|
ScriptChunk chunk = script.getChunks().get(i);
|
||||||
if(chunk.isOpCode()) {
|
if(chunk.isOpCode()) {
|
||||||
if(chunk.getOpcode() == ScriptOpCodes.OP_0 && witnessScript != null) {
|
append(chunk.toString(), "script-opcode");
|
||||||
append("<empty>", "script-other");
|
|
||||||
} else {
|
|
||||||
append(chunk.toString(), "script-opcode");
|
|
||||||
}
|
|
||||||
} else if(chunk.isPubKey()) {
|
} else if(chunk.isPubKey()) {
|
||||||
append("<pubkey" + pubKeyCount++ + ">", "script-pubkey");
|
append("<pubkey" + pubKeyCount++ + ">", "script-pubkey");
|
||||||
} else if(chunk.isSignature()) {
|
} else if(chunk.isSignature()) {
|
||||||
|
|
|
||||||
|
|
@ -60,7 +60,14 @@ public class SearchWalletDialog extends Dialog<Entry> {
|
||||||
dialogPane.getStylesheets().add(AppServices.class.getResource("search.css").toExternalForm());
|
dialogPane.getStylesheets().add(AppServices.class.getResource("search.css").toExternalForm());
|
||||||
AppServices.setStageIcon(dialogPane.getScene().getWindow());
|
AppServices.setStageIcon(dialogPane.getScene().getWindow());
|
||||||
dialogPane.setHeaderText(showWallet ? "Search All Wallets" : "Search Wallet " + walletForms.get(0).getMasterWallet().getName());
|
dialogPane.setHeaderText(showWallet ? "Search All Wallets" : "Search Wallet " + walletForms.get(0).getMasterWallet().getName());
|
||||||
dialogPane.setGraphic(new DialogImage(DialogImage.Type.SPARROW));
|
|
||||||
|
Image image = new Image("image/sparrow-small.png", 50, 50, false, false);
|
||||||
|
if(!image.isError()) {
|
||||||
|
ImageView imageView = new ImageView();
|
||||||
|
imageView.setSmooth(false);
|
||||||
|
imageView.setImage(image);
|
||||||
|
dialogPane.setGraphic(imageView);
|
||||||
|
}
|
||||||
|
|
||||||
VBox vBox = new VBox();
|
VBox vBox = new VBox();
|
||||||
vBox.setSpacing(20);
|
vBox.setSpacing(20);
|
||||||
|
|
|
||||||
|
|
@ -5,49 +5,36 @@ import com.sparrowwallet.drongo.BitcoinUnit;
|
||||||
import com.sparrowwallet.drongo.OsType;
|
import com.sparrowwallet.drongo.OsType;
|
||||||
import com.sparrowwallet.drongo.address.Address;
|
import com.sparrowwallet.drongo.address.Address;
|
||||||
import com.sparrowwallet.drongo.address.InvalidAddressException;
|
import com.sparrowwallet.drongo.address.InvalidAddressException;
|
||||||
import com.sparrowwallet.drongo.dns.DnsPayment;
|
|
||||||
import com.sparrowwallet.drongo.dns.DnsPaymentCache;
|
|
||||||
import com.sparrowwallet.drongo.dns.DnsPaymentResolver;
|
|
||||||
import com.sparrowwallet.drongo.dns.DnsPaymentValidationException;
|
|
||||||
import com.sparrowwallet.drongo.protocol.Transaction;
|
import com.sparrowwallet.drongo.protocol.Transaction;
|
||||||
import com.sparrowwallet.drongo.silentpayments.SilentPayment;
|
|
||||||
import com.sparrowwallet.drongo.silentpayments.SilentPaymentAddress;
|
|
||||||
import com.sparrowwallet.drongo.uri.BitcoinURIParseException;
|
|
||||||
import com.sparrowwallet.drongo.wallet.Payment;
|
import com.sparrowwallet.drongo.wallet.Payment;
|
||||||
import com.sparrowwallet.sparrow.AppServices;
|
import com.sparrowwallet.sparrow.AppServices;
|
||||||
import com.sparrowwallet.sparrow.EventManager;
|
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
|
||||||
import com.sparrowwallet.sparrow.event.RequestConnectEvent;
|
|
||||||
import com.sparrowwallet.sparrow.glyphfont.GlyphUtils;
|
|
||||||
import com.sparrowwallet.sparrow.io.Config;
|
|
||||||
import javafx.application.Platform;
|
|
||||||
import javafx.collections.FXCollections;
|
import javafx.collections.FXCollections;
|
||||||
import javafx.collections.ObservableList;
|
import javafx.collections.ObservableList;
|
||||||
import javafx.concurrent.Service;
|
|
||||||
import javafx.concurrent.Task;
|
|
||||||
import javafx.event.ActionEvent;
|
|
||||||
import javafx.scene.Node;
|
import javafx.scene.Node;
|
||||||
import javafx.scene.control.*;
|
import javafx.scene.control.*;
|
||||||
|
import javafx.scene.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 SendToAddressCellType SEND_TO_ADDRESS = new SendToAddressCellType();
|
public static final AddressCellType ADDRESS = new AddressCellType();
|
||||||
|
|
||||||
public SendToManyDialog(BitcoinUnit bitcoinUnit, List<Payment> payments) {
|
public SendToManyDialog(BitcoinUnit bitcoinUnit) {
|
||||||
this.bitcoinUnit = bitcoinUnit;
|
this.bitcoinUnit = bitcoinUnit;
|
||||||
|
|
||||||
final DialogPane dialogPane = new SendToManyDialogPane();
|
final DialogPane dialogPane = new SendToManyDialogPane();
|
||||||
|
|
@ -55,10 +42,10 @@ public class SendToManyDialog extends Dialog<List<Payment>> {
|
||||||
setTitle("Send to Many");
|
setTitle("Send to Many");
|
||||||
dialogPane.getStylesheets().add(AppServices.class.getResource("general.css").toExternalForm());
|
dialogPane.getStylesheets().add(AppServices.class.getResource("general.css").toExternalForm());
|
||||||
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));
|
Image image = new Image("/image/sparrow-small.png");
|
||||||
|
dialogPane.setGraphic(new ImageView(image));
|
||||||
|
|
||||||
List<Payment> initialPayments = IntStream.range(0, 100)
|
List<Payment> initialPayments = IntStream.range(0, 100).mapToObj(i -> new Payment(null, null, -1, false)).collect(Collectors.toList());
|
||||||
.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) {
|
||||||
|
|
@ -83,16 +70,14 @@ public class SendToManyDialog extends Dialog<List<Payment>> {
|
||||||
dialogPane.setContent(stackPane);
|
dialogPane.setContent(stackPane);
|
||||||
|
|
||||||
dialogPane.getButtonTypes().addAll(ButtonType.OK, ButtonType.CANCEL);
|
dialogPane.getButtonTypes().addAll(ButtonType.OK, ButtonType.CANCEL);
|
||||||
Button okButton = (Button) dialogPane.lookupButton(ButtonType.OK);
|
|
||||||
okButton.addEventFilter(ActionEvent.ACTION, event -> {
|
|
||||||
getPayments();
|
|
||||||
event.consume();
|
|
||||||
});
|
|
||||||
|
|
||||||
final ButtonType loadCsvButtonType = new javafx.scene.control.ButtonType("Load CSV", ButtonBar.ButtonData.LEFT);
|
final ButtonType loadCsvButtonType = new javafx.scene.control.ButtonType("Load CSV", ButtonBar.ButtonData.LEFT);
|
||||||
dialogPane.getButtonTypes().add(loadCsvButtonType);
|
dialogPane.getButtonTypes().add(loadCsvButtonType);
|
||||||
|
|
||||||
setResultConverter((_) -> null);
|
setResultConverter((dialogButton) -> {
|
||||||
|
ButtonBar.ButtonData data = dialogButton == null ? null : dialogButton.getButtonData();
|
||||||
|
return data == ButtonBar.ButtonData.OK_DONE ? getPayments() : null;
|
||||||
|
});
|
||||||
|
|
||||||
dialogPane.setPrefWidth(850);
|
dialogPane.setPrefWidth(850);
|
||||||
dialogPane.setPrefHeight(500);
|
dialogPane.setPrefHeight(500);
|
||||||
|
|
@ -102,24 +87,18 @@ public class SendToManyDialog extends Dialog<List<Payment>> {
|
||||||
}
|
}
|
||||||
|
|
||||||
private Grid getGrid(List<Payment> payments) {
|
private Grid getGrid(List<Payment> payments) {
|
||||||
return createGrid(payments.stream().map(payment -> new SendToPayment(payment, SendToAddress.fromPayment(payment))).collect(Collectors.toList()));
|
int rowCount = payments.size();
|
||||||
}
|
|
||||||
|
|
||||||
private Grid createGrid(List<SendToPayment> sendToPayments) {
|
|
||||||
int rowCount = sendToPayments.size();
|
|
||||||
int columnCount = 3;
|
int columnCount = 3;
|
||||||
GridBase grid = new GridBase(rowCount, columnCount);
|
GridBase grid = new GridBase(rowCount, columnCount);
|
||||||
ObservableList<ObservableList<SpreadsheetCell>> rows = FXCollections.observableArrayList();
|
ObservableList<ObservableList<SpreadsheetCell>> rows = FXCollections.observableArrayList();
|
||||||
for(int row = 0; row < grid.getRowCount(); ++row) {
|
for(int row = 0; row < grid.getRowCount(); ++row) {
|
||||||
SendToPayment sendToPayment = sendToPayments.get(row);
|
|
||||||
final ObservableList<SpreadsheetCell> list = FXCollections.observableArrayList();
|
final ObservableList<SpreadsheetCell> list = FXCollections.observableArrayList();
|
||||||
|
|
||||||
SendToAddress sendToAddress = sendToPayment.sendToAddress();
|
SpreadsheetCell addressCell = ADDRESS.createCell(row, 0, 1, 1, payments.get(row).getAddress());
|
||||||
SpreadsheetCell addressCell = SEND_TO_ADDRESS.createCell(row, 0, 1, 1, sendToAddress);
|
|
||||||
addressCell.getStyleClass().add("fixed-width");
|
addressCell.getStyleClass().add("fixed-width");
|
||||||
list.add(addressCell);
|
list.add(addressCell);
|
||||||
|
|
||||||
double amount = (double)sendToPayment.payment().getAmount();
|
double amount = (double)payments.get(row).getAmount();
|
||||||
if(bitcoinUnit == BitcoinUnit.BTC) {
|
if(bitcoinUnit == BitcoinUnit.BTC) {
|
||||||
amount = amount / Transaction.SATOSHIS_PER_BITCOIN;
|
amount = amount / Transaction.SATOSHIS_PER_BITCOIN;
|
||||||
}
|
}
|
||||||
|
|
@ -131,7 +110,7 @@ public class SendToManyDialog extends Dialog<List<Payment>> {
|
||||||
}
|
}
|
||||||
list.add(amountCell);
|
list.add(amountCell);
|
||||||
|
|
||||||
list.add(SpreadsheetCellType.STRING.createCell(row, 2, 1, 1, sendToPayment.payment().getLabel()));
|
list.add(SpreadsheetCellType.STRING.createCell(row, 2, 1, 1, payments.get(row).getLabel()));
|
||||||
rows.add(list);
|
rows.add(list);
|
||||||
}
|
}
|
||||||
grid.setRows(rows);
|
grid.setRows(rows);
|
||||||
|
|
@ -140,49 +119,32 @@ public class SendToManyDialog extends Dialog<List<Payment>> {
|
||||||
return grid;
|
return grid;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void getPayments() {
|
private List<Payment> getPayments() {
|
||||||
if(needsResolution() && Config.get().hasServer() && !AppServices.isConnected() && !AppServices.isConnecting()) {
|
List<Payment> payments = new ArrayList<>();
|
||||||
if(Config.get().getConnectToResolve() == null || Config.get().getConnectToResolve() == Boolean.FALSE) {
|
Grid grid = spreadsheetView.getGrid();
|
||||||
Platform.runLater(() -> {
|
String firstLabel = null;
|
||||||
ConfirmationAlert confirmationAlert = new ConfirmationAlert("Connect to resolve?", "You are currently offline. Connect to resolve the addresses?", ButtonType.NO, ButtonType.YES);
|
for(int row = 0; row < grid.getRowCount(); row++) {
|
||||||
Optional<ButtonType> optType = confirmationAlert.showAndWait();
|
|
||||||
if(confirmationAlert.isDontAskAgain() && optType.isPresent()) {
|
|
||||||
Config.get().setConnectToResolve(optType.get() == ButtonType.YES);
|
|
||||||
}
|
|
||||||
if(optType.isPresent() && optType.get() == ButtonType.YES) {
|
|
||||||
EventManager.get().post(new RequestConnectEvent());
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
Platform.runLater(() -> EventManager.get().post(new RequestConnectEvent()));
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
CreatePaymentsService createPaymentsService = new CreatePaymentsService();
|
|
||||||
createPaymentsService.setOnSucceeded(_ -> {
|
|
||||||
List<Payment> payments = createPaymentsService.getValue();
|
|
||||||
if(payments != null) {
|
|
||||||
setResult(payments);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
createPaymentsService.setOnFailed(event -> {
|
|
||||||
Throwable ex = event.getSource().getException();
|
|
||||||
AppServices.showErrorDialog("Error creating payments", ex.getMessage());
|
|
||||||
});
|
|
||||||
createPaymentsService.start();
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean needsResolution() {
|
|
||||||
for(int row = 0; row < spreadsheetView.getGrid().getRowCount(); row++) {
|
|
||||||
ObservableList<SpreadsheetCell> rowCells = spreadsheetView.getItems().get(row);
|
ObservableList<SpreadsheetCell> rowCells = spreadsheetView.getItems().get(row);
|
||||||
SendToAddress sendToAddress = (SendToAddress)rowCells.getFirst().getItem();
|
Address address = (Address)rowCells.get(0).getItem();
|
||||||
if(sendToAddress.hrn != null && DnsPaymentCache.getDnsPayment(sendToAddress.hrn) == null) {
|
Double value = (Double)rowCells.get(1).getItem();
|
||||||
return true;
|
String label = (String)rowCells.get(2).getItem();
|
||||||
|
if(firstLabel == null) {
|
||||||
|
firstLabel = label;
|
||||||
|
}
|
||||||
|
if(label == null || label.isEmpty()) {
|
||||||
|
label = firstLabel;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(address != null && value != null) {
|
||||||
|
if(bitcoinUnit == BitcoinUnit.BTC) {
|
||||||
|
value = value * Transaction.SATOSHIS_PER_BITCOIN;
|
||||||
|
}
|
||||||
|
|
||||||
|
payments.add(new Payment(address, label, value.longValue(), false));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
return payments;
|
||||||
}
|
}
|
||||||
|
|
||||||
private class SendToManyDialogPane extends DialogPane {
|
private class SendToManyDialogPane extends DialogPane {
|
||||||
|
|
@ -192,7 +154,7 @@ public class SendToManyDialog extends Dialog<List<Payment>> {
|
||||||
if(buttonType.getButtonData() == ButtonBar.ButtonData.LEFT) {
|
if(buttonType.getButtonData() == ButtonBar.ButtonData.LEFT) {
|
||||||
Button loadButton = new Button(buttonType.getText());
|
Button loadButton = new Button(buttonType.getText());
|
||||||
loadButton.setGraphicTextGap(5);
|
loadButton.setGraphicTextGap(5);
|
||||||
loadButton.setGraphic(GlyphUtils.getUpArrowGlyph());
|
loadButton.setGraphic(getGlyph(FontAwesome5.Glyph.ARROW_UP));
|
||||||
final ButtonBar.ButtonData buttonData = buttonType.getButtonData();
|
final ButtonBar.ButtonData buttonData = buttonType.getButtonData();
|
||||||
ButtonBar.setButtonData(loadButton, buttonData);
|
ButtonBar.setButtonData(loadButton, buttonData);
|
||||||
loadButton.setOnAction(event -> {
|
loadButton.setOnAction(event -> {
|
||||||
|
|
@ -207,7 +169,7 @@ public class SendToManyDialog extends Dialog<List<Payment>> {
|
||||||
File file = fileChooser.showOpenDialog(this.getScene().getWindow());
|
File file = fileChooser.showOpenDialog(this.getScene().getWindow());
|
||||||
if(file != null) {
|
if(file != null) {
|
||||||
try {
|
try {
|
||||||
List<SendToPayment> csvPayments = new ArrayList<>();
|
List<Payment> csvPayments = new ArrayList<>();
|
||||||
try(Reader reader = new FileReader(file, StandardCharsets.UTF_8)) {
|
try(Reader reader = new FileReader(file, StandardCharsets.UTF_8)) {
|
||||||
CsvReader csvReader = new CsvReader(reader);
|
CsvReader csvReader = new CsvReader(reader);
|
||||||
while(csvReader.readRecord()) {
|
while(csvReader.readRecord()) {
|
||||||
|
|
@ -223,22 +185,9 @@ public class SendToManyDialog extends Dialog<List<Payment>> {
|
||||||
} else {
|
} else {
|
||||||
amount = Long.parseLong(csvReader.get(1).replace(",", ""));
|
amount = Long.parseLong(csvReader.get(1).replace(",", ""));
|
||||||
}
|
}
|
||||||
|
Address address = Address.fromString(csvReader.get(0));
|
||||||
String label = csvReader.get(2);
|
String label = csvReader.get(2);
|
||||||
Optional<String> optDnsPaymentHrn = DnsPayment.getHrn(csvReader.get(0));
|
csvPayments.add(new Payment(address, label, amount, false));
|
||||||
if(optDnsPaymentHrn.isPresent()) {
|
|
||||||
Payment payment = new Payment(null, label, amount, false);
|
|
||||||
csvPayments.add(new SendToPayment(payment, new SendToAddress(optDnsPaymentHrn.get())));
|
|
||||||
} else {
|
|
||||||
try {
|
|
||||||
SilentPaymentAddress silentPaymentAddress = SilentPaymentAddress.from(csvReader.get(0));
|
|
||||||
Payment payment = new SilentPayment(silentPaymentAddress, label, amount, false);
|
|
||||||
csvPayments.add(new SendToPayment(payment, SendToAddress.fromPayment(payment)));
|
|
||||||
} catch(Exception e) {
|
|
||||||
Address address = Address.fromString(csvReader.get(0));
|
|
||||||
Payment payment = new Payment(address, label, amount, false);
|
|
||||||
csvPayments.add(new SendToPayment(payment, SendToAddress.fromPayment(payment)));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch(NumberFormatException e) {
|
} catch(NumberFormatException e) {
|
||||||
//ignore and continue - probably a header line
|
//ignore and continue - probably a header line
|
||||||
} catch(InvalidAddressException e) {
|
} catch(InvalidAddressException e) {
|
||||||
|
|
@ -251,7 +200,7 @@ public class SendToManyDialog extends Dialog<List<Payment>> {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
spreadsheetView.setGrid(createGrid(csvPayments));
|
spreadsheetView.setGrid(getGrid(csvPayments));
|
||||||
}
|
}
|
||||||
} catch(IOException e) {
|
} catch(IOException e) {
|
||||||
AppServices.showErrorDialog("Cannot load CSV", e.getMessage());
|
AppServices.showErrorDialog("Cannot load CSV", e.getMessage());
|
||||||
|
|
@ -266,18 +215,24 @@ public class SendToManyDialog extends Dialog<List<Payment>> {
|
||||||
|
|
||||||
return button;
|
return button;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private Glyph getGlyph(FontAwesome5.Glyph glyphName) {
|
||||||
|
Glyph glyph = new Glyph(FontAwesome5.FONT_NAME, glyphName);
|
||||||
|
glyph.setFontSize(11);
|
||||||
|
return glyph;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static class SendToAddressCellType extends SpreadsheetCellType<SendToAddress> {
|
public static class AddressCellType extends SpreadsheetCellType<Address> {
|
||||||
public SendToAddressCellType() {
|
public AddressCellType() {
|
||||||
this(new StringConverterWithFormat<>(new SendToAddressStringConverter()) {
|
this(new StringConverterWithFormat<>(new AddressStringConverter()) {
|
||||||
@Override
|
@Override
|
||||||
public String toString(SendToAddress item) {
|
public String toString(Address item) {
|
||||||
return toStringFormat(item, ""); //$NON-NLS-1$
|
return toStringFormat(item, ""); //$NON-NLS-1$
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public SendToAddress fromString(String str) {
|
public Address fromString(String str) {
|
||||||
if(str == null || str.isEmpty()) { //$NON-NLS-1$
|
if(str == null || str.isEmpty()) { //$NON-NLS-1$
|
||||||
return null;
|
return null;
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -286,7 +241,7 @@ public class SendToManyDialog extends Dialog<List<Payment>> {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String toStringFormat(SendToAddress item, String format) {
|
public String toStringFormat(Address item, String format) {
|
||||||
try {
|
try {
|
||||||
if(item == null) {
|
if(item == null) {
|
||||||
return ""; //$NON-NLS-1$
|
return ""; //$NON-NLS-1$
|
||||||
|
|
@ -300,7 +255,7 @@ public class SendToManyDialog extends Dialog<List<Payment>> {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public SendToAddressCellType(StringConverter<SendToAddress> converter) {
|
public AddressCellType(StringConverter<Address> converter) {
|
||||||
super(converter);
|
super(converter);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -310,7 +265,7 @@ public class SendToManyDialog extends Dialog<List<Payment>> {
|
||||||
}
|
}
|
||||||
|
|
||||||
public SpreadsheetCell createCell(final int row, final int column, final int rowSpan, final int columnSpan,
|
public SpreadsheetCell createCell(final int row, final int column, final int rowSpan, final int columnSpan,
|
||||||
final SendToAddress value) {
|
final Address value) {
|
||||||
SpreadsheetCell cell = new SpreadsheetCellBase(row, column, rowSpan, columnSpan, this);
|
SpreadsheetCell cell = new SpreadsheetCellBase(row, column, rowSpan, columnSpan, this);
|
||||||
cell.setItem(value);
|
cell.setItem(value);
|
||||||
return cell;
|
return cell;
|
||||||
|
|
@ -323,7 +278,7 @@ public class SendToManyDialog extends Dialog<List<Payment>> {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean match(Object value, Object... options) {
|
public boolean match(Object value, Object... options) {
|
||||||
if(value instanceof SendToAddress)
|
if(value instanceof Address)
|
||||||
return true;
|
return true;
|
||||||
else {
|
else {
|
||||||
try {
|
try {
|
||||||
|
|
@ -336,9 +291,9 @@ public class SendToManyDialog extends Dialog<List<Payment>> {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public SendToAddress convertValue(Object value) {
|
public Address convertValue(Object value) {
|
||||||
if(value instanceof SendToAddress)
|
if(value instanceof Address)
|
||||||
return (SendToAddress)value;
|
return (Address)value;
|
||||||
else {
|
else {
|
||||||
try {
|
try {
|
||||||
return converter.fromString(value == null ? null : value.toString());
|
return converter.fromString(value == null ? null : value.toString());
|
||||||
|
|
@ -349,155 +304,13 @@ public class SendToManyDialog extends Dialog<List<Payment>> {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String toString(SendToAddress item) {
|
public String toString(Address item) {
|
||||||
return converter.toString(item);
|
return converter.toString(item);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String toString(SendToAddress item, String format) {
|
public String toString(Address item, String format) {
|
||||||
return ((StringConverterWithFormat<SendToAddress>)converter).toStringFormat(item, format);
|
return ((StringConverterWithFormat<Address>)converter).toStringFormat(item, format);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
public static class SendToAddress {
|
|
||||||
private final String hrn;
|
|
||||||
private final Address address;
|
|
||||||
private final SilentPaymentAddress silentPaymentAddress;
|
|
||||||
|
|
||||||
public SendToAddress(String hrn) {
|
|
||||||
this.hrn = hrn;
|
|
||||||
this.address = null;
|
|
||||||
this.silentPaymentAddress = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
public SendToAddress(Address address) {
|
|
||||||
this.hrn = null;
|
|
||||||
this.address = address;
|
|
||||||
this.silentPaymentAddress = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
public SendToAddress(SilentPaymentAddress silentPaymentAddress) {
|
|
||||||
this.hrn = null;
|
|
||||||
this.address = null;
|
|
||||||
this.silentPaymentAddress = silentPaymentAddress;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String toString() {
|
|
||||||
return hrn == null ? silentPaymentAddress == null ? (address == null ? null : address.toString()) : silentPaymentAddress.toString() : hrn;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static SendToAddress fromPayment(Payment payment) {
|
|
||||||
DnsPayment dnsPayment = DnsPaymentCache.getDnsPayment(payment);
|
|
||||||
if(dnsPayment != null) {
|
|
||||||
return new SendToAddress(dnsPayment.hrn());
|
|
||||||
}
|
|
||||||
return payment instanceof SilentPayment ? new SendToAddress(((SilentPayment)payment).getSilentPaymentAddress()) : new SendToAddress(payment.getAddress());
|
|
||||||
}
|
|
||||||
|
|
||||||
public Payment toPayment(String label, long value, boolean sendMax) throws DnsPaymentValidationException, IOException, ExecutionException, InterruptedException, BitcoinURIParseException {
|
|
||||||
if(hrn != null) {
|
|
||||||
DnsPayment dnsPayment = DnsPaymentCache.getDnsPayment(hrn);
|
|
||||||
if(dnsPayment == null) {
|
|
||||||
DnsPaymentResolver resolver = new DnsPaymentResolver(hrn);
|
|
||||||
Optional<DnsPayment> optDnsPayment = resolver.resolve();
|
|
||||||
if(optDnsPayment.isPresent()) {
|
|
||||||
dnsPayment = optDnsPayment.get();
|
|
||||||
if(dnsPayment.hasAddress()) {
|
|
||||||
DnsPaymentCache.putDnsPayment(dnsPayment.bitcoinURI().getAddress(), dnsPayment);
|
|
||||||
} else if(dnsPayment.hasSilentPaymentAddress()) {
|
|
||||||
DnsPaymentCache.putDnsPayment(dnsPayment.bitcoinURI().getSilentPaymentAddress(), dnsPayment);
|
|
||||||
}
|
|
||||||
return getPayment(optDnsPayment.get(), label, value, sendMax);
|
|
||||||
} else {
|
|
||||||
throw new IllegalArgumentException("Payment to " + hrn + " could not be resolved.");
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return getPayment(dnsPayment, label, value, sendMax);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if(silentPaymentAddress != null) {
|
|
||||||
return new SilentPayment(silentPaymentAddress, label, value, sendMax);
|
|
||||||
} else {
|
|
||||||
return new Payment(address, label, value, sendMax);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static Payment getPayment(DnsPayment dnsPayment, String label, long value, boolean sendMax) {
|
|
||||||
if(dnsPayment.hasAddress()) {
|
|
||||||
return new Payment(dnsPayment.bitcoinURI().getAddress(), label, value, sendMax);
|
|
||||||
} else if(dnsPayment.hasSilentPaymentAddress()) {
|
|
||||||
return new SilentPayment(dnsPayment.bitcoinURI().getSilentPaymentAddress(), label, value, sendMax);
|
|
||||||
} else {
|
|
||||||
throw new IllegalArgumentException("Payment to " + dnsPayment + " has no associated address.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static class SendToAddressStringConverter extends StringConverter<SendToAddress> {
|
|
||||||
private final AddressStringConverter addressStringConverter = new AddressStringConverter();
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public SendToAddress fromString(String value) {
|
|
||||||
Optional<String> optDnsPaymentHrn = DnsPayment.getHrn(value);
|
|
||||||
if(optDnsPaymentHrn.isPresent()) {
|
|
||||||
return new SendToAddress(optDnsPaymentHrn.get());
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
SilentPaymentAddress silentPaymentAddress = SilentPaymentAddress.from(value);
|
|
||||||
return new SendToAddress(silentPaymentAddress);
|
|
||||||
} catch(Exception e) {
|
|
||||||
Address address = addressStringConverter.fromString(value);
|
|
||||||
return address == null ? null : new SendToAddress(address);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String toString(SendToAddress value) {
|
|
||||||
return value.toString();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private class CreatePaymentsService extends Service<List<Payment>> {
|
|
||||||
@Override
|
|
||||||
protected Task<List<Payment>> createTask() {
|
|
||||||
return new Task<>() {
|
|
||||||
@Override
|
|
||||||
protected List<Payment> call() throws Exception {
|
|
||||||
return getPayments();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
private List<Payment> getPayments() throws DnsPaymentValidationException, IOException, ExecutionException, InterruptedException, BitcoinURIParseException {
|
|
||||||
List<Payment> payments = new ArrayList<>();
|
|
||||||
Grid grid = spreadsheetView.getGrid();
|
|
||||||
String firstLabel = null;
|
|
||||||
for(int row = 0; row < grid.getRowCount(); row++) {
|
|
||||||
ObservableList<SpreadsheetCell> rowCells = spreadsheetView.getItems().get(row);
|
|
||||||
SendToAddress sendToAddress = (SendToAddress)rowCells.get(0).getItem();
|
|
||||||
Double value = (Double)rowCells.get(1).getItem();
|
|
||||||
String label = (String)rowCells.get(2).getItem();
|
|
||||||
if(firstLabel == null) {
|
|
||||||
firstLabel = label;
|
|
||||||
}
|
|
||||||
if(label == null || label.isEmpty()) {
|
|
||||||
label = firstLabel;
|
|
||||||
}
|
|
||||||
|
|
||||||
if(sendToAddress != null && value != null) {
|
|
||||||
if(bitcoinUnit == BitcoinUnit.BTC) {
|
|
||||||
value = value * Transaction.SATOSHIS_PER_BITCOIN;
|
|
||||||
}
|
|
||||||
|
|
||||||
payments.add(sendToAddress.toPayment(label, value.longValue(), false));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return payments;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private record SendToPayment(Payment payment, SendToAddress sendToAddress) {}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,12 +3,13 @@ package com.sparrowwallet.sparrow.control;
|
||||||
import com.sparrowwallet.sparrow.AppServices;
|
import com.sparrowwallet.sparrow.AppServices;
|
||||||
import javafx.beans.property.*;
|
import javafx.beans.property.*;
|
||||||
import javafx.concurrent.Worker;
|
import javafx.concurrent.Worker;
|
||||||
import javafx.scene.Node;
|
|
||||||
import javafx.scene.control.DialogPane;
|
import javafx.scene.control.DialogPane;
|
||||||
|
import javafx.scene.image.Image;
|
||||||
|
import javafx.scene.image.ImageView;
|
||||||
import org.controlsfx.dialog.ProgressDialog;
|
import org.controlsfx.dialog.ProgressDialog;
|
||||||
|
|
||||||
public class ServiceProgressDialog extends ProgressDialog {
|
public class ServiceProgressDialog extends ProgressDialog {
|
||||||
public ServiceProgressDialog(String title, String header, Node graphic, Worker<?> worker) {
|
public ServiceProgressDialog(String title, String header, String imagePath, Worker<?> worker) {
|
||||||
super(worker);
|
super(worker);
|
||||||
|
|
||||||
final DialogPane dialogPane = getDialogPane();
|
final DialogPane dialogPane = getDialogPane();
|
||||||
|
|
@ -19,7 +20,8 @@ public class ServiceProgressDialog extends ProgressDialog {
|
||||||
setHeaderText(header);
|
setHeaderText(header);
|
||||||
|
|
||||||
dialogPane.getStyleClass().remove("progress-dialog");
|
dialogPane.getStyleClass().remove("progress-dialog");
|
||||||
dialogPane.setGraphic(graphic);
|
Image image = new Image(imagePath);
|
||||||
|
dialogPane.setGraphic(new ImageView(image));
|
||||||
}
|
}
|
||||||
|
|
||||||
public static class ProxyWorker implements Worker<Boolean> {
|
public static class ProxyWorker implements Worker<Boolean> {
|
||||||
|
|
|
||||||
|
|
@ -44,7 +44,8 @@ public class TextAreaDialog extends Dialog<String> {
|
||||||
final DialogPane dialogPane = new TextAreaDialogPane();
|
final DialogPane dialogPane = new TextAreaDialogPane();
|
||||||
setDialogPane(dialogPane);
|
setDialogPane(dialogPane);
|
||||||
|
|
||||||
dialogPane.setGraphic(new DialogImage(DialogImage.Type.SPARROW));
|
Image image = new Image("/image/sparrow-small.png");
|
||||||
|
dialogPane.setGraphic(new ImageView(image));
|
||||||
|
|
||||||
HBox hbox = new HBox();
|
HBox hbox = new HBox();
|
||||||
this.textArea = new TextArea(defaultValue);
|
this.textArea = new TextArea(defaultValue);
|
||||||
|
|
|
||||||
|
|
@ -29,7 +29,8 @@ public class TextfieldDialog extends Dialog<String> {
|
||||||
final DialogPane dialogPane = getDialogPane();
|
final DialogPane dialogPane = getDialogPane();
|
||||||
setDialogPane(dialogPane);
|
setDialogPane(dialogPane);
|
||||||
|
|
||||||
dialogPane.setGraphic(new DialogImage(DialogImage.Type.SPARROW));
|
Image image = new Image("/image/sparrow-small.png");
|
||||||
|
dialogPane.setGraphic(new ImageView(image));
|
||||||
|
|
||||||
HBox hbox = new HBox();
|
HBox hbox = new HBox();
|
||||||
this.textField = new TextField(defaultValue);
|
this.textField = new TextField(defaultValue);
|
||||||
|
|
|
||||||
|
|
@ -2,13 +2,14 @@ package com.sparrowwallet.sparrow.control;
|
||||||
|
|
||||||
import com.sparrowwallet.drongo.KeyDerivation;
|
import com.sparrowwallet.drongo.KeyDerivation;
|
||||||
import com.sparrowwallet.drongo.wallet.Wallet;
|
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||||
import com.sparrowwallet.drongo.wallet.WalletModel;
|
|
||||||
import com.sparrowwallet.sparrow.AppServices;
|
import com.sparrowwallet.sparrow.AppServices;
|
||||||
import javafx.application.Platform;
|
import javafx.application.Platform;
|
||||||
import javafx.geometry.Insets;
|
import javafx.geometry.Insets;
|
||||||
import javafx.geometry.Pos;
|
import javafx.geometry.Pos;
|
||||||
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.layout.HBox;
|
import javafx.scene.layout.HBox;
|
||||||
import javafx.scene.layout.Priority;
|
import javafx.scene.layout.Priority;
|
||||||
import javafx.scene.layout.VBox;
|
import javafx.scene.layout.VBox;
|
||||||
|
|
@ -22,18 +23,17 @@ public class TitledDescriptionPane extends TitledPane {
|
||||||
protected Hyperlink showHideLink;
|
protected Hyperlink showHideLink;
|
||||||
protected HBox buttonBox;
|
protected HBox buttonBox;
|
||||||
|
|
||||||
public TitledDescriptionPane(String title, String description, String content, WalletModel walletModel) {
|
public TitledDescriptionPane(String title, String description, String content, String imageUrl) {
|
||||||
getStylesheets().add(AppServices.class.getResource("general.css").toExternalForm());
|
getStylesheets().add(AppServices.class.getResource("general.css").toExternalForm());
|
||||||
getStyleClass().add("titled-description-pane");
|
getStyleClass().add("titled-description-pane");
|
||||||
setAccessibleText(title);
|
|
||||||
|
|
||||||
setPadding(Insets.EMPTY);
|
setPadding(Insets.EMPTY);
|
||||||
setGraphic(getTitle(title, description, walletModel));
|
setGraphic(getTitle(title, description, imageUrl));
|
||||||
setContent(getContentBox(content));
|
setContent(getContentBox(content));
|
||||||
removeArrow();
|
removeArrow();
|
||||||
}
|
}
|
||||||
|
|
||||||
protected Node getTitle(String title, String description, WalletModel walletModel) {
|
protected Node getTitle(String title, String description, String imageUrl) {
|
||||||
HBox listItem = new HBox();
|
HBox listItem = new HBox();
|
||||||
listItem.setPadding(new Insets(10, 20, 10, 10));
|
listItem.setPadding(new Insets(10, 20, 10, 10));
|
||||||
listItem.setSpacing(10);
|
listItem.setSpacing(10);
|
||||||
|
|
@ -43,8 +43,12 @@ public class TitledDescriptionPane extends TitledPane {
|
||||||
imageBox.setMinHeight(50);
|
imageBox.setMinHeight(50);
|
||||||
listItem.getChildren().add(imageBox);
|
listItem.getChildren().add(imageBox);
|
||||||
|
|
||||||
WalletModelImage walletModelImage = new WalletModelImage(walletModel);
|
Image image = new Image(imageUrl, 50, 50, true, true);
|
||||||
imageBox.getChildren().add(walletModelImage);
|
if (!image.isError()) {
|
||||||
|
ImageView imageView = new ImageView();
|
||||||
|
imageView.setImage(image);
|
||||||
|
imageBox.getChildren().add(imageView);
|
||||||
|
}
|
||||||
|
|
||||||
VBox labelsBox = new VBox();
|
VBox labelsBox = new VBox();
|
||||||
labelsBox.setSpacing(5);
|
labelsBox.setSpacing(5);
|
||||||
|
|
|
||||||
|
|
@ -3,22 +3,19 @@ package com.sparrowwallet.sparrow.control;
|
||||||
import com.sparrowwallet.drongo.KeyPurpose;
|
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.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.UnitFormat;
|
||||||
|
import com.sparrowwallet.sparrow.AppServices;
|
||||||
|
import com.sparrowwallet.sparrow.EventManager;
|
||||||
|
import com.sparrowwallet.sparrow.Theme;
|
||||||
import com.sparrowwallet.sparrow.event.ExcludeUtxoEvent;
|
import com.sparrowwallet.sparrow.event.ExcludeUtxoEvent;
|
||||||
import com.sparrowwallet.sparrow.event.ReplaceChangeAddressEvent;
|
import com.sparrowwallet.sparrow.event.ReplaceChangeAddressEvent;
|
||||||
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
|
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
|
||||||
import com.sparrowwallet.sparrow.glyphfont.GlyphUtils;
|
import com.sparrowwallet.sparrow.glyphfont.GlyphUtils;
|
||||||
import com.sparrowwallet.sparrow.io.Config;
|
import com.sparrowwallet.sparrow.io.Config;
|
||||||
import com.sparrowwallet.sparrow.net.ExchangeSource;
|
|
||||||
import com.sparrowwallet.sparrow.wallet.OptimizationStrategy;
|
import com.sparrowwallet.sparrow.wallet.OptimizationStrategy;
|
||||||
import javafx.beans.property.BooleanProperty;
|
import javafx.beans.property.BooleanProperty;
|
||||||
import javafx.beans.property.ObjectProperty;
|
import javafx.beans.property.ObjectProperty;
|
||||||
|
|
@ -26,7 +23,6 @@ import javafx.beans.property.SimpleBooleanProperty;
|
||||||
import javafx.beans.property.SimpleObjectProperty;
|
import javafx.beans.property.SimpleObjectProperty;
|
||||||
import javafx.embed.swing.SwingFXUtils;
|
import javafx.embed.swing.SwingFXUtils;
|
||||||
import javafx.event.EventHandler;
|
import javafx.event.EventHandler;
|
||||||
import javafx.geometry.HPos;
|
|
||||||
import javafx.geometry.Insets;
|
import javafx.geometry.Insets;
|
||||||
import javafx.geometry.Pos;
|
import javafx.geometry.Pos;
|
||||||
import javafx.scene.Group;
|
import javafx.scene.Group;
|
||||||
|
|
@ -43,7 +39,10 @@ import javafx.scene.paint.Color;
|
||||||
import javafx.scene.shape.Circle;
|
import javafx.scene.shape.Circle;
|
||||||
import javafx.scene.shape.CubicCurve;
|
import javafx.scene.shape.CubicCurve;
|
||||||
import javafx.scene.shape.Line;
|
import javafx.scene.shape.Line;
|
||||||
import javafx.stage.*;
|
import javafx.stage.FileChooser;
|
||||||
|
import javafx.stage.Modality;
|
||||||
|
import javafx.stage.Stage;
|
||||||
|
import javafx.stage.StageStyle;
|
||||||
import javafx.util.Duration;
|
import javafx.util.Duration;
|
||||||
import org.controlsfx.glyphfont.Glyph;
|
import org.controlsfx.glyphfont.Glyph;
|
||||||
|
|
||||||
|
|
@ -108,7 +107,6 @@ public class TransactionDiagram extends GridPane {
|
||||||
expandedDiagram.setId("transactionDiagram");
|
expandedDiagram.setId("transactionDiagram");
|
||||||
expandedDiagram.setExpanded(true);
|
expandedDiagram.setExpanded(true);
|
||||||
expandedDiagram.setFinal(isFinal());
|
expandedDiagram.setFinal(isFinal());
|
||||||
expandedDiagram.setMaxWidth(AppServices.getActiveWindow().getWidth() - 200);
|
|
||||||
updateDerivedDiagram(expandedDiagram);
|
updateDerivedDiagram(expandedDiagram);
|
||||||
|
|
||||||
HBox buttonBox = new HBox();
|
HBox buttonBox = new HBox();
|
||||||
|
|
@ -126,7 +124,7 @@ public class TransactionDiagram extends GridPane {
|
||||||
AppServices.setStageIcon(stage);
|
AppServices.setStageIcon(stage);
|
||||||
stage.setScene(scene);
|
stage.setScene(scene);
|
||||||
stage.setOnShowing(e -> {
|
stage.setOnShowing(e -> {
|
||||||
AppServices.moveToActiveWindowScreen(stage, expandedDiagram.getMaxWidth(), 460);
|
AppServices.moveToActiveWindowScreen(stage, 600, 460);
|
||||||
});
|
});
|
||||||
stage.setOnHidden(e -> {
|
stage.setOnHidden(e -> {
|
||||||
expandedDiagram = null;
|
expandedDiagram = null;
|
||||||
|
|
@ -143,39 +141,6 @@ public class TransactionDiagram extends GridPane {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
public TransactionDiagram() {
|
|
||||||
ColumnConstraints col1 = new ColumnConstraints();
|
|
||||||
col1.setPrefWidth(22);
|
|
||||||
col1.setHgrow(Priority.NEVER);
|
|
||||||
|
|
||||||
ColumnConstraints col2 = new ColumnConstraints();
|
|
||||||
col2.setHgrow(Priority.ALWAYS);
|
|
||||||
col2.setPercentWidth(25);
|
|
||||||
col2.setFillWidth(true);
|
|
||||||
|
|
||||||
ColumnConstraints col3 = new ColumnConstraints();
|
|
||||||
col3.setPrefWidth(140);
|
|
||||||
col3.setHgrow(Priority.NEVER);
|
|
||||||
|
|
||||||
ColumnConstraints col4 = new ColumnConstraints();
|
|
||||||
Label label = new Label();
|
|
||||||
col4.setMinWidth(TextUtils.computeTextWidth(label.getFont(), "Transaction", 0) + 20);
|
|
||||||
col4.setHgrow(Priority.NEVER);
|
|
||||||
col4.setHalignment(HPos.CENTER);
|
|
||||||
|
|
||||||
ColumnConstraints col5 = new ColumnConstraints();
|
|
||||||
col5.setPrefWidth(140);
|
|
||||||
col5.setHgrow(Priority.NEVER);
|
|
||||||
|
|
||||||
ColumnConstraints col6 = new ColumnConstraints();
|
|
||||||
col6.setHgrow(Priority.ALWAYS);
|
|
||||||
col6.setPercentWidth(25);
|
|
||||||
col6.setFillWidth(true);
|
|
||||||
|
|
||||||
getColumnConstraints().addAll(col1, col2, col3, col4, col5, col6);
|
|
||||||
setPadding(new Insets(0, 0, 0, 40));
|
|
||||||
}
|
|
||||||
|
|
||||||
public void update(WalletTransaction walletTx) {
|
public void update(WalletTransaction walletTx) {
|
||||||
setMinHeight(getDiagramHeight());
|
setMinHeight(getDiagramHeight());
|
||||||
setMaxHeight(getDiagramHeight());
|
setMaxHeight(getDiagramHeight());
|
||||||
|
|
@ -204,7 +169,7 @@ public class TransactionDiagram extends GridPane {
|
||||||
|
|
||||||
VBox messagePane = new VBox();
|
VBox messagePane = new VBox();
|
||||||
messagePane.setPrefHeight(getDiagramHeight());
|
messagePane.setPrefHeight(getDiagramHeight());
|
||||||
messagePane.setPadding(new Insets(0, 10, 0, 10));
|
messagePane.setPadding(new Insets(0, 10, 0, 280));
|
||||||
messagePane.setAlignment(Pos.CENTER);
|
messagePane.setAlignment(Pos.CENTER);
|
||||||
messagePane.getChildren().add(createSpacer());
|
messagePane.getChildren().add(createSpacer());
|
||||||
|
|
||||||
|
|
@ -264,14 +229,6 @@ public class TransactionDiagram extends GridPane {
|
||||||
GridPane.setConstraints(outputsPane, 5, 0);
|
GridPane.setConstraints(outputsPane, 5, 0);
|
||||||
|
|
||||||
getChildren().clear();
|
getChildren().clear();
|
||||||
|
|
||||||
List<Payment> userPayments = getUserPayments();
|
|
||||||
if(!isFinal() && userPayments.size() > 1) {
|
|
||||||
Pane totalsPane = getTotalsPane(userPayments);
|
|
||||||
GridPane.setConstraints(totalsPane, 2, 0, 3, 1);
|
|
||||||
getChildren().add(totalsPane);
|
|
||||||
}
|
|
||||||
|
|
||||||
getChildren().addAll(inputsTypePane, inputsPane, inputsLinesPane, txPane, outputsLinesPane, outputsPane);
|
getChildren().addAll(inputsTypePane, inputsPane, inputsLinesPane, txPane, outputsLinesPane, outputsPane);
|
||||||
|
|
||||||
if(contextMenu == null) {
|
if(contextMenu == null) {
|
||||||
|
|
@ -447,6 +404,8 @@ public class TransactionDiagram extends GridPane {
|
||||||
|
|
||||||
private Pane getInputsLabels(List<Map<BlockTransactionHashIndex, WalletNode>> displayedUtxoSets) {
|
private Pane getInputsLabels(List<Map<BlockTransactionHashIndex, WalletNode>> displayedUtxoSets) {
|
||||||
VBox inputsBox = new VBox();
|
VBox inputsBox = new VBox();
|
||||||
|
inputsBox.setMaxWidth(isExpanded() ? 300 : 150);
|
||||||
|
inputsBox.setPrefWidth(isExpanded() ? 230 : 150);
|
||||||
inputsBox.setPadding(new Insets(0, 10, 0, 10));
|
inputsBox.setPadding(new Insets(0, 10, 0, 10));
|
||||||
inputsBox.minHeightProperty().bind(minHeightProperty());
|
inputsBox.minHeightProperty().bind(minHeightProperty());
|
||||||
inputsBox.setAlignment(Pos.BASELINE_RIGHT);
|
inputsBox.setAlignment(Pos.BASELINE_RIGHT);
|
||||||
|
|
@ -531,11 +490,6 @@ public class TransactionDiagram extends GridPane {
|
||||||
}
|
}
|
||||||
tooltip.setShowDelay(new Duration(TOOLTIP_SHOW_DELAY));
|
tooltip.setShowDelay(new Duration(TOOLTIP_SHOW_DELAY));
|
||||||
tooltip.setShowDuration(Duration.INDEFINITE);
|
tooltip.setShowDuration(Duration.INDEFINITE);
|
||||||
tooltip.setWrapText(true);
|
|
||||||
Window activeWindow = AppServices.getActiveWindow();
|
|
||||||
if(activeWindow != null) {
|
|
||||||
tooltip.setMaxWidth(activeWindow.getWidth());
|
|
||||||
}
|
|
||||||
if(!tooltip.getText().isEmpty()) {
|
if(!tooltip.getText().isEmpty()) {
|
||||||
label.setTooltip(tooltip);
|
label.setTooltip(tooltip);
|
||||||
}
|
}
|
||||||
|
|
@ -659,10 +613,6 @@ public class TransactionDiagram extends GridPane {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private List<Payment> getUserPayments() {
|
|
||||||
return walletTx.getPayments().stream().filter(payment -> payment.getType() == Payment.Type.DEFAULT || payment.getType() == Payment.Type.ANCHOR).toList();
|
|
||||||
}
|
|
||||||
|
|
||||||
private Pane getOutputsLines(List<Payment> displayedPayments) {
|
private Pane getOutputsLines(List<Payment> displayedPayments) {
|
||||||
VBox pane = new VBox();
|
VBox pane = new VBox();
|
||||||
Group group = new Group();
|
Group group = new Group();
|
||||||
|
|
@ -678,8 +628,7 @@ public class TransactionDiagram extends GridPane {
|
||||||
|
|
||||||
double width = 140.0;
|
double width = 140.0;
|
||||||
long sum = walletTx.getTotal();
|
long sum = walletTx.getTotal();
|
||||||
List<Long> values = walletTx.getOutputs().stream().filter(output -> !(output instanceof WalletTransaction.NonAddressOutput))
|
List<Long> values = walletTx.getTransaction().getOutputs().stream().filter(txo -> txo.getScript().getToAddress() != null).map(TransactionOutput::getValue).collect(Collectors.toList());
|
||||||
.map(output -> output.getTransactionOutput().getValue()).collect(Collectors.toList());
|
|
||||||
values.add(walletTx.getFee());
|
values.add(walletTx.getFee());
|
||||||
int numOutputs = displayedPayments.size() + walletTx.getChangeMap().size() + 1;
|
int numOutputs = displayedPayments.size() + walletTx.getChangeMap().size() + 1;
|
||||||
for(int i = 1; i <= numOutputs; i++) {
|
for(int i = 1; i <= numOutputs; i++) {
|
||||||
|
|
@ -715,6 +664,8 @@ public class TransactionDiagram extends GridPane {
|
||||||
|
|
||||||
private Pane getOutputsLabels(List<Payment> displayedPayments) {
|
private Pane getOutputsLabels(List<Payment> displayedPayments) {
|
||||||
VBox outputsBox = new VBox();
|
VBox outputsBox = new VBox();
|
||||||
|
outputsBox.setMaxWidth(isExpanded() ? 350 : 150);
|
||||||
|
outputsBox.setPrefWidth(isExpanded() ? 230 : 150);
|
||||||
outputsBox.setPadding(new Insets(0, 20, 0, 10));
|
outputsBox.setPadding(new Insets(0, 20, 0, 10));
|
||||||
outputsBox.setAlignment(Pos.BASELINE_LEFT);
|
outputsBox.setAlignment(Pos.BASELINE_LEFT);
|
||||||
outputsBox.getChildren().add(createSpacer());
|
outputsBox.getChildren().add(createSpacer());
|
||||||
|
|
@ -722,26 +673,20 @@ public class TransactionDiagram extends GridPane {
|
||||||
List<OutputNode> outputNodes = new ArrayList<>();
|
List<OutputNode> outputNodes = new ArrayList<>();
|
||||||
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").contains(style)) || payment instanceof AdditionalPayment;
|
||||||
Label recipientLabel = new Label(payment.getLabel() == null || payment.getType() == Payment.Type.FAKE_MIX || payment.getType() == Payment.Type.MIX ? payment.toString().substring(0, 8) + "..." : payment.getLabel(), outputGlyph);
|
Label recipientLabel = new Label(payment.getLabel() == null || payment.getType() == Payment.Type.FAKE_MIX || payment.getType() == Payment.Type.MIX ? payment.getAddress().toString().substring(0, 8) + "..." : payment.getLabel(), outputGlyph);
|
||||||
recipientLabel.getStyleClass().add("output-label");
|
recipientLabel.getStyleClass().add("output-label");
|
||||||
recipientLabel.getStyleClass().add(labelledPayment ? "payment-label" : "recipient-label");
|
recipientLabel.getStyleClass().add(labelledPayment ? "payment-label" : "recipient-label");
|
||||||
Wallet toWallet = walletTx.getToWallet(AppServices.get().getOpenWallets().keySet(), payment);
|
Wallet toWallet = walletTx.getToWallet(AppServices.get().getOpenWallets().keySet(), payment);
|
||||||
WalletNode toNode = payment instanceof WalletNodePayment walletNodePayment ? walletNodePayment.getWalletNode() : null;
|
WalletNode toNode = walletTx.getWallet() != null && !walletTx.getWallet().isBip47() ? walletTx.getAddressNodeMap().get(payment.getAddress()) : null;
|
||||||
Wallet toBip47Wallet = getBip47SendWallet(payment);
|
Wallet toBip47Wallet = getBip47SendWallet(payment);
|
||||||
DnsPayment dnsPayment = DnsPaymentCache.getDnsPayment(payment);
|
|
||||||
Tooltip recipientTooltip = new Tooltip((toWallet == null ? (toNode != null ? "Consolidate " : "Pay ") : "Receive ")
|
Tooltip recipientTooltip = new Tooltip((toWallet == null ? (toNode != null ? "Consolidate " : "Pay ") : "Receive ")
|
||||||
+ getSatsValue(payment.getAmount()) + " sats to "
|
+ getSatsValue(payment.getAmount()) + " sats to "
|
||||||
+ (payment instanceof AdditionalPayment ? (isExpanded() ? "\n" : "(click to expand)\n") + payment : (toWallet == null ? (dnsPayment == null ? (payment.getLabel() == null ? (toNode != null ? toNode : (toBip47Wallet == null ? "external address" : toBip47Wallet.getDisplayName())) : payment.getLabel()) : dnsPayment.toString()) : toWallet.getFullDisplayName()) + "\n" + payment.getDisplayAddress())
|
+ (payment instanceof AdditionalPayment ? (isExpanded() ? "\n" : "(click to expand)\n") + payment : (toWallet == null ? (payment.getLabel() == null ? (toNode != null ? toNode : (toBip47Wallet == null ? "external address" : toBip47Wallet.getDisplayName())) : payment.getLabel()) : toWallet.getFullDisplayName()) + "\n" + payment.getAddress().toString())
|
||||||
+ (walletTx.isDuplicateAddress(payment) ? " (Duplicate)" : ""));
|
+ (walletTx.isDuplicateAddress(payment) ? " (Duplicate)" : ""));
|
||||||
recipientTooltip.getStyleClass().add("recipient-label");
|
recipientTooltip.getStyleClass().add("recipient-label");
|
||||||
recipientTooltip.setShowDelay(new Duration(TOOLTIP_SHOW_DELAY));
|
recipientTooltip.setShowDelay(new Duration(TOOLTIP_SHOW_DELAY));
|
||||||
recipientTooltip.setShowDuration(Duration.INDEFINITE);
|
recipientTooltip.setShowDuration(Duration.INDEFINITE);
|
||||||
recipientTooltip.setWrapText(true);
|
|
||||||
Window activeWindow = AppServices.getActiveWindow();
|
|
||||||
if(activeWindow != null) {
|
|
||||||
recipientTooltip.setMaxWidth(activeWindow.getWidth());
|
|
||||||
}
|
|
||||||
recipientLabel.setTooltip(recipientTooltip);
|
recipientLabel.setTooltip(recipientTooltip);
|
||||||
HBox paymentBox = new HBox();
|
HBox paymentBox = new HBox();
|
||||||
paymentBox.setAlignment(Pos.CENTER_LEFT);
|
paymentBox.setAlignment(Pos.CENTER_LEFT);
|
||||||
|
|
@ -757,13 +702,7 @@ public class TransactionDiagram extends GridPane {
|
||||||
paymentBox.getChildren().addAll(region, amountLabel);
|
paymentBox.getChildren().addAll(region, amountLabel);
|
||||||
}
|
}
|
||||||
|
|
||||||
if(payment instanceof SilentPayment silentPayment) {
|
outputNodes.add(new OutputNode(paymentBox, payment.getAddress(), payment.getAmount()));
|
||||||
outputNodes.add(new OutputNode(paymentBox, silentPayment.isAddressComputed() ? silentPayment.getAddress() : null, payment.getAmount(), null, silentPayment.getSilentPaymentAddress()));
|
|
||||||
} else {
|
|
||||||
Wallet bip47Wallet = toWallet != null && toWallet.isBip47() ? toWallet : (toBip47Wallet != null && toBip47Wallet.isBip47() ? toBip47Wallet : null);
|
|
||||||
PaymentCode paymentCode = bip47Wallet == null ? null : bip47Wallet.getKeystores().getFirst().getExternalPaymentCode();
|
|
||||||
outputNodes.add(new OutputNode(paymentBox, payment.getAddress(), payment.getAmount(), paymentCode, null));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Set<Integer> seenIndexes = new HashSet<>();
|
Set<Integer> seenIndexes = new HashSet<>();
|
||||||
|
|
@ -827,7 +766,7 @@ public class TransactionDiagram extends GridPane {
|
||||||
outputsBox.getChildren().add(outputNode.outputLabel);
|
outputsBox.getChildren().add(outputNode.outputLabel);
|
||||||
outputsBox.getChildren().add(createSpacer());
|
outputsBox.getChildren().add(createSpacer());
|
||||||
|
|
||||||
ContextMenu contextMenu = new LabelContextMenu(outputNode.address, outputNode.amount, outputNode.paymentCode, outputNode.silentPaymentAddress);
|
ContextMenu contextMenu = new LabelContextMenu(outputNode.address, outputNode.amount);
|
||||||
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);
|
||||||
}
|
}
|
||||||
|
|
@ -836,7 +775,7 @@ public class TransactionDiagram extends GridPane {
|
||||||
boolean highFee = (walletTx.getFeePercentage() > 0.1);
|
boolean highFee = (walletTx.getFeePercentage() > 0.1);
|
||||||
Label feeLabel = highFee ? new Label("High Fee", getFeeWarningGlyph()) : new Label("Fee", getFeeGlyph());
|
Label feeLabel = highFee ? new Label("High Fee", getFeeWarningGlyph()) : new Label("Fee", getFeeGlyph());
|
||||||
feeLabel.getStyleClass().addAll("output-label", "fee-label");
|
feeLabel.getStyleClass().addAll("output-label", "fee-label");
|
||||||
String percentage = walletTx.getFeePercentage() < 0.0001d ? "<0.01" : String.format("%.2f", walletTx.getFeePercentage() * 100.0);
|
String percentage = String.format("%.2f", walletTx.getFeePercentage() * 100.0);
|
||||||
Tooltip feeTooltip = new Tooltip(walletTx.getFee() < 0 ? "Unknown fee" : "Fee of " + getSatsValue(walletTx.getFee()) + " sats (" + percentage + "%)");
|
Tooltip feeTooltip = new Tooltip(walletTx.getFee() < 0 ? "Unknown fee" : "Fee of " + getSatsValue(walletTx.getFee()) + " sats (" + percentage + "%)");
|
||||||
feeTooltip.getStyleClass().add("fee-tooltip");
|
feeTooltip.getStyleClass().add("fee-tooltip");
|
||||||
feeTooltip.setShowDelay(new Duration(TOOLTIP_SHOW_DELAY));
|
feeTooltip.setShowDelay(new Duration(TOOLTIP_SHOW_DELAY));
|
||||||
|
|
@ -890,33 +829,6 @@ public class TransactionDiagram extends GridPane {
|
||||||
return txPane;
|
return txPane;
|
||||||
}
|
}
|
||||||
|
|
||||||
private Pane getTotalsPane(List<Payment> userPayments) {
|
|
||||||
VBox totalsBox = new VBox();
|
|
||||||
totalsBox.setPadding(new Insets(0, 0, 15, 0));
|
|
||||||
totalsBox.setAlignment(Pos.CENTER);
|
|
||||||
|
|
||||||
long amount = userPayments.stream().mapToLong(Payment::getAmount).sum();
|
|
||||||
|
|
||||||
HBox coinLabelBox = new HBox();
|
|
||||||
coinLabelBox.setAlignment(Pos.CENTER);
|
|
||||||
CoinLabel totalCoinLabel = new CoinLabel();
|
|
||||||
totalCoinLabel.setValue(amount);
|
|
||||||
coinLabelBox.getChildren().addAll(totalCoinLabel, new Label(" in "), new Label(Long.toString(userPayments.size())), new Label(" payments"));
|
|
||||||
totalsBox.getChildren().addAll(createSpacer(), coinLabelBox);
|
|
||||||
|
|
||||||
CurrencyRate currencyRate = AppServices.getFiatCurrencyExchangeRate();
|
|
||||||
if(currencyRate != null && currencyRate.isAvailable() && Config.get().getExchangeSource() != ExchangeSource.NONE) {
|
|
||||||
HBox fiatLabelBox = new HBox();
|
|
||||||
fiatLabelBox.setAlignment(Pos.CENTER);
|
|
||||||
FiatLabel fiatLabel = new FiatLabel();
|
|
||||||
fiatLabel.set(currencyRate, amount);
|
|
||||||
fiatLabelBox.getChildren().add(fiatLabel);
|
|
||||||
totalsBox.getChildren().add(fiatLabelBox);
|
|
||||||
}
|
|
||||||
|
|
||||||
return totalsBox;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void saveAsImage() {
|
private void saveAsImage() {
|
||||||
Stage window = new Stage();
|
Stage window = new Stage();
|
||||||
FileChooser fileChooser = new FileChooser();
|
FileChooser fileChooser = new FileChooser();
|
||||||
|
|
@ -1002,11 +914,8 @@ public class TransactionDiagram extends GridPane {
|
||||||
}
|
}
|
||||||
|
|
||||||
private int getOutputIndex(Address address, long amount, Collection<Integer> seenIndexes) {
|
private int getOutputIndex(Address address, long amount, Collection<Integer> seenIndexes) {
|
||||||
List<TransactionOutput> addressOutputs = walletTx.getOutputs().stream().filter(output -> !(output instanceof WalletTransaction.NonAddressOutput))
|
List<TransactionOutput> addressOutputs = walletTx.getTransaction().getOutputs().stream().filter(txOutput -> txOutput.getScript().getToAddress() != null).collect(Collectors.toList());
|
||||||
.map(WalletTransaction.Output::getTransactionOutput).collect(Collectors.toList());
|
TransactionOutput output = addressOutputs.stream().filter(txOutput -> address.equals(txOutput.getScript().getToAddress()) && txOutput.getValue() == amount && !seenIndexes.contains(txOutput.getIndex())).findFirst().orElseThrow();
|
||||||
TransactionOutput output = addressOutputs.stream()
|
|
||||||
.filter(txOutput -> address.equals(txOutput.getScript().getToAddress()) && txOutput.getValue() == amount && !seenIndexes.contains(txOutput.getIndex()))
|
|
||||||
.findFirst().orElseThrow();
|
|
||||||
return addressOutputs.indexOf(output);
|
return addressOutputs.indexOf(output);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1156,7 +1065,7 @@ public class TransactionDiagram extends GridPane {
|
||||||
}
|
}
|
||||||
|
|
||||||
public String toString() {
|
public String toString() {
|
||||||
return additionalPayments.stream().map(Payment::toString).collect(Collectors.joining("\n"));
|
return additionalPayments.stream().map(payment -> payment.getAddress().toString()).collect(Collectors.joining("\n"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1164,28 +1073,16 @@ public class TransactionDiagram extends GridPane {
|
||||||
public Pane outputLabel;
|
public Pane outputLabel;
|
||||||
public Address address;
|
public Address address;
|
||||||
public long amount;
|
public long amount;
|
||||||
public PaymentCode paymentCode;
|
|
||||||
public SilentPaymentAddress silentPaymentAddress;
|
|
||||||
|
|
||||||
public OutputNode(Pane outputLabel, Address address, long amount) {
|
public OutputNode(Pane outputLabel, Address address, long amount) {
|
||||||
this(outputLabel, address, amount, null, null);
|
|
||||||
}
|
|
||||||
|
|
||||||
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.silentPaymentAddress = silentPaymentAddress;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private class LabelContextMenu extends ContextMenu {
|
private class LabelContextMenu extends ContextMenu {
|
||||||
public LabelContextMenu(Address address, long value) {
|
public LabelContextMenu(Address address, long value) {
|
||||||
this(address, value, null, null);
|
|
||||||
}
|
|
||||||
|
|
||||||
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 -> {
|
||||||
|
|
@ -1222,28 +1119,6 @@ public class TransactionDiagram extends GridPane {
|
||||||
Clipboard.getSystemClipboard().setContent(content);
|
Clipboard.getSystemClipboard().setContent(content);
|
||||||
});
|
});
|
||||||
getItems().addAll(copySatsValue, copyBtcValue);
|
getItems().addAll(copySatsValue, copyBtcValue);
|
||||||
|
|
||||||
if(paymentCode != null) {
|
|
||||||
MenuItem copyPaymentCode = new MenuItem("Copy Payment Code");
|
|
||||||
copyPaymentCode.setOnAction(AE -> {
|
|
||||||
hide();
|
|
||||||
ClipboardContent content = new ClipboardContent();
|
|
||||||
content.putString(paymentCode.toString());
|
|
||||||
Clipboard.getSystemClipboard().setContent(content);
|
|
||||||
});
|
|
||||||
getItems().add(copyPaymentCode);
|
|
||||||
}
|
|
||||||
|
|
||||||
if(silentPaymentAddress != null) {
|
|
||||||
MenuItem copySilentPaymentAddress = new MenuItem("Copy Silent Payment Address");
|
|
||||||
copySilentPaymentAddress.setOnAction(AE -> {
|
|
||||||
hide();
|
|
||||||
ClipboardContent content = new ClipboardContent();
|
|
||||||
content.putString(silentPaymentAddress.toString());
|
|
||||||
Clipboard.getSystemClipboard().setContent(content);
|
|
||||||
});
|
|
||||||
getItems().add(copySilentPaymentAddress);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -90,20 +90,20 @@ public class TransactionDiagramLabel extends HBox {
|
||||||
outputLabels.add(mixOutputLabel);
|
outputLabels.add(mixOutputLabel);
|
||||||
}
|
}
|
||||||
} else if(walletTx.getPayments().size() >= 5 && walletTx.getPayments().stream().mapToLong(Payment::getAmount).distinct().count() <= 1 && walletTx.getWallet() != null
|
} else if(walletTx.getPayments().size() >= 5 && walletTx.getPayments().stream().mapToLong(Payment::getAmount).distinct().count() <= 1 && walletTx.getWallet() != null
|
||||||
&& walletTx.getWallet().getStandardAccountType() == StandardAccount.WHIRLPOOL_POSTMIX && !walletTx.getWalletNodePayments().isEmpty()) {
|
&& walletTx.getWallet().getStandardAccountType() == StandardAccount.WHIRLPOOL_POSTMIX && walletTx.getPayments().stream().anyMatch(walletTx::isConsolidationSend)) {
|
||||||
OutputLabel remixOutputLabel = getRemixOutputLabel(transactionDiagram, walletTx.getPayments());
|
OutputLabel remixOutputLabel = getRemixOutputLabel(transactionDiagram, walletTx.getPayments());
|
||||||
if(remixOutputLabel != null) {
|
if(remixOutputLabel != null) {
|
||||||
outputLabels.add(remixOutputLabel);
|
outputLabels.add(remixOutputLabel);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
List<Payment> payments = walletTx.getExternalPayments().stream().filter(payment -> payment.getType() == Payment.Type.DEFAULT).collect(Collectors.toList());
|
List<Payment> payments = walletTx.getPayments().stream().filter(payment -> payment.getType() == Payment.Type.DEFAULT && !walletTx.isConsolidationSend(payment)).collect(Collectors.toList());
|
||||||
List<OutputLabel> paymentLabels = payments.stream().map(payment -> getOutputLabel(transactionDiagram, payment)).collect(Collectors.toList());
|
List<OutputLabel> paymentLabels = payments.stream().map(payment -> getOutputLabel(transactionDiagram, payment)).collect(Collectors.toList());
|
||||||
if(walletTx.getSelectedUtxos().values().stream().allMatch(Objects::isNull)) {
|
if(walletTx.getSelectedUtxos().values().stream().allMatch(Objects::isNull)) {
|
||||||
paymentLabels.sort(Comparator.comparingInt(paymentLabel -> (paymentLabel.text.startsWith("Receive") ? 0 : 1)));
|
paymentLabels.sort(Comparator.comparingInt(paymentLabel -> (paymentLabel.text.startsWith("Receive") ? 0 : 1)));
|
||||||
}
|
}
|
||||||
outputLabels.addAll(paymentLabels);
|
outputLabels.addAll(paymentLabels);
|
||||||
|
|
||||||
List<Payment> consolidations = walletTx.getWalletNodePayments().stream().filter(payment -> payment.getType() == Payment.Type.DEFAULT).collect(Collectors.toList());
|
List<Payment> consolidations = walletTx.getPayments().stream().filter(payment -> payment.getType() == Payment.Type.DEFAULT && walletTx.isConsolidationSend(payment)).collect(Collectors.toList());
|
||||||
outputLabels.addAll(consolidations.stream().map(consolidation -> getOutputLabel(transactionDiagram, consolidation)).collect(Collectors.toList()));
|
outputLabels.addAll(consolidations.stream().map(consolidation -> getOutputLabel(transactionDiagram, consolidation)).collect(Collectors.toList()));
|
||||||
|
|
||||||
List<Payment> mixes = walletTx.getPayments().stream().filter(payment -> payment.getType() == Payment.Type.MIX || payment.getType() == Payment.Type.FAKE_MIX).collect(Collectors.toList());
|
List<Payment> mixes = walletTx.getPayments().stream().filter(payment -> payment.getType() == Payment.Type.MIX || payment.getType() == Payment.Type.FAKE_MIX).collect(Collectors.toList());
|
||||||
|
|
@ -203,10 +203,10 @@ public class TransactionDiagramLabel extends HBox {
|
||||||
private OutputLabel getOutputLabel(TransactionDiagram transactionDiagram, Payment payment) {
|
private OutputLabel getOutputLabel(TransactionDiagram transactionDiagram, Payment payment) {
|
||||||
WalletTransaction walletTx = transactionDiagram.getWalletTransaction();
|
WalletTransaction walletTx = transactionDiagram.getWalletTransaction();
|
||||||
Wallet toWallet = walletTx.getToWallet(AppServices.get().getOpenWallets().keySet(), payment);
|
Wallet toWallet = walletTx.getToWallet(AppServices.get().getOpenWallets().keySet(), payment);
|
||||||
WalletNode toNode = payment instanceof WalletNodePayment walletNodePayment ? walletNodePayment.getWalletNode() : null;
|
WalletNode toNode = walletTx.getWallet() != null && !walletTx.getWallet().isBip47() ? walletTx.getAddressNodeMap().get(payment.getAddress()) : null;
|
||||||
|
|
||||||
Glyph glyph = GlyphUtils.getOutputGlyph(transactionDiagram.getWalletTransaction(), payment);
|
Glyph glyph = GlyphUtils.getOutputGlyph(transactionDiagram.getWalletTransaction(), payment);
|
||||||
String text = (toWallet == null ? (toNode != null ? "Consolidate " : "Pay ") : "Receive ") + transactionDiagram.getSatsValue(payment.getAmount()) + " sats to " + payment;
|
String text = (toWallet == null ? (toNode != null ? "Consolidate " : "Pay ") : "Receive ") + transactionDiagram.getSatsValue(payment.getAmount()) + " sats to " + payment.getAddress().toString();
|
||||||
|
|
||||||
return getOutputLabel(glyph, text);
|
return getOutputLabel(glyph, text);
|
||||||
}
|
}
|
||||||
|
|
@ -227,8 +227,7 @@ public class TransactionDiagramLabel extends HBox {
|
||||||
}
|
}
|
||||||
|
|
||||||
Glyph glyph = GlyphUtils.getFeeGlyph();
|
Glyph glyph = GlyphUtils.getFeeGlyph();
|
||||||
String percentage = walletTx.getFeePercentage() < 0.0001d ? "<0.01" : String.format("%.2f", walletTx.getFeePercentage() * 100.0);
|
String text = "Fee of " + transactionDiagram.getSatsValue(walletTx.getFee()) + " sats (" + String.format("%.2f", walletTx.getFeePercentage() * 100.0) + "%)";
|
||||||
String text = "Fee of " + transactionDiagram.getSatsValue(walletTx.getFee()) + " sats (" + percentage + "%)";
|
|
||||||
|
|
||||||
return getOutputLabel(glyph, text);
|
return getOutputLabel(glyph, text);
|
||||||
}
|
}
|
||||||
|
|
@ -240,7 +239,7 @@ public class TransactionDiagramLabel extends HBox {
|
||||||
icon.setGraphic(glyph);
|
icon.setGraphic(glyph);
|
||||||
|
|
||||||
CopyableLabel label = new CopyableLabel();
|
CopyableLabel label = new CopyableLabel();
|
||||||
label.setFont(Font.font("Fragment Mono Italic", 13));
|
label.setFont(Font.font("Roboto Mono Italic", 13));
|
||||||
label.setText(text);
|
label.setText(text);
|
||||||
|
|
||||||
HBox output = new HBox(5);
|
HBox output = new HBox(5);
|
||||||
|
|
|
||||||
|
|
@ -28,8 +28,7 @@ public class UsbStatusButton extends MenuButton {
|
||||||
for(Device device : devices) {
|
for(Device device : devices) {
|
||||||
MenuItem deviceItem = new MenuItem(device.getModel().toDisplayString());
|
MenuItem deviceItem = new MenuItem(device.getModel().toDisplayString());
|
||||||
if(!device.isNeedsPinSent() && (device.getModel() == WalletModel.TREZOR_1 || device.getModel() == WalletModel.TREZOR_T || device.getModel() == WalletModel.TREZOR_SAFE_3 ||
|
if(!device.isNeedsPinSent() && (device.getModel() == WalletModel.TREZOR_1 || device.getModel() == WalletModel.TREZOR_T || device.getModel() == WalletModel.TREZOR_SAFE_3 ||
|
||||||
device.getModel() == WalletModel.TREZOR_SAFE_5 || device.getModel() == WalletModel.KEEPKEY || device.getModel() == WalletModel.BITBOX_02 ||
|
device.getModel() == WalletModel.TREZOR_SAFE_5 || device.getModel() == WalletModel.KEEPKEY || device.getModel() == WalletModel.BITBOX_02)) {
|
||||||
device.getModel() == WalletModel.ONEKEY_CLASSIC_1S || device.getModel() == WalletModel.ONEKEY_PRO)) {
|
|
||||||
deviceItem = new Menu(device.getModel().toDisplayString());
|
deviceItem = new Menu(device.getModel().toDisplayString());
|
||||||
MenuItem toggleItem = new MenuItem("Toggle Passphrase" + (!device.getModel().externalPassphraseEntry() ? "" : (device.isNeedsPassphraseSent() ? " Off" : " On")));
|
MenuItem toggleItem = new MenuItem("Toggle Passphrase" + (!device.getModel().externalPassphraseEntry() ? "" : (device.isNeedsPassphraseSent() ? " Off" : " On")));
|
||||||
toggleItem.setOnAction(event -> {
|
toggleItem.setOnAction(event -> {
|
||||||
|
|
|
||||||
|
|
@ -18,8 +18,8 @@ import java.util.List;
|
||||||
public class WalletExportDialog extends Dialog<Wallet> {
|
public class WalletExportDialog extends Dialog<Wallet> {
|
||||||
private Wallet wallet;
|
private Wallet wallet;
|
||||||
|
|
||||||
public WalletExportDialog(WalletForm selectedWalletForm, List<WalletForm> allWalletForms) {
|
public WalletExportDialog(WalletForm walletForm) {
|
||||||
this.wallet = selectedWalletForm.getWallet();
|
this.wallet = walletForm.getWallet();
|
||||||
|
|
||||||
EventManager.get().register(this);
|
EventManager.get().register(this);
|
||||||
setOnCloseRequest(event -> {
|
setOnCloseRequest(event -> {
|
||||||
|
|
@ -45,10 +45,10 @@ public class WalletExportDialog extends Dialog<Wallet> {
|
||||||
|
|
||||||
List<WalletExport> exporters;
|
List<WalletExport> exporters;
|
||||||
if(wallet.getPolicyType() == PolicyType.SINGLE) {
|
if(wallet.getPolicyType() == PolicyType.SINGLE) {
|
||||||
exporters = List.of(new Electrum(), new ElectrumPersonalServer(), new Descriptor(), new SpecterDesktop(), new Sparrow(), new WalletLabels(allWalletForms), new WalletTransactions(selectedWalletForm));
|
exporters = List.of(new Electrum(), new ElectrumPersonalServer(), new Descriptor(), new SpecterDesktop(), new Sparrow(), new WalletLabels(), new WalletTransactions(walletForm));
|
||||||
} else if(wallet.getPolicyType() == PolicyType.MULTI) {
|
} else if(wallet.getPolicyType() == PolicyType.MULTI) {
|
||||||
exporters = List.of(new Bip129(), new CaravanMultisig(), new ColdcardMultisig(), new CoboVaultMultisig(), new Electrum(), new ElectrumPersonalServer(), new KeystoneMultisig(),
|
exporters = List.of(new Bip129(), new CaravanMultisig(), new ColdcardMultisig(), new CoboVaultMultisig(), new Electrum(), new ElectrumPersonalServer(), new KeystoneMultisig(),
|
||||||
new Descriptor(), new JadeMultisig(), new PassportMultisig(), new SpecterDesktop(), new BlueWalletMultisig(), new SpecterDIY(), new Sparrow(), new WalletLabels(allWalletForms), new WalletTransactions(selectedWalletForm));
|
new Descriptor(), new JadeMultisig(), new PassportMultisig(), new SpecterDesktop(), new BlueWalletMultisig(), new SpecterDIY(), new Sparrow(), new WalletLabels(), new WalletTransactions(walletForm));
|
||||||
} else {
|
} else {
|
||||||
throw new UnsupportedOperationException("Cannot export wallet with policy type " + wallet.getPolicyType());
|
throw new UnsupportedOperationException("Cannot export wallet with policy type " + wallet.getPolicyType());
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -74,15 +74,33 @@ public class WalletIcon extends StackPane {
|
||||||
|
|
||||||
SVGImage svgImage;
|
SVGImage svgImage;
|
||||||
if(Config.get().getTheme() == Theme.DARK) {
|
if(Config.get().getTheme() == Theme.DARK) {
|
||||||
svgImage = loadSVGImage("/image/walletmodel/" + walletModel.getType() + "-icon-invert.svg");
|
svgImage = loadSVGImage("/image/" + walletModel.getType() + "-icon-invert.svg");
|
||||||
} else {
|
} else {
|
||||||
svgImage = loadSVGImage("/image/walletmodel/" + walletModel.getType() + "-icon.svg");
|
svgImage = loadSVGImage("/image/" + walletModel.getType() + "-icon.svg");
|
||||||
}
|
}
|
||||||
|
|
||||||
if(svgImage != null) {
|
if(svgImage != null) {
|
||||||
getChildren().add(svgImage);
|
getChildren().add(svgImage);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Image image = null;
|
||||||
|
if(Config.get().getTheme() == Theme.DARK) {
|
||||||
|
image = loadImage("image/" + walletModel.getType() + "-icon-invert.png");
|
||||||
|
}
|
||||||
|
|
||||||
|
if(image == null) {
|
||||||
|
image = loadImage("image/" + walletModel.getType() + "-icon.png");
|
||||||
|
}
|
||||||
|
|
||||||
|
if(image == null) {
|
||||||
|
image = loadImage("image/" + walletModel.getType() + ".png");
|
||||||
|
}
|
||||||
|
|
||||||
|
if(image != null && !image.isError()) {
|
||||||
|
ImageView imageView = new ImageView(image);
|
||||||
|
getChildren().add(imageView);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -109,6 +127,16 @@ public class WalletIcon extends StackPane {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private Image loadImage(String imageName) {
|
||||||
|
try {
|
||||||
|
return new Image(imageName, 15, 15, true, true);
|
||||||
|
} catch(Exception e) {
|
||||||
|
//ignore
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
private void addWalletIcon(String walletId) {
|
private void addWalletIcon(String walletId) {
|
||||||
Image image = new Image(PROTOCOL + ":" + walletId.replaceAll(" ", "%20").replaceAll("#", "%23") + "?" + QUERY, WIDTH, HEIGHT, true, false);
|
Image image = new Image(PROTOCOL + ":" + walletId.replaceAll(" ", "%20").replaceAll("#", "%23") + "?" + QUERY, WIDTH, HEIGHT, true, false);
|
||||||
getChildren().clear();
|
getChildren().clear();
|
||||||
|
|
|
||||||
|
|
@ -1,78 +0,0 @@
|
||||||
package com.sparrowwallet.sparrow.control;
|
|
||||||
|
|
||||||
import com.sparrowwallet.drongo.wallet.WalletModel;
|
|
||||||
import com.sparrowwallet.sparrow.AppServices;
|
|
||||||
import com.sparrowwallet.sparrow.Theme;
|
|
||||||
import com.sparrowwallet.sparrow.io.Config;
|
|
||||||
import javafx.beans.NamedArg;
|
|
||||||
import javafx.beans.property.ObjectProperty;
|
|
||||||
import javafx.beans.property.SimpleObjectProperty;
|
|
||||||
import javafx.scene.image.Image;
|
|
||||||
import javafx.scene.layout.StackPane;
|
|
||||||
import org.girod.javafx.svgimage.SVGImage;
|
|
||||||
import org.girod.javafx.svgimage.SVGLoader;
|
|
||||||
import org.slf4j.Logger;
|
|
||||||
import org.slf4j.LoggerFactory;
|
|
||||||
|
|
||||||
import java.net.URL;
|
|
||||||
|
|
||||||
public class WalletModelImage extends StackPane {
|
|
||||||
private static final Logger log = LoggerFactory.getLogger(WalletModelImage.class);
|
|
||||||
|
|
||||||
public static final int WIDTH = 50;
|
|
||||||
public static final int HEIGHT = 50;
|
|
||||||
|
|
||||||
private final ObjectProperty<WalletModel> walletModelProperty = new SimpleObjectProperty<>();
|
|
||||||
|
|
||||||
public WalletModelImage() {
|
|
||||||
setPrefSize(WIDTH, HEIGHT);
|
|
||||||
walletModelProperty.addListener((observable, oldValue, walletModel) -> {
|
|
||||||
refresh(walletModel);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public WalletModelImage(@NamedArg("walletModel") WalletModel walletModel) {
|
|
||||||
this();
|
|
||||||
walletModelProperty.set(walletModel);
|
|
||||||
}
|
|
||||||
|
|
||||||
public WalletModel getWalletModel() {
|
|
||||||
return walletModelProperty.get();
|
|
||||||
}
|
|
||||||
|
|
||||||
public ObjectProperty<WalletModel> walletModelProperty() {
|
|
||||||
return walletModelProperty;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void refresh() {
|
|
||||||
WalletModel walletModel = getWalletModel();
|
|
||||||
refresh(walletModel);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected void refresh(WalletModel walletModel) {
|
|
||||||
SVGImage svgImage;
|
|
||||||
if(Config.get().getTheme() == Theme.DARK) {
|
|
||||||
svgImage = loadSVGImage("/image/walletmodel/" + walletModel.getType() + "-invert.svg");
|
|
||||||
} else {
|
|
||||||
svgImage = loadSVGImage("/image/walletmodel/" + walletModel.getType() + ".svg");
|
|
||||||
}
|
|
||||||
|
|
||||||
if(svgImage != null) {
|
|
||||||
getChildren().clear();
|
|
||||||
getChildren().add(svgImage);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private SVGImage loadSVGImage(String imageName) {
|
|
||||||
try {
|
|
||||||
URL url = AppServices.class.getResource(imageName);
|
|
||||||
if(url != null) {
|
|
||||||
return SVGLoader.load(url);
|
|
||||||
}
|
|
||||||
} catch(Exception e) {
|
|
||||||
log.error("Could not find image " + imageName);
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -17,7 +17,6 @@ import javafx.scene.control.*;
|
||||||
import javafx.scene.image.Image;
|
import javafx.scene.image.Image;
|
||||||
import javafx.scene.image.ImageView;
|
import javafx.scene.image.ImageView;
|
||||||
import javafx.scene.layout.HBox;
|
import javafx.scene.layout.HBox;
|
||||||
import javafx.scene.layout.Priority;
|
|
||||||
import javafx.scene.layout.VBox;
|
import javafx.scene.layout.VBox;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
|
@ -44,7 +43,14 @@ public class WalletSummaryDialog extends Dialog<Void> {
|
||||||
|
|
||||||
AppServices.setStageIcon(dialogPane.getScene().getWindow());
|
AppServices.setStageIcon(dialogPane.getScene().getWindow());
|
||||||
dialogPane.setHeaderText("Wallet Summary for " + (allOpenWallets ? "All Open Wallets" : masterWallets.get(0).getName()));
|
dialogPane.setHeaderText("Wallet Summary for " + (allOpenWallets ? "All Open Wallets" : masterWallets.get(0).getName()));
|
||||||
dialogPane.setGraphic(new DialogImage(DialogImage.Type.SPARROW));
|
|
||||||
|
Image image = new Image("image/sparrow-small.png", 50, 50, false, false);
|
||||||
|
if(!image.isError()) {
|
||||||
|
ImageView imageView = new ImageView();
|
||||||
|
imageView.setSmooth(false);
|
||||||
|
imageView.setImage(image);
|
||||||
|
dialogPane.setGraphic(imageView);
|
||||||
|
}
|
||||||
|
|
||||||
HBox hBox = new HBox(40);
|
HBox hBox = new HBox(40);
|
||||||
|
|
||||||
|
|
@ -104,7 +110,6 @@ public class WalletSummaryDialog extends Dialog<Void> {
|
||||||
vBox.getChildren().add(table);
|
vBox.getChildren().add(table);
|
||||||
|
|
||||||
hBox.getChildren().add(vBox);
|
hBox.getChildren().add(vBox);
|
||||||
HBox.setHgrow(vBox, Priority.ALWAYS);
|
|
||||||
|
|
||||||
Wallet balanceWallet;
|
Wallet balanceWallet;
|
||||||
if(allOpenWallets) {
|
if(allOpenWallets) {
|
||||||
|
|
|
||||||
|
|
@ -1,82 +0,0 @@
|
||||||
package com.sparrowwallet.sparrow.control;
|
|
||||||
|
|
||||||
import com.sparrowwallet.drongo.OsType;
|
|
||||||
|
|
||||||
public enum WebcamPixelFormat {
|
|
||||||
//Only V4L2 formats defined in linux/videodev2.h are required here, declared in order of priority for supported formats
|
|
||||||
PIX_FMT_RGB24("RGB3", true),
|
|
||||||
PIX_FMT_YUYV("YUYV", true),
|
|
||||||
PIX_FMT_NV12("NV12", true),
|
|
||||||
PIX_FMT_YU12("YU12", true),
|
|
||||||
PIX_FMT_MJPG("MJPG", true);
|
|
||||||
|
|
||||||
private final String name;
|
|
||||||
private final boolean supported;
|
|
||||||
|
|
||||||
WebcamPixelFormat(String name, boolean supported) {
|
|
||||||
this.name = name;
|
|
||||||
this.supported = supported;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getName() {
|
|
||||||
return name;
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean isSupported() {
|
|
||||||
return supported;
|
|
||||||
}
|
|
||||||
|
|
||||||
public int getFourCC() {
|
|
||||||
char a = name.charAt(0);
|
|
||||||
char b = name.charAt(1);
|
|
||||||
char c = name.charAt(2);
|
|
||||||
char d = name.charAt(3);
|
|
||||||
return ((int) a) | ((int) b << 8) | ((int) c << 16) | ((int) d << 24);
|
|
||||||
}
|
|
||||||
|
|
||||||
public String toString() {
|
|
||||||
return name;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static WebcamPixelFormat fromFourCC(int fourCC) {
|
|
||||||
String strFourCC = fourCCToString(fourCC);
|
|
||||||
for(WebcamPixelFormat pixelFormat : WebcamPixelFormat.values()) {
|
|
||||||
if(pixelFormat.getName().equalsIgnoreCase(strFourCC)) {
|
|
||||||
return pixelFormat;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static String fourCCToString(int fourCC) {
|
|
||||||
int fccVal = fourCC;
|
|
||||||
int tmp = fccVal;
|
|
||||||
|
|
||||||
if(OsType.getCurrent() == OsType.MACOS) {
|
|
||||||
tmp = ((tmp >> 16) & 0x0000FFFF) | ((tmp << 16) & 0xFFFF0000);
|
|
||||||
tmp = ((tmp & 0x00FF00FF) << 8) | ((tmp & 0xFF00FF00) >>> 8);
|
|
||||||
}
|
|
||||||
|
|
||||||
fccVal = tmp;
|
|
||||||
|
|
||||||
StringBuilder v = new StringBuilder(4);
|
|
||||||
for(int i = 0; i < 4; i++) {
|
|
||||||
char c = (char) (fccVal & 0xFF);
|
|
||||||
v.append(c);
|
|
||||||
fccVal >>>= 8;
|
|
||||||
}
|
|
||||||
|
|
||||||
return v.toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
public static int getPriority(WebcamPixelFormat pixelFormat) {
|
|
||||||
if(pixelFormat == null) {
|
|
||||||
return values().length;
|
|
||||||
} else if(pixelFormat.isSupported()) {
|
|
||||||
return pixelFormat.ordinal();
|
|
||||||
} else {
|
|
||||||
return values().length + 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,71 +0,0 @@
|
||||||
package com.sparrowwallet.sparrow.control;
|
|
||||||
|
|
||||||
import org.openpnp.capture.CaptureFormat;
|
|
||||||
|
|
||||||
import java.util.Arrays;
|
|
||||||
|
|
||||||
public enum WebcamResolution implements Comparable<WebcamResolution> {
|
|
||||||
VGA("480p", 640, 480),
|
|
||||||
HD("720p", 1280, 720),
|
|
||||||
FHD("1080p", 1920, 1080),
|
|
||||||
UHD4K("4K", 3840, 2160);
|
|
||||||
|
|
||||||
private final String name;
|
|
||||||
private final int width;
|
|
||||||
private final int height;
|
|
||||||
|
|
||||||
WebcamResolution(String name, int width, int height) {
|
|
||||||
this.name = name;
|
|
||||||
this.width = width;
|
|
||||||
this.height = height;
|
|
||||||
}
|
|
||||||
|
|
||||||
public int getPixelsCount() {
|
|
||||||
return this.width * this.height;
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean isStandardAspect() {
|
|
||||||
return Arrays.equals(getAspectRatio(), new int[]{4, 3});
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean isWidescreenAspect() {
|
|
||||||
return Arrays.equals(getAspectRatio(), new int[]{16, 9});
|
|
||||||
}
|
|
||||||
|
|
||||||
public int[] getAspectRatio() {
|
|
||||||
int factor = this.getCommonFactor(this.width, this.height);
|
|
||||||
int wr = this.width / factor;
|
|
||||||
int hr = this.height / factor;
|
|
||||||
return new int[] {wr, hr};
|
|
||||||
}
|
|
||||||
|
|
||||||
private int getCommonFactor(int width, int height) {
|
|
||||||
return height == 0 ? width : this.getCommonFactor(height, width % height);
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getName() {
|
|
||||||
return name;
|
|
||||||
}
|
|
||||||
|
|
||||||
public int getWidth() {
|
|
||||||
return this.width;
|
|
||||||
}
|
|
||||||
|
|
||||||
public int getHeight() {
|
|
||||||
return this.height;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String toString() {
|
|
||||||
return name;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static WebcamResolution from(CaptureFormat captureFormat) {
|
|
||||||
for(WebcamResolution resolution : values()) {
|
|
||||||
if(captureFormat.getFormatInfo().width == resolution.width && captureFormat.getFormatInfo().height == resolution.height) {
|
|
||||||
return resolution;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -0,0 +1,372 @@
|
||||||
|
package com.sparrowwallet.sparrow.control;
|
||||||
|
|
||||||
|
import com.github.sarxos.webcam.*;
|
||||||
|
import com.github.sarxos.webcam.ds.buildin.natives.Device;
|
||||||
|
import com.github.sarxos.webcam.ds.buildin.natives.DeviceList;
|
||||||
|
import com.github.sarxos.webcam.ds.buildin.natives.OpenIMAJGrabber;
|
||||||
|
import org.bridj.Pointer;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
import java.awt.*;
|
||||||
|
import java.awt.color.ColorSpace;
|
||||||
|
import java.awt.image.*;
|
||||||
|
import java.nio.ByteBuffer;
|
||||||
|
import java.util.Hashtable;
|
||||||
|
import java.util.Iterator;
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
|
import java.util.concurrent.atomic.AtomicInteger;
|
||||||
|
|
||||||
|
@SuppressWarnings("deprecation")
|
||||||
|
public class WebcamScanDevice implements WebcamDevice, WebcamDevice.BufferAccess, Runnable, WebcamDevice.FPSSource {
|
||||||
|
private static final Logger LOG = LoggerFactory.getLogger(WebcamScanDevice.class);
|
||||||
|
private static final int DEVICE_BUFFER_SIZE = 5;
|
||||||
|
private static final Dimension[] DIMENSIONS;
|
||||||
|
private static final int[] BAND_OFFSETS;
|
||||||
|
private static final int[] BITS;
|
||||||
|
private static final int[] OFFSET;
|
||||||
|
private static final int DATA_TYPE = 0;
|
||||||
|
private static final ColorSpace COLOR_SPACE;
|
||||||
|
public static final int SCAN_LOOP_WAIT_MILLIS = 100;
|
||||||
|
private int timeout = 5000;
|
||||||
|
private OpenIMAJGrabber grabber = null;
|
||||||
|
private Device device = null;
|
||||||
|
private Dimension size = null;
|
||||||
|
private ComponentSampleModel smodel = null;
|
||||||
|
private ColorModel cmodel = null;
|
||||||
|
private boolean failOnSizeMismatch = false;
|
||||||
|
private final AtomicBoolean disposed = new AtomicBoolean(false);
|
||||||
|
private final AtomicBoolean open = new AtomicBoolean(false);
|
||||||
|
private final AtomicBoolean fresh = new AtomicBoolean(false);
|
||||||
|
private Thread refresher = null;
|
||||||
|
private String name = null;
|
||||||
|
private String id = null;
|
||||||
|
private String fullname = null;
|
||||||
|
private long t1 = -1L;
|
||||||
|
private long t2 = -1L;
|
||||||
|
private volatile double fps = 0.0D;
|
||||||
|
|
||||||
|
protected WebcamScanDevice(Device device) {
|
||||||
|
this.device = device;
|
||||||
|
this.name = device.getNameStr();
|
||||||
|
this.id = device.getIdentifierStr();
|
||||||
|
this.fullname = String.format("%s %s", this.name, this.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getName() {
|
||||||
|
return this.fullname;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getDeviceName() {
|
||||||
|
return this.name;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getDeviceId() {
|
||||||
|
return this.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Device getDeviceRef() {
|
||||||
|
return this.device;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Dimension[] getResolutions() {
|
||||||
|
return DIMENSIONS;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Dimension getResolution() {
|
||||||
|
if (this.size == null) {
|
||||||
|
this.size = this.getResolutions()[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.size;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setResolution(Dimension size) {
|
||||||
|
if (size == null) {
|
||||||
|
throw new IllegalArgumentException("Size cannot be null");
|
||||||
|
} else if (this.open.get()) {
|
||||||
|
throw new IllegalStateException("Cannot change resolution when webcam is open, please close it first");
|
||||||
|
} else {
|
||||||
|
this.size = size;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public ByteBuffer getImageBytes() {
|
||||||
|
if (this.disposed.get()) {
|
||||||
|
LOG.debug("Webcam is disposed, image will be null");
|
||||||
|
return null;
|
||||||
|
} else if (!this.open.get()) {
|
||||||
|
LOG.debug("Webcam is closed, image will be null");
|
||||||
|
return null;
|
||||||
|
} else {
|
||||||
|
if (this.fresh.compareAndSet(false, true)) {
|
||||||
|
this.updateFrameBuffer();
|
||||||
|
}
|
||||||
|
|
||||||
|
LOG.trace("Webcam grabber get image pointer");
|
||||||
|
Pointer<Byte> image = this.grabber.getImage();
|
||||||
|
this.fresh.set(false);
|
||||||
|
if (image == null) {
|
||||||
|
LOG.warn("Null array pointer found instead of image");
|
||||||
|
return null;
|
||||||
|
} else {
|
||||||
|
int length = this.size.width * this.size.height * 3;
|
||||||
|
LOG.trace("Webcam device get buffer, read {} bytes", length);
|
||||||
|
return image.getByteBuffer((long)length);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void getImageBytes(ByteBuffer target) {
|
||||||
|
if (this.disposed.get()) {
|
||||||
|
LOG.debug("Webcam is disposed, image will be null");
|
||||||
|
} else if (!this.open.get()) {
|
||||||
|
LOG.debug("Webcam is closed, image will be null");
|
||||||
|
} else {
|
||||||
|
int minSize = this.size.width * this.size.height * 3;
|
||||||
|
int curSize = target.remaining();
|
||||||
|
if (minSize > curSize) {
|
||||||
|
throw new IllegalArgumentException(String.format("Not enough remaining space in target buffer (%d necessary vs %d remaining)", minSize, curSize));
|
||||||
|
} else {
|
||||||
|
if (this.fresh.compareAndSet(false, true)) {
|
||||||
|
this.updateFrameBuffer();
|
||||||
|
}
|
||||||
|
|
||||||
|
LOG.trace("Webcam grabber get image pointer");
|
||||||
|
Pointer<Byte> image = this.grabber.getImage();
|
||||||
|
this.fresh.set(false);
|
||||||
|
if (image == null) {
|
||||||
|
LOG.warn("Null array pointer found instead of image");
|
||||||
|
} else {
|
||||||
|
LOG.trace("Webcam device read buffer {} bytes", minSize);
|
||||||
|
image = image.validBytes((long)minSize);
|
||||||
|
image.getBytes(target);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public BufferedImage getImage() {
|
||||||
|
ByteBuffer buffer = this.getImageBytes();
|
||||||
|
if (buffer == null) {
|
||||||
|
LOG.error("Images bytes buffer is null!");
|
||||||
|
return null;
|
||||||
|
} else {
|
||||||
|
byte[] bytes = new byte[this.size.width * this.size.height * 3];
|
||||||
|
byte[][] data = new byte[][]{bytes};
|
||||||
|
buffer.get(bytes);
|
||||||
|
DataBufferByte dbuf = new DataBufferByte(data, bytes.length, OFFSET);
|
||||||
|
WritableRaster raster = Raster.createWritableRaster(this.smodel, dbuf, (Point)null);
|
||||||
|
BufferedImage bi = new BufferedImage(this.cmodel, raster, false, (Hashtable)null);
|
||||||
|
bi.flush();
|
||||||
|
return bi;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void open() {
|
||||||
|
if (!this.disposed.get()) {
|
||||||
|
LOG.debug("Opening webcam device {}", this.getName());
|
||||||
|
if (this.size == null) {
|
||||||
|
this.size = this.getResolutions()[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.size == null) {
|
||||||
|
throw new RuntimeException("The resolution size cannot be null");
|
||||||
|
} else {
|
||||||
|
LOG.debug("Webcam device {} starting session, size {}", this.device.getIdentifierStr(), this.size);
|
||||||
|
this.grabber = new OpenIMAJGrabber();
|
||||||
|
DeviceList list = (DeviceList)this.grabber.getVideoDevices().get();
|
||||||
|
Iterator var2 = list.asArrayList().iterator();
|
||||||
|
|
||||||
|
while(var2.hasNext()) {
|
||||||
|
Device d = (Device)var2.next();
|
||||||
|
d.getNameStr();
|
||||||
|
d.getIdentifierStr();
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean started = this.grabber.startSession(this.size.width, this.size.height, 50, Pointer.pointerTo(this.device));
|
||||||
|
if (!started) {
|
||||||
|
throw new WebcamException("Cannot start native grabber!");
|
||||||
|
} else {
|
||||||
|
this.grabber.setTimeout(this.timeout);
|
||||||
|
LOG.debug("Webcam device session started");
|
||||||
|
Dimension size2 = new Dimension(this.grabber.getWidth(), this.grabber.getHeight());
|
||||||
|
int w1 = this.size.width;
|
||||||
|
int w2 = size2.width;
|
||||||
|
int h1 = this.size.height;
|
||||||
|
int h2 = size2.height;
|
||||||
|
if (w1 != w2 || h1 != h2) {
|
||||||
|
if (this.failOnSizeMismatch) {
|
||||||
|
throw new WebcamException(String.format("Different size obtained vs requested - [%dx%d] vs [%dx%d]", w1, h1, w2, h2));
|
||||||
|
}
|
||||||
|
|
||||||
|
Object[] args = new Object[]{w1, h1, w2, h2, w2, h2};
|
||||||
|
LOG.warn("Different size obtained vs requested - [{}x{}] vs [{}x{}]. Setting correct one. New size is [{}x{}]", args);
|
||||||
|
this.size = new Dimension(w2, h2);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.smodel = new ComponentSampleModel(0, this.size.width, this.size.height, 3, this.size.width * 3, BAND_OFFSETS);
|
||||||
|
this.cmodel = new ComponentColorModel(COLOR_SPACE, BITS, false, false, 1, 0);
|
||||||
|
LOG.debug("Clear memory buffer");
|
||||||
|
this.clearMemoryBuffer();
|
||||||
|
LOG.debug("Webcam device {} is now open", this);
|
||||||
|
this.open.set(true);
|
||||||
|
this.refresher = this.startFramesRefresher();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void clearMemoryBuffer() {
|
||||||
|
for(int i = 0; i < 5; ++i) {
|
||||||
|
this.grabber.nextFrame();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private Thread startFramesRefresher() {
|
||||||
|
Thread refresher = new Thread(this, String.format("frames-refresher-[%s]", this.id));
|
||||||
|
refresher.setUncaughtExceptionHandler(WebcamExceptionHandler.getInstance());
|
||||||
|
refresher.setDaemon(true);
|
||||||
|
refresher.start();
|
||||||
|
return refresher;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void close() {
|
||||||
|
if (this.open.compareAndSet(true, false)) {
|
||||||
|
LOG.debug("Closing webcam device");
|
||||||
|
this.grabber.stopSession();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void dispose() {
|
||||||
|
if (this.disposed.compareAndSet(false, true)) {
|
||||||
|
LOG.debug("Disposing webcam device {}", this.getName());
|
||||||
|
this.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setFailOnSizeMismatch(boolean fail) {
|
||||||
|
this.failOnSizeMismatch = fail;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isOpen() {
|
||||||
|
return this.open.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getTimeout() {
|
||||||
|
return this.timeout;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setTimeout(int timeout) {
|
||||||
|
if (this.isOpen()) {
|
||||||
|
throw new WebcamException("Timeout must be set before webcam is open");
|
||||||
|
} else {
|
||||||
|
this.timeout = timeout;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateFrameBuffer() {
|
||||||
|
LOG.trace("Next frame");
|
||||||
|
if (this.t1 == -1L || this.t2 == -1L) {
|
||||||
|
this.t1 = System.currentTimeMillis();
|
||||||
|
this.t2 = System.currentTimeMillis();
|
||||||
|
}
|
||||||
|
|
||||||
|
int result = (new WebcamScanDevice.NextFrameTask(this)).nextFrame();
|
||||||
|
this.t1 = this.t2;
|
||||||
|
this.t2 = System.currentTimeMillis();
|
||||||
|
this.fps = (4.0D * this.fps + (double)(1000L / (this.t2 - this.t1 + 1L))) / 5.0D;
|
||||||
|
if (result == -1) {
|
||||||
|
LOG.error("Timeout when requesting image!");
|
||||||
|
} else if (result < -1) {
|
||||||
|
LOG.error("Error requesting new frame!");
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public void run() {
|
||||||
|
do {
|
||||||
|
try {
|
||||||
|
Thread.sleep(SCAN_LOOP_WAIT_MILLIS);
|
||||||
|
} catch(InterruptedException e) {
|
||||||
|
//ignore
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Thread.interrupted()) {
|
||||||
|
LOG.debug("Refresher has been interrupted");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.open.get()) {
|
||||||
|
LOG.debug("Cancelling refresher");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.updateFrameBuffer();
|
||||||
|
} while(this.open.get());
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public double getFPS() {
|
||||||
|
return this.fps;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean equals(Object o) {
|
||||||
|
if(this == o) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if(o == null || getClass() != o.getClass()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
WebcamScanDevice that = (WebcamScanDevice) o;
|
||||||
|
return Objects.equals(fullname, that.fullname);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int hashCode() {
|
||||||
|
return Objects.hash(fullname);
|
||||||
|
}
|
||||||
|
|
||||||
|
static {
|
||||||
|
DIMENSIONS = new Dimension[]{WebcamResolution.QQVGA.getSize(), WebcamResolution.QVGA.getSize(), WebcamResolution.VGA.getSize()};
|
||||||
|
BAND_OFFSETS = new int[]{0, 1, 2};
|
||||||
|
BITS = new int[]{8, 8, 8};
|
||||||
|
OFFSET = new int[]{0};
|
||||||
|
COLOR_SPACE = ColorSpace.getInstance(1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
private class NextFrameTask extends WebcamTask {
|
||||||
|
private final AtomicInteger result = new AtomicInteger(0);
|
||||||
|
|
||||||
|
public NextFrameTask(WebcamDevice device) {
|
||||||
|
super(device);
|
||||||
|
}
|
||||||
|
|
||||||
|
public int nextFrame() {
|
||||||
|
try {
|
||||||
|
this.process();
|
||||||
|
} catch (InterruptedException var2) {
|
||||||
|
WebcamScanDevice.LOG.debug("Image buffer request interrupted", var2);
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.result.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void handle() {
|
||||||
|
WebcamScanDevice device = (WebcamScanDevice)this.getDevice();
|
||||||
|
if (device.isOpen()) {
|
||||||
|
try {
|
||||||
|
Thread.sleep(SCAN_LOOP_WAIT_MILLIS);
|
||||||
|
} catch(InterruptedException e) {
|
||||||
|
//ignore
|
||||||
|
}
|
||||||
|
|
||||||
|
this.result.set(WebcamScanDevice.this.grabber.nextFrame());
|
||||||
|
WebcamScanDevice.this.fresh.set(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,45 @@
|
||||||
|
package com.sparrowwallet.sparrow.control;
|
||||||
|
|
||||||
|
import com.github.sarxos.webcam.WebcamDevice;
|
||||||
|
import com.github.sarxos.webcam.ds.buildin.WebcamDefaultDevice;
|
||||||
|
import com.github.sarxos.webcam.ds.buildin.WebcamDefaultDriver;
|
||||||
|
import javafx.collections.FXCollections;
|
||||||
|
import javafx.collections.ObservableList;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public class WebcamScanDriver extends WebcamDefaultDriver {
|
||||||
|
private static final ObservableList<WebcamDevice> webcamDevices = FXCollections.observableArrayList();
|
||||||
|
private static boolean rescan;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<WebcamDevice> getDevices() {
|
||||||
|
if(rescan || webcamDevices.isEmpty()) {
|
||||||
|
List<WebcamDevice> devices = super.getDevices();
|
||||||
|
List<WebcamDevice> scanDevices = new ArrayList<>();
|
||||||
|
for(WebcamDevice device : devices) {
|
||||||
|
WebcamDefaultDevice defaultDevice = (WebcamDefaultDevice)device;
|
||||||
|
WebcamScanDevice scanDevice = new WebcamScanDevice(defaultDevice.getDeviceRef());
|
||||||
|
if(scanDevices.stream().noneMatch(dev -> ((WebcamScanDevice)dev).getDeviceName().equals(scanDevice.getDeviceName()))) {
|
||||||
|
scanDevices.add(scanDevice);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
List<WebcamDevice> newDevices = new ArrayList<>(scanDevices);
|
||||||
|
newDevices.removeAll(webcamDevices);
|
||||||
|
webcamDevices.addAll(newDevices);
|
||||||
|
webcamDevices.removeIf(device -> !scanDevices.contains(device));
|
||||||
|
}
|
||||||
|
|
||||||
|
return webcamDevices;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static ObservableList<WebcamDevice> getFoundDevices() {
|
||||||
|
return webcamDevices;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void rescan() {
|
||||||
|
rescan = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,13 +1,12 @@
|
||||||
package com.sparrowwallet.sparrow.control;
|
package com.sparrowwallet.sparrow.control;
|
||||||
|
|
||||||
|
import com.github.sarxos.webcam.*;
|
||||||
import com.google.zxing.*;
|
import com.google.zxing.*;
|
||||||
import com.google.zxing.client.j2se.BufferedImageLuminanceSource;
|
import com.google.zxing.client.j2se.BufferedImageLuminanceSource;
|
||||||
import com.google.zxing.common.HybridBinarizer;
|
import com.google.zxing.common.HybridBinarizer;
|
||||||
import com.google.zxing.qrcode.QRCodeReader;
|
import com.google.zxing.qrcode.QRCodeReader;
|
||||||
import com.sparrowwallet.bokmakierie.Bokmakierie;
|
import com.sparrowwallet.bokmakierie.Bokmakierie;
|
||||||
import com.sparrowwallet.drongo.OsType;
|
|
||||||
import com.sparrowwallet.sparrow.io.Config;
|
import com.sparrowwallet.sparrow.io.Config;
|
||||||
import com.sparrowwallet.sparrow.io.ZBar;
|
|
||||||
import javafx.beans.property.BooleanProperty;
|
import javafx.beans.property.BooleanProperty;
|
||||||
import javafx.beans.property.ObjectProperty;
|
import javafx.beans.property.ObjectProperty;
|
||||||
import javafx.beans.property.SimpleBooleanProperty;
|
import javafx.beans.property.SimpleBooleanProperty;
|
||||||
|
|
@ -16,8 +15,7 @@ import javafx.concurrent.ScheduledService;
|
||||||
import javafx.concurrent.Task;
|
import javafx.concurrent.Task;
|
||||||
import javafx.embed.swing.SwingFXUtils;
|
import javafx.embed.swing.SwingFXUtils;
|
||||||
import javafx.scene.image.Image;
|
import javafx.scene.image.Image;
|
||||||
import org.openpnp.capture.*;
|
import net.sourceforge.zbar.ZBar;
|
||||||
import org.openpnp.capture.library.OpenpnpCaptureLibrary;
|
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
|
@ -25,84 +23,38 @@ import java.awt.*;
|
||||||
import java.awt.geom.RoundRectangle2D;
|
import java.awt.geom.RoundRectangle2D;
|
||||||
import java.awt.image.BufferedImage;
|
import java.awt.image.BufferedImage;
|
||||||
import java.awt.image.WritableRaster;
|
import java.awt.image.WritableRaster;
|
||||||
import java.util.*;
|
import java.util.Arrays;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.concurrent.Semaphore;
|
import java.util.Map;
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
import java.util.concurrent.atomic.AtomicBoolean;
|
|
||||||
import java.util.function.Function;
|
|
||||||
import java.util.stream.Collectors;
|
|
||||||
import java.util.stream.Stream;
|
|
||||||
|
|
||||||
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> availableDevices;
|
|
||||||
private Set<WebcamResolution> resolutions;
|
|
||||||
|
|
||||||
private WebcamResolution resolution;
|
private WebcamResolution resolution;
|
||||||
private CaptureDevice device;
|
private WebcamDevice device;
|
||||||
|
private final WebcamListener listener;
|
||||||
|
private final WebcamUpdater.DelayCalculator delayCalculator;
|
||||||
private final BooleanProperty opening = new SimpleBooleanProperty(false);
|
private final BooleanProperty opening = 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);
|
||||||
|
|
||||||
private static final int QR_SAMPLE_PERIOD_MILLIS = 200;
|
private static final int QR_SAMPLE_PERIOD_MILLIS = 200;
|
||||||
|
|
||||||
private final OpenPnpCapture capture;
|
private Webcam cam;
|
||||||
private CaptureStream stream;
|
|
||||||
private PropertyLimits zoomLimits;
|
|
||||||
private long lastQrSampleTime;
|
private long lastQrSampleTime;
|
||||||
private final Reader qrReader;
|
private final Reader qrReader;
|
||||||
private final Bokmakierie bokmakierie;
|
private final Bokmakierie bokmakierie;
|
||||||
|
|
||||||
static {
|
static {
|
||||||
if(log.isTraceEnabled()) {
|
Webcam.setDriver(new WebcamScanDriver());
|
||||||
OpenpnpCaptureLibrary.INSTANCE.Cap_setLogLevel(8);
|
|
||||||
} else if(log.isDebugEnabled()) {
|
|
||||||
OpenpnpCaptureLibrary.INSTANCE.Cap_setLogLevel(7);
|
|
||||||
} else if(log.isInfoEnabled()) {
|
|
||||||
OpenpnpCaptureLibrary.INSTANCE.Cap_setLogLevel(6);
|
|
||||||
}
|
|
||||||
|
|
||||||
OpenpnpCaptureLibrary.INSTANCE.Cap_installCustomLogFunction((level, ptr) -> {
|
|
||||||
switch(level) {
|
|
||||||
case 0:
|
|
||||||
case 1:
|
|
||||||
case 2:
|
|
||||||
case 3:
|
|
||||||
String err = ptr.getString(0).trim();
|
|
||||||
if(err.equals("tjDecompressHeader2 failed: No error") || err.matches("getPropertyLimits.*failed on.*")) { //Safe to ignore
|
|
||||||
log.debug(err);
|
|
||||||
} else {
|
|
||||||
log.error(err);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case 4:
|
|
||||||
case 5:
|
|
||||||
case 6:
|
|
||||||
log.info(ptr.getString(0).trim());
|
|
||||||
break;
|
|
||||||
case 7:
|
|
||||||
log.debug(ptr.getString(0).trim());
|
|
||||||
break;
|
|
||||||
case 8:
|
|
||||||
default:
|
|
||||||
log.trace(ptr.getString(0).trim());
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public WebcamService(WebcamResolution requestedResolution, CaptureDevice requestedDevice) {
|
public WebcamService(WebcamResolution resolution, WebcamDevice device, WebcamListener listener, WebcamUpdater.DelayCalculator delayCalculator) {
|
||||||
this.capture = new OpenPnpCapture();
|
this.resolution = resolution;
|
||||||
this.resolution = requestedResolution;
|
this.device = device;
|
||||||
this.device = requestedDevice;
|
this.listener = listener;
|
||||||
|
this.delayCalculator = delayCalculator;
|
||||||
this.lastQrSampleTime = System.currentTimeMillis();
|
this.lastQrSampleTime = System.currentTimeMillis();
|
||||||
this.qrReader = new QRCodeReader();
|
this.qrReader = new QRCodeReader();
|
||||||
this.bokmakierie = new Bokmakierie();
|
this.bokmakierie = new Bokmakierie();
|
||||||
|
|
@ -110,115 +62,50 @@ public class WebcamService extends ScheduledService<Image> {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Task<Image> createTask() {
|
public Task<Image> createTask() {
|
||||||
return new Task<>() {
|
return new Task<Image>() {
|
||||||
@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(devices == null) {
|
if(cam == null) {
|
||||||
devices = capture.getDevices();
|
List<Webcam> webcams = Webcam.getWebcams(1, TimeUnit.MINUTES);
|
||||||
availableDevices = new ArrayList<>(devices);
|
if(webcams.isEmpty()) {
|
||||||
|
throw new UnsupportedOperationException("No camera available.");
|
||||||
if(devices.isEmpty()) {
|
|
||||||
throw new UnsupportedOperationException("No cameras available");
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
while(stream == null && !availableDevices.isEmpty()) {
|
cam = webcams.get(0);
|
||||||
CaptureDevice selectedDevice = availableDevices.stream().filter(d -> !d.getFormats().isEmpty()).findFirst().orElse(availableDevices.getFirst());
|
|
||||||
|
|
||||||
if(device != null) {
|
if(device != null) {
|
||||||
for(CaptureDevice webcam : availableDevices) {
|
for(Webcam webcam : webcams) {
|
||||||
if(webcam.equals(device)) {
|
if(webcam.getDevice().getName().equals(device.getName())) {
|
||||||
selectedDevice = webcam;
|
cam = webcam;
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if(Config.get().getWebcamDevice() != null) {
|
} else if(Config.get().getWebcamDevice() != null) {
|
||||||
for(CaptureDevice webcam : availableDevices) {
|
for(Webcam webcam : webcams) {
|
||||||
if(webcam.getUniqueId().equals(Config.get().getWebcamDeviceId())) {
|
if(webcam.getDevice().getName().equals(Config.get().getWebcamDevice())) {
|
||||||
selectedDevice = webcam;
|
cam = webcam;
|
||||||
break;
|
|
||||||
}
|
|
||||||
if(webcam.getName().equals(Config.get().getWebcamDevice())) {
|
|
||||||
selectedDevice = webcam;
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
device = selectedDevice;
|
device = cam.getDevice();
|
||||||
|
|
||||||
if(device.getFormats().isEmpty()) {
|
cam.setCustomViewSizes(resolution.getSize());
|
||||||
throw new UnsupportedOperationException("No resolutions supported by camera " + device.getName());
|
cam.setViewSize(resolution.getSize());
|
||||||
}
|
if(!Arrays.asList(cam.getWebcamListeners()).contains(listener)) {
|
||||||
|
cam.addWebcamListener(listener);
|
||||||
List<CaptureFormat> deviceFormats = new ArrayList<>(device.getFormats());
|
|
||||||
|
|
||||||
//On *nix prioritise supported camera pixel formats, preferring RGB3, then YUYV, then MJPG
|
|
||||||
//On macOS and Windows, camera pixel format is largely abstracted away
|
|
||||||
if(OsType.getCurrent() == OsType.UNIX) {
|
|
||||||
deviceFormats.sort((f1, f2) -> {
|
|
||||||
WebcamPixelFormat pf1 = WebcamPixelFormat.fromFourCC(f1.getFormatInfo().fourcc);
|
|
||||||
WebcamPixelFormat pf2 = WebcamPixelFormat.fromFourCC(f2.getFormatInfo().fourcc);
|
|
||||||
return Integer.compare(WebcamPixelFormat.getPriority(pf1), WebcamPixelFormat.getPriority(pf2));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
Map<WebcamResolution, CaptureFormat> supportedResolutions = deviceFormats.stream()
|
|
||||||
.filter(f -> WebcamResolution.from(f) != null)
|
|
||||||
.collect(Collectors.toMap(WebcamResolution::from, Function.identity(), (u, v) -> u, TreeMap::new));
|
|
||||||
resolutions = supportedResolutions.keySet();
|
|
||||||
|
|
||||||
CaptureFormat format = supportedResolutions.get(resolution);
|
|
||||||
if(format == null) {
|
|
||||||
if(!supportedResolutions.isEmpty()) {
|
|
||||||
resolution = getNearestEnum(resolution, supportedResolutions.keySet().toArray(new WebcamResolution[0]));
|
|
||||||
format = supportedResolutions.get(resolution);
|
|
||||||
} else {
|
|
||||||
format = device.getFormats().getFirst();
|
|
||||||
log.warn("Could not get standard capture resolution, using " + format.getFormatInfo().width + "x" + format.getFormatInfo().height);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//On Linux, formats not defined in WebcamPixelFormat are unsupported
|
|
||||||
if(OsType.getCurrent() == OsType.UNIX && WebcamPixelFormat.fromFourCC(format.getFormatInfo().fourcc) == null) {
|
|
||||||
log.warn("Unsupported camera pixel format " + WebcamPixelFormat.fourCCToString(format.getFormatInfo().fourcc));
|
|
||||||
}
|
|
||||||
|
|
||||||
if(log.isDebugEnabled()) {
|
|
||||||
log.debug("Opening capture stream on " + device + " with format " + format.getFormatInfo().width + "x" + format.getFormatInfo().height + " (" + WebcamPixelFormat.fourCCToString(format.getFormatInfo().fourcc) + ")");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
opening.set(true);
|
opening.set(true);
|
||||||
stream = device.openStream(format);
|
cam.open(true, delayCalculator);
|
||||||
opening.set(false);
|
opening.set(false);
|
||||||
|
|
||||||
try {
|
|
||||||
zoomLimits = stream.getPropertyLimits(CaptureProperty.Zoom);
|
|
||||||
} catch(Throwable e) {
|
|
||||||
log.debug("Error getting zoom limits on " + device + ", assuming no zoom function");
|
|
||||||
}
|
|
||||||
|
|
||||||
if(stream == null) {
|
|
||||||
availableDevices.remove(device);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if(stream == null) {
|
BufferedImage originalImage = cam.getImage();
|
||||||
throw new UnsupportedOperationException("No usable cameras available, tried " + devices);
|
if(originalImage == null) {
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
opened.set(true);
|
|
||||||
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);
|
||||||
BufferedImage framedImage = getFramedImage(originalImage, cropped);
|
BufferedImage framedImage = getFramedImage(originalImage, cropped);
|
||||||
|
|
@ -234,7 +121,6 @@ public class WebcamService extends ScheduledService<Image> {
|
||||||
return image;
|
return image;
|
||||||
} finally {
|
} finally {
|
||||||
opening.set(false);
|
opening.set(false);
|
||||||
taskSemaphore.release();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
@ -242,66 +128,17 @@ public class WebcamService extends ScheduledService<Image> {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void reset() {
|
public void reset() {
|
||||||
stream = null;
|
cam = null;
|
||||||
zoomLimits = null;
|
|
||||||
cancelRequested.set(false);
|
|
||||||
super.reset();
|
super.reset();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean cancel() {
|
public boolean cancel() {
|
||||||
cancelRequested.set(true);
|
if(cam != null && !cam.close()) {
|
||||||
boolean cancelled = super.cancel();
|
cam.close();
|
||||||
|
|
||||||
try {
|
|
||||||
if(taskSemaphore.tryAcquire(1, TimeUnit.SECONDS)) {
|
|
||||||
taskSemaphore.release();
|
|
||||||
} else {
|
|
||||||
log.error("Timed out waiting for task semaphore to be available to cancel, cancelling anyway");
|
|
||||||
}
|
|
||||||
} catch(InterruptedException e) {
|
|
||||||
log.error("Interrupted while waiting for task semaphore to be available to cancel, cancelling anyway");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if(stream != null) {
|
return super.cancel();
|
||||||
stream.close();
|
|
||||||
opened.set(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
return cancelled;
|
|
||||||
}
|
|
||||||
|
|
||||||
public synchronized void close() {
|
|
||||||
if(!captureClosed.get()) {
|
|
||||||
captureClosed.set(true);
|
|
||||||
capture.close();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public PropertyLimits getZoomLimits() {
|
|
||||||
return zoomLimits;
|
|
||||||
}
|
|
||||||
|
|
||||||
public int getZoom() {
|
|
||||||
if(stream != null && zoomLimits != null) {
|
|
||||||
try {
|
|
||||||
return stream.getProperty(CaptureProperty.Zoom);
|
|
||||||
} catch(Exception e) {
|
|
||||||
log.error("Error getting zoom property on " + device, e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setZoom(int value) {
|
|
||||||
if(stream != null && zoomLimits != null) {
|
|
||||||
try {
|
|
||||||
stream.setProperty(CaptureProperty.Zoom, value);
|
|
||||||
} catch(Exception e) {
|
|
||||||
log.error("Error setting zoom property on " + device, e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void readQR(BufferedImage wideImage, BufferedImage croppedImage) {
|
private void readQR(BufferedImage wideImage, BufferedImage croppedImage) {
|
||||||
|
|
@ -319,6 +156,9 @@ public class WebcamService extends ScheduledService<Image> {
|
||||||
}
|
}
|
||||||
|
|
||||||
private Result readQR(BufferedImage bufferedImage) {
|
private Result readQR(BufferedImage bufferedImage) {
|
||||||
|
LuminanceSource source = new BufferedImageLuminanceSource(bufferedImage);
|
||||||
|
BinaryBitmap bitmap = new BinaryBitmap(new HybridBinarizer(source));
|
||||||
|
|
||||||
try {
|
try {
|
||||||
com.sparrowwallet.bokmakierie.Result result = bokmakierie.scan(bufferedImage);
|
com.sparrowwallet.bokmakierie.Result result = bokmakierie.scan(bufferedImage);
|
||||||
if(result != null) {
|
if(result != null) {
|
||||||
|
|
@ -336,8 +176,6 @@ public class WebcamService extends ScheduledService<Image> {
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
LuminanceSource source = new BufferedImageLuminanceSource(bufferedImage);
|
|
||||||
BinaryBitmap bitmap = new BinaryBitmap(new HybridBinarizer(source));
|
|
||||||
return qrReader.decode(bitmap, Map.of(DecodeHintType.TRY_HARDER, Boolean.TRUE));
|
return qrReader.decode(bitmap, Map.of(DecodeHintType.TRY_HARDER, Boolean.TRUE));
|
||||||
} catch(ReaderException e) {
|
} catch(ReaderException e) {
|
||||||
// fall thru, it means there is no QR code in image
|
// fall thru, it means there is no QR code in image
|
||||||
|
|
@ -351,7 +189,7 @@ public class WebcamService extends ScheduledService<Image> {
|
||||||
g2d.drawImage(image, 0, 0, null);
|
g2d.drawImage(image, 0, 0, null);
|
||||||
float[] dash1 = {10.0f};
|
float[] dash1 = {10.0f};
|
||||||
g2d.setColor(Color.BLACK);
|
g2d.setColor(Color.BLACK);
|
||||||
g2d.setStroke(new BasicStroke(resolution.isWidescreenAspect() ? 3.0f : 1.5f, BasicStroke.CAP_BUTT, BasicStroke.JOIN_MITER, 10.0f, dash1, 0.0f));
|
g2d.setStroke(new BasicStroke(resolution == WebcamResolution.HD ? 3.0f : 1.5f, BasicStroke.CAP_BUTT, BasicStroke.JOIN_MITER, 10.0f, dash1, 0.0f));
|
||||||
g2d.draw(new RoundRectangle2D.Double(cropped.x, cropped.y, cropped.length, cropped.length, 10, 10));
|
g2d.draw(new RoundRectangle2D.Double(cropped.x, cropped.y, cropped.length, cropped.length, 10, 10));
|
||||||
g2d.dispose();
|
g2d.dispose();
|
||||||
return clone;
|
return clone;
|
||||||
|
|
@ -388,18 +226,6 @@ public class WebcamService extends ScheduledService<Image> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<CaptureDevice> getDevices() {
|
|
||||||
return devices;
|
|
||||||
}
|
|
||||||
|
|
||||||
public List<CaptureDevice> getAvailableDevices() {
|
|
||||||
return availableDevices;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Set<WebcamResolution> getResolutions() {
|
|
||||||
return resolutions;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Result getResult() {
|
public Result getResult() {
|
||||||
return resultProperty.get();
|
return resultProperty.get();
|
||||||
}
|
}
|
||||||
|
|
@ -409,69 +235,33 @@ public class WebcamService extends ScheduledService<Image> {
|
||||||
}
|
}
|
||||||
|
|
||||||
public int getCamWidth() {
|
public int getCamWidth() {
|
||||||
return resolution.getWidth();
|
return resolution.getSize().width;
|
||||||
}
|
}
|
||||||
|
|
||||||
public int getCamHeight() {
|
public int getCamHeight() {
|
||||||
return resolution.getHeight();
|
return resolution.getSize().height;
|
||||||
}
|
|
||||||
|
|
||||||
public WebcamResolution getResolution() {
|
|
||||||
return resolution;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setResolution(WebcamResolution resolution) {
|
public void setResolution(WebcamResolution resolution) {
|
||||||
this.resolution = resolution;
|
this.resolution = resolution;
|
||||||
}
|
}
|
||||||
|
|
||||||
public CaptureDevice getDevice() {
|
public WebcamDevice getDevice() {
|
||||||
return device;
|
return device;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setDevice(CaptureDevice device) {
|
public void setDevice(WebcamDevice device) {
|
||||||
this.device = device;
|
this.device = device;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public boolean isOpening() {
|
||||||
|
return opening.get();
|
||||||
|
}
|
||||||
|
|
||||||
public BooleanProperty openingProperty() {
|
public BooleanProperty openingProperty() {
|
||||||
return opening;
|
return opening;
|
||||||
}
|
}
|
||||||
|
|
||||||
public BooleanProperty openedProperty() {
|
|
||||||
return opened;
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean getCancelRequested() {
|
|
||||||
return cancelRequested.get();
|
|
||||||
}
|
|
||||||
|
|
||||||
public static <T extends Enum<T>> T getNearestEnum(T target) {
|
|
||||||
return getNearestEnum(target, target.getDeclaringClass().getEnumConstants());
|
|
||||||
}
|
|
||||||
|
|
||||||
public static <T extends Enum<T>> T getNearestEnum(T target, T[] values) {
|
|
||||||
if(values == null || values.length == 0) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
int targetOrdinal = target.ordinal();
|
|
||||||
if(values.length == 1) {
|
|
||||||
return values[0];
|
|
||||||
}
|
|
||||||
|
|
||||||
for(int i = 0; i < values.length; i++) {
|
|
||||||
if(targetOrdinal < values[i].ordinal()) {
|
|
||||||
if(i == 0) {
|
|
||||||
return values[0];
|
|
||||||
}
|
|
||||||
int diffToPrev = Math.abs(targetOrdinal - values[i - 1].ordinal());
|
|
||||||
int diffToNext = Math.abs(targetOrdinal - values[i].ordinal());
|
|
||||||
return diffToPrev <= diffToNext ? values[i - 1] : values[i];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return values[values.length - 1];
|
|
||||||
}
|
|
||||||
|
|
||||||
private static class CroppedDimension {
|
private static class CroppedDimension {
|
||||||
public int x;
|
public int x;
|
||||||
public int y;
|
public int y;
|
||||||
|
|
|
||||||
|
|
@ -46,22 +46,9 @@ public class WebcamView {
|
||||||
imageView.setOnContextMenuRequested(event -> {
|
imageView.setOnContextMenuRequested(event -> {
|
||||||
contextMenu.show(imageView, event.getScreenX(), event.getScreenY());
|
contextMenu.show(imageView, event.getScreenX(), event.getScreenY());
|
||||||
});
|
});
|
||||||
imageView.setOnScroll(scrollEvent -> {
|
|
||||||
if(service.isRunning() && scrollEvent.getDeltaY() != 0 && service.getZoomLimits() != null) {
|
|
||||||
int currentZoom = service.getZoom();
|
|
||||||
if(currentZoom >= 0) {
|
|
||||||
int newZoom = scrollEvent.getDeltaY() > 0 ? Math.round(currentZoom * 1.1f) : Math.round(currentZoom * 0.9f);
|
|
||||||
newZoom = Math.max(newZoom, service.getZoomLimits().getMin());
|
|
||||||
newZoom = Math.min(newZoom, service.getZoomLimits().getMax());
|
|
||||||
if(newZoom != currentZoom) {
|
|
||||||
service.setZoom(newZoom);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
service.valueProperty().addListener((observable, oldValue, newValue) -> {
|
service.valueProperty().addListener((observable, oldValue, newValue) -> {
|
||||||
if(newValue != null && !service.getCancelRequested()) {
|
if(newValue != null) {
|
||||||
imageProperty.set(newValue);
|
imageProperty.set(newValue);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
@ -70,29 +57,27 @@ public class WebcamView {
|
||||||
this.view = new Region() {
|
this.view = new Region() {
|
||||||
{
|
{
|
||||||
service.stateProperty().addListener((obs, oldState, newState) -> {
|
service.stateProperty().addListener((obs, oldState, newState) -> {
|
||||||
switch(newState) {
|
switch (newState) {
|
||||||
case READY:
|
case READY:
|
||||||
if(imageProperty.get() == null) {
|
if(imageProperty.get() == null) {
|
||||||
statusPlaceholder.setText("Initializing");
|
statusPlaceholder.setText("Initializing");
|
||||||
getChildren().setAll(statusPlaceholder);
|
getChildren().setAll(statusPlaceholder);
|
||||||
}
|
}
|
||||||
break;
|
break ;
|
||||||
case SCHEDULED:
|
case SCHEDULED:
|
||||||
if(imageProperty.get() == null) {
|
if(imageProperty.get() == null) {
|
||||||
statusPlaceholder.setText("Waiting");
|
statusPlaceholder.setText("Waiting");
|
||||||
getChildren().setAll(statusPlaceholder);
|
getChildren().setAll(statusPlaceholder);
|
||||||
}
|
}
|
||||||
break;
|
break ;
|
||||||
case RUNNING:
|
case RUNNING:
|
||||||
if(imageProperty.get() == null) {
|
|
||||||
imageView.imageProperty().unbind();
|
|
||||||
imageView.imageProperty().bind(imageProperty);
|
|
||||||
getChildren().setAll(imageView);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case CANCELLED:
|
|
||||||
imageProperty.set(null);
|
|
||||||
imageView.imageProperty().unbind();
|
imageView.imageProperty().unbind();
|
||||||
|
imageView.imageProperty().bind(imageProperty);
|
||||||
|
getChildren().setAll(imageView);
|
||||||
|
break ;
|
||||||
|
case CANCELLED:
|
||||||
|
imageView.imageProperty().unbind();
|
||||||
|
imageView.setImage(null);
|
||||||
statusPlaceholder.setText("Stopped");
|
statusPlaceholder.setText("Stopped");
|
||||||
getChildren().setAll(statusPlaceholder);
|
getChildren().setAll(statusPlaceholder);
|
||||||
break;
|
break;
|
||||||
|
|
@ -108,6 +93,7 @@ public class WebcamView {
|
||||||
statusPlaceholder.setText("");
|
statusPlaceholder.setText("");
|
||||||
getChildren().clear();
|
getChildren().clear();
|
||||||
}
|
}
|
||||||
|
requestLayout();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -116,14 +102,14 @@ public class WebcamView {
|
||||||
super.layoutChildren();
|
super.layoutChildren();
|
||||||
double w = getWidth();
|
double w = getWidth();
|
||||||
double h = getHeight();
|
double h = getHeight();
|
||||||
if(service.isRunning()) {
|
if (service.isRunning()) {
|
||||||
imageView.setFitWidth(w);
|
imageView.setFitWidth(w);
|
||||||
imageView.setFitHeight(h);
|
imageView.setFitHeight(h);
|
||||||
imageView.resizeRelocate(0, 0, w, h);
|
imageView.resizeRelocate(0, 0, w, h);
|
||||||
} else {
|
} else {
|
||||||
double labelHeight = statusPlaceholder.prefHeight(w);
|
double labelHeight = statusPlaceholder.prefHeight(w);
|
||||||
double labelWidth = statusPlaceholder.prefWidth(labelHeight);
|
double labelWidth = statusPlaceholder.prefWidth(labelHeight);
|
||||||
statusPlaceholder.resizeRelocate((w - labelWidth) / 2, (h - labelHeight) / 2, labelWidth, labelHeight);
|
statusPlaceholder.resizeRelocate((w - labelWidth)/2, (h-labelHeight)/2, labelWidth, labelHeight);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -36,7 +36,7 @@ public class XprvKeystoreImportPane extends TitledDescriptionPane {
|
||||||
private ExtendedKey xprv;
|
private ExtendedKey xprv;
|
||||||
|
|
||||||
public XprvKeystoreImportPane(Wallet wallet, KeystoreXprvImport importer, KeyDerivation defaultDerivation) {
|
public XprvKeystoreImportPane(Wallet wallet, KeystoreXprvImport importer, KeyDerivation defaultDerivation) {
|
||||||
super(importer.getName(), "Extended key import", importer.getKeystoreImportDescription(), importer.getWalletModel());
|
super(importer.getName(), "Extended key import", importer.getKeystoreImportDescription(), "image/" + importer.getWalletModel().getType() + ".png");
|
||||||
this.wallet = wallet;
|
this.wallet = wallet;
|
||||||
this.importer = importer;
|
this.importer = importer;
|
||||||
this.defaultDerivation = defaultDerivation;
|
this.defaultDerivation = defaultDerivation;
|
||||||
|
|
@ -46,7 +46,7 @@ public class XprvKeystoreImportPane extends TitledDescriptionPane {
|
||||||
}
|
}
|
||||||
|
|
||||||
public XprvKeystoreImportPane(Keystore keystore) {
|
public XprvKeystoreImportPane(Keystore keystore) {
|
||||||
super("Master Private Key", "BIP32 key", "", WalletModel.SEED);
|
super("Master Private Key", "BIP32 key", "", "image/" + WalletModel.SEED.getType() + ".png");
|
||||||
this.wallet = null;
|
this.wallet = null;
|
||||||
this.importer = null;
|
this.importer = null;
|
||||||
this.defaultDerivation = keystore.getKeyDerivation();
|
this.defaultDerivation = keystore.getKeyDerivation();
|
||||||
|
|
|
||||||
|
|
@ -1,23 +0,0 @@
|
||||||
package com.sparrowwallet.sparrow.event;
|
|
||||||
|
|
||||||
import com.sparrowwallet.sparrow.BlockSummary;
|
|
||||||
|
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
public class BlockSummaryEvent {
|
|
||||||
private final Map<Integer, BlockSummary> blockSummaryMap;
|
|
||||||
private final Double nextBlockMedianFeeRate;
|
|
||||||
|
|
||||||
public BlockSummaryEvent(Map<Integer, BlockSummary> blockSummaryMap, Double nextBlockMedianFeeRate) {
|
|
||||||
this.blockSummaryMap = blockSummaryMap;
|
|
||||||
this.nextBlockMedianFeeRate = nextBlockMedianFeeRate;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Map<Integer, BlockSummary> getBlockSummaryMap() {
|
|
||||||
return blockSummaryMap;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Double getNextBlockMedianFeeRate() {
|
|
||||||
return nextBlockMedianFeeRate;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
package com.sparrowwallet.sparrow.event;
|
package com.sparrowwallet.sparrow.event;
|
||||||
|
|
||||||
import com.sparrowwallet.drongo.protocol.BlockHeader;
|
import com.sparrowwallet.drongo.protocol.BlockHeader;
|
||||||
import com.sparrowwallet.sparrow.AppServices;
|
|
||||||
import com.sparrowwallet.sparrow.net.MempoolRateSize;
|
import com.sparrowwallet.sparrow.net.MempoolRateSize;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
@ -14,7 +13,6 @@ public class ConnectionEvent extends FeeRatesUpdatedEvent {
|
||||||
private final int blockHeight;
|
private final int blockHeight;
|
||||||
private final BlockHeader blockHeader;
|
private final BlockHeader blockHeader;
|
||||||
private final Double minimumRelayFeeRate;
|
private final Double minimumRelayFeeRate;
|
||||||
private final Double previousMinimumRelayFeeRate;
|
|
||||||
|
|
||||||
public ConnectionEvent(List<String> serverVersion, String serverBanner, int blockHeight, BlockHeader blockHeader, Map<Integer, Double> targetBlockFeeRates, Set<MempoolRateSize> mempoolRateSizes, Double minimumRelayFeeRate) {
|
public ConnectionEvent(List<String> serverVersion, String serverBanner, int blockHeight, BlockHeader blockHeader, Map<Integer, Double> targetBlockFeeRates, Set<MempoolRateSize> mempoolRateSizes, Double minimumRelayFeeRate) {
|
||||||
super(targetBlockFeeRates, mempoolRateSizes);
|
super(targetBlockFeeRates, mempoolRateSizes);
|
||||||
|
|
@ -23,7 +21,6 @@ public class ConnectionEvent extends FeeRatesUpdatedEvent {
|
||||||
this.blockHeight = blockHeight;
|
this.blockHeight = blockHeight;
|
||||||
this.blockHeader = blockHeader;
|
this.blockHeader = blockHeader;
|
||||||
this.minimumRelayFeeRate = minimumRelayFeeRate;
|
this.minimumRelayFeeRate = minimumRelayFeeRate;
|
||||||
this.previousMinimumRelayFeeRate = AppServices.getMinimumRelayFeeRate();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<String> getServerVersion() {
|
public List<String> getServerVersion() {
|
||||||
|
|
@ -45,8 +42,4 @@ public class ConnectionEvent extends FeeRatesUpdatedEvent {
|
||||||
public Double getMinimumRelayFeeRate() {
|
public Double getMinimumRelayFeeRate() {
|
||||||
return minimumRelayFeeRate;
|
return minimumRelayFeeRate;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Double getPreviousMinimumRelayFeeRate() {
|
|
||||||
return previousMinimumRelayFeeRate;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,23 +7,13 @@ 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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,17 +0,0 @@
|
||||||
package com.sparrowwallet.sparrow.event;
|
|
||||||
|
|
||||||
import com.sparrowwallet.drongo.wallet.Payment;
|
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
public class RequestSendToManyEvent {
|
|
||||||
private final List<Payment> payments;
|
|
||||||
|
|
||||||
public RequestSendToManyEvent(List<Payment> payments) {
|
|
||||||
this.payments = payments;
|
|
||||||
}
|
|
||||||
|
|
||||||
public List<Payment> getPayments() {
|
|
||||||
return payments;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -17,13 +17,12 @@ public class SpendUtxoEvent {
|
||||||
private final boolean requireAllUtxos;
|
private final boolean requireAllUtxos;
|
||||||
private final BlockTransaction replacedTransaction;
|
private final BlockTransaction replacedTransaction;
|
||||||
private final PaymentCode paymentCode;
|
private final PaymentCode paymentCode;
|
||||||
private final boolean allowPaymentChanges;
|
|
||||||
|
|
||||||
public SpendUtxoEvent(Wallet wallet, List<BlockTransactionHashIndex> utxos) {
|
public SpendUtxoEvent(Wallet wallet, List<BlockTransactionHashIndex> utxos) {
|
||||||
this(wallet, utxos, null, null, null, false, null, true);
|
this(wallet, utxos, null, null, null, false, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
public SpendUtxoEvent(Wallet wallet, List<BlockTransactionHashIndex> utxos, List<Payment> payments, List<byte[]> opReturns, Long fee, boolean requireAllUtxos, BlockTransaction replacedTransaction, boolean allowPaymentChanges) {
|
public SpendUtxoEvent(Wallet wallet, List<BlockTransactionHashIndex> utxos, List<Payment> payments, List<byte[]> opReturns, Long fee, boolean requireAllUtxos, BlockTransaction replacedTransaction) {
|
||||||
this.wallet = wallet;
|
this.wallet = wallet;
|
||||||
this.utxos = utxos;
|
this.utxos = utxos;
|
||||||
this.payments = payments;
|
this.payments = payments;
|
||||||
|
|
@ -32,7 +31,6 @@ public class SpendUtxoEvent {
|
||||||
this.requireAllUtxos = requireAllUtxos;
|
this.requireAllUtxos = requireAllUtxos;
|
||||||
this.replacedTransaction = replacedTransaction;
|
this.replacedTransaction = replacedTransaction;
|
||||||
this.paymentCode = null;
|
this.paymentCode = null;
|
||||||
this.allowPaymentChanges = allowPaymentChanges;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public SpendUtxoEvent(Wallet wallet, List<Payment> payments, List<byte[]> opReturns, PaymentCode paymentCode) {
|
public SpendUtxoEvent(Wallet wallet, List<Payment> payments, List<byte[]> opReturns, PaymentCode paymentCode) {
|
||||||
|
|
@ -44,7 +42,6 @@ public class SpendUtxoEvent {
|
||||||
this.requireAllUtxos = false;
|
this.requireAllUtxos = false;
|
||||||
this.replacedTransaction = null;
|
this.replacedTransaction = null;
|
||||||
this.paymentCode = paymentCode;
|
this.paymentCode = paymentCode;
|
||||||
this.allowPaymentChanges = false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public Wallet getWallet() {
|
public Wallet getWallet() {
|
||||||
|
|
@ -78,8 +75,4 @@ public class SpendUtxoEvent {
|
||||||
public PaymentCode getPaymentCode() {
|
public PaymentCode getPaymentCode() {
|
||||||
return paymentCode;
|
return paymentCode;
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean allowPaymentChanges() {
|
|
||||||
return allowPaymentChanges;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,9 +0,0 @@
|
||||||
package com.sparrowwallet.sparrow.event;
|
|
||||||
|
|
||||||
import com.sparrowwallet.drongo.protocol.Transaction;
|
|
||||||
|
|
||||||
public class TransactionOutputsChangedEvent extends TransactionChangedEvent {
|
|
||||||
public TransactionOutputsChangedEvent(Transaction transaction) {
|
|
||||||
super(transaction);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -14,16 +14,9 @@ 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) {
|
||||||
|
|
@ -77,8 +70,4 @@ public class WalletNodeHistoryChangedEvent {
|
||||||
public String getScriptHash() {
|
public String getScriptHash() {
|
||||||
return scriptHash;
|
return scriptHash;
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getStatus() {
|
|
||||||
return status;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,13 @@
|
||||||
package com.sparrowwallet.sparrow.event;
|
package com.sparrowwallet.sparrow.event;
|
||||||
|
|
||||||
import com.sparrowwallet.sparrow.control.WebcamResolution;
|
|
||||||
|
|
||||||
public class WebcamResolutionChangedEvent {
|
public class WebcamResolutionChangedEvent {
|
||||||
private final WebcamResolution resolution;
|
private final boolean hdResolution;
|
||||||
|
|
||||||
public WebcamResolutionChangedEvent(WebcamResolution resolution) {
|
public WebcamResolutionChangedEvent(boolean hdResolution) {
|
||||||
this.resolution = resolution;
|
this.hdResolution = hdResolution;
|
||||||
}
|
}
|
||||||
|
|
||||||
public WebcamResolution getResolution() {
|
public boolean isHdResolution() {
|
||||||
return resolution;
|
return hdResolution;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,6 @@ public class FontAwesome5 extends GlyphFont {
|
||||||
*/
|
*/
|
||||||
public static enum Glyph implements INamedCharacter {
|
public static enum Glyph implements INamedCharacter {
|
||||||
ADJUST('\uf042'),
|
ADJUST('\uf042'),
|
||||||
ANCHOR('\uf13d'),
|
|
||||||
ARROW_CIRCLE_DOWN('\uf0ab'),
|
ARROW_CIRCLE_DOWN('\uf0ab'),
|
||||||
ANGLE_DOUBLE_RIGHT('\uf101'),
|
ANGLE_DOUBLE_RIGHT('\uf101'),
|
||||||
ARROW_DOWN('\uf063'),
|
ARROW_DOWN('\uf063'),
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
package com.sparrowwallet.sparrow.glyphfont;
|
package com.sparrowwallet.sparrow.glyphfont;
|
||||||
|
|
||||||
import com.sparrowwallet.drongo.wallet.Payment;
|
import com.sparrowwallet.drongo.wallet.Payment;
|
||||||
import com.sparrowwallet.drongo.wallet.WalletNodePayment;
|
|
||||||
import com.sparrowwallet.drongo.wallet.WalletTransaction;
|
import com.sparrowwallet.drongo.wallet.WalletTransaction;
|
||||||
import com.sparrowwallet.sparrow.AppServices;
|
import com.sparrowwallet.sparrow.AppServices;
|
||||||
import com.sparrowwallet.sparrow.control.TransactionDiagram;
|
import com.sparrowwallet.sparrow.control.TransactionDiagram;
|
||||||
|
|
@ -14,9 +13,7 @@ public class GlyphUtils {
|
||||||
return getMixGlyph();
|
return getMixGlyph();
|
||||||
} else if(payment.getType().equals(Payment.Type.FAKE_MIX)) {
|
} else if(payment.getType().equals(Payment.Type.FAKE_MIX)) {
|
||||||
return getFakeMixGlyph();
|
return getFakeMixGlyph();
|
||||||
} else if(payment.getType().equals(Payment.Type.ANCHOR)) {
|
} else if(walletTx.isConsolidationSend(payment)) {
|
||||||
return getAnchorGlyph();
|
|
||||||
} else if(payment instanceof WalletNodePayment) {
|
|
||||||
return getConsolidationGlyph();
|
return getConsolidationGlyph();
|
||||||
} else if(walletTx.isPremixSend(payment)) {
|
} else if(walletTx.isPremixSend(payment)) {
|
||||||
return getPremixGlyph();
|
return getPremixGlyph();
|
||||||
|
|
@ -214,24 +211,10 @@ 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");
|
||||||
downGlyph.setFontSize(12);
|
downGlyph.setFontSize(12);
|
||||||
return downGlyph;
|
return downGlyph;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static Glyph getAnchorGlyph() {
|
|
||||||
Glyph anchorGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.ANCHOR);
|
|
||||||
anchorGlyph.getStyleClass().add("anchor-icon");
|
|
||||||
anchorGlyph.setFontSize(12);
|
|
||||||
return anchorGlyph;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -192,7 +192,7 @@ public class Bip129 implements KeystoreFileExport, KeystoreFileImport, WalletExp
|
||||||
public void exportWallet(Wallet wallet, OutputStream outputStream, String password) throws ExportException {
|
public void exportWallet(Wallet wallet, OutputStream outputStream, String password) throws ExportException {
|
||||||
try {
|
try {
|
||||||
String record = "BSMS 1.0\n" +
|
String record = "BSMS 1.0\n" +
|
||||||
OutputDescriptor.getOutputDescriptor(wallet, KeyPurpose.DEFAULT_PURPOSES, null) +
|
OutputDescriptor.getOutputDescriptor(wallet) +
|
||||||
"\n/0/*,/1/*\n" +
|
"\n/0/*,/1/*\n" +
|
||||||
wallet.getNode(KeyPurpose.RECEIVE).getChildren().iterator().next().getAddress();
|
wallet.getNode(KeyPurpose.RECEIVE).getChildren().iterator().next().getAddress();
|
||||||
outputStream.write(record.getBytes(StandardCharsets.UTF_8));
|
outputStream.write(record.getBytes(StandardCharsets.UTF_8));
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@ import java.io.InputStream;
|
||||||
public class BlueWalletMultisig extends ColdcardMultisig {
|
public class BlueWalletMultisig extends ColdcardMultisig {
|
||||||
@Override
|
@Override
|
||||||
public String getName() {
|
public String getName() {
|
||||||
return "BlueWallet Vault Multisig";
|
return "Blue Wallet Vault Multisig";
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
@ -21,7 +21,7 @@ public class BlueWalletMultisig extends ColdcardMultisig {
|
||||||
public Wallet importWallet(InputStream inputStream, String password) throws ImportException {
|
public Wallet importWallet(InputStream inputStream, String password) throws ImportException {
|
||||||
Wallet wallet = super.importWallet(inputStream, password);
|
Wallet wallet = super.importWallet(inputStream, password);
|
||||||
for(Keystore keystore : wallet.getKeystores()) {
|
for(Keystore keystore : wallet.getKeystores()) {
|
||||||
keystore.setLabel(keystore.getLabel().replace("Coldcard", "BlueWallet"));
|
keystore.setLabel(keystore.getLabel().replace("Coldcard", "Blue Wallet"));
|
||||||
keystore.setWalletModel(WalletModel.BLUE_WALLET);
|
keystore.setWalletModel(WalletModel.BLUE_WALLET);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -30,12 +30,12 @@ public class BlueWalletMultisig extends ColdcardMultisig {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String getWalletImportDescription() {
|
public String getWalletImportDescription() {
|
||||||
return "Import file or QR created by using the Wallet > Export Coordination Setup feature on your BlueWallet Vault wallet.";
|
return "Import file or QR created by using the Wallet > Export Coordination Setup feature on your Blue Wallet Vault wallet.";
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String getWalletExportDescription() {
|
public String getWalletExportDescription() {
|
||||||
return "Export file that can be read by BlueWallet using the Add Wallet > Vault > Import wallet feature.";
|
return "Export file that can be read by Blue Wallet using the Add Wallet > Vault > Import wallet feature.";
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
|
||||||
|
|
@ -2,12 +2,10 @@ 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;
|
||||||
import com.sparrowwallet.sparrow.control.QRDensity;
|
import com.sparrowwallet.sparrow.control.QRDensity;
|
||||||
import com.sparrowwallet.sparrow.control.WebcamResolution;
|
|
||||||
import com.sparrowwallet.sparrow.net.*;
|
import com.sparrowwallet.sparrow.net.*;
|
||||||
import com.sparrowwallet.sparrow.wallet.FeeRatesSelection;
|
import com.sparrowwallet.sparrow.wallet.FeeRatesSelection;
|
||||||
import com.sparrowwallet.sparrow.wallet.OptimizationStrategy;
|
import com.sparrowwallet.sparrow.wallet.OptimizationStrategy;
|
||||||
|
|
@ -53,19 +51,15 @@ 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;
|
||||||
private int enumerateHwPeriod = ENUMERATE_HW_PERIOD_SECS;
|
private int enumerateHwPeriod = ENUMERATE_HW_PERIOD_SECS;
|
||||||
private QRDensity qrDensity;
|
private QRDensity qrDensity;
|
||||||
private WebcamResolution webcamResolution;
|
private Boolean hdCapture;
|
||||||
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;
|
||||||
|
|
@ -74,7 +68,6 @@ 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;
|
||||||
|
|
@ -85,7 +78,6 @@ public class Config {
|
||||||
private int maxPageSize = DEFAULT_PAGE_SIZE;
|
private int maxPageSize = DEFAULT_PAGE_SIZE;
|
||||||
private boolean usePayNym;
|
private boolean usePayNym;
|
||||||
private boolean mempoolFullRbf;
|
private boolean mempoolFullRbf;
|
||||||
private double minRelayFeeRate = Transaction.DEFAULT_MIN_RELAY_FEE;
|
|
||||||
private Double appWidth;
|
private Double appWidth;
|
||||||
private Double appHeight;
|
private Double appHeight;
|
||||||
|
|
||||||
|
|
@ -354,34 +346,6 @@ 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() {
|
||||||
|
|
@ -419,12 +383,16 @@ public class Config {
|
||||||
flush();
|
flush();
|
||||||
}
|
}
|
||||||
|
|
||||||
public WebcamResolution getWebcamResolution() {
|
public Boolean getHdCapture() {
|
||||||
return webcamResolution;
|
return hdCapture;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setWebcamResolution(WebcamResolution webcamResolution) {
|
public Boolean isHdCapture() {
|
||||||
this.webcamResolution = webcamResolution;
|
return hdCapture != null && hdCapture;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setHdCapture(Boolean hdCapture) {
|
||||||
|
this.hdCapture = hdCapture;
|
||||||
flush();
|
flush();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -450,15 +418,6 @@ public class Config {
|
||||||
flush();
|
flush();
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getWebcamDeviceId() {
|
|
||||||
return webcamDeviceId;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setWebcamDeviceId(String webcamDeviceId) {
|
|
||||||
this.webcamDeviceId = webcamDeviceId;
|
|
||||||
flush();
|
|
||||||
}
|
|
||||||
|
|
||||||
public ServerType getServerType() {
|
public ServerType getServerType() {
|
||||||
return serverType;
|
return serverType;
|
||||||
}
|
}
|
||||||
|
|
@ -593,15 +552,6 @@ 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;
|
||||||
}
|
}
|
||||||
|
|
@ -720,14 +670,6 @@ public class Config {
|
||||||
flush();
|
flush();
|
||||||
}
|
}
|
||||||
|
|
||||||
public double getMinRelayFeeRate() {
|
|
||||||
return minRelayFeeRate;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setMinRelayFeeRate(double minRelayFeeRate) {
|
|
||||||
this.minRelayFeeRate = minRelayFeeRate;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Double getAppWidth() {
|
public Double getAppWidth() {
|
||||||
return appWidth;
|
return appWidth;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,9 @@
|
||||||
package com.sparrowwallet.sparrow.io;
|
package com.sparrowwallet.sparrow.io;
|
||||||
|
|
||||||
import com.sparrowwallet.drongo.KeyDerivation;
|
|
||||||
import com.sparrowwallet.drongo.KeyPurpose;
|
import com.sparrowwallet.drongo.KeyPurpose;
|
||||||
import com.sparrowwallet.drongo.OutputDescriptor;
|
import com.sparrowwallet.drongo.OutputDescriptor;
|
||||||
import com.sparrowwallet.drongo.wallet.Keystore;
|
|
||||||
import com.sparrowwallet.drongo.wallet.Wallet;
|
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||||
import com.sparrowwallet.drongo.wallet.WalletModel;
|
import com.sparrowwallet.drongo.wallet.WalletModel;
|
||||||
import com.sparrowwallet.sparrow.wallet.KeystoreController;
|
|
||||||
|
|
||||||
import java.io.*;
|
import java.io.*;
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
|
|
@ -95,7 +92,7 @@ public class Descriptor implements WalletImport, WalletExport {
|
||||||
InputStream secondClone = new ByteArrayInputStream(baos.toByteArray());
|
InputStream secondClone = new ByteArrayInputStream(baos.toByteArray());
|
||||||
|
|
||||||
try {
|
try {
|
||||||
return ensureKeyDerivations(PdfUtils.getOutputDescriptor(firstClone).toWallet());
|
return PdfUtils.getOutputDescriptor(firstClone).toWallet();
|
||||||
} catch(Exception e) {
|
} catch(Exception e) {
|
||||||
//ignore
|
//ignore
|
||||||
}
|
}
|
||||||
|
|
@ -103,7 +100,7 @@ public class Descriptor implements WalletImport, WalletExport {
|
||||||
List<String> paragraphs = getParagraphs(secondClone);
|
List<String> paragraphs = getParagraphs(secondClone);
|
||||||
for(String paragraph : paragraphs) {
|
for(String paragraph : paragraphs) {
|
||||||
OutputDescriptor descriptor = OutputDescriptor.getOutputDescriptor(paragraph);
|
OutputDescriptor descriptor = OutputDescriptor.getOutputDescriptor(paragraph);
|
||||||
return ensureKeyDerivations(descriptor.toWallet());
|
return descriptor.toWallet();
|
||||||
}
|
}
|
||||||
|
|
||||||
throw new ImportException("Could not find an output descriptor in the file");
|
throw new ImportException("Could not find an output descriptor in the file");
|
||||||
|
|
@ -119,34 +116,24 @@ public class Descriptor implements WalletImport, WalletExport {
|
||||||
BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8));
|
BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8));
|
||||||
for(String line : reader.lines().map(String::trim).toArray(String[]::new)) {
|
for(String line : reader.lines().map(String::trim).toArray(String[]::new)) {
|
||||||
if(line.isEmpty()) {
|
if(line.isEmpty()) {
|
||||||
if(!paragraph.isEmpty()) {
|
if(paragraph.length() > 0) {
|
||||||
paragraphs.add(paragraph.toString());
|
paragraphs.add(paragraph.toString());
|
||||||
paragraph.setLength(0);
|
paragraph.setLength(0);
|
||||||
}
|
}
|
||||||
} else if(line.startsWith("#")) {
|
} else if(line.startsWith("#")) {
|
||||||
continue;
|
continue;
|
||||||
} else {
|
} else {
|
||||||
paragraph.append(line.replaceFirst("^.+:", "").trim());
|
paragraph.append(line);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if(!paragraph.isEmpty()) {
|
if(paragraph.length() > 0) {
|
||||||
paragraphs.add(paragraph.toString());
|
paragraphs.add(paragraph.toString());
|
||||||
}
|
}
|
||||||
|
|
||||||
return paragraphs;
|
return paragraphs;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static Wallet ensureKeyDerivations(Wallet wallet) {
|
|
||||||
for(Keystore keystore : wallet.getKeystores()) {
|
|
||||||
if(keystore.getKeyDerivation().getMasterFingerprint() == null || keystore.getKeyDerivation().getDerivationPath() == null) {
|
|
||||||
keystore.setKeyDerivation(new KeyDerivation(KeystoreController.DEFAULT_WATCH_ONLY_FINGERPRINT, wallet.getScriptType().getDefaultDerivationPath()));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return wallet;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean isWalletImportScannable() {
|
public boolean isWalletImportScannable() {
|
||||||
return true;
|
return true;
|
||||||
|
|
|
||||||
|
|
@ -37,8 +37,7 @@ 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("# First close Sparrow and edit your config file in Sparrow home to set \"legacyServer\": true\n");
|
writer.write("# Copy the lines below into the relevant sections in your EPS config.ini file\n\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);
|
||||||
|
|
|
||||||
5
src/main/java/com/sparrowwallet/sparrow/io/FileType.java
Normal file
5
src/main/java/com/sparrowwallet/sparrow/io/FileType.java
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
package com.sparrowwallet.sparrow.io;
|
||||||
|
|
||||||
|
public enum FileType {
|
||||||
|
TEXT, JSON, BINARY, UNKNOWN;
|
||||||
|
}
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
package com.sparrowwallet.sparrow.io;
|
package com.sparrowwallet.sparrow.io;
|
||||||
|
|
||||||
import com.sparrowwallet.drongo.ExtendedKey;
|
import com.sparrowwallet.drongo.ExtendedKey;
|
||||||
import com.sparrowwallet.drongo.IOUtils;
|
|
||||||
import com.sparrowwallet.drongo.OsType;
|
import com.sparrowwallet.drongo.OsType;
|
||||||
import com.sparrowwallet.drongo.OutputDescriptor;
|
import com.sparrowwallet.drongo.OutputDescriptor;
|
||||||
import com.sparrowwallet.drongo.protocol.ScriptType;
|
import com.sparrowwallet.drongo.protocol.ScriptType;
|
||||||
|
|
@ -57,7 +56,7 @@ public class Hwi {
|
||||||
return lark.enumerate().stream().map(Device::fromHardwareClient).toList();
|
return lark.enumerate().stream().map(Device::fromHardwareClient).toList();
|
||||||
} catch(Throwable e) {
|
} catch(Throwable e) {
|
||||||
log.error("Error enumerating USB devices", e);
|
log.error("Error enumerating USB devices", e);
|
||||||
throw new ImportException(e.getMessage() == null || e.getMessage().isEmpty() ? "Error scanning, check devices are ready" : e.getMessage(), e);
|
throw new ImportException("Error scanning" + (e.getMessage() == null || e.getMessage().isEmpty() ? ", check devices are ready" : ": " + e.getMessage()), e);
|
||||||
} finally {
|
} finally {
|
||||||
isPromptActive = false;
|
isPromptActive = false;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
153
src/main/java/com/sparrowwallet/sparrow/io/IOUtils.java
Normal file
153
src/main/java/com/sparrowwallet/sparrow/io/IOUtils.java
Normal file
|
|
@ -0,0 +1,153 @@
|
||||||
|
package com.sparrowwallet.sparrow.io;
|
||||||
|
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
import java.io.*;
|
||||||
|
import java.net.URI;
|
||||||
|
import java.net.URISyntaxException;
|
||||||
|
import java.net.URL;
|
||||||
|
import java.net.URLDecoder;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.nio.file.FileSystems;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.security.SecureRandom;
|
||||||
|
import java.util.*;
|
||||||
|
import java.util.jar.JarEntry;
|
||||||
|
import java.util.jar.JarFile;
|
||||||
|
|
||||||
|
public class IOUtils {
|
||||||
|
private static final Logger log = LoggerFactory.getLogger(IOUtils.class);
|
||||||
|
|
||||||
|
public static FileType getFileType(File file) {
|
||||||
|
try {
|
||||||
|
String type = Files.probeContentType(file.toPath());
|
||||||
|
if(type == null) {
|
||||||
|
if(file.getName().toLowerCase(Locale.ROOT).endsWith("txn") || file.getName().toLowerCase(Locale.ROOT).endsWith("psbt")) {
|
||||||
|
return FileType.TEXT;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(file.exists()) {
|
||||||
|
try(BufferedReader br = new BufferedReader(new InputStreamReader(new FileInputStream(file), StandardCharsets.UTF_8))) {
|
||||||
|
String line = br.readLine();
|
||||||
|
if(line != null) {
|
||||||
|
if(line.startsWith("01000000") || line.startsWith("cHNid")) {
|
||||||
|
return FileType.TEXT;
|
||||||
|
} else if(line.startsWith("{")) {
|
||||||
|
return FileType.JSON;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return FileType.BINARY;
|
||||||
|
} else if (type.equals("application/json")) {
|
||||||
|
return FileType.JSON;
|
||||||
|
} else if (type.startsWith("text")) {
|
||||||
|
return FileType.TEXT;
|
||||||
|
}
|
||||||
|
} catch (IOException e) {
|
||||||
|
//ignore
|
||||||
|
}
|
||||||
|
|
||||||
|
return FileType.UNKNOWN;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* List directory contents for a resource folder. Not recursive.
|
||||||
|
* This is basically a brute-force implementation.
|
||||||
|
* Works for regular files, JARs and Java modules.
|
||||||
|
*
|
||||||
|
* @param clazz Any java class that lives in the same place as the resources you want.
|
||||||
|
* @param path Should end with "/", but not start with one.
|
||||||
|
* @return Just the name of each member item, not the full paths.
|
||||||
|
* @throws URISyntaxException
|
||||||
|
* @throws IOException
|
||||||
|
*/
|
||||||
|
public static String[] getResourceListing(Class clazz, String path) throws URISyntaxException, IOException {
|
||||||
|
URL dirURL = clazz.getClassLoader().getResource(path);
|
||||||
|
if(dirURL != null && dirURL.getProtocol().equals("file")) {
|
||||||
|
/* A file path: easy enough */
|
||||||
|
return new File(dirURL.toURI()).list();
|
||||||
|
}
|
||||||
|
|
||||||
|
if(dirURL == null) {
|
||||||
|
/*
|
||||||
|
* In case of a jar file, we can't actually find a directory.
|
||||||
|
* Have to assume the same jar as clazz.
|
||||||
|
*/
|
||||||
|
String me = clazz.getName().replace(".", "/")+".class";
|
||||||
|
dirURL = clazz.getClassLoader().getResource(me);
|
||||||
|
}
|
||||||
|
|
||||||
|
if(dirURL.getProtocol().equals("jar")) {
|
||||||
|
/* A JAR path */
|
||||||
|
String jarPath = dirURL.getPath().substring(5, dirURL.getPath().indexOf("!")); //strip out only the JAR file
|
||||||
|
JarFile jar = new JarFile(URLDecoder.decode(jarPath, "UTF-8"));
|
||||||
|
Enumeration<JarEntry> entries = jar.entries(); //gives ALL entries in jar
|
||||||
|
Set<String> result = new HashSet<String>(); //avoid duplicates in case it is a subdirectory
|
||||||
|
while(entries.hasMoreElements()) {
|
||||||
|
String name = entries.nextElement().getName();
|
||||||
|
if(name.startsWith(path)) { //filter according to the path
|
||||||
|
String entry = name.substring(path.length());
|
||||||
|
int checkSubdir = entry.indexOf("/");
|
||||||
|
if (checkSubdir >= 0) {
|
||||||
|
// if it is a subdirectory, we just return the directory name
|
||||||
|
entry = entry.substring(0, checkSubdir);
|
||||||
|
}
|
||||||
|
if(!entry.isEmpty()) {
|
||||||
|
result.add(entry);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result.toArray(new String[result.size()]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if(dirURL.getProtocol().equals("jrt")) {
|
||||||
|
java.nio.file.FileSystem jrtFs = FileSystems.newFileSystem(URI.create("jrt:/"), Collections.emptyMap());
|
||||||
|
Path resourcePath = jrtFs.getPath("modules/com.sparrowwallet.sparrow", path);
|
||||||
|
return Files.list(resourcePath).map(filePath -> filePath.getFileName().toString()).toArray(String[]::new);
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new UnsupportedOperationException("Cannot list files for URL " + dirURL);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean deleteDirectory(File directory) {
|
||||||
|
try {
|
||||||
|
Files.walk(directory.toPath())
|
||||||
|
.sorted(Comparator.reverseOrder())
|
||||||
|
.map(Path::toFile)
|
||||||
|
.forEach(File::delete);
|
||||||
|
} catch(IOException e) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean secureDelete(File file) {
|
||||||
|
if(file.exists()) {
|
||||||
|
long length = file.length();
|
||||||
|
SecureRandom random = new SecureRandom();
|
||||||
|
byte[] data = new byte[1024*1024];
|
||||||
|
random.nextBytes(data);
|
||||||
|
try(RandomAccessFile raf = new RandomAccessFile(file, "rws")) {
|
||||||
|
raf.seek(0);
|
||||||
|
raf.getFilePointer();
|
||||||
|
int pos = 0;
|
||||||
|
while(pos < length) {
|
||||||
|
raf.write(data);
|
||||||
|
pos += data.length;
|
||||||
|
}
|
||||||
|
} catch(IOException e) {
|
||||||
|
log.warn("Error overwriting file for deletion: " + file.getName(), e);
|
||||||
|
}
|
||||||
|
|
||||||
|
return file.delete();
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -2,8 +2,6 @@ package com.sparrowwallet.sparrow.io;
|
||||||
|
|
||||||
import com.google.gson.*;
|
import com.google.gson.*;
|
||||||
import com.sparrowwallet.drongo.ExtendedKey;
|
import com.sparrowwallet.drongo.ExtendedKey;
|
||||||
import com.sparrowwallet.drongo.FileType;
|
|
||||||
import com.sparrowwallet.drongo.IOUtils;
|
|
||||||
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.address.InvalidAddressException;
|
import com.sparrowwallet.drongo.address.InvalidAddressException;
|
||||||
|
|
|
||||||
|
|
@ -684,7 +684,7 @@ public class Storage {
|
||||||
|
|
||||||
public static Executor getSingleThreadedExecutor() {
|
public static Executor getSingleThreadedExecutor() {
|
||||||
if(singleThreadedExecutor == null) {
|
if(singleThreadedExecutor == null) {
|
||||||
BasicThreadFactory factory = BasicThreadFactory.builder().namingPattern("LoadWalletService-single").daemon(true).priority(Thread.MIN_PRIORITY).build();
|
BasicThreadFactory factory = new BasicThreadFactory.Builder().namingPattern("LoadWalletService-single").daemon(true).priority(Thread.MIN_PRIORITY).build();
|
||||||
singleThreadedExecutor = Executors.newSingleThreadScheduledExecutor(factory);
|
singleThreadedExecutor = Executors.newSingleThreadScheduledExecutor(factory);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,43 +1,33 @@
|
||||||
package com.sparrowwallet.sparrow.io;
|
package com.sparrowwallet.sparrow.io;
|
||||||
|
|
||||||
import com.csvreader.CsvReader;
|
import com.csvreader.CsvReader;
|
||||||
import com.google.gson.*;
|
import com.google.gson.Gson;
|
||||||
import com.sparrowwallet.drongo.KeyDerivation;
|
|
||||||
import com.sparrowwallet.drongo.KeyPurpose;
|
import com.sparrowwallet.drongo.KeyPurpose;
|
||||||
import com.sparrowwallet.drongo.OutputDescriptor;
|
import com.sparrowwallet.drongo.OutputDescriptor;
|
||||||
import com.sparrowwallet.drongo.Utils;
|
import com.sparrowwallet.drongo.Utils;
|
||||||
import com.sparrowwallet.drongo.protocol.*;
|
|
||||||
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.event.KeystoreLabelsChangedEvent;
|
import com.sparrowwallet.sparrow.event.KeystoreLabelsChangedEvent;
|
||||||
import com.sparrowwallet.sparrow.event.WalletEntryLabelsChangedEvent;
|
import com.sparrowwallet.sparrow.event.WalletEntryLabelsChangedEvent;
|
||||||
import com.sparrowwallet.sparrow.event.WalletUtxoStatusChangedEvent;
|
import com.sparrowwallet.sparrow.event.WalletUtxoStatusChangedEvent;
|
||||||
import com.sparrowwallet.sparrow.net.ExchangeSource;
|
|
||||||
import com.sparrowwallet.sparrow.wallet.*;
|
import com.sparrowwallet.sparrow.wallet.*;
|
||||||
import org.apache.commons.lang3.time.DateUtils;
|
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
import java.io.*;
|
import java.io.*;
|
||||||
import java.math.BigDecimal;
|
|
||||||
import java.math.RoundingMode;
|
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
import java.text.DateFormat;
|
|
||||||
import java.text.ParseException;
|
|
||||||
import java.text.SimpleDateFormat;
|
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
import java.util.regex.Matcher;
|
|
||||||
import java.util.regex.Pattern;
|
|
||||||
import java.util.stream.Collectors;
|
|
||||||
import java.util.stream.Stream;
|
|
||||||
|
|
||||||
public class WalletLabels implements WalletImport, WalletExport {
|
public class WalletLabels implements WalletImport, WalletExport {
|
||||||
private static final Logger log = LoggerFactory.getLogger(WalletLabels.class);
|
private static final Logger log = LoggerFactory.getLogger(WalletLabels.class);
|
||||||
private static final long ONE_DAY = 24*60*60*1000L;
|
|
||||||
|
|
||||||
private final List<WalletForm> walletForms;
|
private final List<WalletForm> walletForms;
|
||||||
|
|
||||||
|
public WalletLabels() {
|
||||||
|
this.walletForms = Collections.emptyList();
|
||||||
|
}
|
||||||
|
|
||||||
public WalletLabels(List<WalletForm> walletForms) {
|
public WalletLabels(List<WalletForm> walletForms) {
|
||||||
this.walletForms = walletForms;
|
this.walletForms = walletForms;
|
||||||
}
|
}
|
||||||
|
|
@ -60,9 +50,8 @@ public class WalletLabels implements WalletImport, WalletExport {
|
||||||
@Override
|
@Override
|
||||||
public void exportWallet(Wallet wallet, OutputStream outputStream, String password) throws ExportException {
|
public void exportWallet(Wallet wallet, OutputStream outputStream, String password) throws ExportException {
|
||||||
List<Label> labels = new ArrayList<>();
|
List<Label> labels = new ArrayList<>();
|
||||||
Map<Date, Double> fiatRates = getFiatRates(walletForms);
|
List<Wallet> allWallets = wallet.isMasterWallet() ? wallet.getAllWallets() : wallet.getMasterWallet().getAllWallets();
|
||||||
for(WalletForm exportWalletForm : walletForms) {
|
for(Wallet exportWallet : allWallets) {
|
||||||
Wallet exportWallet = exportWalletForm.getWallet();
|
|
||||||
OutputDescriptor outputDescriptor = OutputDescriptor.getOutputDescriptor(exportWallet);
|
OutputDescriptor outputDescriptor = OutputDescriptor.getOutputDescriptor(exportWallet);
|
||||||
String origin = outputDescriptor.toString(true, false, false);
|
String origin = outputDescriptor.toString(true, false, false);
|
||||||
|
|
||||||
|
|
@ -72,43 +61,34 @@ public class WalletLabels implements WalletImport, WalletExport {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Set<Sha256Hash> confirmingTxs = new HashSet<>();
|
for(BlockTransaction blkTx : exportWallet.getWalletTransactions().values()) {
|
||||||
WalletTransactionsEntry walletTransactionsEntry = exportWalletForm.getWalletTransactionsEntry();
|
if(blkTx.getLabel() != null && !blkTx.getLabel().isEmpty()) {
|
||||||
for(Entry entry : walletTransactionsEntry.getChildren()) {
|
labels.add(new Label(Type.tx, blkTx.getHashAsString(), blkTx.getLabel(), origin, null));
|
||||||
TransactionEntry txEntry = (TransactionEntry)entry;
|
|
||||||
BlockTransaction blkTx = txEntry.getBlockTransaction();
|
|
||||||
labels.add(new TransactionLabel(blkTx.getHashAsString(), blkTx.getLabel(), origin,
|
|
||||||
txEntry.isConfirming() ? null : blkTx.getHeight(), blkTx.getDate(),
|
|
||||||
getFee(walletTransactionsEntry.getWallet(), blkTx), txEntry.getValue(),
|
|
||||||
getFiatValue(blkTx.getDate(), Transaction.SATOSHIS_PER_BITCOIN, fiatRates)));
|
|
||||||
if(txEntry.isConfirming()) {
|
|
||||||
confirmingTxs.add(blkTx.getHash());
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for(WalletNode addressNode : exportWallet.getWalletAddresses().values()) {
|
for(WalletNode addressNode : exportWallet.getWalletAddresses().values()) {
|
||||||
labels.add(new AddressLabel(addressNode.getAddress().toString(), addressNode.getLabel(), origin, addressNode.getDerivationPath().substring(1),
|
if(addressNode.getLabel() != null && !addressNode.getLabel().isEmpty()) {
|
||||||
addressNode.getTransactionOutputs().stream().flatMap(txo -> txo.isSpent() ? Stream.of(txo, txo.getSpentBy()) : Stream.of(txo))
|
labels.add(new Label(Type.addr, addressNode.getAddress().toString(), addressNode.getLabel(), null, null));
|
||||||
.filter(ref -> !confirmingTxs.contains(ref.getHash())).map(BlockTransactionHash::getHeight).toList()));
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for(Map.Entry<BlockTransactionHashIndex, WalletNode> txoEntry : exportWallet.getWalletTxos().entrySet()) {
|
for(BlockTransactionHashIndex txo : exportWallet.getWalletTxos().keySet()) {
|
||||||
BlockTransactionHashIndex txo = txoEntry.getKey();
|
String spendable = (txo.isSpent() ? null : txo.getStatus() == Status.FROZEN ? "false" : "true");
|
||||||
WalletNode addressNode = txoEntry.getValue();
|
if(txo.getLabel() != null && !txo.getLabel().isEmpty()) {
|
||||||
Boolean spendable = (txo.isSpent() ? null : txo.getStatus() != Status.FROZEN);
|
labels.add(new Label(Type.output, txo.toString(), txo.getLabel(), null, spendable));
|
||||||
labels.add(new InputOutputLabel(Type.output, txo.toString(), txo.getLabel(), origin, spendable, addressNode.getDerivationPath().substring(1), txo.getValue(),
|
} else if(!txo.isSpent()) {
|
||||||
confirmingTxs.contains(txo.getHash()) ? null : txo.getHeight(), txo.getDate(), getFiatValue(txo, fiatRates)));
|
labels.add(new Label(Type.output, txo.toString(), null, null, spendable));
|
||||||
|
}
|
||||||
|
|
||||||
if(txo.isSpent()) {
|
if(txo.isSpent() && txo.getSpentBy().getLabel() != null && !txo.getSpentBy().getLabel().isEmpty()) {
|
||||||
BlockTransactionHashIndex txi = txo.getSpentBy();
|
labels.add(new Label(Type.input, txo.getSpentBy().toString(), txo.getSpentBy().getLabel(), null, null));
|
||||||
labels.add(new InputOutputLabel(Type.input, txi.toString(), txi.getLabel(), origin, null, addressNode.getDerivationPath().substring(1), txi.getValue(),
|
|
||||||
confirmingTxs.contains(txi.getHash()) ? null : txi.getHeight(), txi.getDate(), getFiatValue(txi, fiatRates)));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
Gson gson = new GsonBuilder().registerTypeAdapter(Date.class, new GsonUTCDateAdapter()).create();
|
Gson gson = new Gson();
|
||||||
BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(outputStream, StandardCharsets.UTF_8));
|
BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(outputStream, StandardCharsets.UTF_8));
|
||||||
|
|
||||||
for(Label label : labels) {
|
for(Label label : labels) {
|
||||||
|
|
@ -134,7 +114,7 @@ public class WalletLabels implements WalletImport, WalletExport {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean isWalletExportScannable() {
|
public boolean isWalletExportScannable() {
|
||||||
return true;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
@ -205,7 +185,7 @@ public class WalletLabels implements WalletImport, WalletExport {
|
||||||
}
|
}
|
||||||
|
|
||||||
OutputDescriptor outputDescriptor = OutputDescriptor.getOutputDescriptor(wallet);
|
OutputDescriptor outputDescriptor = OutputDescriptor.getOutputDescriptor(wallet);
|
||||||
Origin origin = Origin.fromOutputDescriptor(outputDescriptor);
|
String origin = outputDescriptor.toString(true, false, false);
|
||||||
|
|
||||||
List<Entry> transactionEntries = walletForm.getWalletTransactionsEntry().getChildren();
|
List<Entry> transactionEntries = walletForm.getWalletTransactionsEntry().getChildren();
|
||||||
List<Entry> addressEntries = new ArrayList<>();
|
List<Entry> addressEntries = new ArrayList<>();
|
||||||
|
|
@ -214,7 +194,7 @@ public class WalletLabels implements WalletImport, WalletExport {
|
||||||
List<Entry> utxoEntries = walletForm.getWalletUtxosEntry().getChildren();
|
List<Entry> utxoEntries = walletForm.getWalletUtxosEntry().getChildren();
|
||||||
|
|
||||||
for(Label label : labels) {
|
for(Label label : labels) {
|
||||||
if(label.origin != null && !Origin.fromString(label.origin).equals(origin)) {
|
if(label.origin != null && !label.origin.equals(origin)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -267,11 +247,11 @@ public class WalletLabels implements WalletImport, WalletExport {
|
||||||
addChangedEntry(changedWalletEntries, txioEntry);
|
addChangedEntry(changedWalletEntries, txioEntry);
|
||||||
}
|
}
|
||||||
|
|
||||||
if(label.type == Type.output && !reference.isSpent() && label.spendable != null) {
|
if(label.type == Type.output && !reference.isSpent()) {
|
||||||
if(!label.spendable && reference.getStatus() != Status.FROZEN) {
|
if("false".equalsIgnoreCase(label.spendable) && reference.getStatus() != Status.FROZEN) {
|
||||||
reference.setStatus(Status.FROZEN);
|
reference.setStatus(Status.FROZEN);
|
||||||
addChangedUtxo(changedWalletUtxoStatuses, txioEntry);
|
addChangedUtxo(changedWalletUtxoStatuses, txioEntry);
|
||||||
} else if(label.spendable && reference.getStatus() == Status.FROZEN) {
|
} else if("true".equalsIgnoreCase(label.spendable) && reference.getStatus() == Status.FROZEN) {
|
||||||
reference.setStatus(null);
|
reference.setStatus(null);
|
||||||
addChangedUtxo(changedWalletUtxoStatuses, txioEntry);
|
addChangedUtxo(changedWalletUtxoStatuses, txioEntry);
|
||||||
}
|
}
|
||||||
|
|
@ -336,7 +316,7 @@ public class WalletLabels implements WalletImport, WalletExport {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean isWalletImportScannable() {
|
public boolean isWalletImportScannable() {
|
||||||
return true;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
@ -344,99 +324,12 @@ public class WalletLabels implements WalletImport, WalletExport {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
private Long getFee(Wallet wallet, BlockTransaction blockTransaction) {
|
|
||||||
long fee = 0L;
|
|
||||||
for(TransactionInput txInput : blockTransaction.getTransaction().getInputs()) {
|
|
||||||
if(txInput.isCoinBase()) {
|
|
||||||
return 0L;
|
|
||||||
}
|
|
||||||
|
|
||||||
BlockTransaction inputTx = wallet.getWalletTransaction(txInput.getOutpoint().getHash());
|
|
||||||
if(inputTx == null || inputTx.getTransaction().getOutputs().size() <= txInput.getOutpoint().getIndex()) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
TransactionOutput spentOutput = inputTx.getTransaction().getOutputs().get((int)txInput.getOutpoint().getIndex());
|
|
||||||
fee += spentOutput.getValue();
|
|
||||||
}
|
|
||||||
|
|
||||||
for(TransactionOutput txOutput : blockTransaction.getTransaction().getOutputs()) {
|
|
||||||
fee -= txOutput.getValue();
|
|
||||||
}
|
|
||||||
|
|
||||||
return fee;
|
|
||||||
}
|
|
||||||
|
|
||||||
private Map<Date, Double> getFiatRates(List<WalletForm> walletForms) {
|
|
||||||
ExchangeSource exchangeSource = getExchangeSource();
|
|
||||||
Currency fiatCurrency = getFiatCurrency();
|
|
||||||
Map<Date, Double> fiatRates = new HashMap<>();
|
|
||||||
if(fiatCurrency != null) {
|
|
||||||
long min = Long.MAX_VALUE;
|
|
||||||
long max = Long.MIN_VALUE;
|
|
||||||
|
|
||||||
for(WalletForm walletForm : walletForms) {
|
|
||||||
WalletTransactionsEntry walletTransactionsEntry = walletForm.getWalletTransactionsEntry();
|
|
||||||
if(!walletTransactionsEntry.getChildren().isEmpty()) {
|
|
||||||
LongSummaryStatistics stats = walletTransactionsEntry.getChildren().stream()
|
|
||||||
.map(entry -> ((TransactionEntry)entry).getBlockTransaction().getDate())
|
|
||||||
.filter(Objects::nonNull)
|
|
||||||
.collect(Collectors.summarizingLong(Date::getTime));
|
|
||||||
min = Math.min(min, stats.getMin());
|
|
||||||
max = Math.max(max, stats.getMax());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if(max > min) {
|
|
||||||
fiatRates = exchangeSource.getHistoricalExchangeRates(fiatCurrency, new Date(min - ONE_DAY), new Date(max));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return fiatRates;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static ExchangeSource getExchangeSource() {
|
|
||||||
return Config.get().getExchangeSource() == null ? ExchangeSource.COINGECKO : Config.get().getExchangeSource();
|
|
||||||
}
|
|
||||||
|
|
||||||
private static Currency getFiatCurrency() {
|
|
||||||
return getExchangeSource() == ExchangeSource.NONE || !AppServices.onlineProperty().get() ? null : Config.get().getFiatCurrency();
|
|
||||||
}
|
|
||||||
|
|
||||||
private Map<Currency, BigDecimal> getFiatValue(TransactionEntry txEntry, Map<Date, Double> fiatRates) {
|
|
||||||
return getFiatValue(txEntry.getBlockTransaction().getDate(), txEntry.getValue(), fiatRates);
|
|
||||||
}
|
|
||||||
|
|
||||||
private Map<Currency, BigDecimal> getFiatValue(BlockTransactionHashIndex ref, Map<Date, Double> fiatRates) {
|
|
||||||
return getFiatValue(ref.getDate(), ref.getValue(), fiatRates);
|
|
||||||
}
|
|
||||||
|
|
||||||
private Map<Currency, BigDecimal> getFiatValue(Date date, long value, Map<Date, Double> fiatRates) {
|
|
||||||
Currency fiatCurrency = getFiatCurrency();
|
|
||||||
if(fiatCurrency != null) {
|
|
||||||
Double dayRate = null;
|
|
||||||
if(date == null) {
|
|
||||||
if(AppServices.getFiatCurrencyExchangeRate() != null) {
|
|
||||||
dayRate = AppServices.getFiatCurrencyExchangeRate().getBtcRate();
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
dayRate = fiatRates.get(DateUtils.truncate(date, Calendar.DAY_OF_MONTH));
|
|
||||||
}
|
|
||||||
|
|
||||||
if(dayRate != null) {
|
|
||||||
BigDecimal fiatValue = BigDecimal.valueOf(dayRate * value / Transaction.SATOSHIS_PER_BITCOIN);
|
|
||||||
return Map.of(fiatCurrency, fiatValue.setScale(fiatCurrency.getDefaultFractionDigits(), RoundingMode.HALF_UP));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private enum Type {
|
private enum Type {
|
||||||
tx, addr, pubkey, input, output, xpub
|
tx, addr, pubkey, input, output, xpub
|
||||||
}
|
}
|
||||||
|
|
||||||
private static class Label {
|
private static class Label {
|
||||||
public Label(Type type, String ref, String label, String origin, Boolean spendable) {
|
public Label(Type type, String ref, String label, String origin, String spendable) {
|
||||||
this.type = type;
|
this.type = type;
|
||||||
this.ref = ref;
|
this.ref = ref;
|
||||||
this.label = label;
|
this.label = label;
|
||||||
|
|
@ -448,119 +341,6 @@ public class WalletLabels implements WalletImport, WalletExport {
|
||||||
String ref;
|
String ref;
|
||||||
String label;
|
String label;
|
||||||
String origin;
|
String origin;
|
||||||
Boolean spendable;
|
String spendable;
|
||||||
}
|
|
||||||
|
|
||||||
private static class TransactionLabel extends Label {
|
|
||||||
public TransactionLabel(String ref, String label, String origin, Integer height, Date time, Long fee, Long value, Map<Currency, BigDecimal> rate) {
|
|
||||||
super(Type.tx, ref, label, origin, null);
|
|
||||||
this.height = height;
|
|
||||||
this.time = time;
|
|
||||||
this.fee = fee;
|
|
||||||
this.value = value;
|
|
||||||
this.rate = rate;
|
|
||||||
}
|
|
||||||
|
|
||||||
Integer height;
|
|
||||||
Date time;
|
|
||||||
Long fee;
|
|
||||||
Long value;
|
|
||||||
Map<Currency, BigDecimal> rate;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static class AddressLabel extends Label {
|
|
||||||
public AddressLabel(String ref, String label, String origin, String keypath, List<Integer> heights) {
|
|
||||||
super(Type.addr, ref, label, origin, null);
|
|
||||||
this.keypath = keypath;
|
|
||||||
this.heights = heights;
|
|
||||||
}
|
|
||||||
|
|
||||||
String keypath;
|
|
||||||
List<Integer> heights;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static class InputOutputLabel extends Label {
|
|
||||||
public InputOutputLabel(Type type, String ref, String label, String origin, Boolean spendable, String keypath, Long value, Integer height, Date time, Map<Currency, BigDecimal> fmv) {
|
|
||||||
super(type, ref, label, origin, spendable);
|
|
||||||
this.keypath = keypath;
|
|
||||||
this.value = value;
|
|
||||||
this.height = height;
|
|
||||||
this.time = time;
|
|
||||||
this.fmv = fmv;
|
|
||||||
}
|
|
||||||
|
|
||||||
String keypath;
|
|
||||||
Long value;
|
|
||||||
Integer height;
|
|
||||||
Date time;
|
|
||||||
Map<Currency, BigDecimal> fmv;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static class GsonUTCDateAdapter implements JsonSerializer<Date>, JsonDeserializer<Date> {
|
|
||||||
private final DateFormat dateFormat;
|
|
||||||
|
|
||||||
public GsonUTCDateAdapter() {
|
|
||||||
dateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'");
|
|
||||||
dateFormat.setTimeZone(TimeZone.getTimeZone("UTC"));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public JsonElement serialize(Date src, java.lang.reflect.Type typeOfSrc, JsonSerializationContext context) {
|
|
||||||
return new JsonPrimitive(dateFormat.format(src));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Date deserialize(JsonElement json, java.lang.reflect.Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
|
|
||||||
try {
|
|
||||||
return dateFormat.parse(json.getAsString());
|
|
||||||
} catch (ParseException e) {
|
|
||||||
throw new JsonParseException(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static class Origin {
|
|
||||||
private static final Pattern KEY_ORIGIN_PATTERN = Pattern.compile("\\[([A-Fa-f0-9]{8})([/\\d'hH]+)?\\]");
|
|
||||||
|
|
||||||
private ScriptType scriptType;
|
|
||||||
private Set<KeyDerivation> keyDerivations;
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public final boolean equals(Object o) {
|
|
||||||
if(this == o) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
if(!(o instanceof Origin origin)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return scriptType == origin.scriptType && keyDerivations.equals(origin.keyDerivations);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int hashCode() {
|
|
||||||
int result = Objects.hashCode(scriptType);
|
|
||||||
result = 31 * result + keyDerivations.hashCode();
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static Origin fromOutputDescriptor(OutputDescriptor outputDescriptor) {
|
|
||||||
Origin origin = new Origin();
|
|
||||||
origin.scriptType = outputDescriptor.getScriptType();
|
|
||||||
origin.keyDerivations = new HashSet<>(outputDescriptor.getExtendedPublicKeysMap().values());
|
|
||||||
return origin;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static Origin fromString(String strOrigin) {
|
|
||||||
Origin origin = new Origin();
|
|
||||||
origin.scriptType = ScriptType.fromDescriptor(strOrigin);
|
|
||||||
origin.keyDerivations = new HashSet<>();
|
|
||||||
Matcher keyOriginMatcher = KEY_ORIGIN_PATTERN.matcher(strOrigin);
|
|
||||||
while(keyOriginMatcher.find()) {
|
|
||||||
byte[] masterFingerprintBytes = keyOriginMatcher.group(1) != null ? Utils.hexToBytes(keyOriginMatcher.group(1)) : new byte[4];
|
|
||||||
origin.keyDerivations.add(new KeyDerivation(Utils.bytesToHex(masterFingerprintBytes), KeyDerivation.writePath(KeyDerivation.parsePath(keyOriginMatcher.group(2)))));
|
|
||||||
}
|
|
||||||
return origin;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue