mirror of
https://github.com/sparrowwallet/sparrow.git
synced 2025-11-05 11:56:37 +00:00
Compare commits
185 commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3a5fa69fb6 | ||
|
|
4774830ce4 | ||
|
|
2f62a9e9c8 | ||
|
|
75bcfe2253 | ||
|
|
bedf1399ea | ||
|
|
58575793ea | ||
|
|
6c9b580d4f | ||
|
|
31909b7a15 | ||
|
|
092267339a | ||
|
|
0974918cff | ||
|
|
0f4c36b3c2 | ||
|
|
e1fe35fb74 | ||
|
|
d37fd00c4b | ||
|
|
5f54f86df7 | ||
|
|
e2fa3df08d | ||
|
|
6d6ede9abe | ||
|
|
cca9ab1056 | ||
|
|
9e33861110 | ||
|
|
c3d3fd1fda | ||
|
|
ca8553ecb8 | ||
|
|
d23ee8c086 | ||
|
|
e776a17ad4 | ||
|
|
480ce1e476 | ||
|
|
656cd90b08 | ||
|
|
8df0777959 | ||
|
|
84566b92e6 | ||
|
|
7802510e58 | ||
|
|
efb1eb1051 | ||
|
|
6240667478 | ||
|
|
2c27112dad | ||
|
|
6d53e1ed1d | ||
|
|
e8c5660897 | ||
|
|
bef6c750bd | ||
|
|
4ec3603789 | ||
|
|
90c9f9733f | ||
|
|
64efcf67d3 | ||
|
|
385d173948 | ||
|
|
d81b868049 | ||
|
|
2ff7a15d1e | ||
|
|
f48fa7e23c | ||
|
|
4632850e1e | ||
|
|
5f62523710 | ||
|
|
9dcf210762 | ||
|
|
c7e9a0a161 | ||
|
|
fa10714844 | ||
|
|
80105aee62 | ||
|
|
3c5fa58a16 | ||
|
|
2a2be2617c | ||
|
|
6c9a0d14cd | ||
|
|
f82fcb58bb | ||
|
|
5ec3bff6a4 | ||
|
|
134dc826ba | ||
|
|
cd2a6623a4 | ||
|
|
49ab9e40e3 | ||
|
|
cec7eac9ac | ||
|
|
33e043fd9a | ||
|
|
3aae26b196 | ||
|
|
73d4fd5049 | ||
|
|
a94380e882 | ||
|
|
e4dd4950bf | ||
|
|
26ce1b3469 | ||
|
|
ebce34f3d1 | ||
|
|
f28e00b97e | ||
|
|
25770c2426 | ||
|
|
799cac7b1f | ||
|
|
c265fd1969 | ||
|
|
890f0476b1 | ||
|
|
4d93381124 | ||
|
|
364909cfa3 | ||
|
|
38f0068411 | ||
|
|
8885e48ed9 | ||
|
|
31ce3ce68a | ||
|
|
b0d0514617 | ||
|
|
d7d23f9b58 | ||
|
|
3fdf093a26 | ||
|
|
74c298fd93 | ||
|
|
4298bfb053 | ||
|
|
231eb13cee | ||
|
|
52470ee6d8 | ||
|
|
853949675e | ||
|
|
098afebbe0 | ||
|
|
63c0a6d6e2 | ||
|
|
77c305f90b | ||
|
|
276f8b4148 | ||
|
|
b3c92617c9 | ||
|
|
58635801fc | ||
|
|
8c32bb3903 | ||
|
|
55a2c86a83 | ||
|
|
345e018eb9 | ||
|
|
45d2dee764 | ||
|
|
250bc84060 | ||
|
|
c3dba8ede6 | ||
|
|
db478f8da6 | ||
|
|
4ab9a9f681 | ||
|
|
c078aea3b4 | ||
|
|
af4a283b3f | ||
|
|
892885c0b1 | ||
|
|
d4a1441d65 | ||
|
|
1605cd2619 | ||
|
|
b4d34aacc5 | ||
|
|
1a4f0113c7 | ||
|
|
055e3ac496 | ||
|
|
d0da85171c | ||
|
|
af4c68a09c | ||
|
|
b1ab157ee3 | ||
|
|
94b27ba7e8 | ||
|
|
e697313259 | ||
|
|
1b0e5e9726 | ||
|
|
df0c4310ca | ||
|
|
474f3a4e91 | ||
|
|
c6e42d8fe2 | ||
|
|
3698ca8e85 | ||
|
|
53c5a8d2df | ||
|
|
3d85491e6b | ||
|
|
c77f52f7f6 | ||
|
|
e3138f3392 | ||
|
|
7a4015fdb5 | ||
|
|
94d15c09e6 | ||
|
|
71ac72e9f6 | ||
|
|
be8b56e355 | ||
|
|
af8505c0eb | ||
|
|
5edabf2e14 | ||
|
|
c73ebdc8a2 | ||
|
|
c9d7b8ef9a | ||
|
|
b3a6340c45 | ||
|
|
0975d12155 | ||
|
|
e31aa7fc80 | ||
|
|
b777c8c64d | ||
|
|
4176f76ffc | ||
|
|
64dac72f4f | ||
|
|
e29559f59c | ||
|
|
b1223ef064 | ||
|
|
6f0a30cc25 | ||
|
|
2fa8e5fd70 | ||
|
|
a8f7ce9e34 | ||
|
|
c946ef7479 | ||
|
|
7fa13901d4 | ||
|
|
8a88488a42 | ||
|
|
25a3f5539d | ||
|
|
520c5f2cfa | ||
|
|
d8877a259c | ||
|
|
7de63b2b5f | ||
|
|
f1c4b8aa69 | ||
|
|
6f6d61fb75 | ||
|
|
2c4de99fad | ||
|
|
3e197eb310 | ||
|
|
bd5af560ff | ||
|
|
3b9551a8c6 | ||
|
|
289a4453a4 | ||
|
|
27e21c890f | ||
|
|
4239a56bc1 | ||
|
|
5c9de07d48 | ||
|
|
9a8a25344a | ||
|
|
be86b4feaa | ||
|
|
37763e9557 | ||
|
|
80c4f4f5f6 | ||
|
|
6c3fe93d1e | ||
|
|
76eff2de48 | ||
|
|
07a6818823 | ||
|
|
2253a1bb97 | ||
|
|
36ee8add08 | ||
|
|
883e75c0df | ||
|
|
cc908b09c7 | ||
|
|
ce963ed5b6 | ||
|
|
951e33dc06 | ||
|
|
6a6a6b1cca | ||
|
|
8953d404fa | ||
|
|
b366177782 | ||
|
|
d0c827c2c7 | ||
|
|
5c29bf51b7 | ||
|
|
d426703dcc | ||
|
|
78f0721168 | ||
|
|
20d3f07059 | ||
|
|
1140a678ad | ||
|
|
6e8d44bc8c | ||
|
|
ad3b384feb | ||
|
|
f38350b38d | ||
|
|
62060c9839 | ||
|
|
8975f6f666 | ||
|
|
c7351cd191 | ||
|
|
62b1dc3900 | ||
|
|
f37ff47850 | ||
|
|
cfaa1f6c6e | ||
|
|
91c94b94eb | ||
|
|
a5eb7da067 |
468 changed files with 7915 additions and 3430 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-20.04, ubuntu-22.04-arm, macos-13, macos-14]
|
os: [windows-2022, ubuntu-22.04, ubuntu-22.04-arm, macos-13, macos-14]
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v5
|
||||||
with:
|
with:
|
||||||
submodules: true
|
submodules: true
|
||||||
- name: Set up JDK 22.0.2
|
- name: Set up JDK 22.0.2
|
||||||
uses: actions/setup-java@v4
|
uses: actions/setup-java@v5
|
||||||
with:
|
with:
|
||||||
distribution: 'temurin'
|
distribution: 'temurin'
|
||||||
java-version: '22.0.2'
|
java-version: '22.0.2'
|
||||||
|
|
@ -30,7 +30,10 @@ jobs:
|
||||||
- name: Package tar distribution
|
- name: Package tar distribution
|
||||||
if: ${{ runner.os == 'Linux' }}
|
if: ${{ runner.os == 'Linux' }}
|
||||||
run: ./gradlew packageTarDistribution
|
run: ./gradlew packageTarDistribution
|
||||||
- name: Upload Artifacts
|
- name: Repackage deb distribution
|
||||||
|
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 }}
|
||||||
|
|
@ -43,9 +46,9 @@ jobs:
|
||||||
- name: Package headless tar distribution
|
- name: Package headless tar distribution
|
||||||
if: ${{ runner.os == 'Linux' }}
|
if: ${{ runner.os == 'Linux' }}
|
||||||
run: ./gradlew -Djava.awt.headless=true packageTarDistribution
|
run: ./gradlew -Djava.awt.headless=true packageTarDistribution
|
||||||
- name: Rename Headless Artifacts
|
- name: Repackage headless deb distribution
|
||||||
if: ${{ runner.os == 'Linux' }}
|
if: ${{ runner.os == 'Linux' }}
|
||||||
run: for f in build/jpackage/sparrow*; do mv -v "$f" "${f/sparrow/sparrow-server}"; done;
|
run: ./repackage.sh
|
||||||
- name: Upload Headless Artifact
|
- name: Upload Headless Artifact
|
||||||
if: ${{ runner.os == 'Linux' }}
|
if: ${{ runner.os == 'Linux' }}
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v4
|
||||||
|
|
|
||||||
311
build.gradle
311
build.gradle
|
|
@ -1,50 +1,34 @@
|
||||||
plugins {
|
plugins {
|
||||||
id 'application'
|
id 'application'
|
||||||
id 'org-openjfx-javafxplugin'
|
id 'org-openjfx-javafxplugin'
|
||||||
id 'org.beryx.jlink' version '3.1.1'
|
id 'org.beryx.jlink' version '3.1.3'
|
||||||
id 'org.gradlex.extra-java-module-info' version '1.9'
|
id 'org.gradlex.extra-java-module-info' version '1.13'
|
||||||
|
id 'io.matthewnelson.kmp.tor.resource-filterjar' version '408.16.3'
|
||||||
}
|
}
|
||||||
|
|
||||||
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"))
|
||||||
|
|
||||||
def vTor = '4.7.13-4'
|
group = 'com.sparrowwallet'
|
||||||
def vKmpTor = '1.4.3'
|
version = '2.3.1'
|
||||||
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 'https://code.sparrowwallet.com/api/packages/sparrowwallet/maven' }
|
maven { url = uri('https://code.sparrowwallet.com/api/packages/sparrowwallet/maven') }
|
||||||
}
|
}
|
||||||
|
|
||||||
tasks.withType(AbstractArchiveTask) {
|
tasks.withType(AbstractArchiveTask).configureEach {
|
||||||
preserveFileTimestamps = false
|
useFileSystemPermissions()
|
||||||
reproducibleFileOrder = true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
javafx {
|
javafx {
|
||||||
|
|
@ -60,20 +44,20 @@ dependencies {
|
||||||
//Any changes to the dependencies must be reflected in the module definitions below!
|
//Any changes to the dependencies must be reflected in the module definitions below!
|
||||||
implementation(project(':drongo'))
|
implementation(project(':drongo'))
|
||||||
implementation(project(':lark'))
|
implementation(project(':lark'))
|
||||||
implementation('com.google.guava:guava:33.0.0-jre')
|
implementation('com.google.guava:guava:33.5.0-jre')
|
||||||
implementation('com.google.code.gson:gson:2.9.1')
|
implementation('com.google.code.gson:gson:2.9.1')
|
||||||
implementation('com.h2database:h2:2.1.214')
|
implementation('com.h2database:h2:2.1.214')
|
||||||
implementation('com.zaxxer:HikariCP:4.0.3') {
|
implementation('com.zaxxer:HikariCP:4.0.3') {
|
||||||
exclude group: 'org.slf4j'
|
exclude group: 'org.slf4j'
|
||||||
}
|
}
|
||||||
implementation('org.jdbi:jdbi3-core:3.20.0') {
|
implementation('org.jdbi:jdbi3-core:3.49.5') {
|
||||||
exclude group: 'org.slf4j'
|
exclude group: 'org.slf4j'
|
||||||
}
|
}
|
||||||
implementation('org.jdbi:jdbi3-sqlobject:3.20.0') {
|
implementation('org.jdbi:jdbi3-sqlobject:3.49.5') {
|
||||||
exclude group: 'org.slf4j'
|
exclude group: 'org.slf4j'
|
||||||
}
|
}
|
||||||
implementation('org.flywaydb:flyway-core:9.1.3')
|
implementation('org.flywaydb:flyway-core:9.22.3')
|
||||||
implementation('org.fxmisc.richtext:richtextfx:0.10.4')
|
implementation('org.fxmisc.richtext:richtextfx:0.11.6')
|
||||||
implementation('no.tornado:tornadofx-controls:1.0.4')
|
implementation('no.tornado:tornadofx-controls:1.0.4')
|
||||||
implementation('com.google.zxing:javase:3.4.0') {
|
implementation('com.google.zxing:javase:3.4.0') {
|
||||||
exclude group: 'com.beust', module: 'jcommander'
|
exclude group: 'com.beust', module: 'jcommander'
|
||||||
|
|
@ -89,34 +73,15 @@ dependencies {
|
||||||
implementation('com.fasterxml.jackson.core:jackson-databind:2.17.2')
|
implementation('com.fasterxml.jackson.core:jackson-databind:2.17.2')
|
||||||
implementation('com.sparrowwallet:hummingbird:1.7.4')
|
implementation('com.sparrowwallet:hummingbird:1.7.4')
|
||||||
implementation('co.nstant.in:cbor:0.9')
|
implementation('co.nstant.in:cbor:0.9')
|
||||||
implementation("com.nativelibs4java:bridj${targetName}:0.7-20140918-3") {
|
implementation('org.openpnp:openpnp-capture-java:0.0.30-1')
|
||||||
exclude group: 'com.google.android.tools', module: 'dx'
|
implementation("io.matthewnelson.kmp-tor:runtime:2.2.1")
|
||||||
}
|
implementation("io.matthewnelson.kmp-tor:resource-exec-tor-gpl:408.16.3")
|
||||||
implementation("com.github.sarxos:webcam-capture${targetName}:0.3.13-SNAPSHOT") {
|
implementation('org.jetbrains.kotlinx:kotlinx-coroutines-javafx:1.10.1') {
|
||||||
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'
|
||||||
}
|
}
|
||||||
if(kmpOs == "linux" && kmpArch == "arm64") {
|
implementation('de.jangassen:nsmenufx:3.1.0') {
|
||||||
implementation("com.sparrowwallet.kmp-tor-binary:kmp-tor-binary-${kmpOs}${kmpArch}-jvm:${vTor}") {
|
exclude group: 'net.java.dev.jna', module: 'jna'
|
||||||
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'
|
||||||
|
|
@ -136,7 +101,7 @@ dependencies {
|
||||||
implementation('com.sparrowwallet:tern:1.0.6')
|
implementation('com.sparrowwallet:tern:1.0.6')
|
||||||
implementation('io.reactivex.rxjava2:rxjava:2.2.15')
|
implementation('io.reactivex.rxjava2:rxjava:2.2.15')
|
||||||
implementation('io.reactivex.rxjava2:rxjavafx:2.2.2')
|
implementation('io.reactivex.rxjava2:rxjavafx:2.2.2')
|
||||||
implementation('org.apache.commons:commons-lang3:3.7')
|
implementation('org.apache.commons:commons-lang3:3.19.0')
|
||||||
implementation('org.apache.commons:commons-compress:1.27.1')
|
implementation('org.apache.commons:commons-compress:1.27.1')
|
||||||
implementation('net.sourceforge.streamsupport:streamsupport:1.7.0')
|
implementation('net.sourceforge.streamsupport:streamsupport:1.7.0')
|
||||||
implementation('com.github.librepdf:openpdf:1.3.30')
|
implementation('com.github.librepdf:openpdf:1.3.30')
|
||||||
|
|
@ -145,6 +110,7 @@ dependencies {
|
||||||
implementation('com.github.hervegirod:fxsvgimage:1.1')
|
implementation('com.github.hervegirod:fxsvgimage:1.1')
|
||||||
implementation('com.sparrowwallet:toucan:0.9.0')
|
implementation('com.sparrowwallet:toucan:0.9.0')
|
||||||
implementation('com.jcraft:jzlib:1.1.3')
|
implementation('com.jcraft:jzlib:1.1.3')
|
||||||
|
implementation('io.github.doblon8:jzbar:0.2.1')
|
||||||
testImplementation('org.junit.jupiter:junit-jupiter-api:5.10.0')
|
testImplementation('org.junit.jupiter:junit-jupiter-api:5.10.0')
|
||||||
testRuntimeOnly('org.junit.jupiter:junit-jupiter-engine:5.10.0')
|
testRuntimeOnly('org.junit.jupiter:junit-jupiter-engine:5.10.0')
|
||||||
testRuntimeOnly('org.junit.platform:junit-platform-launcher')
|
testRuntimeOnly('org.junit.platform:junit-platform-launcher')
|
||||||
|
|
@ -177,6 +143,12 @@ application {
|
||||||
mainClass = 'com.sparrowwallet.sparrow.SparrowWallet'
|
mainClass = 'com.sparrowwallet.sparrow.SparrowWallet'
|
||||||
|
|
||||||
applicationDefaultJvmArgs = ["-XX:+HeapDumpOnOutOfMemoryError",
|
applicationDefaultJvmArgs = ["-XX:+HeapDumpOnOutOfMemoryError",
|
||||||
|
"--enable-native-access=com.sparrowwallet.drongo",
|
||||||
|
"--enable-native-access=com.sun.jna",
|
||||||
|
"--enable-native-access=javafx.graphics",
|
||||||
|
"--enable-native-access=com.fazecast.jSerialComm",
|
||||||
|
"--enable-native-access=org.usb4java",
|
||||||
|
"--enable-native-access=io.github.doblon8.jzbar",
|
||||||
"--add-opens=javafx.graphics/com.sun.javafx.css=org.controlsfx.controls",
|
"--add-opens=javafx.graphics/com.sun.javafx.css=org.controlsfx.controls",
|
||||||
"--add-opens=javafx.graphics/javafx.scene=org.controlsfx.controls",
|
"--add-opens=javafx.graphics/javafx.scene=org.controlsfx.controls",
|
||||||
"--add-opens=javafx.controls/com.sun.javafx.scene.control.behavior=org.controlsfx.controls",
|
"--add-opens=javafx.controls/com.sun.javafx.scene.control.behavior=org.controlsfx.controls",
|
||||||
|
|
@ -186,11 +158,6 @@ application {
|
||||||
"--add-opens=javafx.controls/javafx.scene.control.cell=com.sparrowwallet.sparrow",
|
"--add-opens=javafx.controls/javafx.scene.control.cell=com.sparrowwallet.sparrow",
|
||||||
"--add-opens=org.controlsfx.controls/impl.org.controlsfx.skin=com.sparrowwallet.sparrow",
|
"--add-opens=org.controlsfx.controls/impl.org.controlsfx.skin=com.sparrowwallet.sparrow",
|
||||||
"--add-opens=org.controlsfx.controls/impl.org.controlsfx.skin=javafx.fxml",
|
"--add-opens=org.controlsfx.controls/impl.org.controlsfx.skin=javafx.fxml",
|
||||||
"--add-opens=javafx.graphics/com.sun.javafx.tk=centerdevice.nsmenufx",
|
|
||||||
"--add-opens=javafx.graphics/com.sun.javafx.tk.quantum=centerdevice.nsmenufx",
|
|
||||||
"--add-opens=javafx.graphics/com.sun.glass.ui=centerdevice.nsmenufx",
|
|
||||||
"--add-opens=javafx.controls/com.sun.javafx.scene.control=centerdevice.nsmenufx",
|
|
||||||
"--add-opens=javafx.graphics/com.sun.javafx.menu=centerdevice.nsmenufx",
|
|
||||||
"--add-opens=javafx.graphics/com.sun.glass.ui=com.sparrowwallet.sparrow",
|
"--add-opens=javafx.graphics/com.sun.glass.ui=com.sparrowwallet.sparrow",
|
||||||
"--add-opens=javafx.graphics/com.sun.javafx.application=com.sparrowwallet.sparrow",
|
"--add-opens=javafx.graphics/com.sun.javafx.application=com.sparrowwallet.sparrow",
|
||||||
"--add-opens=javafx.graphics/javafx.scene.input=com.sparrowwallet.sparrow",
|
"--add-opens=javafx.graphics/javafx.scene.input=com.sparrowwallet.sparrow",
|
||||||
|
|
@ -201,8 +168,7 @@ application {
|
||||||
"--add-reads=org.flywaydb.core=java.desktop"]
|
"--add-reads=org.flywaydb.core=java.desktop"]
|
||||||
|
|
||||||
if(os.macOsX) {
|
if(os.macOsX) {
|
||||||
applicationDefaultJvmArgs += ["-Dprism.lcdtext=false", "-Xdock:name=Sparrow", "-Xdock:icon=/Users/scy/git/sparrow/src/main/resources/sparrow-large.png",
|
applicationDefaultJvmArgs += ["-Dprism.lcdtext=false", "-Xdock:name=Sparrow"]
|
||||||
"--add-opens=javafx.graphics/com.sun.glass.ui.mac=centerdevice.nsmenufx"]
|
|
||||||
}
|
}
|
||||||
if(headless) {
|
if(headless) {
|
||||||
applicationDefaultJvmArgs += ["-Dglass.platform=Monocle", "-Dmonocle.platform=Headless", "-Dprism.order=sw"]
|
applicationDefaultJvmArgs += ["-Dglass.platform=Monocle", "-Dmonocle.platform=Headless", "-Dprism.order=sw"]
|
||||||
|
|
@ -225,7 +191,14 @@ jlink {
|
||||||
options = ['--strip-native-commands', '--strip-java-debug-attributes', '--compress', 'zip-6', '--no-header-files', '--no-man-pages', '--ignore-signing-information', '--exclude-files', '**.png', '--exclude-resources', 'glob:/com.sparrowwallet.merged.module/META-INF/*']
|
options = ['--strip-native-commands', '--strip-java-debug-attributes', '--compress', 'zip-6', '--no-header-files', '--no-man-pages', '--ignore-signing-information', '--exclude-files', '**.png', '--exclude-resources', 'glob:/com.sparrowwallet.merged.module/META-INF/*']
|
||||||
launcher {
|
launcher {
|
||||||
name = 'sparrow'
|
name = 'sparrow'
|
||||||
jvmArgs = ["--add-opens=javafx.graphics/com.sun.javafx.css=org.controlsfx.controls",
|
jvmArgs = ["--enable-native-access=com.sparrowwallet.drongo",
|
||||||
|
"--enable-native-access=com.sun.jna",
|
||||||
|
"--enable-native-access=javafx.graphics",
|
||||||
|
"--enable-native-access=com.sparrowwallet.merged.module",
|
||||||
|
"--enable-native-access=com.fazecast.jSerialComm",
|
||||||
|
"--enable-native-access=org.usb4java",
|
||||||
|
"--enable-native-access=io.github.doblon8.jzbar",
|
||||||
|
"--add-opens=javafx.graphics/com.sun.javafx.css=org.controlsfx.controls",
|
||||||
"--add-opens=javafx.graphics/javafx.scene=org.controlsfx.controls",
|
"--add-opens=javafx.graphics/javafx.scene=org.controlsfx.controls",
|
||||||
"--add-opens=javafx.controls/com.sun.javafx.scene.control.behavior=org.controlsfx.controls",
|
"--add-opens=javafx.controls/com.sun.javafx.scene.control.behavior=org.controlsfx.controls",
|
||||||
"--add-opens=javafx.controls/com.sun.javafx.scene.control.inputmap=org.controlsfx.controls",
|
"--add-opens=javafx.controls/com.sun.javafx.scene.control.inputmap=org.controlsfx.controls",
|
||||||
|
|
@ -234,11 +207,6 @@ jlink {
|
||||||
"--add-opens=javafx.controls/javafx.scene.control.cell=com.sparrowwallet.sparrow",
|
"--add-opens=javafx.controls/javafx.scene.control.cell=com.sparrowwallet.sparrow",
|
||||||
"--add-opens=org.controlsfx.controls/impl.org.controlsfx.skin=com.sparrowwallet.sparrow",
|
"--add-opens=org.controlsfx.controls/impl.org.controlsfx.skin=com.sparrowwallet.sparrow",
|
||||||
"--add-opens=org.controlsfx.controls/impl.org.controlsfx.skin=javafx.fxml",
|
"--add-opens=org.controlsfx.controls/impl.org.controlsfx.skin=javafx.fxml",
|
||||||
"--add-opens=javafx.graphics/com.sun.javafx.tk=centerdevice.nsmenufx",
|
|
||||||
"--add-opens=javafx.graphics/com.sun.javafx.tk.quantum=centerdevice.nsmenufx",
|
|
||||||
"--add-opens=javafx.graphics/com.sun.glass.ui=centerdevice.nsmenufx",
|
|
||||||
"--add-opens=javafx.controls/com.sun.javafx.scene.control=centerdevice.nsmenufx",
|
|
||||||
"--add-opens=javafx.graphics/com.sun.javafx.menu=centerdevice.nsmenufx",
|
|
||||||
"--add-opens=javafx.graphics/com.sun.glass.ui=com.sparrowwallet.sparrow",
|
"--add-opens=javafx.graphics/com.sun.glass.ui=com.sparrowwallet.sparrow",
|
||||||
"--add-opens=javafx.graphics/javafx.scene.input=com.sparrowwallet.sparrow",
|
"--add-opens=javafx.graphics/javafx.scene.input=com.sparrowwallet.sparrow",
|
||||||
"--add-opens=javafx.graphics/com.sun.javafx.application=com.sparrowwallet.sparrow",
|
"--add-opens=javafx.graphics/com.sun.javafx.application=com.sparrowwallet.sparrow",
|
||||||
|
|
@ -256,6 +224,8 @@ 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"]
|
||||||
|
|
||||||
|
|
@ -273,7 +243,7 @@ jlink {
|
||||||
jpackage {
|
jpackage {
|
||||||
imageName = "Sparrow"
|
imageName = "Sparrow"
|
||||||
installerName = "Sparrow"
|
installerName = "Sparrow"
|
||||||
appVersion = "${sparrowVersion}"
|
appVersion = "${version}"
|
||||||
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']
|
||||||
|
|
@ -284,11 +254,13 @@ jlink {
|
||||||
}
|
}
|
||||||
if(os.linux) {
|
if(os.linux) {
|
||||||
if(headless) {
|
if(headless) {
|
||||||
installerOptions = ['--license-file', 'LICENSE', '--resource-dir', "src/main/deploy/package/linux-headless/${osArch}"]
|
installerName = "sparrowserver"
|
||||||
|
installerOptions = ['--license-file', 'LICENSE']
|
||||||
} else {
|
} else {
|
||||||
installerOptions += ['--resource-dir', 'src/main/deploy/package/linux/', '--linux-shortcut', '--linux-menu-group', 'Sparrow']
|
installerName = "sparrowwallet"
|
||||||
|
installerOptions += ['--linux-shortcut', '--linux-menu-group', 'Sparrow']
|
||||||
}
|
}
|
||||||
installerOptions += ['--linux-app-category', 'utils', '--linux-app-release', '1', '--linux-rpm-license-type', 'ASL 2.0', '--linux-deb-maintainer', 'mail@sparrowwallet.com']
|
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']
|
||||||
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) {
|
||||||
|
|
@ -306,13 +278,15 @@ 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) {
|
||||||
commandLine 'icacls', "$buildDir\\image\\legal", '/grant', 'Users:(OI)(CI)F', '/T'
|
def usersGroup = '*S-1-5-32-545' // Windows "Users" group SID (language-independent)
|
||||||
|
commandLine 'icacls', "$buildDir\\image\\legal", '/grant', "${usersGroup}:(OI)(CI)F", '/T'
|
||||||
} else {
|
} else {
|
||||||
commandLine 'chmod', '-R', 'u+w', "$buildDir/image/legal"
|
commandLine 'chmod', '-R', 'u+w', "$buildDir/image/legal"
|
||||||
}
|
}
|
||||||
|
|
@ -324,12 +298,42 @@ 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-${sparrowVersion}.zip"
|
archiveFileName = "Sparrow-${version}.zip"
|
||||||
destinationDirectory = file("$buildDir/jpackage")
|
destinationDirectory = file("$buildDir/jpackage")
|
||||||
preserveFileTimestamps = os.macOsX
|
preserveFileTimestamps = os.macOsX
|
||||||
from("$buildDir/jpackage/") {
|
from("$buildDir/jpackage/") {
|
||||||
|
|
@ -340,7 +344,7 @@ tasks.register('packageZipDistribution', Zip) {
|
||||||
|
|
||||||
tasks.register('packageTarDistribution', Tar) {
|
tasks.register('packageTarDistribution', Tar) {
|
||||||
dependsOn removeGroupWritePermission
|
dependsOn removeGroupWritePermission
|
||||||
archiveFileName = "sparrow-${sparrowVersion}-${releaseArch}.tar.gz"
|
archiveFileName = "sparrow${headless ? 'server': 'wallet'}-${version}-${releaseArch}.tar.gz"
|
||||||
destinationDirectory = file("$buildDir/jpackage")
|
destinationDirectory = file("$buildDir/jpackage")
|
||||||
compression = Compression.GZIP
|
compression = Compression.GZIP
|
||||||
from("$buildDir/jpackage/") {
|
from("$buildDir/jpackage/") {
|
||||||
|
|
@ -376,24 +380,11 @@ extraJavaModuleInfo {
|
||||||
requires('org.slf4j')
|
requires('org.slf4j')
|
||||||
requires('com.fasterxml.jackson.databind')
|
requires('com.fasterxml.jackson.databind')
|
||||||
}
|
}
|
||||||
module("com.nativelibs4java:bridj${targetName}", 'com.nativelibs4java.bridj') {
|
module('org.openpnp:openpnp-capture-java', 'openpnp.capture.java') {
|
||||||
exports('org.bridj')
|
exports('org.openpnp.capture')
|
||||||
exports('org.bridj.cpp')
|
exports('org.openpnp.capture.library')
|
||||||
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.nativelibs4java.bridj')
|
requires('com.sun.jna')
|
||||||
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')
|
||||||
|
|
@ -401,21 +392,6 @@ 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')
|
||||||
|
|
@ -425,10 +401,10 @@ extraJavaModuleInfo {
|
||||||
requires('javafx.graphics')
|
requires('javafx.graphics')
|
||||||
requires('org.fxmisc.flowless')
|
requires('org.fxmisc.flowless')
|
||||||
requires('org.reactfx.reactfx')
|
requires('org.reactfx.reactfx')
|
||||||
requires('org.fxmisc.undo.undofx')
|
requires('org.fxmisc.undo')
|
||||||
requires('org.fxmisc.wellbehaved')
|
requires('org.fxmisc.wellbehaved')
|
||||||
}
|
}
|
||||||
module('org.fxmisc.undo:undofx', 'org.fxmisc.undo.undofx') {
|
module('org.fxmisc.undo:undofx', 'org.fxmisc.undo') {
|
||||||
requires('javafx.base')
|
requires('javafx.base')
|
||||||
requires('javafx.controls')
|
requires('javafx.controls')
|
||||||
requires('javafx.graphics')
|
requires('javafx.graphics')
|
||||||
|
|
@ -485,119 +461,6 @@ 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')
|
||||||
}
|
}
|
||||||
|
|
@ -613,3 +476,7 @@ extraJavaModuleInfo {
|
||||||
exports('com.jcraft.jzlib')
|
exports('com.jcraft.jzlib')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
kmpTorResourceFilterJar {
|
||||||
|
keepTorCompilation("current","current")
|
||||||
|
}
|
||||||
|
|
@ -4,13 +4,12 @@ plugins {
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation 'com.google.gradle:osdetector-gradle-plugin:1.7.3'
|
implementation 'com.google.gradle:osdetector-gradle-plugin:1.7.3'
|
||||||
implementation 'org.javamodularity:moduleplugin:1.8.14'
|
|
||||||
}
|
}
|
||||||
|
|
||||||
repositories {
|
repositories {
|
||||||
mavenCentral()
|
mavenCentral()
|
||||||
maven {
|
maven {
|
||||||
url "https://plugins.gradle.org/m2/"
|
url = uri("https://plugins.gradle.org/m2/")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -32,7 +32,6 @@ package org.openjfx.gradle;
|
||||||
import com.google.gradle.osdetector.OsDetectorPlugin;
|
import com.google.gradle.osdetector.OsDetectorPlugin;
|
||||||
import org.gradle.api.Plugin;
|
import org.gradle.api.Plugin;
|
||||||
import org.gradle.api.Project;
|
import org.gradle.api.Project;
|
||||||
import org.javamodularity.moduleplugin.ModuleSystemPlugin;
|
|
||||||
import org.openjfx.gradle.tasks.ExecTask;
|
import org.openjfx.gradle.tasks.ExecTask;
|
||||||
|
|
||||||
public class JavaFXPlugin implements Plugin<Project> {
|
public class JavaFXPlugin implements Plugin<Project> {
|
||||||
|
|
@ -40,10 +39,9 @@ public class JavaFXPlugin implements Plugin<Project> {
|
||||||
@Override
|
@Override
|
||||||
public void apply(Project project) {
|
public void apply(Project project) {
|
||||||
project.getPlugins().apply(OsDetectorPlugin.class);
|
project.getPlugins().apply(OsDetectorPlugin.class);
|
||||||
project.getPlugins().apply(ModuleSystemPlugin.class);
|
|
||||||
|
|
||||||
project.getExtensions().create("javafx", JavaFXOptions.class, project);
|
project.getExtensions().create("javafx", JavaFXOptions.class, project);
|
||||||
|
|
||||||
project.getTasks().create("configJavafxRun", ExecTask.class, project);
|
project.getTasks().register("configJavafxRun", ExecTask.class, project);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -33,27 +33,19 @@ import org.gradle.api.DefaultTask;
|
||||||
import org.gradle.api.GradleException;
|
import org.gradle.api.GradleException;
|
||||||
import org.gradle.api.Project;
|
import org.gradle.api.Project;
|
||||||
import org.gradle.api.file.FileCollection;
|
import org.gradle.api.file.FileCollection;
|
||||||
import org.gradle.api.logging.Logger;
|
|
||||||
import org.gradle.api.logging.Logging;
|
|
||||||
import org.gradle.api.plugins.ApplicationPlugin;
|
import org.gradle.api.plugins.ApplicationPlugin;
|
||||||
import org.gradle.api.tasks.JavaExec;
|
import org.gradle.api.tasks.JavaExec;
|
||||||
import org.gradle.api.tasks.TaskAction;
|
import org.gradle.api.tasks.TaskAction;
|
||||||
import org.javamodularity.moduleplugin.extensions.RunModuleOptions;
|
|
||||||
import org.openjfx.gradle.JavaFXModule;
|
import org.openjfx.gradle.JavaFXModule;
|
||||||
import org.openjfx.gradle.JavaFXOptions;
|
import org.openjfx.gradle.JavaFXOptions;
|
||||||
import org.openjfx.gradle.JavaFXPlatform;
|
import org.openjfx.gradle.JavaFXPlatform;
|
||||||
|
|
||||||
import javax.inject.Inject;
|
import javax.inject.Inject;
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.List;
|
|
||||||
import java.util.TreeSet;
|
import java.util.TreeSet;
|
||||||
|
|
||||||
public class ExecTask extends DefaultTask {
|
public class ExecTask extends DefaultTask {
|
||||||
|
|
||||||
private static final Logger LOGGER = Logging.getLogger(ExecTask.class);
|
|
||||||
|
|
||||||
private final Project project;
|
private final Project project;
|
||||||
private JavaExec execTask;
|
private JavaExec execTask;
|
||||||
|
|
||||||
|
|
@ -78,37 +70,11 @@ public class ExecTask extends DefaultTask {
|
||||||
|
|
||||||
var definedJavaFXModuleNames = new TreeSet<>(javaFXOptions.getModules());
|
var definedJavaFXModuleNames = new TreeSet<>(javaFXOptions.getModules());
|
||||||
if (!definedJavaFXModuleNames.isEmpty()) {
|
if (!definedJavaFXModuleNames.isEmpty()) {
|
||||||
RunModuleOptions moduleOptions = execTask.getExtensions().findByType(RunModuleOptions.class);
|
|
||||||
|
|
||||||
final FileCollection classpathWithoutJavaFXJars = execTask.getClasspath().filter(
|
final FileCollection classpathWithoutJavaFXJars = execTask.getClasspath().filter(
|
||||||
jar -> Arrays.stream(JavaFXModule.values()).noneMatch(javaFXModule -> jar.getName().contains(javaFXModule.getArtifactName()))
|
jar -> Arrays.stream(JavaFXModule.values()).noneMatch(javaFXModule -> jar.getName().contains(javaFXModule.getArtifactName()))
|
||||||
);
|
);
|
||||||
final FileCollection javaFXPlatformJars = execTask.getClasspath().filter(jar -> isJavaFXJar(jar, javaFXOptions.getPlatform()));
|
final FileCollection javaFXPlatformJars = execTask.getClasspath().filter(jar -> isJavaFXJar(jar, javaFXOptions.getPlatform()));
|
||||||
|
execTask.setClasspath(classpathWithoutJavaFXJars.plus(javaFXPlatformJars));
|
||||||
if (moduleOptions != null) {
|
|
||||||
LOGGER.info("Modular JavaFX application found");
|
|
||||||
// Remove empty JavaFX jars from classpath
|
|
||||||
execTask.setClasspath(classpathWithoutJavaFXJars.plus(javaFXPlatformJars));
|
|
||||||
definedJavaFXModuleNames.forEach(javaFXModule -> moduleOptions.getAddModules().add(javaFXModule));
|
|
||||||
} else {
|
|
||||||
LOGGER.info("Non-modular JavaFX application found");
|
|
||||||
// Remove all JavaFX jars from classpath
|
|
||||||
execTask.setClasspath(classpathWithoutJavaFXJars);
|
|
||||||
|
|
||||||
var javaFXModuleJvmArgs = List.of("--module-path", javaFXPlatformJars.getAsPath());
|
|
||||||
|
|
||||||
var jvmArgs = new ArrayList<String>();
|
|
||||||
jvmArgs.add("--add-modules");
|
|
||||||
jvmArgs.add(String.join(",", definedJavaFXModuleNames));
|
|
||||||
|
|
||||||
List<String> execJvmArgs = execTask.getJvmArgs();
|
|
||||||
if (execJvmArgs != null) {
|
|
||||||
jvmArgs.addAll(execJvmArgs);
|
|
||||||
}
|
|
||||||
jvmArgs.addAll(javaFXModuleJvmArgs);
|
|
||||||
|
|
||||||
execTask.setJvmArgs(jvmArgs);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
throw new GradleException("Run task not found. Please, make sure the Application plugin is applied");
|
throw new GradleException("Run task not found. Please, make sure the Application plugin is applied");
|
||||||
|
|
|
||||||
|
|
@ -83,7 +83,7 @@ sudo apt install -y rpm fakeroot binutils
|
||||||
First, assign a temporary variable in your shell for the specific release you want to build. For the current one specify:
|
First, assign a temporary variable in your shell for the specific release you want to build. For the current one specify:
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
GIT_TAG="2.0.0"
|
GIT_TAG="2.3.0"
|
||||||
```
|
```
|
||||||
|
|
||||||
The project can then be initially cloned as follows:
|
The project can then be initially cloned as follows:
|
||||||
|
|
|
||||||
2
drongo
2
drongo
|
|
@ -1 +1 @@
|
||||||
Subproject commit ca758e128876470f673b5955d75d5311b47c6938
|
Subproject commit e975cbe6f8d8574785124e6db5780d0541e20024
|
||||||
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
Binary file not shown.
2
gradle/wrapper/gradle-wrapper.properties
vendored
2
gradle/wrapper/gradle-wrapper.properties
vendored
|
|
@ -1,6 +1,6 @@
|
||||||
distributionBase=GRADLE_USER_HOME
|
distributionBase=GRADLE_USER_HOME
|
||||||
distributionPath=wrapper/dists
|
distributionPath=wrapper/dists
|
||||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-bin.zip
|
distributionUrl=https\://services.gradle.org/distributions/gradle-9.1.0-bin.zip
|
||||||
networkTimeout=10000
|
networkTimeout=10000
|
||||||
validateDistributionUrl=true
|
validateDistributionUrl=true
|
||||||
zipStoreBase=GRADLE_USER_HOME
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
|
|
|
||||||
15
gradlew
vendored
15
gradlew
vendored
|
|
@ -1,7 +1,7 @@
|
||||||
#!/bin/sh
|
#!/bin/sh
|
||||||
|
|
||||||
#
|
#
|
||||||
# Copyright © 2015-2021 the original authors.
|
# Copyright © 2015 the original authors.
|
||||||
#
|
#
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
# you may not use this file except in compliance with the License.
|
# you may not use this file except in compliance with the License.
|
||||||
|
|
@ -15,6 +15,8 @@
|
||||||
# See the License for the specific language governing permissions and
|
# See the License for the specific language governing permissions and
|
||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
#
|
#
|
||||||
|
# SPDX-License-Identifier: Apache-2.0
|
||||||
|
#
|
||||||
|
|
||||||
##############################################################################
|
##############################################################################
|
||||||
#
|
#
|
||||||
|
|
@ -55,7 +57,7 @@
|
||||||
# Darwin, MinGW, and NonStop.
|
# Darwin, MinGW, and NonStop.
|
||||||
#
|
#
|
||||||
# (3) This script is generated from the Groovy template
|
# (3) This script is generated from the Groovy template
|
||||||
# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
|
# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
|
||||||
# within the Gradle project.
|
# within the Gradle project.
|
||||||
#
|
#
|
||||||
# You can find Gradle at https://github.com/gradle/gradle/.
|
# You can find Gradle at https://github.com/gradle/gradle/.
|
||||||
|
|
@ -84,7 +86,7 @@ done
|
||||||
# shellcheck disable=SC2034
|
# shellcheck disable=SC2034
|
||||||
APP_BASE_NAME=${0##*/}
|
APP_BASE_NAME=${0##*/}
|
||||||
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
|
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
|
||||||
APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit
|
APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit
|
||||||
|
|
||||||
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
||||||
MAX_FD=maximum
|
MAX_FD=maximum
|
||||||
|
|
@ -112,7 +114,6 @@ case "$( uname )" in #(
|
||||||
NONSTOP* ) nonstop=true ;;
|
NONSTOP* ) nonstop=true ;;
|
||||||
esac
|
esac
|
||||||
|
|
||||||
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
|
|
||||||
|
|
||||||
|
|
||||||
# Determine the Java command to use to start the JVM.
|
# Determine the Java command to use to start the JVM.
|
||||||
|
|
@ -170,7 +171,6 @@ fi
|
||||||
# For Cygwin or MSYS, switch paths to Windows format before running java
|
# For Cygwin or MSYS, switch paths to Windows format before running java
|
||||||
if "$cygwin" || "$msys" ; then
|
if "$cygwin" || "$msys" ; then
|
||||||
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
|
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
|
||||||
CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
|
|
||||||
|
|
||||||
JAVACMD=$( cygpath --unix "$JAVACMD" )
|
JAVACMD=$( cygpath --unix "$JAVACMD" )
|
||||||
|
|
||||||
|
|
@ -203,15 +203,14 @@ fi
|
||||||
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
|
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
|
||||||
|
|
||||||
# Collect all arguments for the java command:
|
# Collect all arguments for the java command:
|
||||||
# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
|
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
|
||||||
# and any embedded shellness will be escaped.
|
# and any embedded shellness will be escaped.
|
||||||
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
|
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
|
||||||
# treated as '${Hostname}' itself on the command line.
|
# treated as '${Hostname}' itself on the command line.
|
||||||
|
|
||||||
set -- \
|
set -- \
|
||||||
"-Dorg.gradle.appname=$APP_BASE_NAME" \
|
"-Dorg.gradle.appname=$APP_BASE_NAME" \
|
||||||
-classpath "$CLASSPATH" \
|
-jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \
|
||||||
org.gradle.wrapper.GradleWrapperMain \
|
|
||||||
"$@"
|
"$@"
|
||||||
|
|
||||||
# Stop when "xargs" is not available.
|
# Stop when "xargs" is not available.
|
||||||
|
|
|
||||||
25
gradlew.bat
vendored
25
gradlew.bat
vendored
|
|
@ -13,6 +13,8 @@
|
||||||
@rem See the License for the specific language governing permissions and
|
@rem See the License for the specific language governing permissions and
|
||||||
@rem limitations under the License.
|
@rem limitations under the License.
|
||||||
@rem
|
@rem
|
||||||
|
@rem SPDX-License-Identifier: Apache-2.0
|
||||||
|
@rem
|
||||||
|
|
||||||
@if "%DEBUG%"=="" @echo off
|
@if "%DEBUG%"=="" @echo off
|
||||||
@rem ##########################################################################
|
@rem ##########################################################################
|
||||||
|
|
@ -43,11 +45,11 @@ set JAVA_EXE=java.exe
|
||||||
%JAVA_EXE% -version >NUL 2>&1
|
%JAVA_EXE% -version >NUL 2>&1
|
||||||
if %ERRORLEVEL% equ 0 goto execute
|
if %ERRORLEVEL% equ 0 goto execute
|
||||||
|
|
||||||
echo.
|
echo. 1>&2
|
||||||
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
|
||||||
echo.
|
echo. 1>&2
|
||||||
echo Please set the JAVA_HOME variable in your environment to match the
|
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
|
||||||
echo location of your Java installation.
|
echo location of your Java installation. 1>&2
|
||||||
|
|
||||||
goto fail
|
goto fail
|
||||||
|
|
||||||
|
|
@ -57,22 +59,21 @@ set JAVA_EXE=%JAVA_HOME%/bin/java.exe
|
||||||
|
|
||||||
if exist "%JAVA_EXE%" goto execute
|
if exist "%JAVA_EXE%" goto execute
|
||||||
|
|
||||||
echo.
|
echo. 1>&2
|
||||||
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
|
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
|
||||||
echo.
|
echo. 1>&2
|
||||||
echo Please set the JAVA_HOME variable in your environment to match the
|
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
|
||||||
echo location of your Java installation.
|
echo location of your Java installation. 1>&2
|
||||||
|
|
||||||
goto fail
|
goto fail
|
||||||
|
|
||||||
:execute
|
:execute
|
||||||
@rem Setup the command line
|
@rem Setup the command line
|
||||||
|
|
||||||
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
|
|
||||||
|
|
||||||
|
|
||||||
@rem Execute Gradle
|
@rem Execute Gradle
|
||||||
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
|
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %*
|
||||||
|
|
||||||
:end
|
:end
|
||||||
@rem End local scope for the variables with windows NT shell
|
@rem End local scope for the variables with windows NT shell
|
||||||
|
|
|
||||||
2
lark
2
lark
|
|
@ -1 +1 @@
|
||||||
Subproject commit b80cbbbc57939284cf270a7b450a0059d5f0ec15
|
Subproject commit 10e8d9cd4bbe9fde4dd93c059e2a9faeec6be3e0
|
||||||
48
repackage.sh
Executable file
48
repackage.sh
Executable file
|
|
@ -0,0 +1,48 @@
|
||||||
|
#!/bin/bash
|
||||||
|
set -e # Exit on any error
|
||||||
|
|
||||||
|
# Define paths
|
||||||
|
BUILD_DIR="build"
|
||||||
|
JPACKAGE_DIR="$BUILD_DIR/jpackage"
|
||||||
|
TEMP_DIR="$BUILD_DIR/repackage"
|
||||||
|
|
||||||
|
# Find the .deb file in build/jpackage (assuming there is only one)
|
||||||
|
DEB_FILE=$(find "$JPACKAGE_DIR" -type f -name "*.deb" -print -quit)
|
||||||
|
|
||||||
|
# Check if a .deb file was found
|
||||||
|
if [ -z "$DEB_FILE" ]; then
|
||||||
|
echo "Error: No .deb file found in $JPACKAGE_DIR"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Extract the filename from the path for later use
|
||||||
|
DEB_FILENAME=$(basename "$DEB_FILE")
|
||||||
|
|
||||||
|
echo "Found .deb file: $DEB_FILENAME"
|
||||||
|
|
||||||
|
# Create a temp directory inside build to avoid file conflicts
|
||||||
|
mkdir -p "$TEMP_DIR"
|
||||||
|
cd "$TEMP_DIR"
|
||||||
|
|
||||||
|
# Extract the .deb file contents
|
||||||
|
ar x "../../$DEB_FILE"
|
||||||
|
|
||||||
|
# Decompress zst files to tar
|
||||||
|
unzstd control.tar.zst
|
||||||
|
unzstd data.tar.zst
|
||||||
|
|
||||||
|
# Compress tar files to xz
|
||||||
|
xz -c control.tar > control.tar.xz
|
||||||
|
xz -c data.tar > data.tar.xz
|
||||||
|
|
||||||
|
# Remove the original .deb file
|
||||||
|
rm "../../$DEB_FILE"
|
||||||
|
|
||||||
|
# Create the new .deb file with xz compression in the original location
|
||||||
|
ar cr "../../$DEB_FILE" debian-binary control.tar.xz data.tar.xz
|
||||||
|
|
||||||
|
# Clean up temp files
|
||||||
|
cd ../..
|
||||||
|
rm -rf "$TEMP_DIR"
|
||||||
|
|
||||||
|
echo "Repackaging complete: $DEB_FILENAME"
|
||||||
|
|
@ -1,9 +0,0 @@
|
||||||
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
|
|
||||||
12
src/main/deploy/package/linux-headless/control
Normal file
12
src/main/deploy/package/linux-headless/control
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
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}
|
||||||
85
src/main/deploy/package/linux-headless/sparrowserver.spec
Executable file
85
src/main/deploy/package/linux-headless/sparrowserver.spec
Executable file
|
|
@ -0,0 +1,85 @@
|
||||||
|
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
|
||||||
|
|
@ -1,9 +0,0 @@
|
||||||
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/sparrow/bin/Sparrow %U
|
Exec=/opt/sparrowwallet/bin/Sparrow %U
|
||||||
Icon=/opt/sparrow/lib/Sparrow.png
|
Icon=/opt/sparrowwallet/lib/Sparrow.png
|
||||||
Terminal=false
|
Terminal=false
|
||||||
Type=Application
|
Type=Application
|
||||||
Categories=Finance;Network;
|
Categories=Finance;Network;
|
||||||
|
|
|
||||||
12
src/main/deploy/package/linux/control
Normal file
12
src/main/deploy/package/linux/control
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
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 sparrow
|
# postinst script for sparrowwallet
|
||||||
#
|
#
|
||||||
# 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/sparrow/lib/sparrow-Sparrow.desktop
|
xdg-desktop-menu install /opt/sparrowwallet/lib/sparrowwallet-Sparrow.desktop
|
||||||
xdg-mime install /opt/sparrow/lib/sparrow-Sparrow-MimeInfo.xml
|
xdg-mime install /opt/sparrowwallet/lib/sparrowwallet-Sparrow-MimeInfo.xml
|
||||||
install -D -m 644 /opt/sparrow/lib/runtime/conf/udev/*.rules /etc/udev/rules.d
|
install -D -m 644 /opt/sparrowwallet/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,19 +1,20 @@
|
||||||
Summary: Sparrow
|
Summary: Sparrow
|
||||||
Name: sparrow
|
Name: sparrowwallet
|
||||||
Version: 2.1.1
|
Version: ${version}
|
||||||
Release: 1
|
Release: 1
|
||||||
License: ASL 2.0
|
License: ASL 2.0
|
||||||
Vendor: Unknown
|
Vendor: Unknown
|
||||||
|
|
||||||
%if "x" != "x"
|
%if "x" != "x"
|
||||||
URL:
|
URL: https://sparrowwallet.com
|
||||||
%endif
|
%endif
|
||||||
|
|
||||||
%if "x/opt" != "x"
|
%if "x/opt" != "x"
|
||||||
Prefix: /opt
|
Prefix: /opt
|
||||||
%endif
|
%endif
|
||||||
|
|
||||||
Provides: sparrow
|
Provides: sparrowwallet
|
||||||
|
Obsoletes: sparrow <= 2.1.4
|
||||||
|
|
||||||
%if "xutils" != "x"
|
%if "xutils" != "x"
|
||||||
Group: utils
|
Group: utils
|
||||||
|
|
@ -40,7 +41,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
|
Sparrow Wallet
|
||||||
|
|
||||||
%global __os_install_post %{nil}
|
%global __os_install_post %{nil}
|
||||||
|
|
||||||
|
|
@ -50,8 +51,8 @@ Sparrow
|
||||||
|
|
||||||
%install
|
%install
|
||||||
rm -rf %{buildroot}
|
rm -rf %{buildroot}
|
||||||
install -d -m 755 %{buildroot}/opt/sparrow
|
install -d -m 755 %{buildroot}/opt/sparrowwallet
|
||||||
cp -r %{_sourcedir}/opt/sparrow/* %{buildroot}/opt/sparrow
|
cp -r %{_sourcedir}/opt/sparrowwallet/* %{buildroot}/opt/sparrowwallet
|
||||||
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
|
||||||
|
|
@ -77,9 +78,9 @@ sed -i -e 's/.*/%dir "&"/' %{package_filelist}
|
||||||
|
|
||||||
%post
|
%post
|
||||||
package_type=rpm
|
package_type=rpm
|
||||||
xdg-desktop-menu install /opt/sparrow/lib/sparrow-Sparrow.desktop
|
xdg-desktop-menu install /opt/sparrowwallet/lib/sparrowwallet-Sparrow.desktop
|
||||||
xdg-mime install /opt/sparrow/lib/sparrow-Sparrow-MimeInfo.xml
|
xdg-mime install /opt/sparrowwallet/lib/sparrowwallet-Sparrow-MimeInfo.xml
|
||||||
install -D -m 644 /opt/sparrow/lib/runtime/conf/udev/*.rules /etc/udev/rules.d
|
install -D -m 644 /opt/sparrowwallet/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
|
||||||
|
|
@ -251,9 +252,9 @@ desktop_trace ()
|
||||||
echo "$@"
|
echo "$@"
|
||||||
}
|
}
|
||||||
|
|
||||||
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.desktop xdg-desktop-menu uninstall /opt/sparrowwallet/lib/sparrowwallet-Sparrow.desktop
|
||||||
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-MimeInfo.xml xdg-mime uninstall /opt/sparrowwallet/lib/sparrowwallet-Sparrow-MimeInfo.xml
|
||||||
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
|
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
|
||||||
|
|
||||||
|
|
||||||
%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.1.1</string>
|
<string>2.3.1</string>
|
||||||
<key>CFBundleSignature</key>
|
<key>CFBundleSignature</key>
|
||||||
<string>????</string>
|
<string>????</string>
|
||||||
<!-- See https://developer.apple.com/app-store/categories/ for list of AppStore categories -->
|
<!-- See https://developer.apple.com/app-store/categories/ for list of AppStore categories -->
|
||||||
|
|
@ -33,8 +33,12 @@
|
||||||
<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,17 +1,16 @@
|
||||||
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.PSBT;
|
import com.sparrowwallet.drongo.psbt.*;
|
||||||
import com.sparrowwallet.drongo.psbt.PSBTInput;
|
import com.sparrowwallet.drongo.silentpayments.SilentPaymentAddress;
|
||||||
import com.sparrowwallet.drongo.psbt.PSBTParseException;
|
|
||||||
import com.sparrowwallet.drongo.psbt.PSBTSignatureException;
|
|
||||||
import com.sparrowwallet.drongo.wallet.*;
|
import com.sparrowwallet.drongo.wallet.*;
|
||||||
import com.sparrowwallet.hummingbird.UR;
|
import com.sparrowwallet.hummingbird.UR;
|
||||||
import com.sparrowwallet.hummingbird.registry.CryptoPSBT;
|
import com.sparrowwallet.hummingbird.registry.CryptoPSBT;
|
||||||
|
|
@ -32,7 +31,7 @@ import com.sparrowwallet.sparrow.transaction.TransactionView;
|
||||||
import com.sparrowwallet.sparrow.wallet.Entry;
|
import com.sparrowwallet.sparrow.wallet.Entry;
|
||||||
import com.sparrowwallet.sparrow.wallet.WalletController;
|
import com.sparrowwallet.sparrow.wallet.WalletController;
|
||||||
import com.sparrowwallet.sparrow.wallet.WalletForm;
|
import com.sparrowwallet.sparrow.wallet.WalletForm;
|
||||||
import de.codecentric.centerdevice.MenuToolkit;
|
import de.jangassen.MenuToolkit;
|
||||||
import javafx.animation.*;
|
import javafx.animation.*;
|
||||||
import javafx.application.Platform;
|
import javafx.application.Platform;
|
||||||
import javafx.beans.binding.Bindings;
|
import javafx.beans.binding.Bindings;
|
||||||
|
|
@ -51,12 +50,14 @@ import javafx.geometry.Side;
|
||||||
import javafx.scene.Node;
|
import javafx.scene.Node;
|
||||||
import javafx.scene.Scene;
|
import javafx.scene.Scene;
|
||||||
import javafx.scene.control.*;
|
import javafx.scene.control.*;
|
||||||
import javafx.scene.image.Image;
|
import javafx.scene.control.Label;
|
||||||
import javafx.scene.image.ImageView;
|
import javafx.scene.control.Menu;
|
||||||
|
import javafx.scene.control.MenuItem;
|
||||||
import javafx.scene.input.*;
|
import javafx.scene.input.*;
|
||||||
import javafx.scene.layout.*;
|
import javafx.scene.layout.*;
|
||||||
import javafx.scene.paint.Color;
|
import javafx.scene.paint.Color;
|
||||||
import javafx.stage.*;
|
import javafx.stage.*;
|
||||||
|
import javafx.stage.Window;
|
||||||
import javafx.util.Duration;
|
import javafx.util.Duration;
|
||||||
import org.controlsfx.control.Notifications;
|
import org.controlsfx.control.Notifications;
|
||||||
import org.controlsfx.control.StatusBar;
|
import org.controlsfx.control.StatusBar;
|
||||||
|
|
@ -71,6 +72,7 @@ import java.nio.charset.StandardCharsets;
|
||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
import java.text.ParseException;
|
import java.text.ParseException;
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
|
import java.util.List;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
import static com.sparrowwallet.sparrow.AppServices.*;
|
import static com.sparrowwallet.sparrow.AppServices.*;
|
||||||
|
|
@ -80,6 +82,7 @@ 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...";
|
||||||
|
|
@ -381,7 +384,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().isHdCapture());
|
useHdCameraResolutionProperty.set(Config.get().getWebcamResolution() == null || Config.get().getWebcamResolution().isWidescreenAspect());
|
||||||
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);
|
||||||
|
|
@ -573,16 +576,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/sparrow/lib/runtime/conf/udev/*.rules /etc/udev/rules.d
|
sudo install -m 644 /opt/sparrowwallet/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/sparrow") && home.endsWith("bin/Sparrow")) {
|
if(home != null && !home.startsWith("/opt/sparrowwallet") && home.endsWith("bin/Sparrow")) {
|
||||||
home = home.replace("bin/Sparrow", "");
|
home = home.replace("bin/Sparrow", "");
|
||||||
commands = commands.replace("/opt/sparrow/", home);
|
commands = commands.replace("/opt/sparrowwallet/", home);
|
||||||
}
|
}
|
||||||
|
|
||||||
TextAreaDialog dialog = new TextAreaDialog(commands, false);
|
TextAreaDialog dialog = new TextAreaDialog(commands, false);
|
||||||
|
|
@ -633,19 +636,10 @@ 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();
|
||||||
|
|
||||||
try {
|
if(Utils.isHex(bytes) || Utils.isBase64(bytes)) {
|
||||||
|
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());
|
||||||
|
|
@ -832,10 +826,10 @@ public class AppController implements Initializable {
|
||||||
try(FileOutputStream outputStream = new FileOutputStream(file)) {
|
try(FileOutputStream outputStream = new FileOutputStream(file)) {
|
||||||
if(asText) {
|
if(asText) {
|
||||||
PrintWriter writer = new PrintWriter(new OutputStreamWriter(outputStream, StandardCharsets.UTF_8));
|
PrintWriter writer = new PrintWriter(new OutputStreamWriter(outputStream, StandardCharsets.UTF_8));
|
||||||
writer.print(transactionTabData.getPsbt().toBase64String(includeXpubs));
|
writer.print(transactionTabData.getPsbt().getForExport().toBase64String(includeXpubs));
|
||||||
writer.flush();
|
writer.flush();
|
||||||
} else {
|
} else {
|
||||||
outputStream.write(transactionTabData.getPsbt().serialize(includeXpubs, true));
|
outputStream.write(transactionTabData.getPsbt().getForExport().serialize(includeXpubs, true));
|
||||||
}
|
}
|
||||||
} catch(IOException e) {
|
} catch(IOException e) {
|
||||||
log.error("Error saving PSBT", e);
|
log.error("Error saving PSBT", e);
|
||||||
|
|
@ -858,7 +852,7 @@ public class AppController implements Initializable {
|
||||||
TabData tabData = (TabData)selectedTab.getUserData();
|
TabData tabData = (TabData)selectedTab.getUserData();
|
||||||
if(tabData.getType() == TabData.TabType.TRANSACTION) {
|
if(tabData.getType() == TabData.TabType.TRANSACTION) {
|
||||||
TransactionTabData transactionTabData = (TransactionTabData)tabData;
|
TransactionTabData transactionTabData = (TransactionTabData)tabData;
|
||||||
String data = asBase64 ? transactionTabData.getPsbt().toBase64String() : transactionTabData.getPsbt().toString();
|
String data = asBase64 ? transactionTabData.getPsbt().getForExport().toBase64String() : transactionTabData.getPsbt().getForExport().toString();
|
||||||
|
|
||||||
ClipboardContent content = new ClipboardContent();
|
ClipboardContent content = new ClipboardContent();
|
||||||
content.putString(data);
|
content.putString(data);
|
||||||
|
|
@ -872,7 +866,7 @@ public class AppController implements Initializable {
|
||||||
if(tabData.getType() == TabData.TabType.TRANSACTION) {
|
if(tabData.getType() == TabData.TabType.TRANSACTION) {
|
||||||
TransactionTabData transactionTabData = (TransactionTabData)tabData;
|
TransactionTabData transactionTabData = (TransactionTabData)tabData;
|
||||||
|
|
||||||
byte[] psbtBytes = transactionTabData.getPsbt().serialize();
|
byte[] psbtBytes = transactionTabData.getPsbt().getForExport().serialize();
|
||||||
CryptoPSBT cryptoPSBT = new CryptoPSBT(psbtBytes);
|
CryptoPSBT cryptoPSBT = new CryptoPSBT(psbtBytes);
|
||||||
BBQR bbqr = new BBQR(BBQRType.PSBT, psbtBytes);
|
BBQR bbqr = new BBQR(BBQRType.PSBT, psbtBytes);
|
||||||
QRDisplayDialog qrDisplayDialog = new QRDisplayDialog(cryptoPSBT.toUR(), bbqr, false, true, false);
|
QRDisplayDialog qrDisplayDialog = new QRDisplayDialog(cryptoPSBT.toUR(), bbqr, false, true, false);
|
||||||
|
|
@ -955,7 +949,11 @@ 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();
|
||||||
Config.get().setHdCapture(item.isSelected());
|
if(Config.get().getWebcamResolution().isStandardAspect() && 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) {
|
||||||
|
|
@ -1040,6 +1038,10 @@ public class AppController implements Initializable {
|
||||||
cmd.add(System.getProperty(JPACKAGE_APP_PATH));
|
cmd.add(System.getProperty(JPACKAGE_APP_PATH));
|
||||||
cmd.addAll(args.toParams());
|
cmd.addAll(args.toParams());
|
||||||
final ProcessBuilder builder = new ProcessBuilder(cmd);
|
final ProcessBuilder builder = new ProcessBuilder(cmd);
|
||||||
|
if(OsType.getCurrent() == OsType.UNIX) {
|
||||||
|
Map<String, String> env = builder.environment();
|
||||||
|
env.remove("LD_LIBRARY_PATH");
|
||||||
|
}
|
||||||
builder.start();
|
builder.start();
|
||||||
quit(event);
|
quit(event);
|
||||||
} catch(Exception e) {
|
} catch(Exception e) {
|
||||||
|
|
@ -1263,6 +1265,10 @@ 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();
|
||||||
|
|
@ -1378,7 +1384,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);
|
WalletExportDialog dlg = new WalletExportDialog(selectedWalletForm, getSelectedWalletForms());
|
||||||
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()) {
|
||||||
|
|
@ -1424,6 +1430,10 @@ public class AppController implements Initializable {
|
||||||
}
|
}
|
||||||
|
|
||||||
public void sendToMany(ActionEvent event) {
|
public void sendToMany(ActionEvent event) {
|
||||||
|
sendToMany(Collections.emptyList());
|
||||||
|
}
|
||||||
|
|
||||||
|
private void sendToMany(List<Payment> initialPayments) {
|
||||||
if(sendToManyDialog != null) {
|
if(sendToManyDialog != null) {
|
||||||
Stage stage = (Stage)sendToManyDialog.getDialogPane().getScene().getWindow();
|
Stage stage = (Stage)sendToManyDialog.getDialogPane().getScene().getWindow();
|
||||||
stage.setAlwaysOnTop(true);
|
stage.setAlwaysOnTop(true);
|
||||||
|
|
@ -1439,7 +1449,7 @@ public class AppController implements Initializable {
|
||||||
bitcoinUnit = wallet.getAutoUnit();
|
bitcoinUnit = wallet.getAutoUnit();
|
||||||
}
|
}
|
||||||
|
|
||||||
sendToManyDialog = new SendToManyDialog(bitcoinUnit);
|
sendToManyDialog = new SendToManyDialog(bitcoinUnit, initialPayments);
|
||||||
sendToManyDialog.initModality(Modality.NONE);
|
sendToManyDialog.initModality(Modality.NONE);
|
||||||
Optional<List<Payment>> optPayments = sendToManyDialog.showAndWait();
|
Optional<List<Payment>> optPayments = sendToManyDialog.showAndWait();
|
||||||
sendToManyDialog = null;
|
sendToManyDialog = null;
|
||||||
|
|
@ -1480,6 +1490,7 @@ 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;
|
||||||
|
|
@ -1890,6 +1901,11 @@ public class AppController implements Initializable {
|
||||||
}
|
}
|
||||||
|
|
||||||
private void addTransactionTab(String name, File file, PSBT psbt) {
|
private void addTransactionTab(String name, File file, PSBT psbt) {
|
||||||
|
//Convert to PSBTv0 first
|
||||||
|
if(psbt.getVersion() != null && psbt.getVersion() >= 2) {
|
||||||
|
psbt.convertVersion(0);
|
||||||
|
}
|
||||||
|
|
||||||
//Add any missing previous outputs if available in open wallets
|
//Add any missing previous outputs if available in open wallets
|
||||||
for(PSBTInput psbtInput : psbt.getPsbtInputs()) {
|
for(PSBTInput psbtInput : psbt.getPsbtInputs()) {
|
||||||
if(psbtInput.getUtxo() == null) {
|
if(psbtInput.getUtxo() == null) {
|
||||||
|
|
@ -1909,6 +1925,39 @@ public class AppController implements Initializable {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//Add DNS payment information if not already cached
|
||||||
|
for(PSBTOutput psbtOutput : psbt.getPsbtOutputs()) {
|
||||||
|
if(psbtOutput.getDnssecProof() != null && !psbtOutput.getDnssecProof().isEmpty()) {
|
||||||
|
Address address = psbtOutput.getScript() != null ? psbtOutput.getScript().getToAddress() : null;
|
||||||
|
if(address != null && DnsPaymentCache.getDnsPayment(address) == null) {
|
||||||
|
try {
|
||||||
|
Optional<DnsPayment> optDnsPayment = psbtOutput.getDnsPayment();
|
||||||
|
if(optDnsPayment.isPresent() && address.equals(optDnsPayment.get().bitcoinURI().getAddress())) {
|
||||||
|
DnsPaymentCache.putDnsPayment(address, optDnsPayment.get());
|
||||||
|
}
|
||||||
|
} catch(Exception e) {
|
||||||
|
log.debug("Error resolving DNS payment", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
SilentPaymentAddress silentPaymentAddress = psbtOutput.getSilentPaymentAddress();
|
||||||
|
if(address != null && silentPaymentAddress == null) {
|
||||||
|
silentPaymentAddress = AppServices.get().getOpenWallets().keySet().stream()
|
||||||
|
.map(wallet -> wallet.getSilentPaymentAddress(address)).filter(Objects::nonNull).findFirst().orElse(null);
|
||||||
|
}
|
||||||
|
if(silentPaymentAddress != null && DnsPaymentCache.getDnsPayment(silentPaymentAddress) == null) {
|
||||||
|
try {
|
||||||
|
Optional<DnsPayment> optDnsPayment = psbtOutput.getDnsPayment();
|
||||||
|
if(optDnsPayment.isPresent() && silentPaymentAddress.equals(optDnsPayment.get().bitcoinURI().getSilentPaymentAddress())) {
|
||||||
|
DnsPaymentCache.putDnsPayment(silentPaymentAddress, optDnsPayment.get());
|
||||||
|
}
|
||||||
|
} catch(Exception e) {
|
||||||
|
log.debug("Error resolving DNS payment", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Window psbtWalletWindow = AppServices.get().getWindowForPSBT(psbt);
|
Window psbtWalletWindow = AppServices.get().getWindowForPSBT(psbt);
|
||||||
if(psbtWalletWindow != null && !tabs.getScene().getWindow().equals(psbtWalletWindow)) {
|
if(psbtWalletWindow != null && !tabs.getScene().getWindow().equals(psbtWalletWindow)) {
|
||||||
EventManager.get().post(new ViewPSBTEvent(psbtWalletWindow, name, file, psbt));
|
EventManager.get().post(new ViewPSBTEvent(psbtWalletWindow, name, file, psbt));
|
||||||
|
|
@ -1992,8 +2041,13 @@ 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);
|
||||||
|
|
@ -2042,23 +2096,33 @@ public class AppController implements Initializable {
|
||||||
}
|
}
|
||||||
|
|
||||||
MenuItem moveRight = new MenuItem("Move Right");
|
MenuItem moveRight = new MenuItem("Move Right");
|
||||||
|
moveRight.setAccelerator(new KeyCodeCombination(KeyCode.RIGHT, KeyCombination.CONTROL_DOWN, KeyCombination.SHIFT_DOWN));
|
||||||
moveRight.setOnAction(event -> {
|
moveRight.setOnAction(event -> {
|
||||||
int index = tabs.getTabs().indexOf(tab);
|
int currentIndex = tabs.getSelectionModel().getSelectedIndex();
|
||||||
|
if(currentIndex + 1 >= tabs.getTabs().size()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Tab selectedTab = tabs.getSelectionModel().getSelectedItem();
|
||||||
tabs.getTabs().removeListener(tabsChangeListener);
|
tabs.getTabs().removeListener(tabsChangeListener);
|
||||||
tabs.getTabs().remove(tab);
|
tabs.getTabs().remove(selectedTab);
|
||||||
tabs.getTabs().add(index + 1, tab);
|
tabs.getTabs().add(currentIndex + 1, selectedTab);
|
||||||
tabs.getTabs().addListener(tabsChangeListener);
|
tabs.getTabs().addListener(tabsChangeListener);
|
||||||
tabs.getSelectionModel().select(tab);
|
tabs.getSelectionModel().select(selectedTab);
|
||||||
EventManager.get().post(new RequestOpenWalletsEvent()); //Rearrange recent files list
|
EventManager.get().post(new RequestOpenWalletsEvent()); //Rearrange recent files list
|
||||||
});
|
});
|
||||||
MenuItem moveLeft = new MenuItem("Move Left");
|
MenuItem moveLeft = new MenuItem("Move Left");
|
||||||
|
moveLeft.setAccelerator(new KeyCodeCombination(KeyCode.LEFT, KeyCombination.CONTROL_DOWN, KeyCombination.SHIFT_DOWN));
|
||||||
moveLeft.setOnAction(event -> {
|
moveLeft.setOnAction(event -> {
|
||||||
int index = tabs.getTabs().indexOf(tab);
|
int currentIndex = tabs.getSelectionModel().getSelectedIndex();
|
||||||
|
if(currentIndex == 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Tab selectedTab = tabs.getSelectionModel().getSelectedItem();
|
||||||
tabs.getTabs().removeListener(tabsChangeListener);
|
tabs.getTabs().removeListener(tabsChangeListener);
|
||||||
tabs.getTabs().remove(tab);
|
tabs.getTabs().remove(selectedTab);
|
||||||
tabs.getTabs().add(index - 1, tab);
|
tabs.getTabs().add(currentIndex - 1, selectedTab);
|
||||||
tabs.getTabs().addListener(tabsChangeListener);
|
tabs.getTabs().addListener(tabsChangeListener);
|
||||||
tabs.getSelectionModel().select(tab);
|
tabs.getSelectionModel().select(selectedTab);
|
||||||
EventManager.get().post(new RequestOpenWalletsEvent()); //Rearrange recent files list
|
EventManager.get().post(new RequestOpenWalletsEvent()); //Rearrange recent files list
|
||||||
});
|
});
|
||||||
contextMenu.getItems().addAll(moveRight, moveLeft);
|
contextMenu.getItems().addAll(moveRight, moveLeft);
|
||||||
|
|
@ -2630,7 +2694,6 @@ 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) + "...";
|
||||||
|
|
@ -2639,10 +2702,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 ImageView(image))
|
.graphic(new DialogImage(DialogImage.Type.SPARROW))
|
||||||
.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 ImageView(image)))
|
.threshold(5, Notifications.create().title("Sparrow").text("Multiple new wallet transactions").graphic(new DialogImage(DialogImage.Type.SPARROW)))
|
||||||
.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)
|
||||||
|
|
@ -2883,6 +2946,7 @@ 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("");
|
||||||
}
|
}
|
||||||
|
|
@ -3103,6 +3167,11 @@ public class AppController implements Initializable {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Subscribe
|
||||||
|
public void requestSendToMany(RequestSendToManyEvent event) {
|
||||||
|
sendToMany(event.getPayments());
|
||||||
|
}
|
||||||
|
|
||||||
@Subscribe
|
@Subscribe
|
||||||
public void functionAction(FunctionActionEvent event) {
|
public void functionAction(FunctionActionEvent event) {
|
||||||
selectTab(event.getWallet());
|
selectTab(event.getWallet());
|
||||||
|
|
@ -3156,7 +3225,7 @@ public class AppController implements Initializable {
|
||||||
|
|
||||||
@Subscribe
|
@Subscribe
|
||||||
public void webcamResolutionChanged(WebcamResolutionChangedEvent event) {
|
public void webcamResolutionChanged(WebcamResolutionChangedEvent event) {
|
||||||
useHdCameraResolutionProperty.set(event.isHdResolution());
|
useHdCameraResolutionProperty.set(event.getResolution().isWidescreenAspect());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Subscribe
|
@Subscribe
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@ 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;
|
||||||
|
|
@ -25,6 +26,8 @@ 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;
|
||||||
|
|
@ -42,7 +45,6 @@ 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;
|
||||||
|
|
@ -66,6 +68,8 @@ 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.*;
|
||||||
|
|
@ -87,8 +91,7 @@ public class AppServices {
|
||||||
private static final String TOR_DEFAULT_PROXY_CIRCUIT_ID = "default";
|
private static final String TOR_DEFAULT_PROXY_CIRCUIT_ID = "default";
|
||||||
|
|
||||||
public static final List<Integer> TARGET_BLOCKS_RANGE = List.of(1, 2, 3, 4, 5, 10, 25, 50);
|
public static final List<Integer> TARGET_BLOCKS_RANGE = List.of(1, 2, 3, 4, 5, 10, 25, 50);
|
||||||
public static final List<Long> LONG_FEE_RATES_RANGE = List.of(1L, 2L, 4L, 8L, 16L, 32L, 64L, 128L, 256L, 512L, 1024L, 2048L, 4096L, 8192L);
|
private static final List<Double> LONG_FEE_RATES_RANGE = List.of(1d, 2d, 4d, 8d, 16d, 32d, 64d, 128d, 256d, 512d, 1024d, 2048d, 4096d, 8192d);
|
||||||
public static final List<Long> FEE_RATES_RANGE = LONG_FEE_RATES_RANGE.subList(0, LONG_FEE_RATES_RANGE.size() - 3);
|
|
||||||
public static final double FALLBACK_FEE_RATE = 20000d / 1000;
|
public static final double FALLBACK_FEE_RATE = 20000d / 1000;
|
||||||
public static final double TESTNET_FALLBACK_FEE_RATE = 1000d / 1000;
|
public static final double TESTNET_FALLBACK_FEE_RATE = 1000d / 1000;
|
||||||
|
|
||||||
|
|
@ -104,6 +107,8 @@ 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);
|
||||||
|
|
@ -126,12 +131,18 @@ 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;
|
||||||
|
|
@ -182,6 +193,12 @@ 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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -195,6 +212,7 @@ public class AppServices {
|
||||||
preventSleepService = createPreventSleepService();
|
preventSleepService = createPreventSleepService();
|
||||||
|
|
||||||
onlineProperty.addListener(onlineServicesListener);
|
onlineProperty.addListener(onlineServicesListener);
|
||||||
|
minimumRelayFeeRate = getConfiguredMinimumRelayFeeRate(config);
|
||||||
|
|
||||||
if(config.getMode() == Mode.ONLINE) {
|
if(config.getMode() == Mode.ONLINE) {
|
||||||
if(config.requiresInternalTor()) {
|
if(config.requiresInternalTor()) {
|
||||||
|
|
@ -261,7 +279,7 @@ public class AppServices {
|
||||||
}
|
}
|
||||||
|
|
||||||
if(Tor.getDefault() != null) {
|
if(Tor.getDefault() != null) {
|
||||||
Tor.getDefault().getTorManager().destroy(true, success -> {});
|
Tor.getDefault().close();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -291,12 +309,6 @@ 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
|
||||||
|
|
@ -480,6 +492,26 @@ 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;
|
||||||
}
|
}
|
||||||
|
|
@ -705,6 +737,10 @@ 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);
|
||||||
|
|
@ -716,6 +752,30 @@ public class AppServices {
|
||||||
return Math.max(minRate, Transaction.DUST_RELAY_TX_FEE);
|
return Math.max(minRate, Transaction.DUST_RELAY_TX_FEE);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static List<Double> getLongFeeRatesRange() {
|
||||||
|
if(minimumRelayFeeRate == null || minimumRelayFeeRate >= Transaction.DEFAULT_MIN_RELAY_FEE) {
|
||||||
|
return LONG_FEE_RATES_RANGE;
|
||||||
|
} else {
|
||||||
|
List<Double> longFeeRatesRange = new ArrayList<>();
|
||||||
|
longFeeRatesRange.add(minimumRelayFeeRate);
|
||||||
|
longFeeRatesRange.addAll(LONG_FEE_RATES_RANGE);
|
||||||
|
return longFeeRatesRange;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static List<Double> getFeeRatesRange() {
|
||||||
|
if(minimumRelayFeeRate == null || minimumRelayFeeRate >= Transaction.DEFAULT_MIN_RELAY_FEE) {
|
||||||
|
return LONG_FEE_RATES_RANGE.subList(0, LONG_FEE_RATES_RANGE.size() - 3);
|
||||||
|
} else {
|
||||||
|
List<Double> longFeeRatesRange = getLongFeeRatesRange();
|
||||||
|
return longFeeRatesRange.subList(0, longFeeRatesRange.size() - 4);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Double getNextBlockMedianFeeRate() {
|
||||||
|
return nextBlockMedianFeeRate == null ? getDefaultFeeRate() : nextBlockMedianFeeRate;
|
||||||
|
}
|
||||||
|
|
||||||
public static double getFallbackFeeRate() {
|
public static double getFallbackFeeRate() {
|
||||||
return Network.get() == Network.MAINNET ? FALLBACK_FEE_RATE : TESTNET_FALLBACK_FEE_RATE;
|
return Network.get() == Network.MAINNET ? FALLBACK_FEE_RATE : TESTNET_FALLBACK_FEE_RATE;
|
||||||
}
|
}
|
||||||
|
|
@ -750,10 +810,18 @@ public class AppServices {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static Double getConfiguredMinimumRelayFeeRate(Config config) {
|
||||||
|
return config.getMinRelayFeeRate() >= 0d && config.getMinRelayFeeRate() < Transaction.DEFAULT_MIN_RELAY_FEE ? config.getMinRelayFeeRate() : null;
|
||||||
|
}
|
||||||
|
|
||||||
public static Double getMinimumRelayFeeRate() {
|
public static Double getMinimumRelayFeeRate() {
|
||||||
return minimumRelayFeeRate == null ? Transaction.DEFAULT_MIN_RELAY_FEE : minimumRelayFeeRate;
|
return minimumRelayFeeRate == null ? Transaction.DEFAULT_MIN_RELAY_FEE : minimumRelayFeeRate;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static Double getServerMinimumRelayFeeRate() {
|
||||||
|
return serverMinimumRelayFeeRate;
|
||||||
|
}
|
||||||
|
|
||||||
public static CurrencyRate getFiatCurrencyExchangeRate() {
|
public static CurrencyRate getFiatCurrencyExchangeRate() {
|
||||||
return fiatCurrencyExchangeRate;
|
return fiatCurrencyExchangeRate;
|
||||||
}
|
}
|
||||||
|
|
@ -767,8 +835,8 @@ public class AppServices {
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void addPayjoinURI(BitcoinURI bitcoinURI) {
|
public static void addPayjoinURI(BitcoinURI bitcoinURI) {
|
||||||
if(bitcoinURI.getPayjoinUrl() == null) {
|
if(bitcoinURI.getPayjoinUrl() == null || bitcoinURI.getAddress() == null) {
|
||||||
throw new IllegalArgumentException("Not a payjoin URI");
|
throw new IllegalArgumentException("Not a valid payjoin URI");
|
||||||
}
|
}
|
||||||
payjoinURIs.put(bitcoinURI.getAddress(), bitcoinURI);
|
payjoinURIs.put(bitcoinURI.getAddress(), bitcoinURI);
|
||||||
}
|
}
|
||||||
|
|
@ -1095,8 +1163,7 @@ 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);
|
||||||
Image image = new Image("/image/sparrow-small.png");
|
walletChoiceDialog.getDialogPane().setGraphic(new DialogImage(DialogImage.Type.SPARROW));
|
||||||
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();
|
||||||
|
|
@ -1108,6 +1175,31 @@ 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) {
|
||||||
|
|
@ -1123,7 +1215,8 @@ 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) {
|
||||||
|
|
@ -1140,7 +1233,7 @@ public class AppServices {
|
||||||
}
|
}
|
||||||
|
|
||||||
public static Font getMonospaceFont() {
|
public static Font getMonospaceFont() {
|
||||||
return Font.font("Roboto Mono", 13);
|
return Font.font("Fragment Mono Regular", 13);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static boolean isOnWayland() {
|
public static boolean isOnWayland() {
|
||||||
|
|
@ -1156,9 +1249,22 @@ public class AppServices {
|
||||||
public void newConnection(ConnectionEvent event) {
|
public void newConnection(ConnectionEvent event) {
|
||||||
currentBlockHeight = event.getBlockHeight();
|
currentBlockHeight = event.getBlockHeight();
|
||||||
System.setProperty(Network.BLOCK_HEIGHT_PROPERTY, Integer.toString(currentBlockHeight));
|
System.setProperty(Network.BLOCK_HEIGHT_PROPERTY, Integer.toString(currentBlockHeight));
|
||||||
minimumRelayFeeRate = Math.max(event.getMinimumRelayFeeRate(), Transaction.DEFAULT_MIN_RELAY_FEE);
|
if(getConfiguredMinimumRelayFeeRate(Config.get()) == null) {
|
||||||
|
minimumRelayFeeRate = event.getMinimumRelayFeeRate() == null ? Transaction.DEFAULT_MIN_RELAY_FEE : event.getMinimumRelayFeeRate();
|
||||||
|
}
|
||||||
|
serverMinimumRelayFeeRate = event.getMinimumRelayFeeRate();
|
||||||
latestBlockHeader = event.getBlockHeader();
|
latestBlockHeader = event.getBlockHeader();
|
||||||
Config.get().addRecentServer();
|
Config.get().addRecentServer();
|
||||||
|
|
||||||
|
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
|
||||||
|
|
@ -1173,11 +1279,22 @@ 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
|
||||||
|
|
@ -1190,10 +1307,8 @@ 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
|
||||||
if(feeRatesService != null && !feeRatesService.isRunning() && Config.get().getMode() != Mode.OFFLINE) {
|
fetchFeeRates();
|
||||||
feeRatesService = createFeeRatesService();
|
fetchBlockSummaries(Collections.emptyList());
|
||||||
feeRatesService.start();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Subscribe
|
@Subscribe
|
||||||
|
|
|
||||||
76
src/main/java/com/sparrowwallet/sparrow/BlockSummary.java
Normal file
76
src/main/java/com/sparrowwallet/sparrow/BlockSummary.java
Normal file
|
|
@ -0,0 +1,76 @@
|
||||||
|
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,10 +72,6 @@ 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()));
|
||||||
|
|
||||||
|
|
@ -117,8 +113,8 @@ public class SparrowDesktop extends Application {
|
||||||
private void initializeFonts() {
|
private void initializeFonts() {
|
||||||
GlyphFontRegistry.register(new FontAwesome5());
|
GlyphFontRegistry.register(new FontAwesome5());
|
||||||
GlyphFontRegistry.register(new FontAwesome5Brands());
|
GlyphFontRegistry.register(new FontAwesome5Brands());
|
||||||
Font.loadFont(AppServices.class.getResourceAsStream("/font/RobotoMono-Regular.ttf"), 13);
|
Font.loadFont(AppServices.class.getResourceAsStream("/font/FragmentMono-Regular.ttf"), 13);
|
||||||
Font.loadFont(AppServices.class.getResourceAsStream("/font/RobotoMono-Italic.ttf"), 11);
|
Font.loadFont(AppServices.class.getResourceAsStream("/font/FragmentMono-Italic.ttf"), 11);
|
||||||
if(OsType.getCurrent() == OsType.MACOS) {
|
if(OsType.getCurrent() == OsType.MACOS) {
|
||||||
Font.loadFont(AppServices.class.getResourceAsStream("/font/LiberationSans-Regular.ttf"), 13);
|
Font.loadFont(AppServices.class.getResourceAsStream("/font/LiberationSans-Regular.ttf"), 13);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,7 @@ import java.util.*;
|
||||||
public class SparrowWallet {
|
public class SparrowWallet {
|
||||||
public static final String APP_ID = "sparrow";
|
public static final String APP_ID = "sparrow";
|
||||||
public static final String APP_NAME = "Sparrow";
|
public static final String APP_NAME = "Sparrow";
|
||||||
public static final String APP_VERSION = "2.1.1";
|
public static final String APP_VERSION = "2.3.1";
|
||||||
public static final String APP_VERSION_SUFFIX = "";
|
public static final String APP_VERSION_SUFFIX = "";
|
||||||
public static final String APP_HOME_PROPERTY = "sparrow.home";
|
public static final String APP_HOME_PROPERTY = "sparrow.home";
|
||||||
public static final String NETWORK_ENV_PROPERTY = "SPARROW_NETWORK";
|
public static final String NETWORK_ENV_PROPERTY = "SPARROW_NETWORK";
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,7 @@ public class WelcomeDialog extends Dialog<Mode> {
|
||||||
welcomeController.initializeView();
|
welcomeController.initializeView();
|
||||||
|
|
||||||
dialogPane.setPrefWidth(600);
|
dialogPane.setPrefWidth(600);
|
||||||
dialogPane.setPrefHeight(520);
|
dialogPane.setPrefHeight(540);
|
||||||
dialogPane.setMinHeight(dialogPane.getPrefHeight());
|
dialogPane.setMinHeight(dialogPane.getPrefHeight());
|
||||||
AppServices.moveToActiveWindowScreen(this);
|
AppServices.moveToActiveWindowScreen(this);
|
||||||
|
|
||||||
|
|
|
||||||
372
src/main/java/com/sparrowwallet/sparrow/control/BlockCube.java
Normal file
372
src/main/java/com/sparrowwallet/sparrow/control/BlockCube.java
Normal file
|
|
@ -0,0 +1,372 @@
|
||||||
|
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)), "image/" + importer.getWalletModel().getType() + ".png");
|
super(importer.getName(), "Place card on reader", importer.getKeystoreImportDescription(getAccount(wallet, requiredDerivation)), importer.getWalletModel());
|
||||||
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,6 +87,8 @@ class CoinCell extends TreeTableCell<Entry, Number> implements ConfirmationsList
|
||||||
} else if(entry instanceof UtxoEntry) {
|
} else if(entry instanceof UtxoEntry) {
|
||||||
setGraphic(null);
|
setGraphic(null);
|
||||||
} else if(entry instanceof HashIndexEntry) {
|
} else if(entry instanceof HashIndexEntry) {
|
||||||
|
tooltip.hideConfirmations();
|
||||||
|
|
||||||
Region node = new Region();
|
Region node = new Region();
|
||||||
node.setPrefWidth(10);
|
node.setPrefWidth(10);
|
||||||
setGraphic(node);
|
setGraphic(node);
|
||||||
|
|
@ -148,6 +150,14 @@ class CoinCell extends TreeTableCell<Entry, Number> implements ConfirmationsList
|
||||||
setTooltipText();
|
setTooltipText();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void hideConfirmations() {
|
||||||
|
showConfirmations = false;
|
||||||
|
isCoinbase = false;
|
||||||
|
confirmationsProperty.unbind();
|
||||||
|
|
||||||
|
setTooltipText();
|
||||||
|
}
|
||||||
|
|
||||||
private void setTooltipText() {
|
private void setTooltipText() {
|
||||||
setText(value + (showConfirmations ? " (" + getConfirmationsDescription() + ")" : ""));
|
setText(value + (showConfirmations ? " (" + getConfirmationsDescription() + ")" : ""));
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -225,6 +225,13 @@ 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,10 +6,16 @@ import javafx.beans.property.SimpleObjectProperty;
|
||||||
import javafx.scene.Cursor;
|
import javafx.scene.Cursor;
|
||||||
import javafx.scene.Node;
|
import javafx.scene.Node;
|
||||||
import javafx.scene.control.ComboBox;
|
import javafx.scene.control.ComboBox;
|
||||||
|
import javafx.scene.control.ContextMenu;
|
||||||
|
import javafx.scene.control.MenuItem;
|
||||||
|
import javafx.scene.control.SeparatorMenuItem;
|
||||||
|
import javafx.scene.input.Clipboard;
|
||||||
import javafx.scene.layout.Region;
|
import javafx.scene.layout.Region;
|
||||||
import javafx.scene.layout.StackPane;
|
import javafx.scene.layout.StackPane;
|
||||||
import org.controlsfx.control.textfield.CustomTextField;
|
import org.controlsfx.control.textfield.CustomTextField;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
public class ComboBoxTextField extends CustomTextField {
|
public class ComboBoxTextField extends CustomTextField {
|
||||||
private final ObjectProperty<ComboBox<?>> comboProperty = new SimpleObjectProperty<>();
|
private final ObjectProperty<ComboBox<?>> comboProperty = new SimpleObjectProperty<>();
|
||||||
|
|
||||||
|
|
@ -68,4 +74,53 @@ public class ComboBoxTextField extends CustomTextField {
|
||||||
public void setComboProperty(ComboBox<?> comboProperty) {
|
public void setComboProperty(ComboBox<?> comboProperty) {
|
||||||
this.comboProperty.set(comboProperty);
|
this.comboProperty.set(comboProperty);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public ContextMenu getCustomContextMenu(List<MenuItem> customItems) {
|
||||||
|
return new CustomContextMenu(customItems);
|
||||||
|
}
|
||||||
|
|
||||||
|
public class CustomContextMenu extends ContextMenu {
|
||||||
|
public CustomContextMenu(List<MenuItem> customItems) {
|
||||||
|
super();
|
||||||
|
setFont(null);
|
||||||
|
|
||||||
|
MenuItem undo = new MenuItem("Undo");
|
||||||
|
undo.setOnAction(_ -> undo());
|
||||||
|
|
||||||
|
MenuItem redo = new MenuItem("Redo");
|
||||||
|
redo.setOnAction(_ -> redo());
|
||||||
|
|
||||||
|
MenuItem cut = new MenuItem("Cut");
|
||||||
|
cut.setOnAction(_ -> cut());
|
||||||
|
|
||||||
|
MenuItem copy = new MenuItem("Copy");
|
||||||
|
copy.setOnAction(_ -> copy());
|
||||||
|
|
||||||
|
MenuItem paste = new MenuItem("Paste");
|
||||||
|
paste.setOnAction(_ -> paste());
|
||||||
|
|
||||||
|
MenuItem delete = new MenuItem("Delete");
|
||||||
|
delete.setOnAction(_ -> deleteText(getSelection()));
|
||||||
|
|
||||||
|
MenuItem selectAll = new MenuItem("Select All");
|
||||||
|
selectAll.setOnAction(_ -> selectAll());
|
||||||
|
|
||||||
|
getItems().addAll(undo, redo, new SeparatorMenuItem(), cut, copy, paste, delete, new SeparatorMenuItem(), selectAll);
|
||||||
|
getItems().addAll(customItems);
|
||||||
|
|
||||||
|
setOnShowing(_ -> {
|
||||||
|
boolean hasSelection = getSelection().getLength() > 0;
|
||||||
|
boolean hasText = getText() != null && !getText().isEmpty();
|
||||||
|
boolean clipboardHasContent = Clipboard.getSystemClipboard().hasString();
|
||||||
|
|
||||||
|
undo.setDisable(!isUndoable());
|
||||||
|
redo.setDisable(!isRedoable());
|
||||||
|
cut.setDisable(!isEditable() || !hasSelection);
|
||||||
|
copy.setDisable(!hasSelection);
|
||||||
|
paste.setDisable(!isEditable() || !clipboardHasContent);
|
||||||
|
delete.setDisable(!hasSelection);
|
||||||
|
selectAll.setDisable(!hasText);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,39 @@
|
||||||
|
package com.sparrowwallet.sparrow.control;
|
||||||
|
|
||||||
|
import com.sparrowwallet.sparrow.AppServices;
|
||||||
|
import javafx.geometry.Insets;
|
||||||
|
import javafx.scene.control.Alert;
|
||||||
|
import javafx.scene.control.ButtonType;
|
||||||
|
import javafx.scene.control.CheckBox;
|
||||||
|
import javafx.scene.control.Label;
|
||||||
|
import javafx.scene.layout.VBox;
|
||||||
|
|
||||||
|
import static com.sparrowwallet.sparrow.AppServices.getActiveWindow;
|
||||||
|
import static com.sparrowwallet.sparrow.AppServices.setStageIcon;
|
||||||
|
|
||||||
|
public class ConfirmationAlert extends Alert {
|
||||||
|
private final CheckBox dontAskAgain;
|
||||||
|
|
||||||
|
public ConfirmationAlert(String title, String contentText, ButtonType... buttons) {
|
||||||
|
super(AlertType.CONFIRMATION, contentText, buttons);
|
||||||
|
|
||||||
|
initOwner(getActiveWindow());
|
||||||
|
setStageIcon(getDialogPane().getScene().getWindow());
|
||||||
|
getDialogPane().getScene().getStylesheets().add(AppServices.class.getResource("general.css").toExternalForm());
|
||||||
|
setTitle(title);
|
||||||
|
setHeaderText(title);
|
||||||
|
|
||||||
|
VBox contentBox = new VBox(20);
|
||||||
|
contentBox.setPadding(new Insets(10, 20, 10, 20));
|
||||||
|
Label contentLabel = new Label(contentText);
|
||||||
|
contentLabel.setWrapText(true);
|
||||||
|
dontAskAgain = new CheckBox("Don't ask again");
|
||||||
|
contentBox.getChildren().addAll(contentLabel, dontAskAgain);
|
||||||
|
|
||||||
|
getDialogPane().setContent(contentBox);
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isDontAskAgain() {
|
||||||
|
return dontAskAgain.isSelected();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -11,6 +11,7 @@ import javafx.scene.control.Tooltip;
|
||||||
import javafx.scene.input.Clipboard;
|
import javafx.scene.input.Clipboard;
|
||||||
import javafx.scene.input.ClipboardContent;
|
import javafx.scene.input.ClipboardContent;
|
||||||
import javafx.event.EventHandler;
|
import javafx.event.EventHandler;
|
||||||
|
import javafx.scene.input.MouseButton;
|
||||||
import javafx.scene.input.MouseEvent;
|
import javafx.scene.input.MouseEvent;
|
||||||
|
|
||||||
public class CopyableCoinLabel extends CopyableLabel {
|
public class CopyableCoinLabel extends CopyableLabel {
|
||||||
|
|
@ -29,6 +30,10 @@ public class CopyableCoinLabel extends CopyableLabel {
|
||||||
valueProperty().addListener((observable, oldValue, newValue) -> setValueAsText((Long)newValue, Config.get().getUnitFormat(), Config.get().getBitcoinUnit()));
|
valueProperty().addListener((observable, oldValue, newValue) -> setValueAsText((Long)newValue, Config.get().getUnitFormat(), Config.get().getBitcoinUnit()));
|
||||||
|
|
||||||
setOnMouseClicked(event -> {
|
setOnMouseClicked(event -> {
|
||||||
|
if(!event.getButton().equals(MouseButton.PRIMARY)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if(bitcoinUnit == null) {
|
if(bitcoinUnit == null) {
|
||||||
bitcoinUnit = Config.get().getBitcoinUnit();
|
bitcoinUnit = Config.get().getBitcoinUnit();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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(), "", "", "image/" + device.getType() + ".png");
|
super(device.getModel().toDisplayString(), "", "", device.getModel());
|
||||||
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(), "", "", "image/" + device.getType() + ".png");
|
super(device.getModel().toDisplayString(), "", "", device.getModel());
|
||||||
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(), "", "", "image/" + device.getType() + ".png");
|
super(device.getModel().toDisplayString(), "", "", device.getModel());
|
||||||
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(), "", "", "image/" + device.getType() + ".png");
|
super(device.getModel().toDisplayString(), "", "", device.getModel());
|
||||||
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(), "", "", "image/" + device.getType() + ".png");
|
super(device.getModel().toDisplayString(), "", "", device.getModel());
|
||||||
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(), "", "", "image/" + device.getType() + ".png");
|
super(device.getModel().toDisplayString(), "", "", device.getModel());
|
||||||
this.deviceOperation = deviceOperation;
|
this.deviceOperation = deviceOperation;
|
||||||
this.wallet = null;
|
this.wallet = null;
|
||||||
this.psbt = null;
|
this.psbt = null;
|
||||||
|
|
@ -453,20 +453,26 @@ public class DevicePane extends TitledDescriptionPane {
|
||||||
});
|
});
|
||||||
vBox.getChildren().addAll(pinField, enterPinButton);
|
vBox.getChildren().addAll(pinField, enterPinButton);
|
||||||
|
|
||||||
TilePane tilePane = new TilePane();
|
GridPane gridPane = new GridPane();
|
||||||
tilePane.setPrefColumns(3);
|
gridPane.setHgap(10);
|
||||||
tilePane.setHgap(10);
|
gridPane.setVgap(10);
|
||||||
tilePane.setVgap(10);
|
gridPane.setMaxWidth(150);
|
||||||
tilePane.setMaxWidth(150);
|
gridPane.setMaxHeight(device.getModel().hasZeroInPin() ? 160 : 120);
|
||||||
tilePane.setMaxHeight(120);
|
|
||||||
|
|
||||||
int[] digits = new int[] {7, 8, 9, 4, 5, 6, 1, 2, 3};
|
int[] digits = device.getModel().hasZeroInPin() ? new int[] {7, 8, 9, 4, 5, 6, 1, 2, 3, 0} : new int[] {7, 8, 9, 4, 5, 6, 1, 2, 3};
|
||||||
for(int i = 0; i < digits.length; i++) {
|
for(int i = 0; i < digits.length; i++) {
|
||||||
Button pinButton = new Button();
|
Button pinButton = new Button();
|
||||||
Glyph circle = new Glyph(FontAwesome5.FONT_NAME, "CIRCLE");
|
Glyph circle = new Glyph(FontAwesome5.FONT_NAME, "CIRCLE");
|
||||||
pinButton.setGraphic(circle);
|
pinButton.setGraphic(circle);
|
||||||
pinButton.setUserData(digits[i]);
|
pinButton.setUserData(digits[i]);
|
||||||
tilePane.getChildren().add(pinButton);
|
GridPane.setRowIndex(pinButton, i / 3);
|
||||||
|
GridPane.setColumnIndex(pinButton, i % 3);
|
||||||
|
if((i / 3) == 3) {
|
||||||
|
GridPane.setHgrow(pinButton, Priority.ALWAYS);
|
||||||
|
GridPane.setColumnSpan(pinButton, 3);
|
||||||
|
pinButton.setMaxWidth(Double.MAX_VALUE);
|
||||||
|
}
|
||||||
|
gridPane.getChildren().add(pinButton);
|
||||||
pinButton.setOnAction(event -> {
|
pinButton.setOnAction(event -> {
|
||||||
pinField.setText(pinField.getText() + pinButton.getUserData());
|
pinField.setText(pinField.getText() + pinButton.getUserData());
|
||||||
});
|
});
|
||||||
|
|
@ -474,7 +480,7 @@ public class DevicePane extends TitledDescriptionPane {
|
||||||
|
|
||||||
HBox contentBox = new HBox();
|
HBox contentBox = new HBox();
|
||||||
contentBox.setSpacing(50);
|
contentBox.setSpacing(50);
|
||||||
contentBox.getChildren().add(tilePane);
|
contentBox.getChildren().add(gridPane);
|
||||||
contentBox.getChildren().add(vBox);
|
contentBox.getChildren().add(vBox);
|
||||||
contentBox.setPadding(new Insets(10, 0, 10, 0));
|
contentBox.setPadding(new Insets(10, 0, 10, 0));
|
||||||
contentBox.setAlignment(Pos.TOP_CENTER);
|
contentBox.setAlignment(Pos.TOP_CENTER);
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,85 @@
|
||||||
|
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,13 +56,15 @@ 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", "zip");
|
private static final List<String> WINDOWS_RELEASE_EXTENSIONS = List.of("exe", "msi", "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_SIGNATURE_SUFFIX = "-manifest.txt.asc";
|
private static final String SPARROW_RELEASE_ALT_PREFIX = "sparrow_";
|
||||||
|
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;
|
||||||
|
|
||||||
|
|
@ -70,6 +72,7 @@ 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();
|
||||||
|
|
@ -81,7 +84,7 @@ public class DownloadVerifierDialog extends Dialog<ButtonBar.ButtonData> {
|
||||||
|
|
||||||
private static File lastFileParent;
|
private static File lastFileParent;
|
||||||
|
|
||||||
public DownloadVerifierDialog(File initialSignatureFile) {
|
public DownloadVerifierDialog(File initialFile) {
|
||||||
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());
|
||||||
|
|
@ -223,11 +226,17 @@ 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(initialSignatureFile != null) {
|
if(initialFile != null) {
|
||||||
javafx.application.Platform.runLater(() -> signature.set(initialSignatureFile));
|
javafx.application.Platform.runLater(() -> {
|
||||||
|
initial.set(initialFile);
|
||||||
|
signature.set(initialFile);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -292,7 +301,7 @@ public class DownloadVerifierDialog extends Dialog<ButtonBar.ButtonData> {
|
||||||
publicKeyDisabled.set(true);
|
publicKeyDisabled.set(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
if(manifest.get().equals(release.get())) {
|
if(manifest.get().equals(release.get()) && !isSparrowManifest(manifest.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());
|
||||||
|
|
@ -455,7 +464,8 @@ public class DownloadVerifierDialog extends Dialog<ButtonBar.ButtonData> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if(providedFile.getName().toLowerCase(Locale.ROOT).startsWith(SPARROW_RELEASE_PREFIX)) {
|
String providedName = providedFile.getName().toLowerCase(Locale.ROOT);
|
||||||
|
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();
|
||||||
|
|
@ -482,6 +492,22 @@ 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(""));
|
||||||
|
|
||||||
|
|
@ -565,7 +591,7 @@ public class DownloadVerifierDialog extends Dialog<ButtonBar.ButtonData> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if(name.startsWith(SPARROW_RELEASE_PREFIX) && file.length() >= MIN_VALID_SPARROW_RELEASE_SIZE) {
|
if((name.startsWith(SPARROW_RELEASE_PREFIX) || name.startsWith(SPARROW_RELEASE_ALT_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();
|
||||||
}
|
}
|
||||||
|
|
@ -574,10 +600,18 @@ 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);
|
||||||
|
|
@ -598,15 +632,8 @@ 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 StackPane();
|
StackPane graphicContainer = new DialogImage(DialogImage.Type.SPARROW);
|
||||||
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,6 +5,8 @@ import com.sparrowwallet.drongo.OsType;
|
||||||
import com.sparrowwallet.drongo.Utils;
|
import com.sparrowwallet.drongo.Utils;
|
||||||
import com.sparrowwallet.drongo.address.Address;
|
import com.sparrowwallet.drongo.address.Address;
|
||||||
import com.sparrowwallet.drongo.protocol.*;
|
import com.sparrowwallet.drongo.protocol.*;
|
||||||
|
import com.sparrowwallet.drongo.silentpayments.SilentPayment;
|
||||||
|
import com.sparrowwallet.drongo.silentpayments.SilentPaymentAddress;
|
||||||
import com.sparrowwallet.drongo.wallet.*;
|
import com.sparrowwallet.drongo.wallet.*;
|
||||||
import com.sparrowwallet.sparrow.AppServices;
|
import com.sparrowwallet.sparrow.AppServices;
|
||||||
import com.sparrowwallet.sparrow.EventManager;
|
import com.sparrowwallet.sparrow.EventManager;
|
||||||
|
|
@ -55,7 +57,7 @@ public class EntryCell extends TreeTableCell<Entry, Entry> implements Confirmati
|
||||||
super.updateItem(entry, empty);
|
super.updateItem(entry, empty);
|
||||||
|
|
||||||
//Return immediately to avoid CPU usage when updating the same invisible cell to determine tableview size (see https://bugs.openjdk.org/browse/JDK-8280442)
|
//Return immediately to avoid CPU usage when updating the same invisible cell to determine tableview size (see https://bugs.openjdk.org/browse/JDK-8280442)
|
||||||
if(this == lastCell && !getTableRow().isVisible()) {
|
if(this == lastCell && !getTableRow().isVisible() && isTableSizeRecalculation()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
lastCell = this;
|
lastCell = this;
|
||||||
|
|
@ -66,8 +68,7 @@ public class EntryCell extends TreeTableCell<Entry, Entry> implements Confirmati
|
||||||
setText(null);
|
setText(null);
|
||||||
setGraphic(null);
|
setGraphic(null);
|
||||||
} else {
|
} else {
|
||||||
if(entry instanceof TransactionEntry) {
|
if(entry instanceof TransactionEntry transactionEntry) {
|
||||||
TransactionEntry transactionEntry = (TransactionEntry)entry;
|
|
||||||
if(transactionEntry.getBlockTransaction().getHeight() == -1) {
|
if(transactionEntry.getBlockTransaction().getHeight() == -1) {
|
||||||
setText("Unconfirmed Parent");
|
setText("Unconfirmed Parent");
|
||||||
setContextMenu(new UnconfirmedTransactionContextMenu(transactionEntry));
|
setContextMenu(new UnconfirmedTransactionContextMenu(transactionEntry));
|
||||||
|
|
@ -101,7 +102,7 @@ public class EntryCell extends TreeTableCell<Entry, Entry> implements Confirmati
|
||||||
actionBox.getChildren().add(viewTransactionButton);
|
actionBox.getChildren().add(viewTransactionButton);
|
||||||
|
|
||||||
BlockTransaction blockTransaction = transactionEntry.getBlockTransaction();
|
BlockTransaction blockTransaction = transactionEntry.getBlockTransaction();
|
||||||
if(blockTransaction.getHeight() <= 0 && canRBF(blockTransaction) &&
|
if(blockTransaction.getHeight() <= 0 && canRBF(blockTransaction, transactionEntry.getWallet()) &&
|
||||||
Config.get().isIncludeMempoolOutputs() && transactionEntry.getWallet().allInputsFromWallet(blockTransaction.getHash())) {
|
Config.get().isIncludeMempoolOutputs() && transactionEntry.getWallet().allInputsFromWallet(blockTransaction.getHash())) {
|
||||||
Button increaseFeeButton = new Button("");
|
Button increaseFeeButton = new Button("");
|
||||||
increaseFeeButton.setGraphic(getIncreaseFeeRBFGlyph());
|
increaseFeeButton.setGraphic(getIncreaseFeeRBFGlyph());
|
||||||
|
|
@ -121,8 +122,7 @@ public class EntryCell extends TreeTableCell<Entry, Entry> implements Confirmati
|
||||||
}
|
}
|
||||||
|
|
||||||
setGraphic(actionBox);
|
setGraphic(actionBox);
|
||||||
} else if(entry instanceof NodeEntry) {
|
} else if(entry instanceof NodeEntry nodeEntry) {
|
||||||
NodeEntry nodeEntry = (NodeEntry)entry;
|
|
||||||
Address address = nodeEntry.getAddress();
|
Address address = nodeEntry.getAddress();
|
||||||
setText(address.toString());
|
setText(address.toString());
|
||||||
setContextMenu(new AddressContextMenu(address, nodeEntry.getOutputDescriptor(), nodeEntry, true, getTreeTableView()));
|
setContextMenu(new AddressContextMenu(address, nodeEntry.getOutputDescriptor(), nodeEntry, true, getTreeTableView()));
|
||||||
|
|
@ -163,8 +163,7 @@ public class EntryCell extends TreeTableCell<Entry, Entry> implements Confirmati
|
||||||
setContextMenu(null);
|
setContextMenu(null);
|
||||||
setGraphic(new HBox());
|
setGraphic(new HBox());
|
||||||
}
|
}
|
||||||
} else if(entry instanceof HashIndexEntry) {
|
} else if(entry instanceof HashIndexEntry hashIndexEntry) {
|
||||||
HashIndexEntry hashIndexEntry = (HashIndexEntry)entry;
|
|
||||||
setText(hashIndexEntry.getDescription());
|
setText(hashIndexEntry.getDescription());
|
||||||
setContextMenu(getTreeTableView().getStyleClass().contains("bip47") ? null : new HashIndexEntryContextMenu(getTreeTableView(), hashIndexEntry));
|
setContextMenu(getTreeTableView().getStyleClass().contains("bip47") ? null : new HashIndexEntryContextMenu(getTreeTableView(), hashIndexEntry));
|
||||||
Tooltip tooltip = new Tooltip();
|
Tooltip tooltip = new Tooltip();
|
||||||
|
|
@ -212,13 +211,14 @@ public class EntryCell extends TreeTableCell<Entry, Entry> implements Confirmati
|
||||||
|
|
||||||
private static void increaseFee(TransactionEntry transactionEntry, boolean cancelTransaction) {
|
private static void increaseFee(TransactionEntry transactionEntry, boolean cancelTransaction) {
|
||||||
BlockTransaction blockTransaction = transactionEntry.getBlockTransaction();
|
BlockTransaction blockTransaction = transactionEntry.getBlockTransaction();
|
||||||
|
boolean silentPaymentTransaction = transactionEntry.getWallet().isSilentPaymentsTransaction(blockTransaction);
|
||||||
Map<BlockTransactionHashIndex, WalletNode> walletTxos = transactionEntry.getWallet().getWalletTxos();
|
Map<BlockTransactionHashIndex, WalletNode> walletTxos = transactionEntry.getWallet().getWalletTxos();
|
||||||
List<BlockTransactionHashIndex> utxos = transactionEntry.getChildren().stream()
|
List<BlockTransactionHashIndex> utxos = transactionEntry.getChildren().stream()
|
||||||
.filter(e -> e instanceof HashIndexEntry)
|
.filter(e -> e instanceof HashIndexEntry)
|
||||||
.map(e -> (HashIndexEntry)e)
|
.map(e -> (HashIndexEntry)e)
|
||||||
.filter(e -> e.getType().equals(HashIndexEntry.Type.INPUT) && e.isSpendable())
|
.filter(e -> e.getType().equals(HashIndexEntry.Type.INPUT) && e.isSpendable())
|
||||||
.map(e -> blockTransaction.getTransaction().getInputs().get((int)e.getHashIndex().getIndex()))
|
.map(e -> blockTransaction.getTransaction().getInputs().get((int)e.getHashIndex().getIndex()))
|
||||||
.filter(i -> Config.get().isMempoolFullRbf() || i.isReplaceByFeeEnabled())
|
.filter(i -> Config.get().isMempoolFullRbf() || i.isReplaceByFeeEnabled() || silentPaymentTransaction)
|
||||||
.map(txInput -> walletTxos.keySet().stream().filter(txo -> txo.getHash().equals(txInput.getOutpoint().getHash()) && txo.getIndex() == txInput.getOutpoint().getIndex()).findFirst().get())
|
.map(txInput -> walletTxos.keySet().stream().filter(txo -> txo.getHash().equals(txInput.getOutpoint().getHash()) && txo.getIndex() == txInput.getOutpoint().getIndex()).findFirst().get())
|
||||||
.collect(Collectors.toList());
|
.collect(Collectors.toList());
|
||||||
|
|
||||||
|
|
@ -243,6 +243,7 @@ public class EntryCell extends TreeTableCell<Entry, Entry> implements Confirmati
|
||||||
.collect(Collectors.toList());
|
.collect(Collectors.toList());
|
||||||
|
|
||||||
boolean consolidationTransaction = consolidationOutputs.size() == blockTransaction.getTransaction().getOutputs().size() && consolidationOutputs.size() == 1;
|
boolean consolidationTransaction = consolidationOutputs.size() == blockTransaction.getTransaction().getOutputs().size() && consolidationOutputs.size() == 1;
|
||||||
|
boolean safeToAddInputsOrOutputs = transactionEntry.getWallet().isSafeToAddInputsOrOutputs(blockTransaction);
|
||||||
long changeTotal = ourOutputs.stream().mapToLong(TransactionOutput::getValue).sum() - consolidationOutputs.stream().mapToLong(TransactionOutput::getValue).sum();
|
long changeTotal = ourOutputs.stream().mapToLong(TransactionOutput::getValue).sum() - consolidationOutputs.stream().mapToLong(TransactionOutput::getValue).sum();
|
||||||
Transaction tx = blockTransaction.getTransaction();
|
Transaction tx = blockTransaction.getTransaction();
|
||||||
double vSize = tx.getVirtualSize();
|
double vSize = tx.getVirtualSize();
|
||||||
|
|
@ -257,7 +258,7 @@ public class EntryCell extends TreeTableCell<Entry, Entry> implements Confirmati
|
||||||
List<OutputGroup> outputGroups = transactionEntry.getWallet().getGroupedUtxos(txoFilters, feeRate, AppServices.getMinimumRelayFeeRate(), Config.get().isGroupByAddress())
|
List<OutputGroup> outputGroups = transactionEntry.getWallet().getGroupedUtxos(txoFilters, feeRate, AppServices.getMinimumRelayFeeRate(), Config.get().isGroupByAddress())
|
||||||
.stream().filter(outputGroup -> outputGroup.getEffectiveValue() >= 0).collect(Collectors.toList());
|
.stream().filter(outputGroup -> outputGroup.getEffectiveValue() >= 0).collect(Collectors.toList());
|
||||||
Collections.shuffle(outputGroups);
|
Collections.shuffle(outputGroups);
|
||||||
while((double)changeTotal / vSize < getMaxFeeRate() && !outputGroups.isEmpty() && !cancelTransaction && !consolidationTransaction) {
|
while((double)changeTotal / vSize < getMaxFeeRate() && !outputGroups.isEmpty() && !cancelTransaction && !consolidationTransaction && safeToAddInputsOrOutputs) {
|
||||||
//If there is insufficient change output, include another random output group so the fee can be increased
|
//If there is insufficient change output, include another random output group so the fee can be increased
|
||||||
OutputGroup outputGroup = outputGroups.remove(0);
|
OutputGroup outputGroup = outputGroups.remove(0);
|
||||||
for(BlockTransactionHashIndex utxo : outputGroup.getUtxos()) {
|
for(BlockTransactionHashIndex utxo : outputGroup.getUtxos()) {
|
||||||
|
|
@ -298,9 +299,13 @@ public class EntryCell extends TreeTableCell<Entry, Entry> implements Confirmati
|
||||||
label += " (Replaced By Fee)";
|
label += " (Replaced By Fee)";
|
||||||
}
|
}
|
||||||
|
|
||||||
if(txOutput.getScript().getToAddress() != null) {
|
Address address = txOutput.getScript().getToAddress();
|
||||||
|
if(address != null) {
|
||||||
|
long value = txOutput.getValue();
|
||||||
//Disable change creation by enabling max payment when there is only one output and no additional UTXOs included
|
//Disable change creation by enabling max payment when there is only one output and no additional UTXOs included
|
||||||
return new Payment(txOutput.getScript().getToAddress(), label, txOutput.getValue(), blockTransaction.getTransaction().getOutputs().size() == 1 && rbfChange == 0);
|
boolean sendMax = blockTransaction.getTransaction().getOutputs().size() == 1 && rbfChange == 0;
|
||||||
|
SilentPaymentAddress silentPaymentAddress = transactionEntry.getWallet().getSilentPaymentAddress(address);
|
||||||
|
return silentPaymentAddress == null ? new Payment(address, label, value, sendMax) : new SilentPayment(silentPaymentAddress, label, value, sendMax);
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
|
|
@ -337,7 +342,7 @@ public class EntryCell extends TreeTableCell<Entry, Entry> implements Confirmati
|
||||||
}
|
}
|
||||||
|
|
||||||
EventManager.get().post(new SendActionEvent(transactionEntry.getWallet(), utxos));
|
EventManager.get().post(new SendActionEvent(transactionEntry.getWallet(), utxos));
|
||||||
Platform.runLater(() -> EventManager.get().post(new SpendUtxoEvent(transactionEntry.getWallet(), utxos, payments, opReturns.isEmpty() ? null : opReturns, rbfFee, true, blockTransaction)));
|
Platform.runLater(() -> EventManager.get().post(new SpendUtxoEvent(transactionEntry.getWallet(), utxos, payments, opReturns.isEmpty() ? null : opReturns, rbfFee, true, blockTransaction, safeToAddInputsOrOutputs)));
|
||||||
}
|
}
|
||||||
|
|
||||||
private static Double getMaxFeeRate() {
|
private static Double getMaxFeeRate() {
|
||||||
|
|
@ -394,11 +399,11 @@ public class EntryCell extends TreeTableCell<Entry, Entry> implements Confirmati
|
||||||
Payment payment = new Payment(freshAddress, label, inputTotal, true);
|
Payment payment = new Payment(freshAddress, label, inputTotal, true);
|
||||||
|
|
||||||
EventManager.get().post(new SendActionEvent(transactionEntry.getWallet(), utxos));
|
EventManager.get().post(new SendActionEvent(transactionEntry.getWallet(), utxos));
|
||||||
Platform.runLater(() -> EventManager.get().post(new SpendUtxoEvent(transactionEntry.getWallet(), utxos, List.of(payment), null, blockTransaction.getFee(), true, null)));
|
Platform.runLater(() -> EventManager.get().post(new SpendUtxoEvent(transactionEntry.getWallet(), utxos, List.of(payment), null, blockTransaction.getFee(), true, null, true)));
|
||||||
}
|
}
|
||||||
|
|
||||||
private static boolean canRBF(BlockTransaction blockTransaction) {
|
private static boolean canRBF(BlockTransaction blockTransaction, Wallet wallet) {
|
||||||
return Config.get().isMempoolFullRbf() || blockTransaction.getTransaction().isReplaceByFee();
|
return Config.get().isMempoolFullRbf() || blockTransaction.getTransaction().isReplaceByFee() || wallet.isSilentPaymentsTransaction(blockTransaction);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static boolean canSignMessage(WalletNode walletNode) {
|
private static boolean canSignMessage(WalletNode walletNode) {
|
||||||
|
|
@ -476,7 +481,7 @@ public class EntryCell extends TreeTableCell<Entry, Entry> implements Confirmati
|
||||||
tooltip += "\nFee rate: " + String.format("%.2f", feeRate) + " sats/vB";
|
tooltip += "\nFee rate: " + String.format("%.2f", feeRate) + " sats/vB";
|
||||||
}
|
}
|
||||||
|
|
||||||
tooltip += "\nRBF: " + (canRBF(transactionEntry.getBlockTransaction()) ? "Enabled" : "Disabled");
|
tooltip += "\nRBF: " + (canRBF(transactionEntry.getBlockTransaction(), transactionEntry.getWallet()) ? "Enabled" : "Disabled");
|
||||||
}
|
}
|
||||||
|
|
||||||
return tooltip;
|
return tooltip;
|
||||||
|
|
@ -544,6 +549,7 @@ public class EntryCell extends TreeTableCell<Entry, Entry> implements Confirmati
|
||||||
|
|
||||||
private static class UnconfirmedTransactionContextMenu extends ContextMenu {
|
private static class UnconfirmedTransactionContextMenu extends ContextMenu {
|
||||||
public UnconfirmedTransactionContextMenu(TransactionEntry transactionEntry) {
|
public UnconfirmedTransactionContextMenu(TransactionEntry transactionEntry) {
|
||||||
|
Wallet wallet = transactionEntry.getWallet();
|
||||||
BlockTransaction blockTransaction = transactionEntry.getBlockTransaction();
|
BlockTransaction blockTransaction = transactionEntry.getBlockTransaction();
|
||||||
MenuItem viewTransaction = new MenuItem("View Transaction");
|
MenuItem viewTransaction = new MenuItem("View Transaction");
|
||||||
viewTransaction.setGraphic(getViewTransactionGlyph());
|
viewTransaction.setGraphic(getViewTransactionGlyph());
|
||||||
|
|
@ -553,7 +559,7 @@ public class EntryCell extends TreeTableCell<Entry, Entry> implements Confirmati
|
||||||
});
|
});
|
||||||
getItems().add(viewTransaction);
|
getItems().add(viewTransaction);
|
||||||
|
|
||||||
if(canRBF(blockTransaction) && Config.get().isIncludeMempoolOutputs() && transactionEntry.getWallet().allInputsFromWallet(blockTransaction.getHash())) {
|
if(canRBF(blockTransaction, wallet) && Config.get().isIncludeMempoolOutputs() && transactionEntry.getWallet().allInputsFromWallet(blockTransaction.getHash())) {
|
||||||
MenuItem increaseFee = new MenuItem("Increase Fee (RBF)");
|
MenuItem increaseFee = new MenuItem("Increase Fee (RBF)");
|
||||||
increaseFee.setGraphic(getIncreaseFeeRBFGlyph());
|
increaseFee.setGraphic(getIncreaseFeeRBFGlyph());
|
||||||
increaseFee.setOnAction(AE -> {
|
increaseFee.setOnAction(AE -> {
|
||||||
|
|
@ -564,7 +570,7 @@ public class EntryCell extends TreeTableCell<Entry, Entry> implements Confirmati
|
||||||
getItems().add(increaseFee);
|
getItems().add(increaseFee);
|
||||||
}
|
}
|
||||||
|
|
||||||
if(canRBF(blockTransaction) && Config.get().isIncludeMempoolOutputs() && transactionEntry.getWallet().allInputsFromWallet(blockTransaction.getHash())) {
|
if(canRBF(blockTransaction, wallet) && Config.get().isIncludeMempoolOutputs() && transactionEntry.getWallet().allInputsFromWallet(blockTransaction.getHash())) {
|
||||||
MenuItem cancelTx = new MenuItem("Cancel Transaction (RBF)");
|
MenuItem cancelTx = new MenuItem("Cancel Transaction (RBF)");
|
||||||
cancelTx.setGraphic(getCancelTransactionRBFGlyph());
|
cancelTx.setGraphic(getCancelTransactionRBFGlyph());
|
||||||
cancelTx.setOnAction(AE -> {
|
cancelTx.setOnAction(AE -> {
|
||||||
|
|
@ -850,4 +856,11 @@ public class EntryCell extends TreeTableCell<Entry, Entry> implements Confirmati
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private boolean isTableSizeRecalculation() {
|
||||||
|
//As per https://bugs.openjdk.org/browse/JDK-8265669 we check for cell visibility to avoid unnecessary recalculation, but this can result in false positives
|
||||||
|
//The method releaseCell in VirtualFlow is responsible for setting accumCell visibility to false after use, so check this method is calling updateItem
|
||||||
|
return StackWalker.getInstance().walk(frames -> frames.anyMatch(frame -> frame.getClassName().equals("javafx.scene.control.skin.VirtualFlow")
|
||||||
|
&& frame.getMethodName().equals("releaseCell")));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
package com.sparrowwallet.sparrow.control;
|
package com.sparrowwallet.sparrow.control;
|
||||||
|
|
||||||
|
import com.sparrowwallet.drongo.protocol.Transaction;
|
||||||
import com.sparrowwallet.sparrow.AppServices;
|
import com.sparrowwallet.sparrow.AppServices;
|
||||||
import com.sparrowwallet.sparrow.net.FeeRatesSource;
|
import com.sparrowwallet.sparrow.net.FeeRatesSource;
|
||||||
import javafx.application.Platform;
|
import javafx.application.Platform;
|
||||||
|
|
@ -7,6 +8,7 @@ import javafx.scene.Node;
|
||||||
import javafx.scene.control.Slider;
|
import javafx.scene.control.Slider;
|
||||||
import javafx.util.StringConverter;
|
import javafx.util.StringConverter;
|
||||||
|
|
||||||
|
import java.text.DecimalFormat;
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
|
@ -14,9 +16,11 @@ import static com.sparrowwallet.sparrow.AppServices.*;
|
||||||
|
|
||||||
public class FeeRangeSlider extends Slider {
|
public class FeeRangeSlider extends Slider {
|
||||||
private static final double FEE_RATE_SCROLL_INCREMENT = 0.01;
|
private static final double FEE_RATE_SCROLL_INCREMENT = 0.01;
|
||||||
|
private static final DecimalFormat INTEGER_FEE_RATE_FORMAT = new DecimalFormat("0");
|
||||||
|
private static final DecimalFormat FRACTIONAL_FEE_RATE_FORMAT = new DecimalFormat("0.###");
|
||||||
|
|
||||||
public FeeRangeSlider() {
|
public FeeRangeSlider() {
|
||||||
super(0, FEE_RATES_RANGE.size() - 1, 0);
|
super(0, AppServices.getFeeRatesRange().size() - 1, 0);
|
||||||
setMajorTickUnit(1);
|
setMajorTickUnit(1);
|
||||||
setMinorTickCount(0);
|
setMinorTickCount(0);
|
||||||
setSnapToTicks(false);
|
setSnapToTicks(false);
|
||||||
|
|
@ -27,11 +31,11 @@ public class FeeRangeSlider extends Slider {
|
||||||
setLabelFormatter(new StringConverter<>() {
|
setLabelFormatter(new StringConverter<>() {
|
||||||
@Override
|
@Override
|
||||||
public String toString(Double object) {
|
public String toString(Double object) {
|
||||||
Long feeRate = LONG_FEE_RATES_RANGE.get(object.intValue());
|
Double feeRate = AppServices.getLongFeeRatesRange().get(object.intValue());
|
||||||
if(isLongFeeRange() && feeRate >= 1000) {
|
if(isLongFeeRange() && feeRate >= 1000) {
|
||||||
return feeRate / 1000 + "k";
|
return INTEGER_FEE_RATE_FORMAT.format(feeRate / 1000) + "k";
|
||||||
}
|
}
|
||||||
return Long.toString(feeRate);
|
return feeRate > 0d && feeRate < Transaction.DEFAULT_MIN_RELAY_FEE ? FRACTIONAL_FEE_RATE_FORMAT.format(feeRate) : INTEGER_FEE_RATE_FORMAT.format(feeRate);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
@ -51,10 +55,10 @@ public class FeeRangeSlider extends Slider {
|
||||||
setOnScroll(event -> {
|
setOnScroll(event -> {
|
||||||
if(event.getDeltaY() != 0) {
|
if(event.getDeltaY() != 0) {
|
||||||
double newFeeRate = getFeeRate() + (event.getDeltaY() > 0 ? FEE_RATE_SCROLL_INCREMENT : -FEE_RATE_SCROLL_INCREMENT);
|
double newFeeRate = getFeeRate() + (event.getDeltaY() > 0 ? FEE_RATE_SCROLL_INCREMENT : -FEE_RATE_SCROLL_INCREMENT);
|
||||||
if(newFeeRate < LONG_FEE_RATES_RANGE.get(0)) {
|
if(newFeeRate < AppServices.getLongFeeRatesRange().getFirst()) {
|
||||||
newFeeRate = LONG_FEE_RATES_RANGE.get(0);
|
newFeeRate = AppServices.getLongFeeRatesRange().getFirst();
|
||||||
} else if(newFeeRate > LONG_FEE_RATES_RANGE.get(LONG_FEE_RATES_RANGE.size() - 1)) {
|
} else if(newFeeRate > AppServices.getLongFeeRatesRange().getLast()) {
|
||||||
newFeeRate = LONG_FEE_RATES_RANGE.get(LONG_FEE_RATES_RANGE.size() - 1);
|
newFeeRate = AppServices.getLongFeeRatesRange().getLast();
|
||||||
}
|
}
|
||||||
setFeeRate(newFeeRate);
|
setFeeRate(newFeeRate);
|
||||||
}
|
}
|
||||||
|
|
@ -62,27 +66,79 @@ public class FeeRangeSlider extends Slider {
|
||||||
}
|
}
|
||||||
|
|
||||||
public double getFeeRate() {
|
public double getFeeRate() {
|
||||||
return Math.pow(2.0, getValue());
|
return getFeeRate(AppServices.getMinimumRelayFeeRate());
|
||||||
|
}
|
||||||
|
|
||||||
|
public double getFeeRate(Double minRelayFeeRate) {
|
||||||
|
if(minRelayFeeRate >= Transaction.DEFAULT_MIN_RELAY_FEE) {
|
||||||
|
return Math.pow(2.0, getValue());
|
||||||
|
}
|
||||||
|
|
||||||
|
if(getValue() < 1.0d) {
|
||||||
|
if(minRelayFeeRate == 0.0d) {
|
||||||
|
return getValue();
|
||||||
|
}
|
||||||
|
return Math.pow(minRelayFeeRate, 1.0d - getValue());
|
||||||
|
}
|
||||||
|
|
||||||
|
return Math.pow(2.0, getValue() - 1.0d);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setFeeRate(double feeRate) {
|
public void setFeeRate(double feeRate) {
|
||||||
double value = Math.log(feeRate) / Math.log(2);
|
setFeeRate(feeRate, AppServices.getMinimumRelayFeeRate());
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setFeeRate(double feeRate, Double minRelayFeeRate) {
|
||||||
|
double value = getValue(feeRate, minRelayFeeRate);
|
||||||
updateMaxFeeRange(value);
|
updateMaxFeeRange(value);
|
||||||
setValue(value);
|
setValue(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private double getValue(double feeRate, Double minRelayFeeRate) {
|
||||||
|
double value;
|
||||||
|
|
||||||
|
if(minRelayFeeRate >= Transaction.DEFAULT_MIN_RELAY_FEE) {
|
||||||
|
value = Math.log(feeRate) / Math.log(2);
|
||||||
|
} else {
|
||||||
|
if(feeRate < Transaction.DEFAULT_MIN_RELAY_FEE) {
|
||||||
|
if(minRelayFeeRate == 0.0d) {
|
||||||
|
return feeRate;
|
||||||
|
}
|
||||||
|
value = 1.0d - (Math.log(feeRate) / Math.log(minRelayFeeRate));
|
||||||
|
} else {
|
||||||
|
value = (Math.log(feeRate) / Math.log(2.0)) + 1.0d;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void updateFeeRange(Double minRelayFeeRate, Double previousMinRelayFeeRate) {
|
||||||
|
if(minRelayFeeRate != null && previousMinRelayFeeRate != null) {
|
||||||
|
setFeeRate(getFeeRate(previousMinRelayFeeRate), minRelayFeeRate);
|
||||||
|
}
|
||||||
|
setMinorTickCount(1);
|
||||||
|
setMinorTickCount(0);
|
||||||
|
}
|
||||||
|
|
||||||
private void updateMaxFeeRange(double value) {
|
private void updateMaxFeeRange(double value) {
|
||||||
if(value >= getMax() && !isLongFeeRange()) {
|
if(value >= getMax() && !isLongFeeRange()) {
|
||||||
setMax(LONG_FEE_RATES_RANGE.size() - 1);
|
if(AppServices.getMinimumRelayFeeRate() < Transaction.DEFAULT_MIN_RELAY_FEE) {
|
||||||
|
setMin(1.0d);
|
||||||
|
}
|
||||||
|
setMax(AppServices.getLongFeeRatesRange().size() - 1);
|
||||||
updateTrackHighlight();
|
updateTrackHighlight();
|
||||||
} else if(value == getMin() && isLongFeeRange()) {
|
} else if(value == getMin() && isLongFeeRange()) {
|
||||||
setMax(FEE_RATES_RANGE.size() - 1);
|
if(AppServices.getMinimumRelayFeeRate() < Transaction.DEFAULT_MIN_RELAY_FEE) {
|
||||||
|
setMin(0.0d);
|
||||||
|
}
|
||||||
|
setMax(AppServices.getFeeRatesRange().size() - 1);
|
||||||
updateTrackHighlight();
|
updateTrackHighlight();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean isLongFeeRange() {
|
public boolean isLongFeeRange() {
|
||||||
return getMax() > FEE_RATES_RANGE.size() - 1;
|
return getMax() > AppServices.getFeeRatesRange().size() - 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void updateTrackHighlight() {
|
public void updateTrackHighlight() {
|
||||||
|
|
@ -137,9 +193,9 @@ public class FeeRangeSlider extends Slider {
|
||||||
}
|
}
|
||||||
|
|
||||||
private int getPercentageOfFeeRange(Double feeRate) {
|
private int getPercentageOfFeeRange(Double feeRate) {
|
||||||
double index = Math.log(feeRate) / Math.log(2);
|
double index = getValue(feeRate, AppServices.getMinimumRelayFeeRate());
|
||||||
if(isLongFeeRange()) {
|
if(isLongFeeRange()) {
|
||||||
index *= ((double)FEE_RATES_RANGE.size() / (LONG_FEE_RATES_RANGE.size())) * 0.99;
|
index *= ((double)AppServices.getFeeRatesRange().size() / (AppServices.getLongFeeRatesRange().size())) * 0.99;
|
||||||
}
|
}
|
||||||
return (int)Math.round(index * 10.0);
|
return (int)Math.round(index * 10.0);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ 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;
|
||||||
|
|
@ -44,8 +45,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, String imageUrl, boolean scannable, boolean fileFormatAvailable) {
|
public FileImportPane(FileImport importer, String title, String description, String content, WalletModel walletModel, boolean scannable, boolean fileFormatAvailable) {
|
||||||
super(title, description, content, imageUrl);
|
super(title, description, content, walletModel);
|
||||||
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(), "image/" + exporter.getWalletModel().getType() + ".png");
|
super(exporter.getName(), "Keystore export", exporter.getKeystoreExportDescription(), exporter.getWalletModel());
|
||||||
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)), "image/" + importer.getWalletModel().getType() + ".png", importer.isKeystoreImportScannable(), importer.isFileFormatAvailable());
|
super(importer, importer.getName(), "Key import", importer.getKeystoreImportDescription(getAccount(wallet, requiredDerivation)), importer.getWalletModel(), 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(), "image/" + exporter.getWalletModel().getType() + ".png");
|
super(exporter.getName(), "Wallet export", exporter.getWalletExportDescription(), exporter.getWalletModel());
|
||||||
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) {
|
} else if(exporter instanceof Bip129 || exporter instanceof WalletLabels) {
|
||||||
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(), "image/" + importer.getWalletModel().getType() + ".png", importer.isWalletImportScannable(), importer.isWalletImportFileFormatAvailable());
|
super(importer, importer.getName(), "Wallet import", importer.getWalletImportDescription(), importer.getWalletModel(), 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(), "image/" + importer.getWalletModel().getType() + ".png", importer.isKeystoreImportScannable(), importer.isFileFormatAvailable());
|
super(importer, importer.getName(), "Wallet import", importer.getKeystoreImportDescription(), importer.getWalletModel(), importer.isKeystoreImportScannable(), importer.isFileFormatAvailable());
|
||||||
this.importer = importer;
|
this.importer = importer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -38,12 +38,23 @@ 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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -121,7 +132,7 @@ class LabelCell extends TextFieldTreeTableCell<Entry, String> implements Confirm
|
||||||
return confirmationsProperty;
|
return confirmationsProperty;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static class LabelContextMenu extends ContextMenu {
|
private 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 -> {
|
||||||
|
|
@ -141,6 +152,13 @@ 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,14 +118,7 @@ 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);
|
||||||
|
|
@ -247,6 +240,9 @@ public class MessageSignDialog extends Dialog<ButtonBar.ButtonData> {
|
||||||
setFormatFromScriptType(address.getScriptType());
|
setFormatFromScriptType(address.getScriptType());
|
||||||
if(wallet != null) {
|
if(wallet != null) {
|
||||||
setWalletNodeFromAddress(wallet, address);
|
setWalletNodeFromAddress(wallet, address);
|
||||||
|
if(walletNode != null) {
|
||||||
|
setFormatFromScriptType(getSigningScriptType(walletNode));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch(InvalidAddressException e) {
|
} catch(InvalidAddressException e) {
|
||||||
//can't happen
|
//can't happen
|
||||||
|
|
@ -280,7 +276,7 @@ public class MessageSignDialog extends Dialog<ButtonBar.ButtonData> {
|
||||||
}
|
}
|
||||||
|
|
||||||
if(wallet != null && walletNode != null) {
|
if(wallet != null && walletNode != null) {
|
||||||
setFormatFromScriptType(wallet.getScriptType());
|
setFormatFromScriptType(getSigningScriptType(walletNode));
|
||||||
} else {
|
} else {
|
||||||
formatGroup.selectToggle(formatElectrum);
|
formatGroup.selectToggle(formatElectrum);
|
||||||
}
|
}
|
||||||
|
|
@ -294,9 +290,13 @@ public class MessageSignDialog extends Dialog<ButtonBar.ButtonData> {
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean canSign(Wallet wallet) {
|
private boolean canSign(Wallet wallet) {
|
||||||
return wallet.getKeystores().get(0).hasPrivateKey()
|
return wallet.getKeystores().getFirst().hasPrivateKey()
|
||||||
|| wallet.getKeystores().get(0).getSource() == KeystoreSource.HW_USB
|
|| wallet.getKeystores().getFirst().getSource() == KeystoreSource.HW_USB
|
||||||
|| wallet.getKeystores().get(0).getWalletModel().isCard();
|
|| wallet.getKeystores().getFirst().getWalletModel().isCard();
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean canSignBip322(Wallet wallet) {
|
||||||
|
return wallet.getKeystores().getFirst().hasPrivateKey();
|
||||||
}
|
}
|
||||||
|
|
||||||
private Address getAddress()throws InvalidAddressException {
|
private Address getAddress()throws InvalidAddressException {
|
||||||
|
|
@ -320,6 +320,11 @@ public class MessageSignDialog extends Dialog<ButtonBar.ButtonData> {
|
||||||
walletNode = wallet.getWalletAddresses().get(address);
|
walletNode = wallet.getWalletAddresses().get(address);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private ScriptType getSigningScriptType(WalletNode walletNode) {
|
||||||
|
ScriptType scriptType = walletNode.getWallet().getScriptType();
|
||||||
|
return canSign(walletNode.getWallet()) && !canSignBip322(walletNode.getWallet()) ? ScriptType.P2PKH : scriptType;
|
||||||
|
}
|
||||||
|
|
||||||
private void setFormatFromScriptType(ScriptType scriptType) {
|
private void setFormatFromScriptType(ScriptType scriptType) {
|
||||||
formatElectrum.setDisable(scriptType == ScriptType.P2TR);
|
formatElectrum.setDisable(scriptType == ScriptType.P2TR);
|
||||||
formatTrezor.setDisable(scriptType == ScriptType.P2TR || scriptType == ScriptType.P2PKH);
|
formatTrezor.setDisable(scriptType == ScriptType.P2TR || scriptType == ScriptType.P2PKH);
|
||||||
|
|
@ -352,7 +357,7 @@ public class MessageSignDialog extends Dialog<ButtonBar.ButtonData> {
|
||||||
|
|
||||||
//Note we can expect a single keystore due to the check in the constructor
|
//Note we can expect a single keystore due to the check in the constructor
|
||||||
Wallet signingWallet = walletNode.getWallet();
|
Wallet signingWallet = walletNode.getWallet();
|
||||||
if(signingWallet.getKeystores().get(0).hasPrivateKey()) {
|
if(signingWallet.getKeystores().getFirst().hasPrivateKey()) {
|
||||||
if(signingWallet.isEncrypted()) {
|
if(signingWallet.isEncrypted()) {
|
||||||
EventManager.get().post(new RequestOpenWalletsEvent());
|
EventManager.get().post(new RequestOpenWalletsEvent());
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -365,7 +370,7 @@ public class MessageSignDialog extends Dialog<ButtonBar.ButtonData> {
|
||||||
|
|
||||||
private void signUnencryptedKeystore(Wallet decryptedWallet) {
|
private void signUnencryptedKeystore(Wallet decryptedWallet) {
|
||||||
try {
|
try {
|
||||||
Keystore keystore = decryptedWallet.getKeystores().get(0);
|
Keystore keystore = decryptedWallet.getKeystores().getFirst();
|
||||||
ECKey privKey = keystore.getKey(walletNode);
|
ECKey privKey = keystore.getKey(walletNode);
|
||||||
String signatureText;
|
String signatureText;
|
||||||
if(isBip322()) {
|
if(isBip322()) {
|
||||||
|
|
@ -385,8 +390,8 @@ public class MessageSignDialog extends Dialog<ButtonBar.ButtonData> {
|
||||||
}
|
}
|
||||||
|
|
||||||
private void signDeviceKeystore(Wallet deviceWallet) {
|
private void signDeviceKeystore(Wallet deviceWallet) {
|
||||||
List<String> fingerprints = List.of(deviceWallet.getKeystores().get(0).getKeyDerivation().getMasterFingerprint());
|
List<String> fingerprints = List.of(deviceWallet.getKeystores().getFirst().getKeyDerivation().getMasterFingerprint());
|
||||||
KeyDerivation fullDerivation = deviceWallet.getKeystores().get(0).getKeyDerivation().extend(walletNode.getDerivation());
|
KeyDerivation fullDerivation = deviceWallet.getKeystores().getFirst().getKeyDerivation().extend(walletNode.getDerivation());
|
||||||
DeviceSignMessageDialog deviceSignMessageDialog = new DeviceSignMessageDialog(fingerprints, deviceWallet, message.getText().trim(), fullDerivation);
|
DeviceSignMessageDialog deviceSignMessageDialog = new DeviceSignMessageDialog(fingerprints, deviceWallet, message.getText().trim(), fullDerivation);
|
||||||
deviceSignMessageDialog.initOwner(getDialogPane().getScene().getWindow());
|
deviceSignMessageDialog.initOwner(getDialogPane().getScene().getWindow());
|
||||||
Optional<String> optSignature = deviceSignMessageDialog.showAndWait();
|
Optional<String> optSignature = deviceSignMessageDialog.showAndWait();
|
||||||
|
|
|
||||||
|
|
@ -50,8 +50,7 @@ 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!");
|
||||||
javafx.scene.image.Image image = new Image("/image/border-wallets.png");
|
dialogPane.setGraphic(new DialogImage(DialogImage.Type.BORDERWALLETS));
|
||||||
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", "", "image/" + WalletModel.SEED.getType() + ".png");
|
super(keystore.getSeed().getType().getName(), keystore.getSeed().needsPassphrase() && (keystore.getSeed().getPassphrase() == null || keystore.getSeed().getPassphrase().length() > 0) ? "Passphrase entered" : "No passphrase", "", WalletModel.SEED);
|
||||||
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", "", "image/" + WalletModel.SEED.getType() + ".png");
|
super(name, "Enter seed words", "", WalletModel.SEED);
|
||||||
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(), "image/" + importer.getWalletModel().getType() + ".png");
|
super(importer.getName(), "Create or enter seed", importer.getKeystoreImportDescription(), importer.getWalletModel());
|
||||||
this.wallet = wallet;
|
this.wallet = wallet;
|
||||||
this.importer = importer;
|
this.importer = importer;
|
||||||
this.defaultDerivation = defaultDerivation;
|
this.defaultDerivation = defaultDerivation;
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ 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;
|
||||||
|
|
@ -51,8 +52,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, String imageUrl) {
|
public MnemonicKeystorePane(String title, String description, String content, WalletModel walletModel) {
|
||||||
super(title, description, content, imageUrl);
|
super(title, description, content, walletModel);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
@ -320,6 +321,7 @@ 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(), "image/" + importer.getWalletModel().getType() + ".png");
|
super(importer.getName(), "Enter seed share", importer.getKeystoreImportDescription(), importer.getWalletModel());
|
||||||
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(), "image/" + importer.getWalletModel().getType() + ".png");
|
super(importer.getName(), "Seed import", importer.getKeystoreImportDescription(), importer.getWalletModel());
|
||||||
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(true);
|
discoverButton.setDefaultButton(AppServices.onlineProperty().get());
|
||||||
discoverButton.managedProperty().bind(discoverButton.visibleProperty());
|
discoverButton.managedProperty().bind(discoverButton.visibleProperty());
|
||||||
discoverButton.setOnAction(event -> {
|
discoverButton.setOnAction(event -> {
|
||||||
discoverWallet();
|
discoverWallet();
|
||||||
|
|
@ -66,6 +66,7 @@ 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 -> {
|
||||||
|
|
@ -196,6 +197,7 @@ 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,6 +14,7 @@ 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;
|
||||||
|
|
@ -61,6 +62,7 @@ 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;
|
||||||
|
|
@ -72,14 +74,7 @@ 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();
|
||||||
|
|
@ -136,6 +131,12 @@ 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();
|
||||||
|
|
@ -355,6 +356,8 @@ 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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -369,7 +372,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...", "/image/sparrow.png", addressUtxosService);
|
ServiceProgressDialog serviceProgressDialog = new ServiceProgressDialog("Address Scan", "Scanning address for transactions...", new DialogImage(DialogImage.Type.SPARROW), addressUtxosService);
|
||||||
serviceProgressDialog.initOwner(getDialogPane().getScene().getWindow());
|
serviceProgressDialog.initOwner(getDialogPane().getScene().getWindow());
|
||||||
AppServices.moveToActiveWindowScreen(serviceProgressDialog);
|
AppServices.moveToActiveWindowScreen(serviceProgressDialog);
|
||||||
}
|
}
|
||||||
|
|
@ -395,14 +398,14 @@ public class PrivateKeySweepDialog extends Dialog<Transaction> {
|
||||||
|
|
||||||
double feeRate = feeRange.getFeeRate();
|
double feeRate = feeRange.getFeeRate();
|
||||||
long fee = (long)Math.ceil(noFeeTransaction.getVirtualSize() * feeRate);
|
long fee = (long)Math.ceil(noFeeTransaction.getVirtualSize() * feeRate);
|
||||||
if(feeRate == Transaction.DEFAULT_MIN_RELAY_FEE) {
|
if(feeRate == AppServices.getMinimumRelayFeeRate() && feeRate > 0d) {
|
||||||
fee++;
|
fee++;
|
||||||
}
|
}
|
||||||
|
|
||||||
long dustThreshold = destAddress.getScriptType().getDustThreshold(sweepOutput, Transaction.DUST_RELAY_TX_FEE);
|
long dustThreshold = destAddress.getScriptType().getDustThreshold(sweepOutput, Transaction.DUST_RELAY_TX_FEE);
|
||||||
if(total - fee <= dustThreshold) {
|
if(total - fee <= dustThreshold) {
|
||||||
feeRate = Transaction.DEFAULT_MIN_RELAY_FEE;
|
feeRate = AppServices.getMinimumRelayFeeRate();
|
||||||
fee = (long)Math.ceil(noFeeTransaction.getVirtualSize() * feeRate) + 1;
|
fee = (long)Math.ceil(noFeeTransaction.getVirtualSize() * feeRate) + (feeRate > 0d ? 1 : 0);
|
||||||
|
|
||||||
if(total - fee <= dustThreshold) {
|
if(total - fee <= dustThreshold) {
|
||||||
AppServices.showErrorDialog("Insufficient funds", "The unspent outputs for this private key contain insufficient funds to spend (" + total + " sats).");
|
AppServices.showErrorDialog("Insufficient funds", "The unspent outputs for this private key contain insufficient funds to spend (" + total + " sats).");
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
package com.sparrowwallet.sparrow.control;
|
package com.sparrowwallet.sparrow.control;
|
||||||
|
|
||||||
import com.github.sarxos.webcam.*;
|
import com.google.common.base.Throwables;
|
||||||
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,7 +27,6 @@ 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;
|
||||||
|
|
@ -39,14 +38,16 @@ 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;
|
||||||
|
|
||||||
|
|
@ -76,108 +77,141 @@ 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<WebcamResolution> webcamResolutionProperty = new SimpleObjectProperty<>(WebcamResolution.VGA);
|
private final ObjectProperty<CaptureDevice> webcamDeviceProperty = new SimpleObjectProperty<>();
|
||||||
|
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 ObjectProperty<WebcamDevice> webcamDeviceProperty = new SimpleObjectProperty<>();
|
private final ObservableList<CaptureDevice> foundDevices = FXCollections.observableList(new ArrayList<>());
|
||||||
|
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().isHdCapture()) {
|
if(Config.get().getWebcamResolution() != null) {
|
||||||
webcamResolutionProperty.set(WebcamResolution.HD);
|
webcamResolutionProperty.set(Config.get().getWebcamResolution());
|
||||||
}
|
}
|
||||||
|
|
||||||
this.webcamService = new WebcamService(webcamResolutionProperty.get(), null, new QRScanListener(), new ScanDelayCalculator());
|
this.webcamService = new WebcamService(webcamResolutionProperty.get(), null);
|
||||||
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());
|
||||||
|
|
||||||
StackPane stackPane = new StackPane();
|
WebcamView webcamView = new WebcamView(webcamService, Config.get().isMirrorCapture());
|
||||||
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 = failedEvent.getSource().getException();
|
Throwable exception = Throwables.getRootCause(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) {
|
||||||
setHeight(newResolution == WebcamResolution.HD ? (getHeight() - 100) : (getHeight() + 100));
|
if(newResolution.isStandardAspect() && oldResolution.isWidescreenAspect()) {
|
||||||
EventManager.get().post(new WebcamResolutionChangedEvent(newResolution == WebcamResolution.HD));
|
setWidth(getWidth());
|
||||||
|
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((observable, oldValue, newValue) -> {
|
webcamDeviceProperty.addListener((_, _, newValue) -> {
|
||||||
Config.get().setWebcamDevice(newValue.getName());
|
Config.get().setWebcamDevice(newValue.getName());
|
||||||
|
Config.get().setWebcamDeviceId(newValue.getUniqueId());
|
||||||
if(!Objects.equals(webcamService.getDevice(), newValue)) {
|
if(!Objects.equals(webcamService.getDevice(), newValue)) {
|
||||||
webcamService.cancel();
|
webcamService.cancel();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
setOnCloseRequest(event -> {
|
setOnCloseRequest(_ -> {
|
||||||
boolean isHdCapture = (webcamResolutionProperty.get() == WebcamResolution.HD);
|
if(webcamResolutionProperty.get() != null) {
|
||||||
if(Config.get().isHdCapture() != isHdCapture) {
|
Config.get().setWebcamResolution(webcamResolutionProperty.get());
|
||||||
Config.get().setHdCapture(isHdCapture);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Platform.runLater(() -> webcamResolutionProperty.set(null));
|
Platform.runLater(() -> {
|
||||||
|
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 hdButtonType = new javafx.scene.control.ButtonType("Use HD Capture", ButtonBar.ButtonData.LEFT);
|
final ButtonType deviceButtonType = new javafx.scene.control.ButtonType("Default Camera", ButtonBar.ButtonData.LEFT);
|
||||||
final ButtonType camButtonType = new javafx.scene.control.ButtonType("Default Camera", ButtonBar.ButtonData.HELP_2);
|
final ButtonType resolutionButtonType = new javafx.scene.control.ButtonType("Resolution", ButtonBar.ButtonData.HELP_2);
|
||||||
dialogPane.getButtonTypes().addAll(hdButtonType, camButtonType, cancelButtonType);
|
dialogPane.getButtonTypes().addAll(deviceButtonType, resolutionButtonType, cancelButtonType);
|
||||||
dialogPane.setPrefWidth(646);
|
dialogPane.setPrefWidth(646);
|
||||||
dialogPane.setPrefHeight(webcamResolutionProperty.get() == WebcamResolution.HD ? 490 : 590);
|
dialogPane.setPrefHeight(webcamResolutionProperty.get().isWidescreenAspect() ? 490 : 590);
|
||||||
dialogPane.setMinHeight(dialogPane.getPrefHeight());
|
dialogPane.setMinHeight(dialogPane.getPrefHeight());
|
||||||
AppServices.moveToActiveWindowScreen(this);
|
AppServices.moveToActiveWindowScreen(this);
|
||||||
|
|
||||||
|
|
@ -685,72 +719,32 @@ 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 = null;
|
Node button;
|
||||||
if(buttonType.getButtonData() == ButtonBar.ButtonData.LEFT) {
|
if(buttonType.getButtonData() == ButtonBar.ButtonData.LEFT) {
|
||||||
ToggleButton hd = new ToggleButton(buttonType.getText());
|
ComboBox<CaptureDevice> devicesCombo = new ComboBox<>(foundDevices);
|
||||||
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(WebcamDevice device) {
|
public String toString(CaptureDevice device) {
|
||||||
return device instanceof WebcamScanDevice ? ((WebcamScanDevice)device).getDeviceName() : "Default Camera";
|
return device != null && device.getName() != null ? device.getName().replaceAll(" \\(.*\\)", "") : "Default Camera";
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public WebcamDevice fromString(String string) {
|
public CaptureDevice fromString(String string) {
|
||||||
throw new UnsupportedOperationException();
|
throw new UnsupportedOperationException();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
devicesCombo.valueProperty().bindBidirectional(webcamDeviceProperty);
|
devicesCombo.valueProperty().bindBidirectional(webcamDeviceProperty);
|
||||||
ButtonBar.setButtonData(devicesCombo, ButtonBar.ButtonData.LEFT);
|
final ButtonBar.ButtonData buttonData = buttonType.getButtonData();
|
||||||
|
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);
|
||||||
}
|
}
|
||||||
|
|
@ -763,19 +757,39 @@ public class QRScanDialog extends Dialog<QRScanDialog.Result> {
|
||||||
button.disableProperty().bind(webcamService.openingProperty());
|
button.disableProperty().bind(webcamService.openingProperty());
|
||||||
return button;
|
return button;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void setHdGraphic(ToggleButton hd, boolean isHd) {
|
public static <T extends Comparable<T>> void updateList(List<T> targetList, Collection<T> sourceList) {
|
||||||
if(isHd) {
|
List<T> sortedSource = new ArrayList<>(sourceList);
|
||||||
hd.setGraphic(getGlyph(FontAwesome5.Glyph.CHECK_CIRCLE));
|
Collections.sort(sortedSource);
|
||||||
|
|
||||||
|
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 {
|
||||||
hd.setGraphic(getGlyph(FontAwesome5.Glyph.BAN));
|
sourceIndex++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private Glyph getGlyph(FontAwesome5.Glyph glyphName) {
|
while (sourceIndex < sortedSource.size()) {
|
||||||
Glyph glyph = new Glyph(FontAwesome5.FONT_NAME, glyphName);
|
targetIter.add(sortedSource.get(sourceIndex));
|
||||||
glyph.setFontSize(11);
|
sourceIndex++;
|
||||||
return glyph;
|
}
|
||||||
|
|
||||||
|
while (targetIter.hasNext()) {
|
||||||
|
targetIter.next();
|
||||||
|
targetIter.remove();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -993,10 +1007,4 @@ 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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,178 @@
|
||||||
|
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,6 +2,7 @@ package com.sparrowwallet.sparrow.control;
|
||||||
|
|
||||||
import com.sparrowwallet.drongo.protocol.Script;
|
import com.sparrowwallet.drongo.protocol.Script;
|
||||||
import com.sparrowwallet.drongo.protocol.ScriptChunk;
|
import com.sparrowwallet.drongo.protocol.ScriptChunk;
|
||||||
|
import com.sparrowwallet.drongo.protocol.ScriptOpCodes;
|
||||||
import javafx.geometry.Pos;
|
import javafx.geometry.Pos;
|
||||||
import org.controlsfx.control.decoration.Decorator;
|
import org.controlsfx.control.decoration.Decorator;
|
||||||
import org.controlsfx.control.decoration.GraphicDecoration;
|
import org.controlsfx.control.decoration.GraphicDecoration;
|
||||||
|
|
@ -53,7 +54,11 @@ public class ScriptArea extends CodeArea {
|
||||||
for (int i = 0; i < script.getChunks().size(); i++) {
|
for (int i = 0; i < script.getChunks().size(); i++) {
|
||||||
ScriptChunk chunk = script.getChunks().get(i);
|
ScriptChunk chunk = script.getChunks().get(i);
|
||||||
if(chunk.isOpCode()) {
|
if(chunk.isOpCode()) {
|
||||||
append(chunk.toString(), "script-opcode");
|
if(chunk.getOpcode() == ScriptOpCodes.OP_0 && witnessScript != null) {
|
||||||
|
append("<empty>", "script-other");
|
||||||
|
} else {
|
||||||
|
append(chunk.toString(), "script-opcode");
|
||||||
|
}
|
||||||
} else if(chunk.isPubKey()) {
|
} else if(chunk.isPubKey()) {
|
||||||
append("<pubkey" + pubKeyCount++ + ">", "script-pubkey");
|
append("<pubkey" + pubKeyCount++ + ">", "script-pubkey");
|
||||||
} else if(chunk.isSignature()) {
|
} else if(chunk.isSignature()) {
|
||||||
|
|
|
||||||
|
|
@ -60,14 +60,7 @@ 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,36 +5,49 @@ import com.sparrowwallet.drongo.BitcoinUnit;
|
||||||
import com.sparrowwallet.drongo.OsType;
|
import com.sparrowwallet.drongo.OsType;
|
||||||
import com.sparrowwallet.drongo.address.Address;
|
import com.sparrowwallet.drongo.address.Address;
|
||||||
import com.sparrowwallet.drongo.address.InvalidAddressException;
|
import com.sparrowwallet.drongo.address.InvalidAddressException;
|
||||||
|
import com.sparrowwallet.drongo.dns.DnsPayment;
|
||||||
|
import com.sparrowwallet.drongo.dns.DnsPaymentCache;
|
||||||
|
import com.sparrowwallet.drongo.dns.DnsPaymentResolver;
|
||||||
|
import com.sparrowwallet.drongo.dns.DnsPaymentValidationException;
|
||||||
import com.sparrowwallet.drongo.protocol.Transaction;
|
import com.sparrowwallet.drongo.protocol.Transaction;
|
||||||
|
import com.sparrowwallet.drongo.silentpayments.SilentPayment;
|
||||||
|
import com.sparrowwallet.drongo.silentpayments.SilentPaymentAddress;
|
||||||
|
import com.sparrowwallet.drongo.uri.BitcoinURIParseException;
|
||||||
import com.sparrowwallet.drongo.wallet.Payment;
|
import com.sparrowwallet.drongo.wallet.Payment;
|
||||||
import com.sparrowwallet.sparrow.AppServices;
|
import com.sparrowwallet.sparrow.AppServices;
|
||||||
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
|
import com.sparrowwallet.sparrow.EventManager;
|
||||||
|
import com.sparrowwallet.sparrow.event.RequestConnectEvent;
|
||||||
|
import com.sparrowwallet.sparrow.glyphfont.GlyphUtils;
|
||||||
|
import com.sparrowwallet.sparrow.io.Config;
|
||||||
|
import javafx.application.Platform;
|
||||||
import javafx.collections.FXCollections;
|
import javafx.collections.FXCollections;
|
||||||
import javafx.collections.ObservableList;
|
import javafx.collections.ObservableList;
|
||||||
|
import javafx.concurrent.Service;
|
||||||
|
import javafx.concurrent.Task;
|
||||||
|
import javafx.event.ActionEvent;
|
||||||
import javafx.scene.Node;
|
import javafx.scene.Node;
|
||||||
import javafx.scene.control.*;
|
import javafx.scene.control.*;
|
||||||
import javafx.scene.image.Image;
|
|
||||||
import javafx.scene.image.ImageView;
|
|
||||||
import javafx.scene.input.Clipboard;
|
import javafx.scene.input.Clipboard;
|
||||||
import javafx.scene.layout.StackPane;
|
import javafx.scene.layout.StackPane;
|
||||||
import javafx.stage.FileChooser;
|
import javafx.stage.FileChooser;
|
||||||
import javafx.util.StringConverter;
|
import javafx.util.StringConverter;
|
||||||
import org.controlsfx.control.spreadsheet.*;
|
import org.controlsfx.control.spreadsheet.*;
|
||||||
import org.controlsfx.glyphfont.Glyph;
|
|
||||||
|
|
||||||
import java.io.*;
|
import java.io.*;
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.concurrent.ExecutionException;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
import java.util.stream.IntStream;
|
import java.util.stream.IntStream;
|
||||||
|
|
||||||
public class SendToManyDialog extends Dialog<List<Payment>> {
|
public class SendToManyDialog extends Dialog<List<Payment>> {
|
||||||
private final BitcoinUnit bitcoinUnit;
|
private final BitcoinUnit bitcoinUnit;
|
||||||
private final SpreadsheetView spreadsheetView;
|
private final SpreadsheetView spreadsheetView;
|
||||||
public static final AddressCellType ADDRESS = new AddressCellType();
|
public static final SendToAddressCellType SEND_TO_ADDRESS = new SendToAddressCellType();
|
||||||
|
|
||||||
public SendToManyDialog(BitcoinUnit bitcoinUnit) {
|
public SendToManyDialog(BitcoinUnit bitcoinUnit, List<Payment> payments) {
|
||||||
this.bitcoinUnit = bitcoinUnit;
|
this.bitcoinUnit = bitcoinUnit;
|
||||||
|
|
||||||
final DialogPane dialogPane = new SendToManyDialogPane();
|
final DialogPane dialogPane = new SendToManyDialogPane();
|
||||||
|
|
@ -42,10 +55,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.");
|
||||||
Image image = new Image("/image/sparrow-small.png");
|
dialogPane.setGraphic(new DialogImage(DialogImage.Type.SPARROW));
|
||||||
dialogPane.setGraphic(new ImageView(image));
|
|
||||||
|
|
||||||
List<Payment> initialPayments = IntStream.range(0, 100).mapToObj(i -> new Payment(null, null, -1, false)).collect(Collectors.toList());
|
List<Payment> initialPayments = IntStream.range(0, 100)
|
||||||
|
.mapToObj(i -> i < payments.size() ? payments.get(i) : new Payment(null, null, -1, false)).collect(Collectors.toList());
|
||||||
Grid grid = getGrid(initialPayments);
|
Grid grid = getGrid(initialPayments);
|
||||||
|
|
||||||
spreadsheetView = new SpreadsheetView(grid) {
|
spreadsheetView = new SpreadsheetView(grid) {
|
||||||
|
|
@ -70,14 +83,16 @@ public class SendToManyDialog extends Dialog<List<Payment>> {
|
||||||
dialogPane.setContent(stackPane);
|
dialogPane.setContent(stackPane);
|
||||||
|
|
||||||
dialogPane.getButtonTypes().addAll(ButtonType.OK, ButtonType.CANCEL);
|
dialogPane.getButtonTypes().addAll(ButtonType.OK, ButtonType.CANCEL);
|
||||||
|
Button okButton = (Button) dialogPane.lookupButton(ButtonType.OK);
|
||||||
|
okButton.addEventFilter(ActionEvent.ACTION, event -> {
|
||||||
|
getPayments();
|
||||||
|
event.consume();
|
||||||
|
});
|
||||||
|
|
||||||
final ButtonType loadCsvButtonType = new javafx.scene.control.ButtonType("Load CSV", ButtonBar.ButtonData.LEFT);
|
final ButtonType loadCsvButtonType = new javafx.scene.control.ButtonType("Load CSV", ButtonBar.ButtonData.LEFT);
|
||||||
dialogPane.getButtonTypes().add(loadCsvButtonType);
|
dialogPane.getButtonTypes().add(loadCsvButtonType);
|
||||||
|
|
||||||
setResultConverter((dialogButton) -> {
|
setResultConverter((_) -> null);
|
||||||
ButtonBar.ButtonData data = dialogButton == null ? null : dialogButton.getButtonData();
|
|
||||||
return data == ButtonBar.ButtonData.OK_DONE ? getPayments() : null;
|
|
||||||
});
|
|
||||||
|
|
||||||
dialogPane.setPrefWidth(850);
|
dialogPane.setPrefWidth(850);
|
||||||
dialogPane.setPrefHeight(500);
|
dialogPane.setPrefHeight(500);
|
||||||
|
|
@ -87,18 +102,24 @@ public class SendToManyDialog extends Dialog<List<Payment>> {
|
||||||
}
|
}
|
||||||
|
|
||||||
private Grid getGrid(List<Payment> payments) {
|
private Grid getGrid(List<Payment> payments) {
|
||||||
int rowCount = payments.size();
|
return createGrid(payments.stream().map(payment -> new SendToPayment(payment, SendToAddress.fromPayment(payment))).collect(Collectors.toList()));
|
||||||
|
}
|
||||||
|
|
||||||
|
private Grid createGrid(List<SendToPayment> sendToPayments) {
|
||||||
|
int rowCount = sendToPayments.size();
|
||||||
int columnCount = 3;
|
int columnCount = 3;
|
||||||
GridBase grid = new GridBase(rowCount, columnCount);
|
GridBase grid = new GridBase(rowCount, columnCount);
|
||||||
ObservableList<ObservableList<SpreadsheetCell>> rows = FXCollections.observableArrayList();
|
ObservableList<ObservableList<SpreadsheetCell>> rows = FXCollections.observableArrayList();
|
||||||
for(int row = 0; row < grid.getRowCount(); ++row) {
|
for(int row = 0; row < grid.getRowCount(); ++row) {
|
||||||
|
SendToPayment sendToPayment = sendToPayments.get(row);
|
||||||
final ObservableList<SpreadsheetCell> list = FXCollections.observableArrayList();
|
final ObservableList<SpreadsheetCell> list = FXCollections.observableArrayList();
|
||||||
|
|
||||||
SpreadsheetCell addressCell = ADDRESS.createCell(row, 0, 1, 1, payments.get(row).getAddress());
|
SendToAddress sendToAddress = sendToPayment.sendToAddress();
|
||||||
|
SpreadsheetCell addressCell = SEND_TO_ADDRESS.createCell(row, 0, 1, 1, sendToAddress);
|
||||||
addressCell.getStyleClass().add("fixed-width");
|
addressCell.getStyleClass().add("fixed-width");
|
||||||
list.add(addressCell);
|
list.add(addressCell);
|
||||||
|
|
||||||
double amount = (double)payments.get(row).getAmount();
|
double amount = (double)sendToPayment.payment().getAmount();
|
||||||
if(bitcoinUnit == BitcoinUnit.BTC) {
|
if(bitcoinUnit == BitcoinUnit.BTC) {
|
||||||
amount = amount / Transaction.SATOSHIS_PER_BITCOIN;
|
amount = amount / Transaction.SATOSHIS_PER_BITCOIN;
|
||||||
}
|
}
|
||||||
|
|
@ -110,7 +131,7 @@ public class SendToManyDialog extends Dialog<List<Payment>> {
|
||||||
}
|
}
|
||||||
list.add(amountCell);
|
list.add(amountCell);
|
||||||
|
|
||||||
list.add(SpreadsheetCellType.STRING.createCell(row, 2, 1, 1, payments.get(row).getLabel()));
|
list.add(SpreadsheetCellType.STRING.createCell(row, 2, 1, 1, sendToPayment.payment().getLabel()));
|
||||||
rows.add(list);
|
rows.add(list);
|
||||||
}
|
}
|
||||||
grid.setRows(rows);
|
grid.setRows(rows);
|
||||||
|
|
@ -119,32 +140,49 @@ public class SendToManyDialog extends Dialog<List<Payment>> {
|
||||||
return grid;
|
return grid;
|
||||||
}
|
}
|
||||||
|
|
||||||
private List<Payment> getPayments() {
|
private void getPayments() {
|
||||||
List<Payment> payments = new ArrayList<>();
|
if(needsResolution() && Config.get().hasServer() && !AppServices.isConnected() && !AppServices.isConnecting()) {
|
||||||
Grid grid = spreadsheetView.getGrid();
|
if(Config.get().getConnectToResolve() == null || Config.get().getConnectToResolve() == Boolean.FALSE) {
|
||||||
String firstLabel = null;
|
Platform.runLater(() -> {
|
||||||
for(int row = 0; row < grid.getRowCount(); row++) {
|
ConfirmationAlert confirmationAlert = new ConfirmationAlert("Connect to resolve?", "You are currently offline. Connect to resolve the addresses?", ButtonType.NO, ButtonType.YES);
|
||||||
|
Optional<ButtonType> optType = confirmationAlert.showAndWait();
|
||||||
|
if(confirmationAlert.isDontAskAgain() && optType.isPresent()) {
|
||||||
|
Config.get().setConnectToResolve(optType.get() == ButtonType.YES);
|
||||||
|
}
|
||||||
|
if(optType.isPresent() && optType.get() == ButtonType.YES) {
|
||||||
|
EventManager.get().post(new RequestConnectEvent());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
Platform.runLater(() -> EventManager.get().post(new RequestConnectEvent()));
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
CreatePaymentsService createPaymentsService = new CreatePaymentsService();
|
||||||
|
createPaymentsService.setOnSucceeded(_ -> {
|
||||||
|
List<Payment> payments = createPaymentsService.getValue();
|
||||||
|
if(payments != null) {
|
||||||
|
setResult(payments);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
createPaymentsService.setOnFailed(event -> {
|
||||||
|
Throwable ex = event.getSource().getException();
|
||||||
|
AppServices.showErrorDialog("Error creating payments", ex.getMessage());
|
||||||
|
});
|
||||||
|
createPaymentsService.start();
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean needsResolution() {
|
||||||
|
for(int row = 0; row < spreadsheetView.getGrid().getRowCount(); row++) {
|
||||||
ObservableList<SpreadsheetCell> rowCells = spreadsheetView.getItems().get(row);
|
ObservableList<SpreadsheetCell> rowCells = spreadsheetView.getItems().get(row);
|
||||||
Address address = (Address)rowCells.get(0).getItem();
|
SendToAddress sendToAddress = (SendToAddress)rowCells.getFirst().getItem();
|
||||||
Double value = (Double)rowCells.get(1).getItem();
|
if(sendToAddress.hrn != null && DnsPaymentCache.getDnsPayment(sendToAddress.hrn) == null) {
|
||||||
String label = (String)rowCells.get(2).getItem();
|
return true;
|
||||||
if(firstLabel == null) {
|
|
||||||
firstLabel = label;
|
|
||||||
}
|
|
||||||
if(label == null || label.isEmpty()) {
|
|
||||||
label = firstLabel;
|
|
||||||
}
|
|
||||||
|
|
||||||
if(address != null && value != null) {
|
|
||||||
if(bitcoinUnit == BitcoinUnit.BTC) {
|
|
||||||
value = value * Transaction.SATOSHIS_PER_BITCOIN;
|
|
||||||
}
|
|
||||||
|
|
||||||
payments.add(new Payment(address, label, value.longValue(), false));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return payments;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
private class SendToManyDialogPane extends DialogPane {
|
private class SendToManyDialogPane extends DialogPane {
|
||||||
|
|
@ -154,7 +192,7 @@ public class SendToManyDialog extends Dialog<List<Payment>> {
|
||||||
if(buttonType.getButtonData() == ButtonBar.ButtonData.LEFT) {
|
if(buttonType.getButtonData() == ButtonBar.ButtonData.LEFT) {
|
||||||
Button loadButton = new Button(buttonType.getText());
|
Button loadButton = new Button(buttonType.getText());
|
||||||
loadButton.setGraphicTextGap(5);
|
loadButton.setGraphicTextGap(5);
|
||||||
loadButton.setGraphic(getGlyph(FontAwesome5.Glyph.ARROW_UP));
|
loadButton.setGraphic(GlyphUtils.getUpArrowGlyph());
|
||||||
final ButtonBar.ButtonData buttonData = buttonType.getButtonData();
|
final ButtonBar.ButtonData buttonData = buttonType.getButtonData();
|
||||||
ButtonBar.setButtonData(loadButton, buttonData);
|
ButtonBar.setButtonData(loadButton, buttonData);
|
||||||
loadButton.setOnAction(event -> {
|
loadButton.setOnAction(event -> {
|
||||||
|
|
@ -169,7 +207,7 @@ public class SendToManyDialog extends Dialog<List<Payment>> {
|
||||||
File file = fileChooser.showOpenDialog(this.getScene().getWindow());
|
File file = fileChooser.showOpenDialog(this.getScene().getWindow());
|
||||||
if(file != null) {
|
if(file != null) {
|
||||||
try {
|
try {
|
||||||
List<Payment> csvPayments = new ArrayList<>();
|
List<SendToPayment> csvPayments = new ArrayList<>();
|
||||||
try(Reader reader = new FileReader(file, StandardCharsets.UTF_8)) {
|
try(Reader reader = new FileReader(file, StandardCharsets.UTF_8)) {
|
||||||
CsvReader csvReader = new CsvReader(reader);
|
CsvReader csvReader = new CsvReader(reader);
|
||||||
while(csvReader.readRecord()) {
|
while(csvReader.readRecord()) {
|
||||||
|
|
@ -185,9 +223,22 @@ public class SendToManyDialog extends Dialog<List<Payment>> {
|
||||||
} else {
|
} else {
|
||||||
amount = Long.parseLong(csvReader.get(1).replace(",", ""));
|
amount = Long.parseLong(csvReader.get(1).replace(",", ""));
|
||||||
}
|
}
|
||||||
Address address = Address.fromString(csvReader.get(0));
|
|
||||||
String label = csvReader.get(2);
|
String label = csvReader.get(2);
|
||||||
csvPayments.add(new Payment(address, label, amount, false));
|
Optional<String> optDnsPaymentHrn = DnsPayment.getHrn(csvReader.get(0));
|
||||||
|
if(optDnsPaymentHrn.isPresent()) {
|
||||||
|
Payment payment = new Payment(null, label, amount, false);
|
||||||
|
csvPayments.add(new SendToPayment(payment, new SendToAddress(optDnsPaymentHrn.get())));
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
SilentPaymentAddress silentPaymentAddress = SilentPaymentAddress.from(csvReader.get(0));
|
||||||
|
Payment payment = new SilentPayment(silentPaymentAddress, label, amount, false);
|
||||||
|
csvPayments.add(new SendToPayment(payment, SendToAddress.fromPayment(payment)));
|
||||||
|
} catch(Exception e) {
|
||||||
|
Address address = Address.fromString(csvReader.get(0));
|
||||||
|
Payment payment = new Payment(address, label, amount, false);
|
||||||
|
csvPayments.add(new SendToPayment(payment, SendToAddress.fromPayment(payment)));
|
||||||
|
}
|
||||||
|
}
|
||||||
} catch(NumberFormatException e) {
|
} catch(NumberFormatException e) {
|
||||||
//ignore and continue - probably a header line
|
//ignore and continue - probably a header line
|
||||||
} catch(InvalidAddressException e) {
|
} catch(InvalidAddressException e) {
|
||||||
|
|
@ -200,7 +251,7 @@ public class SendToManyDialog extends Dialog<List<Payment>> {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
spreadsheetView.setGrid(getGrid(csvPayments));
|
spreadsheetView.setGrid(createGrid(csvPayments));
|
||||||
}
|
}
|
||||||
} catch(IOException e) {
|
} catch(IOException e) {
|
||||||
AppServices.showErrorDialog("Cannot load CSV", e.getMessage());
|
AppServices.showErrorDialog("Cannot load CSV", e.getMessage());
|
||||||
|
|
@ -215,24 +266,18 @@ public class SendToManyDialog extends Dialog<List<Payment>> {
|
||||||
|
|
||||||
return button;
|
return button;
|
||||||
}
|
}
|
||||||
|
|
||||||
private Glyph getGlyph(FontAwesome5.Glyph glyphName) {
|
|
||||||
Glyph glyph = new Glyph(FontAwesome5.FONT_NAME, glyphName);
|
|
||||||
glyph.setFontSize(11);
|
|
||||||
return glyph;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static class AddressCellType extends SpreadsheetCellType<Address> {
|
public static class SendToAddressCellType extends SpreadsheetCellType<SendToAddress> {
|
||||||
public AddressCellType() {
|
public SendToAddressCellType() {
|
||||||
this(new StringConverterWithFormat<>(new AddressStringConverter()) {
|
this(new StringConverterWithFormat<>(new SendToAddressStringConverter()) {
|
||||||
@Override
|
@Override
|
||||||
public String toString(Address item) {
|
public String toString(SendToAddress item) {
|
||||||
return toStringFormat(item, ""); //$NON-NLS-1$
|
return toStringFormat(item, ""); //$NON-NLS-1$
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Address fromString(String str) {
|
public SendToAddress fromString(String str) {
|
||||||
if(str == null || str.isEmpty()) { //$NON-NLS-1$
|
if(str == null || str.isEmpty()) { //$NON-NLS-1$
|
||||||
return null;
|
return null;
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -241,7 +286,7 @@ public class SendToManyDialog extends Dialog<List<Payment>> {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String toStringFormat(Address item, String format) {
|
public String toStringFormat(SendToAddress item, String format) {
|
||||||
try {
|
try {
|
||||||
if(item == null) {
|
if(item == null) {
|
||||||
return ""; //$NON-NLS-1$
|
return ""; //$NON-NLS-1$
|
||||||
|
|
@ -255,7 +300,7 @@ public class SendToManyDialog extends Dialog<List<Payment>> {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public AddressCellType(StringConverter<Address> converter) {
|
public SendToAddressCellType(StringConverter<SendToAddress> converter) {
|
||||||
super(converter);
|
super(converter);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -265,7 +310,7 @@ public class SendToManyDialog extends Dialog<List<Payment>> {
|
||||||
}
|
}
|
||||||
|
|
||||||
public SpreadsheetCell createCell(final int row, final int column, final int rowSpan, final int columnSpan,
|
public SpreadsheetCell createCell(final int row, final int column, final int rowSpan, final int columnSpan,
|
||||||
final Address value) {
|
final SendToAddress value) {
|
||||||
SpreadsheetCell cell = new SpreadsheetCellBase(row, column, rowSpan, columnSpan, this);
|
SpreadsheetCell cell = new SpreadsheetCellBase(row, column, rowSpan, columnSpan, this);
|
||||||
cell.setItem(value);
|
cell.setItem(value);
|
||||||
return cell;
|
return cell;
|
||||||
|
|
@ -278,7 +323,7 @@ public class SendToManyDialog extends Dialog<List<Payment>> {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean match(Object value, Object... options) {
|
public boolean match(Object value, Object... options) {
|
||||||
if(value instanceof Address)
|
if(value instanceof SendToAddress)
|
||||||
return true;
|
return true;
|
||||||
else {
|
else {
|
||||||
try {
|
try {
|
||||||
|
|
@ -291,9 +336,9 @@ public class SendToManyDialog extends Dialog<List<Payment>> {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Address convertValue(Object value) {
|
public SendToAddress convertValue(Object value) {
|
||||||
if(value instanceof Address)
|
if(value instanceof SendToAddress)
|
||||||
return (Address)value;
|
return (SendToAddress)value;
|
||||||
else {
|
else {
|
||||||
try {
|
try {
|
||||||
return converter.fromString(value == null ? null : value.toString());
|
return converter.fromString(value == null ? null : value.toString());
|
||||||
|
|
@ -304,13 +349,155 @@ public class SendToManyDialog extends Dialog<List<Payment>> {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String toString(Address item) {
|
public String toString(SendToAddress item) {
|
||||||
return converter.toString(item);
|
return converter.toString(item);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String toString(Address item, String format) {
|
public String toString(SendToAddress item, String format) {
|
||||||
return ((StringConverterWithFormat<Address>)converter).toStringFormat(item, format);
|
return ((StringConverterWithFormat<SendToAddress>)converter).toStringFormat(item, format);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
public static class SendToAddress {
|
||||||
|
private final String hrn;
|
||||||
|
private final Address address;
|
||||||
|
private final SilentPaymentAddress silentPaymentAddress;
|
||||||
|
|
||||||
|
public SendToAddress(String hrn) {
|
||||||
|
this.hrn = hrn;
|
||||||
|
this.address = null;
|
||||||
|
this.silentPaymentAddress = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public SendToAddress(Address address) {
|
||||||
|
this.hrn = null;
|
||||||
|
this.address = address;
|
||||||
|
this.silentPaymentAddress = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public SendToAddress(SilentPaymentAddress silentPaymentAddress) {
|
||||||
|
this.hrn = null;
|
||||||
|
this.address = null;
|
||||||
|
this.silentPaymentAddress = silentPaymentAddress;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String toString() {
|
||||||
|
return hrn == null ? silentPaymentAddress == null ? (address == null ? null : address.toString()) : silentPaymentAddress.toString() : hrn;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static SendToAddress fromPayment(Payment payment) {
|
||||||
|
DnsPayment dnsPayment = DnsPaymentCache.getDnsPayment(payment);
|
||||||
|
if(dnsPayment != null) {
|
||||||
|
return new SendToAddress(dnsPayment.hrn());
|
||||||
|
}
|
||||||
|
return payment instanceof SilentPayment ? new SendToAddress(((SilentPayment)payment).getSilentPaymentAddress()) : new SendToAddress(payment.getAddress());
|
||||||
|
}
|
||||||
|
|
||||||
|
public Payment toPayment(String label, long value, boolean sendMax) throws DnsPaymentValidationException, IOException, ExecutionException, InterruptedException, BitcoinURIParseException {
|
||||||
|
if(hrn != null) {
|
||||||
|
DnsPayment dnsPayment = DnsPaymentCache.getDnsPayment(hrn);
|
||||||
|
if(dnsPayment == null) {
|
||||||
|
DnsPaymentResolver resolver = new DnsPaymentResolver(hrn);
|
||||||
|
Optional<DnsPayment> optDnsPayment = resolver.resolve();
|
||||||
|
if(optDnsPayment.isPresent()) {
|
||||||
|
dnsPayment = optDnsPayment.get();
|
||||||
|
if(dnsPayment.hasAddress()) {
|
||||||
|
DnsPaymentCache.putDnsPayment(dnsPayment.bitcoinURI().getAddress(), dnsPayment);
|
||||||
|
} else if(dnsPayment.hasSilentPaymentAddress()) {
|
||||||
|
DnsPaymentCache.putDnsPayment(dnsPayment.bitcoinURI().getSilentPaymentAddress(), dnsPayment);
|
||||||
|
}
|
||||||
|
return getPayment(optDnsPayment.get(), label, value, sendMax);
|
||||||
|
} else {
|
||||||
|
throw new IllegalArgumentException("Payment to " + hrn + " could not be resolved.");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return getPayment(dnsPayment, label, value, sendMax);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if(silentPaymentAddress != null) {
|
||||||
|
return new SilentPayment(silentPaymentAddress, label, value, sendMax);
|
||||||
|
} else {
|
||||||
|
return new Payment(address, label, value, sendMax);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Payment getPayment(DnsPayment dnsPayment, String label, long value, boolean sendMax) {
|
||||||
|
if(dnsPayment.hasAddress()) {
|
||||||
|
return new Payment(dnsPayment.bitcoinURI().getAddress(), label, value, sendMax);
|
||||||
|
} else if(dnsPayment.hasSilentPaymentAddress()) {
|
||||||
|
return new SilentPayment(dnsPayment.bitcoinURI().getSilentPaymentAddress(), label, value, sendMax);
|
||||||
|
} else {
|
||||||
|
throw new IllegalArgumentException("Payment to " + dnsPayment + " has no associated address.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class SendToAddressStringConverter extends StringConverter<SendToAddress> {
|
||||||
|
private final AddressStringConverter addressStringConverter = new AddressStringConverter();
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public SendToAddress fromString(String value) {
|
||||||
|
Optional<String> optDnsPaymentHrn = DnsPayment.getHrn(value);
|
||||||
|
if(optDnsPaymentHrn.isPresent()) {
|
||||||
|
return new SendToAddress(optDnsPaymentHrn.get());
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
SilentPaymentAddress silentPaymentAddress = SilentPaymentAddress.from(value);
|
||||||
|
return new SendToAddress(silentPaymentAddress);
|
||||||
|
} catch(Exception e) {
|
||||||
|
Address address = addressStringConverter.fromString(value);
|
||||||
|
return address == null ? null : new SendToAddress(address);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString(SendToAddress value) {
|
||||||
|
return value.toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class CreatePaymentsService extends Service<List<Payment>> {
|
||||||
|
@Override
|
||||||
|
protected Task<List<Payment>> createTask() {
|
||||||
|
return new Task<>() {
|
||||||
|
@Override
|
||||||
|
protected List<Payment> call() throws Exception {
|
||||||
|
return getPayments();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<Payment> getPayments() throws DnsPaymentValidationException, IOException, ExecutionException, InterruptedException, BitcoinURIParseException {
|
||||||
|
List<Payment> payments = new ArrayList<>();
|
||||||
|
Grid grid = spreadsheetView.getGrid();
|
||||||
|
String firstLabel = null;
|
||||||
|
for(int row = 0; row < grid.getRowCount(); row++) {
|
||||||
|
ObservableList<SpreadsheetCell> rowCells = spreadsheetView.getItems().get(row);
|
||||||
|
SendToAddress sendToAddress = (SendToAddress)rowCells.get(0).getItem();
|
||||||
|
Double value = (Double)rowCells.get(1).getItem();
|
||||||
|
String label = (String)rowCells.get(2).getItem();
|
||||||
|
if(firstLabel == null) {
|
||||||
|
firstLabel = label;
|
||||||
|
}
|
||||||
|
if(label == null || label.isEmpty()) {
|
||||||
|
label = firstLabel;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(sendToAddress != null && value != null) {
|
||||||
|
if(bitcoinUnit == BitcoinUnit.BTC) {
|
||||||
|
value = value * Transaction.SATOSHIS_PER_BITCOIN;
|
||||||
|
}
|
||||||
|
|
||||||
|
payments.add(sendToAddress.toPayment(label, value.longValue(), false));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return payments;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private record SendToPayment(Payment payment, SendToAddress sendToAddress) {}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,13 +3,12 @@ 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, String imagePath, Worker<?> worker) {
|
public ServiceProgressDialog(String title, String header, Node graphic, Worker<?> worker) {
|
||||||
super(worker);
|
super(worker);
|
||||||
|
|
||||||
final DialogPane dialogPane = getDialogPane();
|
final DialogPane dialogPane = getDialogPane();
|
||||||
|
|
@ -20,8 +19,7 @@ public class ServiceProgressDialog extends ProgressDialog {
|
||||||
setHeaderText(header);
|
setHeaderText(header);
|
||||||
|
|
||||||
dialogPane.getStyleClass().remove("progress-dialog");
|
dialogPane.getStyleClass().remove("progress-dialog");
|
||||||
Image image = new Image(imagePath);
|
dialogPane.setGraphic(graphic);
|
||||||
dialogPane.setGraphic(new ImageView(image));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static class ProxyWorker implements Worker<Boolean> {
|
public static class ProxyWorker implements Worker<Boolean> {
|
||||||
|
|
|
||||||
|
|
@ -44,8 +44,7 @@ public class TextAreaDialog extends Dialog<String> {
|
||||||
final DialogPane dialogPane = new TextAreaDialogPane();
|
final DialogPane dialogPane = new TextAreaDialogPane();
|
||||||
setDialogPane(dialogPane);
|
setDialogPane(dialogPane);
|
||||||
|
|
||||||
Image image = new Image("/image/sparrow-small.png");
|
dialogPane.setGraphic(new DialogImage(DialogImage.Type.SPARROW));
|
||||||
dialogPane.setGraphic(new ImageView(image));
|
|
||||||
|
|
||||||
HBox hbox = new HBox();
|
HBox hbox = new HBox();
|
||||||
this.textArea = new TextArea(defaultValue);
|
this.textArea = new TextArea(defaultValue);
|
||||||
|
|
|
||||||
|
|
@ -29,8 +29,7 @@ public class TextfieldDialog extends Dialog<String> {
|
||||||
final DialogPane dialogPane = getDialogPane();
|
final DialogPane dialogPane = getDialogPane();
|
||||||
setDialogPane(dialogPane);
|
setDialogPane(dialogPane);
|
||||||
|
|
||||||
Image image = new Image("/image/sparrow-small.png");
|
dialogPane.setGraphic(new DialogImage(DialogImage.Type.SPARROW));
|
||||||
dialogPane.setGraphic(new ImageView(image));
|
|
||||||
|
|
||||||
HBox hbox = new HBox();
|
HBox hbox = new HBox();
|
||||||
this.textField = new TextField(defaultValue);
|
this.textField = new TextField(defaultValue);
|
||||||
|
|
|
||||||
|
|
@ -2,14 +2,13 @@ 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;
|
||||||
|
|
@ -23,17 +22,18 @@ 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, String imageUrl) {
|
public TitledDescriptionPane(String title, String description, String content, WalletModel walletModel) {
|
||||||
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, imageUrl));
|
setGraphic(getTitle(title, description, walletModel));
|
||||||
setContent(getContentBox(content));
|
setContent(getContentBox(content));
|
||||||
removeArrow();
|
removeArrow();
|
||||||
}
|
}
|
||||||
|
|
||||||
protected Node getTitle(String title, String description, String imageUrl) {
|
protected Node getTitle(String title, String description, WalletModel walletModel) {
|
||||||
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,12 +43,8 @@ public class TitledDescriptionPane extends TitledPane {
|
||||||
imageBox.setMinHeight(50);
|
imageBox.setMinHeight(50);
|
||||||
listItem.getChildren().add(imageBox);
|
listItem.getChildren().add(imageBox);
|
||||||
|
|
||||||
Image image = new Image(imageUrl, 50, 50, true, true);
|
WalletModelImage walletModelImage = new WalletModelImage(walletModel);
|
||||||
if (!image.isError()) {
|
imageBox.getChildren().add(walletModelImage);
|
||||||
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,19 +3,22 @@ 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.UnitFormat;
|
import com.sparrowwallet.sparrow.*;
|
||||||
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;
|
||||||
|
|
@ -23,6 +26,7 @@ import javafx.beans.property.SimpleBooleanProperty;
|
||||||
import javafx.beans.property.SimpleObjectProperty;
|
import javafx.beans.property.SimpleObjectProperty;
|
||||||
import javafx.embed.swing.SwingFXUtils;
|
import javafx.embed.swing.SwingFXUtils;
|
||||||
import javafx.event.EventHandler;
|
import javafx.event.EventHandler;
|
||||||
|
import javafx.geometry.HPos;
|
||||||
import javafx.geometry.Insets;
|
import javafx.geometry.Insets;
|
||||||
import javafx.geometry.Pos;
|
import javafx.geometry.Pos;
|
||||||
import javafx.scene.Group;
|
import javafx.scene.Group;
|
||||||
|
|
@ -39,10 +43,7 @@ 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.FileChooser;
|
import javafx.stage.*;
|
||||||
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;
|
||||||
|
|
||||||
|
|
@ -107,6 +108,7 @@ public class TransactionDiagram extends GridPane {
|
||||||
expandedDiagram.setId("transactionDiagram");
|
expandedDiagram.setId("transactionDiagram");
|
||||||
expandedDiagram.setExpanded(true);
|
expandedDiagram.setExpanded(true);
|
||||||
expandedDiagram.setFinal(isFinal());
|
expandedDiagram.setFinal(isFinal());
|
||||||
|
expandedDiagram.setMaxWidth(AppServices.getActiveWindow().getWidth() - 200);
|
||||||
updateDerivedDiagram(expandedDiagram);
|
updateDerivedDiagram(expandedDiagram);
|
||||||
|
|
||||||
HBox buttonBox = new HBox();
|
HBox buttonBox = new HBox();
|
||||||
|
|
@ -124,7 +126,7 @@ public class TransactionDiagram extends GridPane {
|
||||||
AppServices.setStageIcon(stage);
|
AppServices.setStageIcon(stage);
|
||||||
stage.setScene(scene);
|
stage.setScene(scene);
|
||||||
stage.setOnShowing(e -> {
|
stage.setOnShowing(e -> {
|
||||||
AppServices.moveToActiveWindowScreen(stage, 600, 460);
|
AppServices.moveToActiveWindowScreen(stage, expandedDiagram.getMaxWidth(), 460);
|
||||||
});
|
});
|
||||||
stage.setOnHidden(e -> {
|
stage.setOnHidden(e -> {
|
||||||
expandedDiagram = null;
|
expandedDiagram = null;
|
||||||
|
|
@ -141,6 +143,39 @@ public class TransactionDiagram extends GridPane {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
public TransactionDiagram() {
|
||||||
|
ColumnConstraints col1 = new ColumnConstraints();
|
||||||
|
col1.setPrefWidth(22);
|
||||||
|
col1.setHgrow(Priority.NEVER);
|
||||||
|
|
||||||
|
ColumnConstraints col2 = new ColumnConstraints();
|
||||||
|
col2.setHgrow(Priority.ALWAYS);
|
||||||
|
col2.setPercentWidth(25);
|
||||||
|
col2.setFillWidth(true);
|
||||||
|
|
||||||
|
ColumnConstraints col3 = new ColumnConstraints();
|
||||||
|
col3.setPrefWidth(140);
|
||||||
|
col3.setHgrow(Priority.NEVER);
|
||||||
|
|
||||||
|
ColumnConstraints col4 = new ColumnConstraints();
|
||||||
|
Label label = new Label();
|
||||||
|
col4.setMinWidth(TextUtils.computeTextWidth(label.getFont(), "Transaction", 0) + 20);
|
||||||
|
col4.setHgrow(Priority.NEVER);
|
||||||
|
col4.setHalignment(HPos.CENTER);
|
||||||
|
|
||||||
|
ColumnConstraints col5 = new ColumnConstraints();
|
||||||
|
col5.setPrefWidth(140);
|
||||||
|
col5.setHgrow(Priority.NEVER);
|
||||||
|
|
||||||
|
ColumnConstraints col6 = new ColumnConstraints();
|
||||||
|
col6.setHgrow(Priority.ALWAYS);
|
||||||
|
col6.setPercentWidth(25);
|
||||||
|
col6.setFillWidth(true);
|
||||||
|
|
||||||
|
getColumnConstraints().addAll(col1, col2, col3, col4, col5, col6);
|
||||||
|
setPadding(new Insets(0, 0, 0, 40));
|
||||||
|
}
|
||||||
|
|
||||||
public void update(WalletTransaction walletTx) {
|
public void update(WalletTransaction walletTx) {
|
||||||
setMinHeight(getDiagramHeight());
|
setMinHeight(getDiagramHeight());
|
||||||
setMaxHeight(getDiagramHeight());
|
setMaxHeight(getDiagramHeight());
|
||||||
|
|
@ -169,7 +204,7 @@ public class TransactionDiagram extends GridPane {
|
||||||
|
|
||||||
VBox messagePane = new VBox();
|
VBox messagePane = new VBox();
|
||||||
messagePane.setPrefHeight(getDiagramHeight());
|
messagePane.setPrefHeight(getDiagramHeight());
|
||||||
messagePane.setPadding(new Insets(0, 10, 0, 280));
|
messagePane.setPadding(new Insets(0, 10, 0, 10));
|
||||||
messagePane.setAlignment(Pos.CENTER);
|
messagePane.setAlignment(Pos.CENTER);
|
||||||
messagePane.getChildren().add(createSpacer());
|
messagePane.getChildren().add(createSpacer());
|
||||||
|
|
||||||
|
|
@ -229,6 +264,14 @@ 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) {
|
||||||
|
|
@ -404,8 +447,6 @@ public class TransactionDiagram extends GridPane {
|
||||||
|
|
||||||
private Pane getInputsLabels(List<Map<BlockTransactionHashIndex, WalletNode>> displayedUtxoSets) {
|
private Pane getInputsLabels(List<Map<BlockTransactionHashIndex, WalletNode>> displayedUtxoSets) {
|
||||||
VBox inputsBox = new VBox();
|
VBox inputsBox = new VBox();
|
||||||
inputsBox.setMaxWidth(isExpanded() ? 300 : 150);
|
|
||||||
inputsBox.setPrefWidth(isExpanded() ? 230 : 150);
|
|
||||||
inputsBox.setPadding(new Insets(0, 10, 0, 10));
|
inputsBox.setPadding(new Insets(0, 10, 0, 10));
|
||||||
inputsBox.minHeightProperty().bind(minHeightProperty());
|
inputsBox.minHeightProperty().bind(minHeightProperty());
|
||||||
inputsBox.setAlignment(Pos.BASELINE_RIGHT);
|
inputsBox.setAlignment(Pos.BASELINE_RIGHT);
|
||||||
|
|
@ -490,6 +531,11 @@ 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);
|
||||||
}
|
}
|
||||||
|
|
@ -613,6 +659,10 @@ 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();
|
||||||
|
|
@ -628,7 +678,8 @@ public class TransactionDiagram extends GridPane {
|
||||||
|
|
||||||
double width = 140.0;
|
double width = 140.0;
|
||||||
long sum = walletTx.getTotal();
|
long sum = walletTx.getTotal();
|
||||||
List<Long> values = walletTx.getTransaction().getOutputs().stream().filter(txo -> txo.getScript().getToAddress() != null).map(TransactionOutput::getValue).collect(Collectors.toList());
|
List<Long> values = walletTx.getOutputs().stream().filter(output -> !(output instanceof WalletTransaction.NonAddressOutput))
|
||||||
|
.map(output -> output.getTransactionOutput().getValue()).collect(Collectors.toList());
|
||||||
values.add(walletTx.getFee());
|
values.add(walletTx.getFee());
|
||||||
int numOutputs = displayedPayments.size() + walletTx.getChangeMap().size() + 1;
|
int numOutputs = displayedPayments.size() + walletTx.getChangeMap().size() + 1;
|
||||||
for(int i = 1; i <= numOutputs; i++) {
|
for(int i = 1; i <= numOutputs; i++) {
|
||||||
|
|
@ -664,8 +715,6 @@ public class TransactionDiagram extends GridPane {
|
||||||
|
|
||||||
private Pane getOutputsLabels(List<Payment> displayedPayments) {
|
private Pane getOutputsLabels(List<Payment> displayedPayments) {
|
||||||
VBox outputsBox = new VBox();
|
VBox outputsBox = new VBox();
|
||||||
outputsBox.setMaxWidth(isExpanded() ? 350 : 150);
|
|
||||||
outputsBox.setPrefWidth(isExpanded() ? 230 : 150);
|
|
||||||
outputsBox.setPadding(new Insets(0, 20, 0, 10));
|
outputsBox.setPadding(new Insets(0, 20, 0, 10));
|
||||||
outputsBox.setAlignment(Pos.BASELINE_LEFT);
|
outputsBox.setAlignment(Pos.BASELINE_LEFT);
|
||||||
outputsBox.getChildren().add(createSpacer());
|
outputsBox.getChildren().add(createSpacer());
|
||||||
|
|
@ -673,20 +722,26 @@ 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").contains(style)) || payment instanceof AdditionalPayment;
|
boolean labelledPayment = outputGlyph.getStyleClass().stream().anyMatch(style -> List.of("premix-icon", "badbank-icon", "whirlpoolfee-icon", "anchor-icon").contains(style)) || payment instanceof AdditionalPayment || payment.getLabel() != null;
|
||||||
Label recipientLabel = new Label(payment.getLabel() == null || payment.getType() == Payment.Type.FAKE_MIX || payment.getType() == Payment.Type.MIX ? payment.getAddress().toString().substring(0, 8) + "..." : payment.getLabel(), outputGlyph);
|
Label recipientLabel = new Label(payment.getLabel() == null || payment.getType() == Payment.Type.FAKE_MIX || payment.getType() == Payment.Type.MIX ? payment.toString().substring(0, 8) + "..." : payment.getLabel(), outputGlyph);
|
||||||
recipientLabel.getStyleClass().add("output-label");
|
recipientLabel.getStyleClass().add("output-label");
|
||||||
recipientLabel.getStyleClass().add(labelledPayment ? "payment-label" : "recipient-label");
|
recipientLabel.getStyleClass().add(labelledPayment ? "payment-label" : "recipient-label");
|
||||||
Wallet toWallet = walletTx.getToWallet(AppServices.get().getOpenWallets().keySet(), payment);
|
Wallet toWallet = walletTx.getToWallet(AppServices.get().getOpenWallets().keySet(), payment);
|
||||||
WalletNode toNode = walletTx.getWallet() != null && !walletTx.getWallet().isBip47() ? walletTx.getAddressNodeMap().get(payment.getAddress()) : null;
|
WalletNode toNode = payment instanceof WalletNodePayment walletNodePayment ? walletNodePayment.getWalletNode() : null;
|
||||||
Wallet toBip47Wallet = getBip47SendWallet(payment);
|
Wallet toBip47Wallet = getBip47SendWallet(payment);
|
||||||
|
DnsPayment dnsPayment = DnsPaymentCache.getDnsPayment(payment);
|
||||||
Tooltip recipientTooltip = new Tooltip((toWallet == null ? (toNode != null ? "Consolidate " : "Pay ") : "Receive ")
|
Tooltip recipientTooltip = new Tooltip((toWallet == null ? (toNode != null ? "Consolidate " : "Pay ") : "Receive ")
|
||||||
+ getSatsValue(payment.getAmount()) + " sats to "
|
+ getSatsValue(payment.getAmount()) + " sats to "
|
||||||
+ (payment instanceof AdditionalPayment ? (isExpanded() ? "\n" : "(click to expand)\n") + payment : (toWallet == null ? (payment.getLabel() == null ? (toNode != null ? toNode : (toBip47Wallet == null ? "external address" : toBip47Wallet.getDisplayName())) : payment.getLabel()) : toWallet.getFullDisplayName()) + "\n" + payment.getAddress().toString())
|
+ (payment instanceof AdditionalPayment ? (isExpanded() ? "\n" : "(click to expand)\n") + payment : (toWallet == null ? (dnsPayment == null ? (payment.getLabel() == null ? (toNode != null ? toNode : (toBip47Wallet == null ? "external address" : toBip47Wallet.getDisplayName())) : payment.getLabel()) : dnsPayment.toString()) : toWallet.getFullDisplayName()) + "\n" + payment.getDisplayAddress())
|
||||||
+ (walletTx.isDuplicateAddress(payment) ? " (Duplicate)" : ""));
|
+ (walletTx.isDuplicateAddress(payment) ? " (Duplicate)" : ""));
|
||||||
recipientTooltip.getStyleClass().add("recipient-label");
|
recipientTooltip.getStyleClass().add("recipient-label");
|
||||||
recipientTooltip.setShowDelay(new Duration(TOOLTIP_SHOW_DELAY));
|
recipientTooltip.setShowDelay(new Duration(TOOLTIP_SHOW_DELAY));
|
||||||
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);
|
||||||
|
|
@ -702,7 +757,13 @@ public class TransactionDiagram extends GridPane {
|
||||||
paymentBox.getChildren().addAll(region, amountLabel);
|
paymentBox.getChildren().addAll(region, amountLabel);
|
||||||
}
|
}
|
||||||
|
|
||||||
outputNodes.add(new OutputNode(paymentBox, payment.getAddress(), payment.getAmount()));
|
if(payment instanceof SilentPayment silentPayment) {
|
||||||
|
outputNodes.add(new OutputNode(paymentBox, silentPayment.isAddressComputed() ? silentPayment.getAddress() : null, payment.getAmount(), null, silentPayment.getSilentPaymentAddress()));
|
||||||
|
} else {
|
||||||
|
Wallet bip47Wallet = toWallet != null && toWallet.isBip47() ? toWallet : (toBip47Wallet != null && toBip47Wallet.isBip47() ? toBip47Wallet : null);
|
||||||
|
PaymentCode paymentCode = bip47Wallet == null ? null : bip47Wallet.getKeystores().getFirst().getExternalPaymentCode();
|
||||||
|
outputNodes.add(new OutputNode(paymentBox, payment.getAddress(), payment.getAmount(), paymentCode, null));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Set<Integer> seenIndexes = new HashSet<>();
|
Set<Integer> seenIndexes = new HashSet<>();
|
||||||
|
|
@ -766,7 +827,7 @@ public class TransactionDiagram extends GridPane {
|
||||||
outputsBox.getChildren().add(outputNode.outputLabel);
|
outputsBox.getChildren().add(outputNode.outputLabel);
|
||||||
outputsBox.getChildren().add(createSpacer());
|
outputsBox.getChildren().add(createSpacer());
|
||||||
|
|
||||||
ContextMenu contextMenu = new LabelContextMenu(outputNode.address, outputNode.amount);
|
ContextMenu contextMenu = new LabelContextMenu(outputNode.address, outputNode.amount, outputNode.paymentCode, outputNode.silentPaymentAddress);
|
||||||
if(!outputNode.outputLabel.getChildren().isEmpty() && outputNode.outputLabel.getChildren().get(0) instanceof Label outputLabelControl) {
|
if(!outputNode.outputLabel.getChildren().isEmpty() && outputNode.outputLabel.getChildren().get(0) instanceof Label outputLabelControl) {
|
||||||
outputLabelControl.setContextMenu(contextMenu);
|
outputLabelControl.setContextMenu(contextMenu);
|
||||||
}
|
}
|
||||||
|
|
@ -775,7 +836,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 = String.format("%.2f", walletTx.getFeePercentage() * 100.0);
|
String percentage = walletTx.getFeePercentage() < 0.0001d ? "<0.01" : 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));
|
||||||
|
|
@ -829,6 +890,33 @@ 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();
|
||||||
|
|
@ -914,8 +1002,11 @@ public class TransactionDiagram extends GridPane {
|
||||||
}
|
}
|
||||||
|
|
||||||
private int getOutputIndex(Address address, long amount, Collection<Integer> seenIndexes) {
|
private int getOutputIndex(Address address, long amount, Collection<Integer> seenIndexes) {
|
||||||
List<TransactionOutput> addressOutputs = walletTx.getTransaction().getOutputs().stream().filter(txOutput -> txOutput.getScript().getToAddress() != null).collect(Collectors.toList());
|
List<TransactionOutput> addressOutputs = walletTx.getOutputs().stream().filter(output -> !(output instanceof WalletTransaction.NonAddressOutput))
|
||||||
TransactionOutput output = addressOutputs.stream().filter(txOutput -> address.equals(txOutput.getScript().getToAddress()) && txOutput.getValue() == amount && !seenIndexes.contains(txOutput.getIndex())).findFirst().orElseThrow();
|
.map(WalletTransaction.Output::getTransactionOutput).collect(Collectors.toList());
|
||||||
|
TransactionOutput output = addressOutputs.stream()
|
||||||
|
.filter(txOutput -> address.equals(txOutput.getScript().getToAddress()) && txOutput.getValue() == amount && !seenIndexes.contains(txOutput.getIndex()))
|
||||||
|
.findFirst().orElseThrow();
|
||||||
return addressOutputs.indexOf(output);
|
return addressOutputs.indexOf(output);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1065,7 +1156,7 @@ public class TransactionDiagram extends GridPane {
|
||||||
}
|
}
|
||||||
|
|
||||||
public String toString() {
|
public String toString() {
|
||||||
return additionalPayments.stream().map(payment -> payment.getAddress().toString()).collect(Collectors.joining("\n"));
|
return additionalPayments.stream().map(Payment::toString).collect(Collectors.joining("\n"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1073,16 +1164,28 @@ 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 -> {
|
||||||
|
|
@ -1119,6 +1222,28 @@ 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.getPayments().stream().anyMatch(walletTx::isConsolidationSend)) {
|
&& walletTx.getWallet().getStandardAccountType() == StandardAccount.WHIRLPOOL_POSTMIX && !walletTx.getWalletNodePayments().isEmpty()) {
|
||||||
OutputLabel remixOutputLabel = getRemixOutputLabel(transactionDiagram, walletTx.getPayments());
|
OutputLabel remixOutputLabel = getRemixOutputLabel(transactionDiagram, walletTx.getPayments());
|
||||||
if(remixOutputLabel != null) {
|
if(remixOutputLabel != null) {
|
||||||
outputLabels.add(remixOutputLabel);
|
outputLabels.add(remixOutputLabel);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
List<Payment> payments = walletTx.getPayments().stream().filter(payment -> payment.getType() == Payment.Type.DEFAULT && !walletTx.isConsolidationSend(payment)).collect(Collectors.toList());
|
List<Payment> payments = walletTx.getExternalPayments().stream().filter(payment -> payment.getType() == Payment.Type.DEFAULT).collect(Collectors.toList());
|
||||||
List<OutputLabel> paymentLabels = payments.stream().map(payment -> getOutputLabel(transactionDiagram, payment)).collect(Collectors.toList());
|
List<OutputLabel> paymentLabels = payments.stream().map(payment -> getOutputLabel(transactionDiagram, payment)).collect(Collectors.toList());
|
||||||
if(walletTx.getSelectedUtxos().values().stream().allMatch(Objects::isNull)) {
|
if(walletTx.getSelectedUtxos().values().stream().allMatch(Objects::isNull)) {
|
||||||
paymentLabels.sort(Comparator.comparingInt(paymentLabel -> (paymentLabel.text.startsWith("Receive") ? 0 : 1)));
|
paymentLabels.sort(Comparator.comparingInt(paymentLabel -> (paymentLabel.text.startsWith("Receive") ? 0 : 1)));
|
||||||
}
|
}
|
||||||
outputLabels.addAll(paymentLabels);
|
outputLabels.addAll(paymentLabels);
|
||||||
|
|
||||||
List<Payment> consolidations = walletTx.getPayments().stream().filter(payment -> payment.getType() == Payment.Type.DEFAULT && walletTx.isConsolidationSend(payment)).collect(Collectors.toList());
|
List<Payment> consolidations = walletTx.getWalletNodePayments().stream().filter(payment -> payment.getType() == Payment.Type.DEFAULT).collect(Collectors.toList());
|
||||||
outputLabels.addAll(consolidations.stream().map(consolidation -> getOutputLabel(transactionDiagram, consolidation)).collect(Collectors.toList()));
|
outputLabels.addAll(consolidations.stream().map(consolidation -> getOutputLabel(transactionDiagram, consolidation)).collect(Collectors.toList()));
|
||||||
|
|
||||||
List<Payment> mixes = walletTx.getPayments().stream().filter(payment -> payment.getType() == Payment.Type.MIX || payment.getType() == Payment.Type.FAKE_MIX).collect(Collectors.toList());
|
List<Payment> mixes = walletTx.getPayments().stream().filter(payment -> payment.getType() == Payment.Type.MIX || payment.getType() == Payment.Type.FAKE_MIX).collect(Collectors.toList());
|
||||||
|
|
@ -203,10 +203,10 @@ public class TransactionDiagramLabel extends HBox {
|
||||||
private OutputLabel getOutputLabel(TransactionDiagram transactionDiagram, Payment payment) {
|
private OutputLabel getOutputLabel(TransactionDiagram transactionDiagram, Payment payment) {
|
||||||
WalletTransaction walletTx = transactionDiagram.getWalletTransaction();
|
WalletTransaction walletTx = transactionDiagram.getWalletTransaction();
|
||||||
Wallet toWallet = walletTx.getToWallet(AppServices.get().getOpenWallets().keySet(), payment);
|
Wallet toWallet = walletTx.getToWallet(AppServices.get().getOpenWallets().keySet(), payment);
|
||||||
WalletNode toNode = walletTx.getWallet() != null && !walletTx.getWallet().isBip47() ? walletTx.getAddressNodeMap().get(payment.getAddress()) : null;
|
WalletNode toNode = payment instanceof WalletNodePayment walletNodePayment ? walletNodePayment.getWalletNode() : null;
|
||||||
|
|
||||||
Glyph glyph = GlyphUtils.getOutputGlyph(transactionDiagram.getWalletTransaction(), payment);
|
Glyph glyph = GlyphUtils.getOutputGlyph(transactionDiagram.getWalletTransaction(), payment);
|
||||||
String text = (toWallet == null ? (toNode != null ? "Consolidate " : "Pay ") : "Receive ") + transactionDiagram.getSatsValue(payment.getAmount()) + " sats to " + payment.getAddress().toString();
|
String text = (toWallet == null ? (toNode != null ? "Consolidate " : "Pay ") : "Receive ") + transactionDiagram.getSatsValue(payment.getAmount()) + " sats to " + payment;
|
||||||
|
|
||||||
return getOutputLabel(glyph, text);
|
return getOutputLabel(glyph, text);
|
||||||
}
|
}
|
||||||
|
|
@ -227,7 +227,8 @@ public class TransactionDiagramLabel extends HBox {
|
||||||
}
|
}
|
||||||
|
|
||||||
Glyph glyph = GlyphUtils.getFeeGlyph();
|
Glyph glyph = GlyphUtils.getFeeGlyph();
|
||||||
String text = "Fee of " + transactionDiagram.getSatsValue(walletTx.getFee()) + " sats (" + String.format("%.2f", walletTx.getFeePercentage() * 100.0) + "%)";
|
String percentage = walletTx.getFeePercentage() < 0.0001d ? "<0.01" : 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);
|
||||||
}
|
}
|
||||||
|
|
@ -239,7 +240,7 @@ public class TransactionDiagramLabel extends HBox {
|
||||||
icon.setGraphic(glyph);
|
icon.setGraphic(glyph);
|
||||||
|
|
||||||
CopyableLabel label = new CopyableLabel();
|
CopyableLabel label = new CopyableLabel();
|
||||||
label.setFont(Font.font("Roboto Mono Italic", 13));
|
label.setFont(Font.font("Fragment Mono Italic", 13));
|
||||||
label.setText(text);
|
label.setText(text);
|
||||||
|
|
||||||
HBox output = new HBox(5);
|
HBox output = new HBox(5);
|
||||||
|
|
|
||||||
|
|
@ -28,7 +28,8 @@ 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 walletForm) {
|
public WalletExportDialog(WalletForm selectedWalletForm, List<WalletForm> allWalletForms) {
|
||||||
this.wallet = walletForm.getWallet();
|
this.wallet = selectedWalletForm.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(), new WalletTransactions(walletForm));
|
exporters = List.of(new Electrum(), new ElectrumPersonalServer(), new Descriptor(), new SpecterDesktop(), new Sparrow(), new WalletLabels(allWalletForms), new WalletTransactions(selectedWalletForm));
|
||||||
} 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(), new WalletTransactions(walletForm));
|
new Descriptor(), new JadeMultisig(), new PassportMultisig(), new SpecterDesktop(), new BlueWalletMultisig(), new SpecterDIY(), new Sparrow(), new WalletLabels(allWalletForms), new WalletTransactions(selectedWalletForm));
|
||||||
} 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,33 +74,15 @@ 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.getType() + "-icon-invert.svg");
|
svgImage = loadSVGImage("/image/walletmodel/" + walletModel.getType() + "-icon-invert.svg");
|
||||||
} else {
|
} else {
|
||||||
svgImage = loadSVGImage("/image/" + walletModel.getType() + "-icon.svg");
|
svgImage = loadSVGImage("/image/walletmodel/" + 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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -127,16 +109,6 @@ 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();
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,78 @@
|
||||||
|
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,6 +17,7 @@ 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;
|
||||||
|
|
@ -43,14 +44,7 @@ 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);
|
||||||
|
|
||||||
|
|
@ -110,6 +104,7 @@ 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) {
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,82 @@
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,71 @@
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,372 +0,0 @@
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,45 +0,0 @@
|
||||||
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,12 +1,13 @@
|
||||||
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;
|
||||||
|
|
@ -15,7 +16,8 @@ import javafx.concurrent.ScheduledService;
|
||||||
import javafx.concurrent.Task;
|
import javafx.concurrent.Task;
|
||||||
import javafx.embed.swing.SwingFXUtils;
|
import javafx.embed.swing.SwingFXUtils;
|
||||||
import javafx.scene.image.Image;
|
import javafx.scene.image.Image;
|
||||||
import net.sourceforge.zbar.ZBar;
|
import org.openpnp.capture.*;
|
||||||
|
import org.openpnp.capture.library.OpenpnpCaptureLibrary;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
|
@ -23,38 +25,84 @@ 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.Arrays;
|
import java.util.*;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.concurrent.Semaphore;
|
||||||
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 WebcamDevice device;
|
private CaptureDevice 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 Webcam cam;
|
private final OpenPnpCapture capture;
|
||||||
|
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 {
|
||||||
Webcam.setDriver(new WebcamScanDriver());
|
if(log.isTraceEnabled()) {
|
||||||
|
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 resolution, WebcamDevice device, WebcamListener listener, WebcamUpdater.DelayCalculator delayCalculator) {
|
public WebcamService(WebcamResolution requestedResolution, CaptureDevice requestedDevice) {
|
||||||
this.resolution = resolution;
|
this.capture = new OpenPnpCapture();
|
||||||
this.device = device;
|
this.resolution = requestedResolution;
|
||||||
this.listener = listener;
|
this.device = requestedDevice;
|
||||||
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();
|
||||||
|
|
@ -62,50 +110,115 @@ public class WebcamService extends ScheduledService<Image> {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Task<Image> createTask() {
|
public Task<Image> createTask() {
|
||||||
return new Task<Image>() {
|
return new Task<>() {
|
||||||
@Override
|
@Override
|
||||||
protected Image call() throws Exception {
|
protected Image call() throws Exception {
|
||||||
try {
|
if(cancelRequested.get() || isCancelled() || captureClosed.get()) {
|
||||||
if(cam == null) {
|
return null;
|
||||||
List<Webcam> webcams = Webcam.getWebcams(1, TimeUnit.MINUTES);
|
}
|
||||||
if(webcams.isEmpty()) {
|
|
||||||
throw new UnsupportedOperationException("No camera available.");
|
|
||||||
}
|
|
||||||
|
|
||||||
cam = webcams.get(0);
|
if(!taskSemaphore.tryAcquire()) {
|
||||||
|
log.warn("Skipped execution of webcam capture task, another task is running");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
if(devices == null) {
|
||||||
|
devices = capture.getDevices();
|
||||||
|
availableDevices = new ArrayList<>(devices);
|
||||||
|
|
||||||
|
if(devices.isEmpty()) {
|
||||||
|
throw new UnsupportedOperationException("No cameras available");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
while(stream == null && !availableDevices.isEmpty()) {
|
||||||
|
CaptureDevice selectedDevice = availableDevices.stream().filter(d -> !d.getFormats().isEmpty()).findFirst().orElse(availableDevices.getFirst());
|
||||||
|
|
||||||
if(device != null) {
|
if(device != null) {
|
||||||
for(Webcam webcam : webcams) {
|
for(CaptureDevice webcam : availableDevices) {
|
||||||
if(webcam.getDevice().getName().equals(device.getName())) {
|
if(webcam.equals(device)) {
|
||||||
cam = webcam;
|
selectedDevice = webcam;
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if(Config.get().getWebcamDevice() != null) {
|
} else if(Config.get().getWebcamDevice() != null) {
|
||||||
for(Webcam webcam : webcams) {
|
for(CaptureDevice webcam : availableDevices) {
|
||||||
if(webcam.getDevice().getName().equals(Config.get().getWebcamDevice())) {
|
if(webcam.getUniqueId().equals(Config.get().getWebcamDeviceId())) {
|
||||||
cam = webcam;
|
selectedDevice = webcam;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if(webcam.getName().equals(Config.get().getWebcamDevice())) {
|
||||||
|
selectedDevice = webcam;
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
device = cam.getDevice();
|
device = selectedDevice;
|
||||||
|
|
||||||
cam.setCustomViewSizes(resolution.getSize());
|
if(device.getFormats().isEmpty()) {
|
||||||
cam.setViewSize(resolution.getSize());
|
throw new UnsupportedOperationException("No resolutions supported by camera " + device.getName());
|
||||||
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);
|
||||||
cam.open(true, delayCalculator);
|
stream = device.openStream(format);
|
||||||
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
BufferedImage originalImage = cam.getImage();
|
if(stream == null) {
|
||||||
if(originalImage == null) {
|
throw new UnsupportedOperationException("No usable cameras available, tried " + devices);
|
||||||
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);
|
||||||
|
|
@ -121,6 +234,7 @@ public class WebcamService extends ScheduledService<Image> {
|
||||||
return image;
|
return image;
|
||||||
} finally {
|
} finally {
|
||||||
opening.set(false);
|
opening.set(false);
|
||||||
|
taskSemaphore.release();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
@ -128,17 +242,66 @@ public class WebcamService extends ScheduledService<Image> {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void reset() {
|
public void reset() {
|
||||||
cam = null;
|
stream = null;
|
||||||
|
zoomLimits = null;
|
||||||
|
cancelRequested.set(false);
|
||||||
super.reset();
|
super.reset();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean cancel() {
|
public boolean cancel() {
|
||||||
if(cam != null && !cam.close()) {
|
cancelRequested.set(true);
|
||||||
cam.close();
|
boolean cancelled = super.cancel();
|
||||||
|
|
||||||
|
try {
|
||||||
|
if(taskSemaphore.tryAcquire(1, TimeUnit.SECONDS)) {
|
||||||
|
taskSemaphore.release();
|
||||||
|
} else {
|
||||||
|
log.error("Timed out waiting for task semaphore to be available to cancel, cancelling anyway");
|
||||||
|
}
|
||||||
|
} catch(InterruptedException e) {
|
||||||
|
log.error("Interrupted while waiting for task semaphore to be available to cancel, cancelling anyway");
|
||||||
}
|
}
|
||||||
|
|
||||||
return super.cancel();
|
if(stream != null) {
|
||||||
|
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) {
|
||||||
|
|
@ -156,9 +319,6 @@ public class WebcamService extends ScheduledService<Image> {
|
||||||
}
|
}
|
||||||
|
|
||||||
private Result readQR(BufferedImage bufferedImage) {
|
private Result readQR(BufferedImage bufferedImage) {
|
||||||
LuminanceSource source = new BufferedImageLuminanceSource(bufferedImage);
|
|
||||||
BinaryBitmap bitmap = new BinaryBitmap(new HybridBinarizer(source));
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
com.sparrowwallet.bokmakierie.Result result = bokmakierie.scan(bufferedImage);
|
com.sparrowwallet.bokmakierie.Result result = bokmakierie.scan(bufferedImage);
|
||||||
if(result != null) {
|
if(result != null) {
|
||||||
|
|
@ -176,6 +336,8 @@ public class WebcamService extends ScheduledService<Image> {
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
LuminanceSource source = new BufferedImageLuminanceSource(bufferedImage);
|
||||||
|
BinaryBitmap bitmap = new BinaryBitmap(new HybridBinarizer(source));
|
||||||
return qrReader.decode(bitmap, Map.of(DecodeHintType.TRY_HARDER, Boolean.TRUE));
|
return qrReader.decode(bitmap, Map.of(DecodeHintType.TRY_HARDER, Boolean.TRUE));
|
||||||
} catch(ReaderException e) {
|
} catch(ReaderException e) {
|
||||||
// fall thru, it means there is no QR code in image
|
// fall thru, it means there is no QR code in image
|
||||||
|
|
@ -189,7 +351,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 == WebcamResolution.HD ? 3.0f : 1.5f, BasicStroke.CAP_BUTT, BasicStroke.JOIN_MITER, 10.0f, dash1, 0.0f));
|
g2d.setStroke(new BasicStroke(resolution.isWidescreenAspect() ? 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;
|
||||||
|
|
@ -226,6 +388,18 @@ 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();
|
||||||
}
|
}
|
||||||
|
|
@ -235,33 +409,69 @@ public class WebcamService extends ScheduledService<Image> {
|
||||||
}
|
}
|
||||||
|
|
||||||
public int getCamWidth() {
|
public int getCamWidth() {
|
||||||
return resolution.getSize().width;
|
return resolution.getWidth();
|
||||||
}
|
}
|
||||||
|
|
||||||
public int getCamHeight() {
|
public int getCamHeight() {
|
||||||
return resolution.getSize().height;
|
return resolution.getHeight();
|
||||||
|
}
|
||||||
|
|
||||||
|
public WebcamResolution getResolution() {
|
||||||
|
return resolution;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setResolution(WebcamResolution resolution) {
|
public void setResolution(WebcamResolution resolution) {
|
||||||
this.resolution = resolution;
|
this.resolution = resolution;
|
||||||
}
|
}
|
||||||
|
|
||||||
public WebcamDevice getDevice() {
|
public CaptureDevice getDevice() {
|
||||||
return device;
|
return device;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setDevice(WebcamDevice device) {
|
public void setDevice(CaptureDevice 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,9 +46,22 @@ 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) {
|
if(newValue != null && !service.getCancelRequested()) {
|
||||||
imageProperty.set(newValue);
|
imageProperty.set(newValue);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
@ -57,27 +70,29 @@ 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:
|
||||||
imageView.imageProperty().unbind();
|
if(imageProperty.get() == null) {
|
||||||
imageView.imageProperty().bind(imageProperty);
|
imageView.imageProperty().unbind();
|
||||||
getChildren().setAll(imageView);
|
imageView.imageProperty().bind(imageProperty);
|
||||||
break ;
|
getChildren().setAll(imageView);
|
||||||
|
}
|
||||||
|
break;
|
||||||
case CANCELLED:
|
case CANCELLED:
|
||||||
|
imageProperty.set(null);
|
||||||
imageView.imageProperty().unbind();
|
imageView.imageProperty().unbind();
|
||||||
imageView.setImage(null);
|
|
||||||
statusPlaceholder.setText("Stopped");
|
statusPlaceholder.setText("Stopped");
|
||||||
getChildren().setAll(statusPlaceholder);
|
getChildren().setAll(statusPlaceholder);
|
||||||
break;
|
break;
|
||||||
|
|
@ -93,7 +108,6 @@ public class WebcamView {
|
||||||
statusPlaceholder.setText("");
|
statusPlaceholder.setText("");
|
||||||
getChildren().clear();
|
getChildren().clear();
|
||||||
}
|
}
|
||||||
requestLayout();
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -102,14 +116,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(), "image/" + importer.getWalletModel().getType() + ".png");
|
super(importer.getName(), "Extended key import", importer.getKeystoreImportDescription(), importer.getWalletModel());
|
||||||
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", "", "image/" + WalletModel.SEED.getType() + ".png");
|
super("Master Private Key", "BIP32 key", "", WalletModel.SEED);
|
||||||
this.wallet = null;
|
this.wallet = null;
|
||||||
this.importer = null;
|
this.importer = null;
|
||||||
this.defaultDerivation = keystore.getKeyDerivation();
|
this.defaultDerivation = keystore.getKeyDerivation();
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,23 @@
|
||||||
|
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,6 +1,7 @@
|
||||||
package com.sparrowwallet.sparrow.event;
|
package com.sparrowwallet.sparrow.event;
|
||||||
|
|
||||||
import com.sparrowwallet.drongo.protocol.BlockHeader;
|
import com.sparrowwallet.drongo.protocol.BlockHeader;
|
||||||
|
import com.sparrowwallet.sparrow.AppServices;
|
||||||
import com.sparrowwallet.sparrow.net.MempoolRateSize;
|
import com.sparrowwallet.sparrow.net.MempoolRateSize;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
@ -13,6 +14,7 @@ public class ConnectionEvent extends FeeRatesUpdatedEvent {
|
||||||
private final int blockHeight;
|
private final int blockHeight;
|
||||||
private final BlockHeader blockHeader;
|
private final BlockHeader blockHeader;
|
||||||
private final Double minimumRelayFeeRate;
|
private final Double minimumRelayFeeRate;
|
||||||
|
private final Double previousMinimumRelayFeeRate;
|
||||||
|
|
||||||
public ConnectionEvent(List<String> serverVersion, String serverBanner, int blockHeight, BlockHeader blockHeader, Map<Integer, Double> targetBlockFeeRates, Set<MempoolRateSize> mempoolRateSizes, Double minimumRelayFeeRate) {
|
public ConnectionEvent(List<String> serverVersion, String serverBanner, int blockHeight, BlockHeader blockHeader, Map<Integer, Double> targetBlockFeeRates, Set<MempoolRateSize> mempoolRateSizes, Double minimumRelayFeeRate) {
|
||||||
super(targetBlockFeeRates, mempoolRateSizes);
|
super(targetBlockFeeRates, mempoolRateSizes);
|
||||||
|
|
@ -21,6 +23,7 @@ public class ConnectionEvent extends FeeRatesUpdatedEvent {
|
||||||
this.blockHeight = blockHeight;
|
this.blockHeight = blockHeight;
|
||||||
this.blockHeader = blockHeader;
|
this.blockHeader = blockHeader;
|
||||||
this.minimumRelayFeeRate = minimumRelayFeeRate;
|
this.minimumRelayFeeRate = minimumRelayFeeRate;
|
||||||
|
this.previousMinimumRelayFeeRate = AppServices.getMinimumRelayFeeRate();
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<String> getServerVersion() {
|
public List<String> getServerVersion() {
|
||||||
|
|
@ -42,4 +45,8 @@ public class ConnectionEvent extends FeeRatesUpdatedEvent {
|
||||||
public Double getMinimumRelayFeeRate() {
|
public Double getMinimumRelayFeeRate() {
|
||||||
return minimumRelayFeeRate;
|
return minimumRelayFeeRate;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Double getPreviousMinimumRelayFeeRate() {
|
||||||
|
return previousMinimumRelayFeeRate;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,13 +7,23 @@ import java.util.Set;
|
||||||
|
|
||||||
public class FeeRatesUpdatedEvent extends MempoolRateSizesUpdatedEvent {
|
public class FeeRatesUpdatedEvent extends MempoolRateSizesUpdatedEvent {
|
||||||
private final Map<Integer, Double> targetBlockFeeRates;
|
private final Map<Integer, Double> targetBlockFeeRates;
|
||||||
|
private final Double nextBlockMedianFeeRate;
|
||||||
|
|
||||||
public FeeRatesUpdatedEvent(Map<Integer, Double> targetBlockFeeRates, Set<MempoolRateSize> mempoolRateSizes) {
|
public FeeRatesUpdatedEvent(Map<Integer, Double> targetBlockFeeRates, Set<MempoolRateSize> mempoolRateSizes) {
|
||||||
|
this(targetBlockFeeRates, mempoolRateSizes, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public FeeRatesUpdatedEvent(Map<Integer, Double> targetBlockFeeRates, Set<MempoolRateSize> mempoolRateSizes, Double nextBlockMedianFeeRate) {
|
||||||
super(mempoolRateSizes);
|
super(mempoolRateSizes);
|
||||||
this.targetBlockFeeRates = targetBlockFeeRates;
|
this.targetBlockFeeRates = targetBlockFeeRates;
|
||||||
|
this.nextBlockMedianFeeRate = nextBlockMedianFeeRate;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Map<Integer, Double> getTargetBlockFeeRates() {
|
public Map<Integer, Double> getTargetBlockFeeRates() {
|
||||||
return targetBlockFeeRates;
|
return targetBlockFeeRates;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Double getNextBlockMedianFeeRate() {
|
||||||
|
return nextBlockMedianFeeRate;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,17 @@
|
||||||
|
package com.sparrowwallet.sparrow.event;
|
||||||
|
|
||||||
|
import com.sparrowwallet.drongo.wallet.Payment;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public class RequestSendToManyEvent {
|
||||||
|
private final List<Payment> payments;
|
||||||
|
|
||||||
|
public RequestSendToManyEvent(List<Payment> payments) {
|
||||||
|
this.payments = payments;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<Payment> getPayments() {
|
||||||
|
return payments;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -17,12 +17,13 @@ public class SpendUtxoEvent {
|
||||||
private final boolean requireAllUtxos;
|
private final boolean requireAllUtxos;
|
||||||
private final BlockTransaction replacedTransaction;
|
private final BlockTransaction replacedTransaction;
|
||||||
private final PaymentCode paymentCode;
|
private final PaymentCode paymentCode;
|
||||||
|
private final boolean allowPaymentChanges;
|
||||||
|
|
||||||
public SpendUtxoEvent(Wallet wallet, List<BlockTransactionHashIndex> utxos) {
|
public SpendUtxoEvent(Wallet wallet, List<BlockTransactionHashIndex> utxos) {
|
||||||
this(wallet, utxos, null, null, null, false, null);
|
this(wallet, utxos, null, null, null, false, null, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
public SpendUtxoEvent(Wallet wallet, List<BlockTransactionHashIndex> utxos, List<Payment> payments, List<byte[]> opReturns, Long fee, boolean requireAllUtxos, BlockTransaction replacedTransaction) {
|
public SpendUtxoEvent(Wallet wallet, List<BlockTransactionHashIndex> utxos, List<Payment> payments, List<byte[]> opReturns, Long fee, boolean requireAllUtxos, BlockTransaction replacedTransaction, boolean allowPaymentChanges) {
|
||||||
this.wallet = wallet;
|
this.wallet = wallet;
|
||||||
this.utxos = utxos;
|
this.utxos = utxos;
|
||||||
this.payments = payments;
|
this.payments = payments;
|
||||||
|
|
@ -31,6 +32,7 @@ public class SpendUtxoEvent {
|
||||||
this.requireAllUtxos = requireAllUtxos;
|
this.requireAllUtxos = requireAllUtxos;
|
||||||
this.replacedTransaction = replacedTransaction;
|
this.replacedTransaction = replacedTransaction;
|
||||||
this.paymentCode = null;
|
this.paymentCode = null;
|
||||||
|
this.allowPaymentChanges = allowPaymentChanges;
|
||||||
}
|
}
|
||||||
|
|
||||||
public SpendUtxoEvent(Wallet wallet, List<Payment> payments, List<byte[]> opReturns, PaymentCode paymentCode) {
|
public SpendUtxoEvent(Wallet wallet, List<Payment> payments, List<byte[]> opReturns, PaymentCode paymentCode) {
|
||||||
|
|
@ -42,6 +44,7 @@ public class SpendUtxoEvent {
|
||||||
this.requireAllUtxos = false;
|
this.requireAllUtxos = false;
|
||||||
this.replacedTransaction = null;
|
this.replacedTransaction = null;
|
||||||
this.paymentCode = paymentCode;
|
this.paymentCode = paymentCode;
|
||||||
|
this.allowPaymentChanges = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Wallet getWallet() {
|
public Wallet getWallet() {
|
||||||
|
|
@ -75,4 +78,8 @@ public class SpendUtxoEvent {
|
||||||
public PaymentCode getPaymentCode() {
|
public PaymentCode getPaymentCode() {
|
||||||
return paymentCode;
|
return paymentCode;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public boolean allowPaymentChanges() {
|
||||||
|
return allowPaymentChanges;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,9 @@
|
||||||
|
package com.sparrowwallet.sparrow.event;
|
||||||
|
|
||||||
|
import com.sparrowwallet.drongo.protocol.Transaction;
|
||||||
|
|
||||||
|
public class TransactionOutputsChangedEvent extends TransactionChangedEvent {
|
||||||
|
public TransactionOutputsChangedEvent(Transaction transaction) {
|
||||||
|
super(transaction);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -14,9 +14,16 @@ import java.util.List;
|
||||||
*/
|
*/
|
||||||
public class WalletNodeHistoryChangedEvent {
|
public class WalletNodeHistoryChangedEvent {
|
||||||
private final String scriptHash;
|
private final String scriptHash;
|
||||||
|
private final String status;
|
||||||
|
|
||||||
public WalletNodeHistoryChangedEvent(String scriptHash) {
|
public WalletNodeHistoryChangedEvent(String scriptHash) {
|
||||||
this.scriptHash = scriptHash;
|
this.scriptHash = scriptHash;
|
||||||
|
this.status = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public WalletNodeHistoryChangedEvent(String scriptHash, String status) {
|
||||||
|
this.scriptHash = scriptHash;
|
||||||
|
this.status = status;
|
||||||
}
|
}
|
||||||
|
|
||||||
public WalletNode getWalletNode(Wallet wallet) {
|
public WalletNode getWalletNode(Wallet wallet) {
|
||||||
|
|
@ -70,4 +77,8 @@ public class WalletNodeHistoryChangedEvent {
|
||||||
public String getScriptHash() {
|
public String getScriptHash() {
|
||||||
return scriptHash;
|
return scriptHash;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public String getStatus() {
|
||||||
|
return status;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,15 @@
|
||||||
package com.sparrowwallet.sparrow.event;
|
package com.sparrowwallet.sparrow.event;
|
||||||
|
|
||||||
public class WebcamResolutionChangedEvent {
|
import com.sparrowwallet.sparrow.control.WebcamResolution;
|
||||||
private final boolean hdResolution;
|
|
||||||
|
|
||||||
public WebcamResolutionChangedEvent(boolean hdResolution) {
|
public class WebcamResolutionChangedEvent {
|
||||||
this.hdResolution = hdResolution;
|
private final WebcamResolution resolution;
|
||||||
|
|
||||||
|
public WebcamResolutionChangedEvent(WebcamResolution resolution) {
|
||||||
|
this.resolution = resolution;
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean isHdResolution() {
|
public WebcamResolution getResolution() {
|
||||||
return hdResolution;
|
return resolution;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,7 @@ 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,6 +1,7 @@
|
||||||
package com.sparrowwallet.sparrow.glyphfont;
|
package com.sparrowwallet.sparrow.glyphfont;
|
||||||
|
|
||||||
import com.sparrowwallet.drongo.wallet.Payment;
|
import com.sparrowwallet.drongo.wallet.Payment;
|
||||||
|
import com.sparrowwallet.drongo.wallet.WalletNodePayment;
|
||||||
import com.sparrowwallet.drongo.wallet.WalletTransaction;
|
import com.sparrowwallet.drongo.wallet.WalletTransaction;
|
||||||
import com.sparrowwallet.sparrow.AppServices;
|
import com.sparrowwallet.sparrow.AppServices;
|
||||||
import com.sparrowwallet.sparrow.control.TransactionDiagram;
|
import com.sparrowwallet.sparrow.control.TransactionDiagram;
|
||||||
|
|
@ -13,7 +14,9 @@ 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(walletTx.isConsolidationSend(payment)) {
|
} else if(payment.getType().equals(Payment.Type.ANCHOR)) {
|
||||||
|
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();
|
||||||
|
|
@ -211,10 +214,24 @@ 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) +
|
OutputDescriptor.getOutputDescriptor(wallet, KeyPurpose.DEFAULT_PURPOSES, null) +
|
||||||
"\n/0/*,/1/*\n" +
|
"\n/0/*,/1/*\n" +
|
||||||
wallet.getNode(KeyPurpose.RECEIVE).getChildren().iterator().next().getAddress();
|
wallet.getNode(KeyPurpose.RECEIVE).getChildren().iterator().next().getAddress();
|
||||||
outputStream.write(record.getBytes(StandardCharsets.UTF_8));
|
outputStream.write(record.getBytes(StandardCharsets.UTF_8));
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@ import java.io.InputStream;
|
||||||
public class BlueWalletMultisig extends ColdcardMultisig {
|
public class BlueWalletMultisig extends ColdcardMultisig {
|
||||||
@Override
|
@Override
|
||||||
public String getName() {
|
public String getName() {
|
||||||
return "Blue Wallet Vault Multisig";
|
return "BlueWallet Vault Multisig";
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
@ -21,7 +21,7 @@ public class BlueWalletMultisig extends ColdcardMultisig {
|
||||||
public Wallet importWallet(InputStream inputStream, String password) throws ImportException {
|
public Wallet importWallet(InputStream inputStream, String password) throws ImportException {
|
||||||
Wallet wallet = super.importWallet(inputStream, password);
|
Wallet wallet = super.importWallet(inputStream, password);
|
||||||
for(Keystore keystore : wallet.getKeystores()) {
|
for(Keystore keystore : wallet.getKeystores()) {
|
||||||
keystore.setLabel(keystore.getLabel().replace("Coldcard", "Blue Wallet"));
|
keystore.setLabel(keystore.getLabel().replace("Coldcard", "BlueWallet"));
|
||||||
keystore.setWalletModel(WalletModel.BLUE_WALLET);
|
keystore.setWalletModel(WalletModel.BLUE_WALLET);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -30,12 +30,12 @@ public class BlueWalletMultisig extends ColdcardMultisig {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String getWalletImportDescription() {
|
public String getWalletImportDescription() {
|
||||||
return "Import file or QR created by using the Wallet > Export Coordination Setup feature on your Blue Wallet Vault wallet.";
|
return "Import file or QR created by using the Wallet > Export Coordination Setup feature on your BlueWallet Vault wallet.";
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String getWalletExportDescription() {
|
public String getWalletExportDescription() {
|
||||||
return "Export file that can be read by Blue Wallet using the Add Wallet > Vault > Import wallet feature.";
|
return "Export file that can be read by BlueWallet using the Add Wallet > Vault > Import wallet feature.";
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
|
||||||
|
|
@ -2,10 +2,12 @@ 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;
|
||||||
|
|
@ -51,15 +53,19 @@ 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 Boolean hdCapture;
|
private WebcamResolution webcamResolution;
|
||||||
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;
|
||||||
|
|
@ -68,6 +74,7 @@ public class Config {
|
||||||
private File coreDataDir;
|
private File coreDataDir;
|
||||||
private String coreAuth;
|
private String coreAuth;
|
||||||
private boolean useLegacyCoreWallet;
|
private boolean useLegacyCoreWallet;
|
||||||
|
private boolean legacyServer;
|
||||||
private Server electrumServer;
|
private Server electrumServer;
|
||||||
private List<Server> recentElectrumServers;
|
private List<Server> recentElectrumServers;
|
||||||
private File electrumServerCert;
|
private File electrumServerCert;
|
||||||
|
|
@ -78,6 +85,7 @@ public class Config {
|
||||||
private int maxPageSize = DEFAULT_PAGE_SIZE;
|
private int maxPageSize = DEFAULT_PAGE_SIZE;
|
||||||
private boolean usePayNym;
|
private boolean usePayNym;
|
||||||
private boolean mempoolFullRbf;
|
private boolean mempoolFullRbf;
|
||||||
|
private double minRelayFeeRate = Transaction.DEFAULT_MIN_RELAY_FEE;
|
||||||
private Double appWidth;
|
private Double appWidth;
|
||||||
private Double appHeight;
|
private Double appHeight;
|
||||||
|
|
||||||
|
|
@ -346,6 +354,34 @@ public class Config {
|
||||||
|
|
||||||
public void setPreventSleep(boolean preventSleep) {
|
public void setPreventSleep(boolean preventSleep) {
|
||||||
this.preventSleep = preventSleep;
|
this.preventSleep = preventSleep;
|
||||||
|
flush();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Boolean getConnectToBroadcast() {
|
||||||
|
return connectToBroadcast;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setConnectToBroadcast(Boolean connectToBroadcast) {
|
||||||
|
this.connectToBroadcast = connectToBroadcast;
|
||||||
|
flush();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Boolean getConnectToResolve() {
|
||||||
|
return connectToResolve;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setConnectToResolve(Boolean connectToResolve) {
|
||||||
|
this.connectToResolve = connectToResolve;
|
||||||
|
flush();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Boolean getSuggestSendToMany() {
|
||||||
|
return suggestSendToMany;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setSuggestSendToMany(Boolean suggestSendToMany) {
|
||||||
|
this.suggestSendToMany = suggestSendToMany;
|
||||||
|
flush();
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<File> getRecentWalletFiles() {
|
public List<File> getRecentWalletFiles() {
|
||||||
|
|
@ -383,16 +419,12 @@ public class Config {
|
||||||
flush();
|
flush();
|
||||||
}
|
}
|
||||||
|
|
||||||
public Boolean getHdCapture() {
|
public WebcamResolution getWebcamResolution() {
|
||||||
return hdCapture;
|
return webcamResolution;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Boolean isHdCapture() {
|
public void setWebcamResolution(WebcamResolution webcamResolution) {
|
||||||
return hdCapture != null && hdCapture;
|
this.webcamResolution = webcamResolution;
|
||||||
}
|
|
||||||
|
|
||||||
public void setHdCapture(Boolean hdCapture) {
|
|
||||||
this.hdCapture = hdCapture;
|
|
||||||
flush();
|
flush();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -418,6 +450,15 @@ public class Config {
|
||||||
flush();
|
flush();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public String getWebcamDeviceId() {
|
||||||
|
return webcamDeviceId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setWebcamDeviceId(String webcamDeviceId) {
|
||||||
|
this.webcamDeviceId = webcamDeviceId;
|
||||||
|
flush();
|
||||||
|
}
|
||||||
|
|
||||||
public ServerType getServerType() {
|
public ServerType getServerType() {
|
||||||
return serverType;
|
return serverType;
|
||||||
}
|
}
|
||||||
|
|
@ -552,6 +593,15 @@ public class Config {
|
||||||
flush();
|
flush();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public boolean isLegacyServer() {
|
||||||
|
return legacyServer;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setLegacyServer(boolean legacyServer) {
|
||||||
|
this.legacyServer = legacyServer;
|
||||||
|
flush();
|
||||||
|
}
|
||||||
|
|
||||||
public Server getElectrumServer() {
|
public Server getElectrumServer() {
|
||||||
return electrumServer;
|
return electrumServer;
|
||||||
}
|
}
|
||||||
|
|
@ -670,6 +720,14 @@ public class Config {
|
||||||
flush();
|
flush();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public double getMinRelayFeeRate() {
|
||||||
|
return minRelayFeeRate;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setMinRelayFeeRate(double minRelayFeeRate) {
|
||||||
|
this.minRelayFeeRate = minRelayFeeRate;
|
||||||
|
}
|
||||||
|
|
||||||
public Double getAppWidth() {
|
public Double getAppWidth() {
|
||||||
return appWidth;
|
return appWidth;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,12 @@
|
||||||
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;
|
||||||
|
|
@ -92,7 +95,7 @@ public class Descriptor implements WalletImport, WalletExport {
|
||||||
InputStream secondClone = new ByteArrayInputStream(baos.toByteArray());
|
InputStream secondClone = new ByteArrayInputStream(baos.toByteArray());
|
||||||
|
|
||||||
try {
|
try {
|
||||||
return PdfUtils.getOutputDescriptor(firstClone).toWallet();
|
return ensureKeyDerivations(PdfUtils.getOutputDescriptor(firstClone).toWallet());
|
||||||
} catch(Exception e) {
|
} catch(Exception e) {
|
||||||
//ignore
|
//ignore
|
||||||
}
|
}
|
||||||
|
|
@ -100,7 +103,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 descriptor.toWallet();
|
return ensureKeyDerivations(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");
|
||||||
|
|
@ -116,24 +119,34 @@ 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.length() > 0) {
|
if(!paragraph.isEmpty()) {
|
||||||
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);
|
paragraph.append(line.replaceFirst("^.+:", "").trim());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if(paragraph.length() > 0) {
|
if(!paragraph.isEmpty()) {
|
||||||
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,7 +37,8 @@ public class ElectrumPersonalServer implements WalletExport {
|
||||||
try {
|
try {
|
||||||
BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(outputStream, StandardCharsets.UTF_8));
|
BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(outputStream, StandardCharsets.UTF_8));
|
||||||
writer.write("# Electrum Personal Server configuration file fragments\n");
|
writer.write("# Electrum Personal Server configuration file fragments\n");
|
||||||
writer.write("# Copy the lines below into the relevant sections in your EPS config.ini file\n\n");
|
writer.write("# First close Sparrow and edit your config file in Sparrow home to set \"legacyServer\": true\n");
|
||||||
|
writer.write("# Then copy the lines below into the relevant sections in your EPS config.ini file\n\n");
|
||||||
writer.write("# Copy into [master-public-keys] section\n");
|
writer.write("# Copy into [master-public-keys] section\n");
|
||||||
Wallet masterWallet = wallet.isMasterWallet() ? wallet : wallet.getMasterWallet();
|
Wallet masterWallet = wallet.isMasterWallet() ? wallet : wallet.getMasterWallet();
|
||||||
writeWalletXpub(masterWallet, writer);
|
writeWalletXpub(masterWallet, writer);
|
||||||
|
|
|
||||||
|
|
@ -1,5 +0,0 @@
|
||||||
package com.sparrowwallet.sparrow.io;
|
|
||||||
|
|
||||||
public enum FileType {
|
|
||||||
TEXT, JSON, BINARY, UNKNOWN;
|
|
||||||
}
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
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;
|
||||||
|
|
@ -56,7 +57,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("Error scanning" + (e.getMessage() == null || e.getMessage().isEmpty() ? ", check devices are ready" : ": " + e.getMessage()), e);
|
throw new ImportException(e.getMessage() == null || e.getMessage().isEmpty() ? "Error scanning, check devices are ready" : e.getMessage(), e);
|
||||||
} finally {
|
} finally {
|
||||||
isPromptActive = false;
|
isPromptActive = false;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,153 +0,0 @@
|
||||||
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,6 +2,8 @@ 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 = new BasicThreadFactory.Builder().namingPattern("LoadWalletService-single").daemon(true).priority(Thread.MIN_PRIORITY).build();
|
BasicThreadFactory factory = BasicThreadFactory.builder().namingPattern("LoadWalletService-single").daemon(true).priority(Thread.MIN_PRIORITY).build();
|
||||||
singleThreadedExecutor = Executors.newSingleThreadScheduledExecutor(factory);
|
singleThreadedExecutor = Executors.newSingleThreadScheduledExecutor(factory);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,33 +1,43 @@
|
||||||
package com.sparrowwallet.sparrow.io;
|
package com.sparrowwallet.sparrow.io;
|
||||||
|
|
||||||
import com.csvreader.CsvReader;
|
import com.csvreader.CsvReader;
|
||||||
import com.google.gson.Gson;
|
import com.google.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;
|
||||||
}
|
}
|
||||||
|
|
@ -50,8 +60,9 @@ 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<>();
|
||||||
List<Wallet> allWallets = wallet.isMasterWallet() ? wallet.getAllWallets() : wallet.getMasterWallet().getAllWallets();
|
Map<Date, Double> fiatRates = getFiatRates(walletForms);
|
||||||
for(Wallet exportWallet : allWallets) {
|
for(WalletForm exportWalletForm : walletForms) {
|
||||||
|
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);
|
||||||
|
|
||||||
|
|
@ -61,34 +72,43 @@ public class WalletLabels implements WalletImport, WalletExport {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for(BlockTransaction blkTx : exportWallet.getWalletTransactions().values()) {
|
Set<Sha256Hash> confirmingTxs = new HashSet<>();
|
||||||
if(blkTx.getLabel() != null && !blkTx.getLabel().isEmpty()) {
|
WalletTransactionsEntry walletTransactionsEntry = exportWalletForm.getWalletTransactionsEntry();
|
||||||
labels.add(new Label(Type.tx, blkTx.getHashAsString(), blkTx.getLabel(), origin, null));
|
for(Entry entry : walletTransactionsEntry.getChildren()) {
|
||||||
|
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()) {
|
||||||
if(addressNode.getLabel() != null && !addressNode.getLabel().isEmpty()) {
|
labels.add(new AddressLabel(addressNode.getAddress().toString(), addressNode.getLabel(), origin, addressNode.getDerivationPath().substring(1),
|
||||||
labels.add(new Label(Type.addr, addressNode.getAddress().toString(), addressNode.getLabel(), null, null));
|
addressNode.getTransactionOutputs().stream().flatMap(txo -> txo.isSpent() ? Stream.of(txo, txo.getSpentBy()) : Stream.of(txo))
|
||||||
}
|
.filter(ref -> !confirmingTxs.contains(ref.getHash())).map(BlockTransactionHash::getHeight).toList()));
|
||||||
}
|
}
|
||||||
|
|
||||||
for(BlockTransactionHashIndex txo : exportWallet.getWalletTxos().keySet()) {
|
for(Map.Entry<BlockTransactionHashIndex, WalletNode> txoEntry : exportWallet.getWalletTxos().entrySet()) {
|
||||||
String spendable = (txo.isSpent() ? null : txo.getStatus() == Status.FROZEN ? "false" : "true");
|
BlockTransactionHashIndex txo = txoEntry.getKey();
|
||||||
if(txo.getLabel() != null && !txo.getLabel().isEmpty()) {
|
WalletNode addressNode = txoEntry.getValue();
|
||||||
labels.add(new Label(Type.output, txo.toString(), txo.getLabel(), null, spendable));
|
Boolean spendable = (txo.isSpent() ? null : txo.getStatus() != Status.FROZEN);
|
||||||
} else if(!txo.isSpent()) {
|
labels.add(new InputOutputLabel(Type.output, txo.toString(), txo.getLabel(), origin, spendable, addressNode.getDerivationPath().substring(1), txo.getValue(),
|
||||||
labels.add(new Label(Type.output, txo.toString(), null, null, spendable));
|
confirmingTxs.contains(txo.getHash()) ? null : txo.getHeight(), txo.getDate(), getFiatValue(txo, fiatRates)));
|
||||||
}
|
|
||||||
|
|
||||||
if(txo.isSpent() && txo.getSpentBy().getLabel() != null && !txo.getSpentBy().getLabel().isEmpty()) {
|
if(txo.isSpent()) {
|
||||||
labels.add(new Label(Type.input, txo.getSpentBy().toString(), txo.getSpentBy().getLabel(), null, null));
|
BlockTransactionHashIndex txi = txo.getSpentBy();
|
||||||
|
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 Gson();
|
Gson gson = new GsonBuilder().registerTypeAdapter(Date.class, new GsonUTCDateAdapter()).create();
|
||||||
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) {
|
||||||
|
|
@ -114,7 +134,7 @@ public class WalletLabels implements WalletImport, WalletExport {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean isWalletExportScannable() {
|
public boolean isWalletExportScannable() {
|
||||||
return false;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
@ -185,7 +205,7 @@ public class WalletLabels implements WalletImport, WalletExport {
|
||||||
}
|
}
|
||||||
|
|
||||||
OutputDescriptor outputDescriptor = OutputDescriptor.getOutputDescriptor(wallet);
|
OutputDescriptor outputDescriptor = OutputDescriptor.getOutputDescriptor(wallet);
|
||||||
String origin = outputDescriptor.toString(true, false, false);
|
Origin origin = Origin.fromOutputDescriptor(outputDescriptor);
|
||||||
|
|
||||||
List<Entry> transactionEntries = walletForm.getWalletTransactionsEntry().getChildren();
|
List<Entry> transactionEntries = walletForm.getWalletTransactionsEntry().getChildren();
|
||||||
List<Entry> addressEntries = new ArrayList<>();
|
List<Entry> addressEntries = new ArrayList<>();
|
||||||
|
|
@ -194,7 +214,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 && !label.origin.equals(origin)) {
|
if(label.origin != null && !Origin.fromString(label.origin).equals(origin)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -247,11 +267,11 @@ public class WalletLabels implements WalletImport, WalletExport {
|
||||||
addChangedEntry(changedWalletEntries, txioEntry);
|
addChangedEntry(changedWalletEntries, txioEntry);
|
||||||
}
|
}
|
||||||
|
|
||||||
if(label.type == Type.output && !reference.isSpent()) {
|
if(label.type == Type.output && !reference.isSpent() && label.spendable != null) {
|
||||||
if("false".equalsIgnoreCase(label.spendable) && reference.getStatus() != Status.FROZEN) {
|
if(!label.spendable && reference.getStatus() != Status.FROZEN) {
|
||||||
reference.setStatus(Status.FROZEN);
|
reference.setStatus(Status.FROZEN);
|
||||||
addChangedUtxo(changedWalletUtxoStatuses, txioEntry);
|
addChangedUtxo(changedWalletUtxoStatuses, txioEntry);
|
||||||
} else if("true".equalsIgnoreCase(label.spendable) && reference.getStatus() == Status.FROZEN) {
|
} else if(label.spendable && reference.getStatus() == Status.FROZEN) {
|
||||||
reference.setStatus(null);
|
reference.setStatus(null);
|
||||||
addChangedUtxo(changedWalletUtxoStatuses, txioEntry);
|
addChangedUtxo(changedWalletUtxoStatuses, txioEntry);
|
||||||
}
|
}
|
||||||
|
|
@ -316,7 +336,7 @@ public class WalletLabels implements WalletImport, WalletExport {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean isWalletImportScannable() {
|
public boolean isWalletImportScannable() {
|
||||||
return false;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
@ -324,12 +344,99 @@ 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, String spendable) {
|
public Label(Type type, String ref, String label, String origin, Boolean spendable) {
|
||||||
this.type = type;
|
this.type = type;
|
||||||
this.ref = ref;
|
this.ref = ref;
|
||||||
this.label = label;
|
this.label = label;
|
||||||
|
|
@ -341,6 +448,119 @@ public class WalletLabels implements WalletImport, WalletExport {
|
||||||
String ref;
|
String ref;
|
||||||
String label;
|
String label;
|
||||||
String origin;
|
String origin;
|
||||||
String spendable;
|
Boolean 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