mirror of
https://github.com/sparrowwallet/sparrow.git
synced 2025-11-05 11:56:37 +00:00
Compare commits
No commits in common. "master" and "1.8.3" have entirely different histories.
645 changed files with 13292 additions and 12774 deletions
23
.github/workflows/package.yaml
vendored
23
.github/workflows/package.yaml
vendored
|
|
@ -10,16 +10,16 @@ jobs:
|
||||||
runs-on: ${{ matrix.os }}
|
runs-on: ${{ matrix.os }}
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
os: [windows-2022, ubuntu-22.04, ubuntu-22.04-arm, macos-13, macos-14]
|
os: [windows-2022, ubuntu-20.04, macos-12]
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v5
|
- uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
submodules: true
|
submodules: true
|
||||||
- name: Set up JDK 22.0.2
|
- name: Set up JDK 18.0.1
|
||||||
uses: actions/setup-java@v5
|
uses: actions/setup-java@v4
|
||||||
with:
|
with:
|
||||||
distribution: 'temurin'
|
distribution: 'temurin'
|
||||||
java-version: '22.0.2'
|
java-version: '18.0.1'
|
||||||
- name: Show Build Versions
|
- name: Show Build Versions
|
||||||
run: ./gradlew -v
|
run: ./gradlew -v
|
||||||
- name: Build with Gradle
|
- name: Build with Gradle
|
||||||
|
|
@ -30,13 +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: Repackage deb distribution
|
- name: Upload Artifacts
|
||||||
if: ${{ runner.os == 'Linux' }}
|
|
||||||
run: ./repackage.sh
|
|
||||||
- name: Upload Artifact
|
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: Sparrow Build - ${{ runner.os }} ${{ runner.arch }}
|
name: Sparrow Build - ${{ runner.os }}
|
||||||
path: |
|
path: |
|
||||||
build/jpackage/*
|
build/jpackage/*
|
||||||
!build/jpackage/Sparrow/
|
!build/jpackage/Sparrow/
|
||||||
|
|
@ -46,14 +43,14 @@ jobs:
|
||||||
- name: Package headless tar distribution
|
- name: Package headless tar distribution
|
||||||
if: ${{ runner.os == 'Linux' }}
|
if: ${{ runner.os == 'Linux' }}
|
||||||
run: ./gradlew -Djava.awt.headless=true packageTarDistribution
|
run: ./gradlew -Djava.awt.headless=true packageTarDistribution
|
||||||
- name: Repackage headless deb distribution
|
- name: Rename Headless Artifacts
|
||||||
if: ${{ runner.os == 'Linux' }}
|
if: ${{ runner.os == 'Linux' }}
|
||||||
run: ./repackage.sh
|
run: for f in build/jpackage/sparrow*; do mv -v "$f" "${f/sparrow/sparrow-server}"; done;
|
||||||
- name: Upload Headless Artifact
|
- name: Upload Headless Artifact
|
||||||
if: ${{ runner.os == 'Linux' }}
|
if: ${{ runner.os == 'Linux' }}
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: Sparrow Build - ${{ runner.os }} ${{ runner.arch }} Headless
|
name: Sparrow Build - ${{ runner.os }} Headless
|
||||||
path: |
|
path: |
|
||||||
build/jpackage/*
|
build/jpackage/*
|
||||||
!build/jpackage/Sparrow/
|
!build/jpackage/Sparrow/
|
||||||
3
.gitmodules
vendored
3
.gitmodules
vendored
|
|
@ -1,6 +1,3 @@
|
||||||
[submodule "drongo"]
|
[submodule "drongo"]
|
||||||
path = drongo
|
path = drongo
|
||||||
url = ../../sparrowwallet/drongo.git
|
url = ../../sparrowwallet/drongo.git
|
||||||
[submodule "lark"]
|
|
||||||
path = lark
|
|
||||||
url = ../../sparrowwallet/lark.git
|
|
||||||
|
|
|
||||||
14
README.md
14
README.md
|
|
@ -16,8 +16,8 @@ or for those without SSH credentials:
|
||||||
|
|
||||||
`git clone --recursive https://github.com/sparrowwallet/sparrow.git`
|
`git clone --recursive https://github.com/sparrowwallet/sparrow.git`
|
||||||
|
|
||||||
In order to build, Sparrow requires Java 22 or higher to be installed.
|
In order to build, Sparrow requires Java 18 or higher to be installed.
|
||||||
The release binaries are built with [Eclipse Temurin 22.0.2+9](https://github.com/adoptium/temurin22-binaries/releases/tag/jdk-22.0.2%2B9).
|
The release binaries are built with [Eclipse Temurin 18.0.1+10](https://github.com/adoptium/temurin18-binaries/releases/tag/jdk-18.0.1%2B10).
|
||||||
|
|
||||||
Other packages may also be necessary to build depending on the platform. On Debian/Ubuntu systems:
|
Other packages may also be necessary to build depending on the platform. On Debian/Ubuntu systems:
|
||||||
|
|
||||||
|
|
@ -44,7 +44,7 @@ If you prefer to run Sparrow directly from source, it can be launched from withi
|
||||||
|
|
||||||
`./sparrow`
|
`./sparrow`
|
||||||
|
|
||||||
Java 22 or higher must be installed.
|
Java 18 or higher must be installed.
|
||||||
|
|
||||||
## Configuration
|
## Configuration
|
||||||
|
|
||||||
|
|
@ -64,12 +64,10 @@ Usage: sparrow [options]
|
||||||
Possible Values: [ERROR, WARN, INFO, DEBUG, TRACE]
|
Possible Values: [ERROR, WARN, INFO, DEBUG, TRACE]
|
||||||
--network, -n
|
--network, -n
|
||||||
Network to use
|
Network to use
|
||||||
Possible Values: [mainnet, testnet, regtest, signet, testnet4]
|
Possible Values: [mainnet, testnet, regtest, signet]
|
||||||
```
|
```
|
||||||
|
|
||||||
Note that testnet currently refers to testnet3.
|
As a fallback, the network (mainnet, testnet, regtest or signet) can also be set using an environment variable `SPARROW_NETWORK`. For example:
|
||||||
|
|
||||||
As a fallback, the network (mainnet, testnet, testnet4, regtest or signet) can also be set using an environment variable `SPARROW_NETWORK`. For example:
|
|
||||||
|
|
||||||
`export SPARROW_NETWORK=testnet`
|
`export SPARROW_NETWORK=testnet`
|
||||||
|
|
||||||
|
|
@ -85,7 +83,7 @@ When not explicitly configured using the command line argument above, Sparrow st
|
||||||
| Linux | ~/.sparrow |
|
| Linux | ~/.sparrow |
|
||||||
| Windows | %APPDATA%/Sparrow |
|
| Windows | %APPDATA%/Sparrow |
|
||||||
|
|
||||||
Testnet3, testnet4, regtest and signet configurations (along with their wallets) are stored in subfolders to allow easy switching between networks.
|
Testnet, regtest and signet configurations (along with their wallets) are stored in subfolders to allow easy switching between networks.
|
||||||
|
|
||||||
## Reporting Issues
|
## Reporting Issues
|
||||||
|
|
||||||
|
|
|
||||||
583
build.gradle
583
build.gradle
|
|
@ -1,38 +1,59 @@
|
||||||
|
import java.awt.GraphicsEnvironment
|
||||||
|
|
||||||
plugins {
|
plugins {
|
||||||
id 'application'
|
id 'application'
|
||||||
id 'org-openjfx-javafxplugin'
|
id 'extra-java-module-info'
|
||||||
id 'org.beryx.jlink' version '3.1.3'
|
id 'org.openjfx.javafxplugin' version '0.1.0'
|
||||||
id 'org.gradlex.extra-java-module-info' version '1.13'
|
id 'org.beryx.jlink' version '3.0.1'
|
||||||
id 'io.matthewnelson.kmp.tor.resource-filterjar' version '408.16.3'
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def sparrowVersion = '1.8.3'
|
||||||
def os = org.gradle.internal.os.OperatingSystem.current()
|
def os = org.gradle.internal.os.OperatingSystem.current()
|
||||||
def osName = os.getFamilyName()
|
def osName = os.getFamilyName()
|
||||||
if(os.macOsX) {
|
if(os.macOsX) {
|
||||||
osName = "osx"
|
osName = "osx"
|
||||||
}
|
}
|
||||||
|
def targetName = ""
|
||||||
def osArch = "x64"
|
def osArch = "x64"
|
||||||
def releaseArch = "x86_64"
|
def releaseArch = "x86_64"
|
||||||
if(System.getProperty("os.arch") == "aarch64") {
|
if(System.getProperty("os.arch") == "aarch64") {
|
||||||
osArch = "aarch64"
|
osArch = "aarch64"
|
||||||
releaseArch = "aarch64"
|
releaseArch = "aarch64"
|
||||||
|
targetName = "-" + osArch
|
||||||
}
|
}
|
||||||
def headless = "true".equals(System.getProperty("java.awt.headless"))
|
def headless = "true".equals(System.getProperty("java.awt.headless"))
|
||||||
|
|
||||||
group = 'com.sparrowwallet'
|
def vTor = '4.7.13-4'
|
||||||
version = '2.3.1'
|
def vKmpTor = '1.4.3'
|
||||||
|
def kmpOs = osName
|
||||||
|
if(os.macOsX) {
|
||||||
|
kmpOs = "macos"
|
||||||
|
} else if(os.windows) {
|
||||||
|
kmpOs = "mingw"
|
||||||
|
}
|
||||||
|
def kmpArch = "x64"
|
||||||
|
if(System.getProperty("os.arch") == "aarch64") {
|
||||||
|
kmpArch = "arm64"
|
||||||
|
}
|
||||||
|
|
||||||
|
group "com.sparrowwallet"
|
||||||
|
version "${sparrowVersion}"
|
||||||
|
|
||||||
repositories {
|
repositories {
|
||||||
mavenCentral()
|
mavenCentral()
|
||||||
maven { url = uri('https://code.sparrowwallet.com/api/packages/sparrowwallet/maven') }
|
maven { url 'https://oss.sonatype.org/content/groups/public' }
|
||||||
|
maven { url 'https://mymavenrepo.com/repo/29EACwkkGcoOKnbx3bxN/' }
|
||||||
|
maven { url 'https://jitpack.io' }
|
||||||
|
maven { url 'https://maven.ecs.soton.ac.uk/content/groups/maven.openimaj.org/' }
|
||||||
}
|
}
|
||||||
|
|
||||||
tasks.withType(AbstractArchiveTask).configureEach {
|
tasks.withType(AbstractArchiveTask) {
|
||||||
useFileSystemPermissions()
|
preserveFileTimestamps = false
|
||||||
|
reproducibleFileOrder = true
|
||||||
}
|
}
|
||||||
|
|
||||||
javafx {
|
javafx {
|
||||||
version = headless ? "18" : "23.0.2"
|
version = "18"
|
||||||
modules = [ 'javafx.controls', 'javafx.fxml', 'javafx.swing', 'javafx.graphics' ]
|
modules = [ 'javafx.controls', 'javafx.fxml', 'javafx.swing', 'javafx.graphics' ]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -43,26 +64,25 @@ java {
|
||||||
dependencies {
|
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('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.49.5') {
|
implementation('org.jdbi:jdbi3-core:3.20.0') {
|
||||||
exclude group: 'org.slf4j'
|
exclude group: 'org.slf4j'
|
||||||
}
|
}
|
||||||
implementation('org.jdbi:jdbi3-sqlobject:3.49.5') {
|
implementation('org.jdbi:jdbi3-sqlobject:3.20.0') {
|
||||||
exclude group: 'org.slf4j'
|
exclude group: 'org.slf4j'
|
||||||
}
|
}
|
||||||
implementation('org.flywaydb:flyway-core:9.22.3')
|
implementation('org.flywaydb:flyway-core:7.10.7-SNAPSHOT')
|
||||||
implementation('org.fxmisc.richtext:richtextfx:0.11.6')
|
implementation('org.fxmisc.richtext:richtextfx:0.10.4')
|
||||||
implementation('no.tornado:tornadofx-controls:1.0.4')
|
implementation('no.tornado:tornadofx-controls:1.0.4')
|
||||||
implementation('com.google.zxing:javase:3.4.0') {
|
implementation('com.google.zxing:javase:3.4.0') {
|
||||||
exclude group: 'com.beust', module: 'jcommander'
|
exclude group: 'com.beust', module: 'jcommander'
|
||||||
}
|
}
|
||||||
implementation('org.jcommander:jcommander:2.0')
|
implementation('com.beust:jcommander:1.81')
|
||||||
implementation('com.github.arteam:simple-json-rpc-core:1.3')
|
implementation('com.github.arteam:simple-json-rpc-core:1.3')
|
||||||
implementation('com.github.arteam:simple-json-rpc-client:1.3') {
|
implementation('com.github.arteam:simple-json-rpc-client:1.3') {
|
||||||
exclude group: 'com.github.arteam', module: 'simple-json-rpc-core'
|
exclude group: 'com.github.arteam', module: 'simple-json-rpc-core'
|
||||||
|
|
@ -70,18 +90,24 @@ dependencies {
|
||||||
implementation('com.github.arteam:simple-json-rpc-server:1.3') {
|
implementation('com.github.arteam:simple-json-rpc-server:1.3') {
|
||||||
exclude group: 'org.slf4j'
|
exclude group: 'org.slf4j'
|
||||||
}
|
}
|
||||||
implementation('com.fasterxml.jackson.core:jackson-databind:2.17.2')
|
implementation('com.sparrowwallet:hummingbird:1.7.3')
|
||||||
implementation('com.sparrowwallet:hummingbird:1.7.4')
|
|
||||||
implementation('co.nstant.in:cbor:0.9')
|
implementation('co.nstant.in:cbor:0.9')
|
||||||
implementation('org.openpnp:openpnp-capture-java:0.0.30-1')
|
implementation("com.nativelibs4java:bridj${targetName}:0.7-20140918-3") {
|
||||||
implementation("io.matthewnelson.kmp-tor:runtime:2.2.1")
|
exclude group: 'com.google.android.tools', module: 'dx'
|
||||||
implementation("io.matthewnelson.kmp-tor:resource-exec-tor-gpl:408.16.3")
|
|
||||||
implementation('org.jetbrains.kotlinx:kotlinx-coroutines-javafx:1.10.1') {
|
|
||||||
exclude group: 'org.jetbrains.kotlin', module: 'kotlin-stdlib-common'
|
|
||||||
}
|
}
|
||||||
implementation('de.jangassen:nsmenufx:3.1.0') {
|
implementation("com.github.sarxos:webcam-capture${targetName}:0.3.13-SNAPSHOT") {
|
||||||
exclude group: 'net.java.dev.jna', module: 'jna'
|
exclude group: 'com.nativelibs4java', module: 'bridj'
|
||||||
}
|
}
|
||||||
|
implementation "io.matthewnelson.kotlin-components:kmp-tor:${vTor}-${vKmpTor}"
|
||||||
|
if(kmpOs == "linux" && kmpArch == "arm64") {
|
||||||
|
implementation("com.sparrowwallet.kmp-tor-binary:kmp-tor-binary-${kmpOs}${kmpArch}-jvm:${vTor}")
|
||||||
|
} else {
|
||||||
|
implementation("io.matthewnelson.kotlin-components:kmp-tor-binary-${kmpOs}${kmpArch}:${vTor}")
|
||||||
|
}
|
||||||
|
implementation("io.matthewnelson.kotlin-components:kmp-tor-binary-extract:${vTor}")
|
||||||
|
implementation("io.matthewnelson.kotlin-components:kmp-tor-ext-callback-manager:${vKmpTor}")
|
||||||
|
implementation('org.jetbrains.kotlinx:kotlinx-coroutines-javafx:1.7.1')
|
||||||
|
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'
|
||||||
|
|
@ -97,20 +123,19 @@ dependencies {
|
||||||
implementation('org.slf4j:jul-to-slf4j:2.0.12') {
|
implementation('org.slf4j:jul-to-slf4j:2.0.12') {
|
||||||
exclude group: 'org.slf4j'
|
exclude group: 'org.slf4j'
|
||||||
}
|
}
|
||||||
|
implementation('com.sparrowwallet.nightjar:nightjar:0.2.40')
|
||||||
implementation('com.sparrowwallet.bokmakierie:bokmakierie:1.0')
|
implementation('com.sparrowwallet.bokmakierie:bokmakierie:1.0')
|
||||||
implementation('com.sparrowwallet:tern:1.0.6')
|
|
||||||
implementation('io.reactivex.rxjava2:rxjava:2.2.15')
|
implementation('io.reactivex.rxjava2:rxjava:2.2.15')
|
||||||
implementation('io.reactivex.rxjava2:rxjavafx:2.2.2')
|
implementation('io.reactivex.rxjava2:rxjavafx:2.2.2')
|
||||||
implementation('org.apache.commons:commons-lang3:3.19.0')
|
implementation('org.apache.commons:commons-lang3:3.7')
|
||||||
implementation('org.apache.commons:commons-compress:1.27.1')
|
implementation('org.apache.commons:commons-compress:1.25.0')
|
||||||
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')
|
||||||
implementation('com.googlecode.lanterna:lanterna:3.1.3')
|
implementation('com.googlecode.lanterna:lanterna:3.1.1')
|
||||||
implementation('net.coobird:thumbnailator:0.4.18')
|
implementation('net.coobird:thumbnailator:0.4.18')
|
||||||
implementation('com.github.hervegirod:fxsvgimage:1.1')
|
implementation('com.github.hervegirod:fxsvgimage:1.0b2')
|
||||||
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')
|
||||||
|
|
@ -135,7 +160,7 @@ processResources {
|
||||||
|
|
||||||
test {
|
test {
|
||||||
useJUnitPlatform()
|
useJUnitPlatform()
|
||||||
jvmArgs = ["--add-opens=java.base/java.io=ALL-UNNAMED", "--add-opens=java.base/java.io=com.google.gson", "--add-reads=org.flywaydb.core=java.desktop"]
|
jvmArgs '--add-opens=java.base/java.io=ALL-UNNAMED'
|
||||||
}
|
}
|
||||||
|
|
||||||
application {
|
application {
|
||||||
|
|
@ -143,12 +168,6 @@ application {
|
||||||
mainClass = 'com.sparrowwallet.sparrow.SparrowWallet'
|
mainClass = 'com.sparrowwallet.sparrow.SparrowWallet'
|
||||||
|
|
||||||
applicationDefaultJvmArgs = ["-XX:+HeapDumpOnOutOfMemoryError",
|
applicationDefaultJvmArgs = ["-XX:+HeapDumpOnOutOfMemoryError",
|
||||||
"--enable-native-access=com.sparrowwallet.drongo",
|
|
||||||
"--enable-native-access=com.sun.jna",
|
|
||||||
"--enable-native-access=javafx.graphics",
|
|
||||||
"--enable-native-access=com.fazecast.jSerialComm",
|
|
||||||
"--enable-native-access=org.usb4java",
|
|
||||||
"--enable-native-access=io.github.doblon8.jzbar",
|
|
||||||
"--add-opens=javafx.graphics/com.sun.javafx.css=org.controlsfx.controls",
|
"--add-opens=javafx.graphics/com.sun.javafx.css=org.controlsfx.controls",
|
||||||
"--add-opens=javafx.graphics/javafx.scene=org.controlsfx.controls",
|
"--add-opens=javafx.graphics/javafx.scene=org.controlsfx.controls",
|
||||||
"--add-opens=javafx.controls/com.sun.javafx.scene.control.behavior=org.controlsfx.controls",
|
"--add-opens=javafx.controls/com.sun.javafx.scene.control.behavior=org.controlsfx.controls",
|
||||||
|
|
@ -158,17 +177,22 @@ 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",
|
||||||
"--add-opens=java.base/java.net=com.sparrowwallet.sparrow",
|
"--add-opens=java.base/java.net=com.sparrowwallet.sparrow",
|
||||||
"--add-opens=java.base/java.io=com.google.gson",
|
"--add-opens=java.base/java.io=com.google.gson",
|
||||||
"--add-opens=java.smartcardio/sun.security.smartcardio=com.sparrowwallet.sparrow",
|
"--add-opens=java.smartcardio/sun.security.smartcardio=com.sparrowwallet.sparrow",
|
||||||
"--add-reads=kotlin.stdlib=kotlinx.coroutines.core",
|
"--add-reads=kotlin.stdlib=kotlinx.coroutines.core"]
|
||||||
"--add-reads=org.flywaydb.core=java.desktop"]
|
|
||||||
|
|
||||||
if(os.macOsX) {
|
if(os.macOsX) {
|
||||||
applicationDefaultJvmArgs += ["-Dprism.lcdtext=false", "-Xdock:name=Sparrow"]
|
applicationDefaultJvmArgs += ["-Dprism.lcdtext=false", "-Xdock:name=Sparrow", "-Xdock:icon=/Users/scy/git/sparrow/src/main/resources/sparrow-large.png",
|
||||||
|
"--add-opens=javafx.graphics/com.sun.glass.ui.mac=centerdevice.nsmenufx"]
|
||||||
}
|
}
|
||||||
if(headless) {
|
if(headless) {
|
||||||
applicationDefaultJvmArgs += ["-Dglass.platform=Monocle", "-Dmonocle.platform=Headless", "-Dprism.order=sw"]
|
applicationDefaultJvmArgs += ["-Dglass.platform=Monocle", "-Dmonocle.platform=Headless", "-Dprism.order=sw"]
|
||||||
|
|
@ -185,20 +209,17 @@ jlink {
|
||||||
requires 'jdk.crypto.cryptoki'
|
requires 'jdk.crypto.cryptoki'
|
||||||
requires 'java.management'
|
requires 'java.management'
|
||||||
requires 'io.leangen.geantyref'
|
requires 'io.leangen.geantyref'
|
||||||
|
uses 'org.flywaydb.core.extensibility.FlywayExtension'
|
||||||
|
uses 'org.flywaydb.core.internal.database.DatabaseType'
|
||||||
uses 'org.eclipse.jetty.http.HttpFieldPreEncoder'
|
uses 'org.eclipse.jetty.http.HttpFieldPreEncoder'
|
||||||
|
uses 'org.eclipse.jetty.websocket.api.extensions.Extension'
|
||||||
|
uses 'org.eclipse.jetty.websocket.common.RemoteEndpointFactory'
|
||||||
}
|
}
|
||||||
|
|
||||||
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', '2', '--no-header-files', '--no-man-pages', '--ignore-signing-information', '--exclude-files', '**.png', '--exclude-resources', 'glob:/com.sparrowwallet.merged.module/META-INF/*']
|
||||||
launcher {
|
launcher {
|
||||||
name = 'sparrow'
|
name = 'sparrow'
|
||||||
jvmArgs = ["--enable-native-access=com.sparrowwallet.drongo",
|
jvmArgs = ["--add-opens=javafx.graphics/com.sun.javafx.css=org.controlsfx.controls",
|
||||||
"--enable-native-access=com.sun.jna",
|
|
||||||
"--enable-native-access=javafx.graphics",
|
|
||||||
"--enable-native-access=com.sparrowwallet.merged.module",
|
|
||||||
"--enable-native-access=com.fazecast.jSerialComm",
|
|
||||||
"--enable-native-access=org.usb4java",
|
|
||||||
"--enable-native-access=io.github.doblon8.jzbar",
|
|
||||||
"--add-opens=javafx.graphics/com.sun.javafx.css=org.controlsfx.controls",
|
|
||||||
"--add-opens=javafx.graphics/javafx.scene=org.controlsfx.controls",
|
"--add-opens=javafx.graphics/javafx.scene=org.controlsfx.controls",
|
||||||
"--add-opens=javafx.controls/com.sun.javafx.scene.control.behavior=org.controlsfx.controls",
|
"--add-opens=javafx.controls/com.sun.javafx.scene.control.behavior=org.controlsfx.controls",
|
||||||
"--add-opens=javafx.controls/com.sun.javafx.scene.control.inputmap=org.controlsfx.controls",
|
"--add-opens=javafx.controls/com.sun.javafx.scene.control.inputmap=org.controlsfx.controls",
|
||||||
|
|
@ -207,6 +228,11 @@ jlink {
|
||||||
"--add-opens=javafx.controls/javafx.scene.control.cell=com.sparrowwallet.sparrow",
|
"--add-opens=javafx.controls/javafx.scene.control.cell=com.sparrowwallet.sparrow",
|
||||||
"--add-opens=org.controlsfx.controls/impl.org.controlsfx.skin=com.sparrowwallet.sparrow",
|
"--add-opens=org.controlsfx.controls/impl.org.controlsfx.skin=com.sparrowwallet.sparrow",
|
||||||
"--add-opens=org.controlsfx.controls/impl.org.controlsfx.skin=javafx.fxml",
|
"--add-opens=org.controlsfx.controls/impl.org.controlsfx.skin=javafx.fxml",
|
||||||
|
"--add-opens=javafx.graphics/com.sun.javafx.tk=centerdevice.nsmenufx",
|
||||||
|
"--add-opens=javafx.graphics/com.sun.javafx.tk.quantum=centerdevice.nsmenufx",
|
||||||
|
"--add-opens=javafx.graphics/com.sun.glass.ui=centerdevice.nsmenufx",
|
||||||
|
"--add-opens=javafx.controls/com.sun.javafx.scene.control=centerdevice.nsmenufx",
|
||||||
|
"--add-opens=javafx.graphics/com.sun.javafx.menu=centerdevice.nsmenufx",
|
||||||
"--add-opens=javafx.graphics/com.sun.glass.ui=com.sparrowwallet.sparrow",
|
"--add-opens=javafx.graphics/com.sun.glass.ui=com.sparrowwallet.sparrow",
|
||||||
"--add-opens=javafx.graphics/javafx.scene.input=com.sparrowwallet.sparrow",
|
"--add-opens=javafx.graphics/javafx.scene.input=com.sparrowwallet.sparrow",
|
||||||
"--add-opens=javafx.graphics/com.sun.javafx.application=com.sparrowwallet.sparrow",
|
"--add-opens=javafx.graphics/com.sun.javafx.application=com.sparrowwallet.sparrow",
|
||||||
|
|
@ -222,12 +248,7 @@ jlink {
|
||||||
"--add-reads=com.sparrowwallet.merged.module=com.fasterxml.jackson.annotation",
|
"--add-reads=com.sparrowwallet.merged.module=com.fasterxml.jackson.annotation",
|
||||||
"--add-reads=com.sparrowwallet.merged.module=com.fasterxml.jackson.core",
|
"--add-reads=com.sparrowwallet.merged.module=com.fasterxml.jackson.core",
|
||||||
"--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=kotlin.stdlib=kotlinx.coroutines.core"]
|
||||||
"--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=org.flywaydb.core=java.desktop"]
|
|
||||||
|
|
||||||
if(os.windows) {
|
if(os.windows) {
|
||||||
jvmArgs += ["-Djavax.accessibility.assistive_technologies", "-Djavax.accessibility.screen_magnifier_present=false"]
|
jvmArgs += ["-Djavax.accessibility.assistive_technologies", "-Djavax.accessibility.screen_magnifier_present=false"]
|
||||||
|
|
@ -243,24 +264,22 @@ jlink {
|
||||||
jpackage {
|
jpackage {
|
||||||
imageName = "Sparrow"
|
imageName = "Sparrow"
|
||||||
installerName = "Sparrow"
|
installerName = "Sparrow"
|
||||||
appVersion = "${version}"
|
appVersion = "${sparrowVersion}"
|
||||||
skipInstaller = os.macOsX || properties.skipInstallers
|
skipInstaller = os.macOsX || properties.skipInstallers
|
||||||
imageOptions = []
|
imageOptions = []
|
||||||
installerOptions = ['--file-associations', 'src/main/deploy/psbt.properties', '--file-associations', 'src/main/deploy/txn.properties', '--file-associations', 'src/main/deploy/asc.properties', '--file-associations', 'src/main/deploy/bitcoin.properties', '--file-associations', 'src/main/deploy/auth47.properties', '--file-associations', 'src/main/deploy/lightning.properties', '--license-file', 'LICENSE']
|
installerOptions = ['--file-associations', 'src/main/deploy/psbt.properties', '--file-associations', 'src/main/deploy/txn.properties', '--file-associations', 'src/main/deploy/asc.properties', '--file-associations', 'src/main/deploy/bitcoin.properties', '--file-associations', 'src/main/deploy/auth47.properties', '--file-associations', 'src/main/deploy/lightning.properties', '--license-file', 'LICENSE']
|
||||||
if(os.windows) {
|
if(os.windows) {
|
||||||
installerOptions += ['--win-per-user-install', '--win-dir-chooser', '--win-menu', '--win-menu-group', 'Sparrow', '--win-shortcut', '--resource-dir', 'src/main/deploy/package/windows/']
|
installerOptions += ['--win-per-user-install', '--win-dir-chooser', '--win-menu', '--win-menu-group', 'Sparrow', '--win-shortcut', '--resource-dir', 'src/main/deploy/package/windows/']
|
||||||
imageOptions += ['--icon', 'src/main/deploy/package/windows/sparrow.ico']
|
imageOptions += ['--icon', 'src/main/deploy/package/windows/sparrow.ico']
|
||||||
installerType = "msi"
|
installerType = "exe"
|
||||||
}
|
}
|
||||||
if(os.linux) {
|
if(os.linux) {
|
||||||
if(headless) {
|
if(headless) {
|
||||||
installerName = "sparrowserver"
|
|
||||||
installerOptions = ['--license-file', 'LICENSE']
|
installerOptions = ['--license-file', 'LICENSE']
|
||||||
} else {
|
} else {
|
||||||
installerName = "sparrowwallet"
|
installerOptions += ['--resource-dir', 'src/main/deploy/package/linux/', '--linux-shortcut', '--linux-menu-group', 'Sparrow']
|
||||||
installerOptions += ['--linux-shortcut', '--linux-menu-group', 'Sparrow']
|
|
||||||
}
|
}
|
||||||
installerOptions += ['--resource-dir', layout.buildDirectory.dir('deploy/package').get().asFile.toString(), '--linux-app-category', 'utils', '--linux-app-release', '1', '--linux-rpm-license-type', 'ASL 2.0', '--linux-deb-maintainer', 'mail@sparrowwallet.com']
|
installerOptions += ['--linux-app-category', 'utils', '--linux-app-release', '1', '--linux-rpm-license-type', 'ASL 2.0', '--linux-deb-maintainer', 'mail@sparrowwallet.com']
|
||||||
imageOptions += ['--icon', 'src/main/deploy/package/linux/Sparrow.png', '--resource-dir', 'src/main/deploy/package/linux/']
|
imageOptions += ['--icon', 'src/main/deploy/package/linux/Sparrow.png', '--resource-dir', 'src/main/deploy/package/linux/']
|
||||||
}
|
}
|
||||||
if(os.macOsX) {
|
if(os.macOsX) {
|
||||||
|
|
@ -269,82 +288,24 @@ jlink {
|
||||||
installerType = "dmg"
|
installerType = "dmg"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if(os.linux) {
|
|
||||||
jpackageImage {
|
|
||||||
dependsOn('prepareModulesDir', 'copyUdevRules')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if(os.linux) {
|
task removeGroupWritePermission(type: Exec) {
|
||||||
tasks.jlink.finalizedBy('addUserWritePermission', 'copyUdevRules')
|
|
||||||
tasks.jpackageImage.finalizedBy('prepareResourceDir')
|
|
||||||
} else {
|
|
||||||
tasks.jlink.finalizedBy('addUserWritePermission')
|
|
||||||
}
|
|
||||||
|
|
||||||
tasks.register('addUserWritePermission', Exec) {
|
|
||||||
if(os.windows) {
|
|
||||||
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 {
|
|
||||||
commandLine 'chmod', '-R', 'u+w', "$buildDir/image/legal"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
tasks.register('copyUdevRules', Copy) {
|
|
||||||
from('lark/src/main/resources/udev')
|
|
||||||
into(layout.buildDirectory.dir('image/conf/udev'))
|
|
||||||
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) {
|
|
||||||
commandLine 'chmod', '-R', 'g-w', "$buildDir/jpackage/Sparrow"
|
commandLine 'chmod', '-R', 'g-w', "$buildDir/jpackage/Sparrow"
|
||||||
}
|
}
|
||||||
|
|
||||||
tasks.register('packageZipDistribution', Zip) {
|
task packageZipDistribution(type: Zip) {
|
||||||
archiveFileName = "Sparrow-${version}.zip"
|
archiveFileName = "Sparrow-${sparrowVersion}.zip"
|
||||||
destinationDirectory = file("$buildDir/jpackage")
|
destinationDirectory = file("$buildDir/jpackage")
|
||||||
preserveFileTimestamps = os.macOsX
|
|
||||||
from("$buildDir/jpackage/") {
|
from("$buildDir/jpackage/") {
|
||||||
include "Sparrow/**"
|
include "Sparrow/**"
|
||||||
include "Sparrow.app/**"
|
include "Sparrow.app/**"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
tasks.register('packageTarDistribution', Tar) {
|
task packageTarDistribution(type: Tar) {
|
||||||
dependsOn removeGroupWritePermission
|
dependsOn removeGroupWritePermission
|
||||||
archiveFileName = "sparrow${headless ? 'server': 'wallet'}-${version}-${releaseArch}.tar.gz"
|
archiveFileName = "sparrow-${sparrowVersion}-${releaseArch}.tar.gz"
|
||||||
destinationDirectory = file("$buildDir/jpackage")
|
destinationDirectory = file("$buildDir/jpackage")
|
||||||
compression = Compression.GZIP
|
compression = Compression.GZIP
|
||||||
from("$buildDir/jpackage/") {
|
from("$buildDir/jpackage/") {
|
||||||
|
|
@ -353,11 +314,60 @@ tasks.register('packageTarDistribution', Tar) {
|
||||||
}
|
}
|
||||||
|
|
||||||
extraJavaModuleInfo {
|
extraJavaModuleInfo {
|
||||||
module('no.tornado:tornadofx-controls', 'tornadofx.controls') {
|
module('jackson-core-2.13.2.jar', 'com.fasterxml.jackson.core', '2.13.2') {
|
||||||
|
exports('com.fasterxml.jackson.core')
|
||||||
|
exports('com.fasterxml.jackson.core.async')
|
||||||
|
exports('com.fasterxml.jackson.core.base')
|
||||||
|
exports('com.fasterxml.jackson.core.exc')
|
||||||
|
exports('com.fasterxml.jackson.core.filter')
|
||||||
|
exports('com.fasterxml.jackson.core.format')
|
||||||
|
exports('com.fasterxml.jackson.core.io')
|
||||||
|
exports('com.fasterxml.jackson.core.json')
|
||||||
|
exports('com.fasterxml.jackson.core.json.async')
|
||||||
|
exports('com.fasterxml.jackson.core.sym')
|
||||||
|
exports('com.fasterxml.jackson.core.type')
|
||||||
|
exports('com.fasterxml.jackson.core.util')
|
||||||
|
uses('com.fasterxml.jackson.core.ObjectCodec')
|
||||||
|
}
|
||||||
|
module('jackson-annotations-2.13.2.jar', 'com.fasterxml.jackson.annotation', '2.13.2') {
|
||||||
|
requires('com.fasterxml.jackson.core')
|
||||||
|
exports('com.fasterxml.jackson.annotation')
|
||||||
|
}
|
||||||
|
module('jackson-databind-2.13.2.jar', 'com.fasterxml.jackson.databind', '2.13.2') {
|
||||||
|
requires('java.desktop')
|
||||||
|
requires('java.logging')
|
||||||
|
requires('com.fasterxml.jackson.annotation')
|
||||||
|
requires('com.fasterxml.jackson.core')
|
||||||
|
requires('java.sql')
|
||||||
|
requires('java.xml')
|
||||||
|
exports('com.fasterxml.jackson.databind')
|
||||||
|
exports('com.fasterxml.jackson.databind.annotation')
|
||||||
|
exports('com.fasterxml.jackson.databind.cfg')
|
||||||
|
exports('com.fasterxml.jackson.databind.deser')
|
||||||
|
exports('com.fasterxml.jackson.databind.deser.impl')
|
||||||
|
exports('com.fasterxml.jackson.databind.deser.std')
|
||||||
|
exports('com.fasterxml.jackson.databind.exc')
|
||||||
|
exports('com.fasterxml.jackson.databind.ext')
|
||||||
|
exports('com.fasterxml.jackson.databind.introspect')
|
||||||
|
exports('com.fasterxml.jackson.databind.json')
|
||||||
|
exports('com.fasterxml.jackson.databind.jsonFormatVisitors')
|
||||||
|
exports('com.fasterxml.jackson.databind.jsonschema')
|
||||||
|
exports('com.fasterxml.jackson.databind.jsontype')
|
||||||
|
exports('com.fasterxml.jackson.databind.jsontype.impl')
|
||||||
|
exports('com.fasterxml.jackson.databind.module')
|
||||||
|
exports('com.fasterxml.jackson.databind.node')
|
||||||
|
exports('com.fasterxml.jackson.databind.ser')
|
||||||
|
exports('com.fasterxml.jackson.databind.ser.impl')
|
||||||
|
exports('com.fasterxml.jackson.databind.ser.std')
|
||||||
|
exports('com.fasterxml.jackson.databind.type')
|
||||||
|
exports('com.fasterxml.jackson.databind.util')
|
||||||
|
uses('com.fasterxml.jackson.databind.Module')
|
||||||
|
}
|
||||||
|
module('tornadofx-controls-1.0.4.jar', 'tornadofx.controls', '1.0.4') {
|
||||||
exports('tornadofx.control')
|
exports('tornadofx.control')
|
||||||
requires('javafx.controls')
|
requires('javafx.controls')
|
||||||
}
|
}
|
||||||
module('com.github.arteam:simple-json-rpc-core', 'simple.json.rpc.core') {
|
module('simple-json-rpc-core-1.3.jar', 'simple.json.rpc.core', '1.3') {
|
||||||
exports('com.github.arteam.simplejsonrpc.core.annotation')
|
exports('com.github.arteam.simplejsonrpc.core.annotation')
|
||||||
exports('com.github.arteam.simplejsonrpc.core.domain')
|
exports('com.github.arteam.simplejsonrpc.core.domain')
|
||||||
requires('com.fasterxml.jackson.core')
|
requires('com.fasterxml.jackson.core')
|
||||||
|
|
@ -365,7 +375,7 @@ extraJavaModuleInfo {
|
||||||
requires('com.fasterxml.jackson.databind')
|
requires('com.fasterxml.jackson.databind')
|
||||||
requires('org.jetbrains.annotations')
|
requires('org.jetbrains.annotations')
|
||||||
}
|
}
|
||||||
module('com.github.arteam:simple-json-rpc-client', 'simple.json.rpc.client') {
|
module('simple-json-rpc-client-1.3.jar', 'simple.json.rpc.client', '1.3') {
|
||||||
exports('com.github.arteam.simplejsonrpc.client')
|
exports('com.github.arteam.simplejsonrpc.client')
|
||||||
exports('com.github.arteam.simplejsonrpc.client.builder')
|
exports('com.github.arteam.simplejsonrpc.client.builder')
|
||||||
exports('com.github.arteam.simplejsonrpc.client.exception')
|
exports('com.github.arteam.simplejsonrpc.client.exception')
|
||||||
|
|
@ -373,26 +383,61 @@ extraJavaModuleInfo {
|
||||||
requires('com.fasterxml.jackson.databind')
|
requires('com.fasterxml.jackson.databind')
|
||||||
requires('simple.json.rpc.core')
|
requires('simple.json.rpc.core')
|
||||||
}
|
}
|
||||||
module('com.github.arteam:simple-json-rpc-server', 'simple.json.rpc.server') {
|
module('simple-json-rpc-server-1.3.jar', 'simple.json.rpc.server', '1.3') {
|
||||||
exports('com.github.arteam.simplejsonrpc.server')
|
exports('com.github.arteam.simplejsonrpc.server')
|
||||||
requires('simple.json.rpc.core')
|
requires('simple.json.rpc.core')
|
||||||
requires('com.google.common')
|
requires('com.google.common')
|
||||||
requires('org.slf4j')
|
requires('org.slf4j')
|
||||||
requires('com.fasterxml.jackson.databind')
|
requires('com.fasterxml.jackson.databind')
|
||||||
}
|
}
|
||||||
module('org.openpnp:openpnp-capture-java', 'openpnp.capture.java') {
|
module("bridj${targetName}-0.7-20140918-3.jar", 'com.nativelibs4java.bridj', '0.7-20140918-3') {
|
||||||
exports('org.openpnp.capture')
|
exports('org.bridj')
|
||||||
exports('org.openpnp.capture.library')
|
exports('org.bridj.cpp')
|
||||||
requires('java.desktop')
|
requires('java.logging')
|
||||||
requires('com.sun.jna')
|
|
||||||
}
|
}
|
||||||
module('net.sourceforge.javacsv:javacsv', 'net.sourceforge.javacsv') {
|
module("webcam-capture${targetName}-0.3.13-SNAPSHOT.jar", 'com.github.sarxos.webcam.capture', '0.3.13-SNAPSHOT') {
|
||||||
|
exports('com.github.sarxos.webcam')
|
||||||
|
exports('com.github.sarxos.webcam.ds.buildin')
|
||||||
|
exports('com.github.sarxos.webcam.ds.buildin.natives')
|
||||||
|
requires('java.desktop')
|
||||||
|
requires('com.nativelibs4java.bridj')
|
||||||
|
requires('org.slf4j')
|
||||||
|
}
|
||||||
|
module('centerdevice-nsmenufx-2.1.7.jar', 'centerdevice.nsmenufx', '2.1.7') {
|
||||||
|
exports('de.codecentric.centerdevice')
|
||||||
|
requires('javafx.base')
|
||||||
|
requires('javafx.controls')
|
||||||
|
requires('javafx.graphics')
|
||||||
|
}
|
||||||
|
module('javacsv-2.0.jar', 'net.sourceforge.javacsv', '2.0') {
|
||||||
exports('com.csvreader')
|
exports('com.csvreader')
|
||||||
}
|
}
|
||||||
module('com.google.guava:listenablefuture|empty-to-avoid-conflict-with-guava', 'com.google.guava.listenablefuture')
|
module('jeromq-0.5.0.jar', 'jeromq', '0.5.0') {
|
||||||
module('com.google.code.findbugs:jsr305', 'com.google.code.findbugs.jsr305')
|
exports('org.zeromq')
|
||||||
|
}
|
||||||
|
module('json-simple-1.1.1.jar', 'json.simple', '1.1.1') {
|
||||||
|
exports('org.json.simple')
|
||||||
|
exports('org.json.simple.parser')
|
||||||
|
}
|
||||||
|
module('listenablefuture-9999.0-empty-to-avoid-conflict-with-guava.jar', 'com.google.guava.listenablefuture', '9999.0-empty-to-avoid-conflict-with-guava')
|
||||||
|
module('jsr305-3.0.2.jar', 'com.google.code.findbugs.jsr305', '3.0.2')
|
||||||
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.fxmisc.richtext:richtextfx', 'org.fxmisc.richtext') {
|
module('jdbi3-core-3.20.0.jar', 'org.jdbi.v3.core', '3.20.0') {
|
||||||
|
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('geantyref-1.3.11.jar', 'io.leangen.geantyref', '1.3.11') {
|
||||||
|
exports('io.leangen.geantyref')
|
||||||
|
}
|
||||||
|
module('richtextfx-0.10.4.jar', 'org.fxmisc.richtext', '0.10.4') {
|
||||||
exports('org.fxmisc.richtext')
|
exports('org.fxmisc.richtext')
|
||||||
exports('org.fxmisc.richtext.event')
|
exports('org.fxmisc.richtext.event')
|
||||||
exports('org.fxmisc.richtext.model')
|
exports('org.fxmisc.richtext.model')
|
||||||
|
|
@ -401,23 +446,23 @@ extraJavaModuleInfo {
|
||||||
requires('javafx.graphics')
|
requires('javafx.graphics')
|
||||||
requires('org.fxmisc.flowless')
|
requires('org.fxmisc.flowless')
|
||||||
requires('org.reactfx.reactfx')
|
requires('org.reactfx.reactfx')
|
||||||
requires('org.fxmisc.undo')
|
requires('org.fxmisc.undo.undofx')
|
||||||
requires('org.fxmisc.wellbehaved')
|
requires('org.fxmisc.wellbehaved')
|
||||||
}
|
}
|
||||||
module('org.fxmisc.undo:undofx', 'org.fxmisc.undo') {
|
module('undofx-2.1.0.jar', 'org.fxmisc.undo.undofx', '2.1.0') {
|
||||||
requires('javafx.base')
|
requires('javafx.base')
|
||||||
requires('javafx.controls')
|
requires('javafx.controls')
|
||||||
requires('javafx.graphics')
|
requires('javafx.graphics')
|
||||||
requires('org.reactfx.reactfx')
|
requires('org.reactfx.reactfx')
|
||||||
}
|
}
|
||||||
module('org.fxmisc.flowless:flowless', 'org.fxmisc.flowless') {
|
module('flowless-0.6.1.jar', 'org.fxmisc.flowless', '0.6.1') {
|
||||||
exports('org.fxmisc.flowless')
|
exports('org.fxmisc.flowless')
|
||||||
requires('javafx.base')
|
requires('javafx.base')
|
||||||
requires('javafx.controls')
|
requires('javafx.controls')
|
||||||
requires('javafx.graphics')
|
requires('javafx.graphics')
|
||||||
requires('org.reactfx.reactfx')
|
requires('org.reactfx.reactfx')
|
||||||
}
|
}
|
||||||
module('org.reactfx:reactfx', 'org.reactfx.reactfx') {
|
module('reactfx-2.0-M5.jar', 'org.reactfx.reactfx', '2.0-M5') {
|
||||||
exports('org.reactfx')
|
exports('org.reactfx')
|
||||||
exports('org.reactfx.value')
|
exports('org.reactfx.value')
|
||||||
exports('org.reactfx.collection')
|
exports('org.reactfx.collection')
|
||||||
|
|
@ -426,57 +471,253 @@ extraJavaModuleInfo {
|
||||||
requires('javafx.graphics')
|
requires('javafx.graphics')
|
||||||
requires('javafx.controls')
|
requires('javafx.controls')
|
||||||
}
|
}
|
||||||
module('io.reactivex.rxjava2:rxjavafx', 'io.reactivex.rxjava2fx') {
|
module('rxjavafx-2.2.2.jar', 'io.reactivex.rxjava2fx', '2.2.2') {
|
||||||
exports('io.reactivex.rxjavafx.schedulers')
|
exports('io.reactivex.rxjavafx.schedulers')
|
||||||
requires('io.reactivex.rxjava2')
|
requires('io.reactivex.rxjava2')
|
||||||
requires('javafx.graphics')
|
requires('javafx.graphics')
|
||||||
}
|
}
|
||||||
module('org.flywaydb:flyway-core', 'org.flywaydb.core') {
|
module('wellbehavedfx-0.3.3.jar', 'org.fxmisc.wellbehaved', '0.3.3') {
|
||||||
exports('org.flywaydb.core')
|
|
||||||
exports('org.flywaydb.core.api')
|
|
||||||
exports('org.flywaydb.core.api.exception')
|
|
||||||
exports('org.flywaydb.core.api.configuration')
|
|
||||||
uses('org.flywaydb.core.extensibility.Plugin')
|
|
||||||
requires('java.sql')
|
|
||||||
}
|
|
||||||
module('org.fxmisc.wellbehaved:wellbehavedfx', 'org.fxmisc.wellbehaved') {
|
|
||||||
requires('javafx.base')
|
requires('javafx.base')
|
||||||
requires('javafx.graphics')
|
requires('javafx.graphics')
|
||||||
}
|
}
|
||||||
module('com.github.jai-imageio:jai-imageio-core', 'com.github.jai.imageio.jai.imageio.core') {
|
module('jai-imageio-core-1.4.0.jar', 'com.github.jai.imageio.jai.imageio.core', '1.4.0')
|
||||||
requires('java.desktop')
|
module('hummingbird-1.6.3.jar', 'com.sparrowwallet.hummingbird', '1.6.3') {
|
||||||
|
exports('com.sparrowwallet.hummingbird')
|
||||||
|
exports('com.sparrowwallet.hummingbird.registry')
|
||||||
|
requires('co.nstant.in.cbor')
|
||||||
}
|
}
|
||||||
module('co.nstant.in:cbor', 'co.nstant.in.cbor') {
|
module('cbor-0.9.jar', 'co.nstant.in.cbor', '0.9') {
|
||||||
exports('co.nstant.in.cbor')
|
exports('co.nstant.in.cbor')
|
||||||
exports('co.nstant.in.cbor.model')
|
exports('co.nstant.in.cbor.model')
|
||||||
exports('co.nstant.in.cbor.builder')
|
exports('co.nstant.in.cbor.builder')
|
||||||
}
|
}
|
||||||
module('net.sourceforge.streamsupport:streamsupport', 'net.sourceforge.streamsupport') {
|
module('nightjar-0.2.40.jar', 'com.sparrowwallet.nightjar', '0.2.40') {
|
||||||
|
requires('com.google.common')
|
||||||
|
requires('net.sourceforge.streamsupport')
|
||||||
|
requires('org.slf4j')
|
||||||
|
requires('org.bouncycastle.provider')
|
||||||
|
requires('com.fasterxml.jackson.databind')
|
||||||
|
requires('com.fasterxml.jackson.annotation')
|
||||||
|
requires('com.fasterxml.jackson.core')
|
||||||
|
requires('ch.qos.logback.classic')
|
||||||
|
requires('org.json')
|
||||||
|
requires('io.reactivex.rxjava2')
|
||||||
|
exports('com.samourai.http.client')
|
||||||
|
exports('com.samourai.tor.client')
|
||||||
|
exports('com.samourai.wallet.api.backend')
|
||||||
|
exports('com.samourai.wallet.api.backend.beans')
|
||||||
|
exports('com.samourai.wallet.client.indexHandler')
|
||||||
|
exports('com.samourai.wallet.hd')
|
||||||
|
exports('com.samourai.wallet.util')
|
||||||
|
exports('com.samourai.wallet.bip47.rpc')
|
||||||
|
exports('com.samourai.wallet.bip47.rpc.java')
|
||||||
|
exports('com.samourai.wallet.cahoots')
|
||||||
|
exports('com.samourai.wallet.cahoots.psbt')
|
||||||
|
exports('com.samourai.wallet.cahoots.stonewallx2')
|
||||||
|
exports('com.samourai.soroban.cahoots')
|
||||||
|
exports('com.samourai.soroban.client')
|
||||||
|
exports('com.samourai.soroban.client.cahoots')
|
||||||
|
exports('com.samourai.soroban.client.meeting')
|
||||||
|
exports('com.samourai.soroban.client.rpc')
|
||||||
|
exports('com.samourai.wallet.send')
|
||||||
|
exports('com.samourai.whirlpool.client.event')
|
||||||
|
exports('com.samourai.whirlpool.client.wallet')
|
||||||
|
exports('com.samourai.whirlpool.client.wallet.beans')
|
||||||
|
exports('com.samourai.whirlpool.client.wallet.data.dataSource')
|
||||||
|
exports('com.samourai.whirlpool.client.wallet.data.dataPersister')
|
||||||
|
exports('com.samourai.whirlpool.client.whirlpool')
|
||||||
|
exports('com.samourai.whirlpool.client.whirlpool.beans')
|
||||||
|
exports('com.samourai.whirlpool.client.wallet.data.pool')
|
||||||
|
exports('com.samourai.whirlpool.client.wallet.data.utxo')
|
||||||
|
exports('com.samourai.whirlpool.client.wallet.data.utxoConfig')
|
||||||
|
exports('com.samourai.whirlpool.client.wallet.data.supplier')
|
||||||
|
exports('com.samourai.whirlpool.client.mix.handler')
|
||||||
|
exports('com.samourai.whirlpool.client.mix.listener')
|
||||||
|
exports('com.samourai.whirlpool.protocol.beans')
|
||||||
|
exports('com.samourai.whirlpool.protocol.rest')
|
||||||
|
exports('com.samourai.whirlpool.client.tx0')
|
||||||
|
exports('com.samourai.wallet.segwit.bech32')
|
||||||
|
exports('com.samourai.whirlpool.client.wallet.data.chain')
|
||||||
|
exports('com.samourai.whirlpool.client.wallet.data.wallet')
|
||||||
|
exports('com.samourai.whirlpool.client.wallet.data.minerFee')
|
||||||
|
exports('com.samourai.whirlpool.client.wallet.data.walletState')
|
||||||
|
exports('com.sparrowwallet.nightjar.http')
|
||||||
|
exports('com.sparrowwallet.nightjar.stomp')
|
||||||
|
exports('com.sparrowwallet.nightjar.tor')
|
||||||
|
}
|
||||||
|
module('throwing-supplier-1.0.3.jar', 'zeroleak.throwingsupplier', '1.0.3') {
|
||||||
|
exports('com.zeroleak.throwingsupplier')
|
||||||
|
}
|
||||||
|
module('okhttp-2.7.5.jar', 'com.squareup.okhttp', '2.7.5') {
|
||||||
|
exports('com.squareup.okhttp')
|
||||||
|
}
|
||||||
|
module('okio-1.6.0.jar', 'com.squareup.okio', '1.6.0') {
|
||||||
|
exports('okio')
|
||||||
|
}
|
||||||
|
module('java-jwt-3.8.1.jar', 'com.auth0.jwt', '3.8.1') {
|
||||||
|
exports('com.auth0.jwt')
|
||||||
|
}
|
||||||
|
module('json-20180130.jar', 'org.json', '1.0') {
|
||||||
|
exports('org.json')
|
||||||
|
}
|
||||||
|
module('scrypt-1.4.0.jar', 'com.lambdaworks.scrypt', '1.4.0') {
|
||||||
|
exports('com.lambdaworks.codec')
|
||||||
|
exports('com.lambdaworks.crypto')
|
||||||
|
}
|
||||||
|
module('streamsupport-1.7.0.jar', 'net.sourceforge.streamsupport', '1.7.0') {
|
||||||
requires('jdk.unsupported')
|
requires('jdk.unsupported')
|
||||||
exports('java8.util')
|
exports('java8.util')
|
||||||
exports('java8.util.function')
|
exports('java8.util.function')
|
||||||
exports('java8.util.stream')
|
exports('java8.util.stream')
|
||||||
}
|
}
|
||||||
module('net.coobird:thumbnailator', 'net.coobird.thumbnailator') {
|
module('protobuf-java-2.6.1.jar', 'com.google.protobuf', '2.6.1') {
|
||||||
|
exports('com.google.protobuf')
|
||||||
|
}
|
||||||
|
module('commons-text-1.2.jar', 'org.apache.commons.text', '1.2') {
|
||||||
|
exports('org.apache.commons.text')
|
||||||
|
}
|
||||||
|
module('jcip-annotations-1.0.jar', 'net.jcip.annotations', '1.0') {
|
||||||
|
exports('net.jcip.annotations')
|
||||||
|
}
|
||||||
|
module('thumbnailator-0.4.18.jar', 'net.coobird.thumbnailator', '0.4.18') {
|
||||||
exports('net.coobird.thumbnailator')
|
exports('net.coobird.thumbnailator')
|
||||||
requires('java.desktop')
|
requires('java.desktop')
|
||||||
}
|
}
|
||||||
module('org.jcommander:jcommander', 'org.jcommander') {
|
module('fxsvgimage-1.0b2.jar', 'com.github.hervegirod', '1.0b2') {
|
||||||
|
exports('org.girod.javafx.svgimage')
|
||||||
|
requires('javafx.graphics')
|
||||||
|
requires('java.xml')
|
||||||
|
}
|
||||||
|
module("kmp-tor-jvm-${vKmpTor}.jar", 'kmp.tor.jvm', "${vTor}-${vKmpTor}") {
|
||||||
|
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("kmp-tor-binary-${kmpOs}${kmpArch}-jvm-${vTor}.jar", "kmp.tor.binary.${kmpOs}${kmpArch}", "${vTor}") {
|
||||||
|
exports("io.matthewnelson.kmp.tor.resource.${kmpOs}.${kmpArch}")
|
||||||
|
exports("kmptor.${kmpOs}.${kmpArch}")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
module("kmp-tor-binary-${kmpOs}${kmpArch}-jvm-${vTor}.jar", "kmp.tor.binary.${kmpOs}${kmpArch}", "${vTor}") {
|
||||||
|
exports("io.matthewnelson.kmp.tor.binary.${kmpOs}.${kmpArch}")
|
||||||
|
exports("kmptor.${kmpOs}.${kmpArch}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
module("kmp-tor-binary-extract-jvm-${vTor}.jar", 'kmp.tor.binary.extract.jvm', "${vTor}") {
|
||||||
|
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("kmp-tor-manager-jvm-${vKmpTor}.jar", 'kmp.tor.manager.jvm', "${vKmpTor}") {
|
||||||
|
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("kmp-tor-manager-common-jvm-${vKmpTor}.jar", 'kmp.tor.manager.common.jvm', "${vKmpTor}") {
|
||||||
|
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("kmp-tor-controller-common-jvm-${vKmpTor}.jar", 'kmp.tor.controller.common.jvm', "${vKmpTor}") {
|
||||||
|
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("kmp-tor-common-jvm-${vKmpTor}.jar", 'kmp.tor.common.jvm', "${vKmpTor}") {
|
||||||
|
exports('io.matthewnelson.kmp.tor.common.address')
|
||||||
|
requires('parcelize.jvm')
|
||||||
|
requires('kotlin.stdlib')
|
||||||
|
}
|
||||||
|
module("kmp-tor-controller-jvm-${vKmpTor}.jar", 'kmp.tor.controller.jvm', "${vKmpTor}") {
|
||||||
|
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("kmp-tor-ext-callback-common-jvm-${vKmpTor}.jar", 'kmp.tor.ext.callback.common.jvm', "${vKmpTor}") {
|
||||||
|
exports('io.matthewnelson.kmp.tor.ext.callback.common')
|
||||||
|
}
|
||||||
|
module("kmp-tor-ext-callback-manager-jvm-${vKmpTor}.jar", 'kmp.tor.ext.callback.manager.jvm', "${vKmpTor}") {
|
||||||
|
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("kmp-tor-ext-callback-manager-common-jvm-${vKmpTor}.jar", 'kmp.tor.ext.callback.manager.common.jvm', "${vKmpTor}") {
|
||||||
|
exports('io.matthewnelson.kmp.tor.ext.callback.manager.common')
|
||||||
|
requires('kmp.tor.ext.callback.controller.common.jvm')
|
||||||
|
}
|
||||||
|
module("kmp-tor-ext-callback-controller-common-jvm-${vKmpTor}.jar", 'kmp.tor.ext.callback.controller.common.jvm', "${vKmpTor}") {
|
||||||
|
exports('io.matthewnelson.kmp.tor.ext.callback.controller.common.control')
|
||||||
|
exports('io.matthewnelson.kmp.tor.ext.callback.controller.common.control.usecase')
|
||||||
|
}
|
||||||
|
module("kmp-tor-binary-geoip-jvm-${vTor}.jar", 'kmp.tor.binary.geoip.jvm', "${vTor}") {
|
||||||
|
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('jnacl-1.0.0.jar', 'eu.neilalexander.jnacl', '1.0.0')
|
||||||
|
module('jcommander-1.81.jar', 'com.beust.jcommander', '1.81') {
|
||||||
exports('com.beust.jcommander')
|
exports('com.beust.jcommander')
|
||||||
}
|
}
|
||||||
module('com.sparrowwallet:hid4java', 'org.hid4java') {
|
module('pgpainless-core-1.6.6.jar', 'org.pgpainless.core', '1.6.6') {
|
||||||
requires('com.sun.jna')
|
exports('org.pgpainless')
|
||||||
exports('org.hid4java')
|
exports('org.pgpainless.key')
|
||||||
exports('org.hid4java.jna')
|
exports('org.pgpainless.key.parsing')
|
||||||
|
exports('org.pgpainless.decryption_verification')
|
||||||
|
exports('org.pgpainless.exception')
|
||||||
|
exports('org.pgpainless.signature')
|
||||||
|
exports('org.pgpainless.util')
|
||||||
|
requires('org.bouncycastle.provider')
|
||||||
|
requires('org.bouncycastle.pg')
|
||||||
|
requires('org.slf4j')
|
||||||
}
|
}
|
||||||
module('com.sparrowwallet:usb4java', 'org.usb4java') {
|
module('jzlib-1.1.3.jar', 'com.jcraft.jzlib', '1.1.3') {
|
||||||
exports('org.usb4java')
|
|
||||||
}
|
|
||||||
module('com.jcraft:jzlib', 'com.jcraft.jzlib') {
|
|
||||||
exports('com.jcraft.jzlib')
|
exports('com.jcraft.jzlib')
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
kmpTorResourceFilterJar {
|
|
||||||
keepTorCompilation("current","current")
|
|
||||||
}
|
}
|
||||||
|
|
@ -3,21 +3,22 @@ plugins {
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation 'com.google.gradle:osdetector-gradle-plugin:1.7.3'
|
implementation 'org.ow2.asm:asm:9.6'
|
||||||
}
|
}
|
||||||
|
|
||||||
repositories {
|
repositories {
|
||||||
mavenCentral()
|
mavenCentral()
|
||||||
maven {
|
maven {
|
||||||
url = uri("https://plugins.gradle.org/m2/")
|
url "https://plugins.gradle.org/m2/"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
gradlePlugin {
|
gradlePlugin {
|
||||||
plugins {
|
plugins {
|
||||||
register("org-openjfx-javafxplugin") {
|
// here we register our plugin with an ID
|
||||||
id = "org-openjfx-javafxplugin"
|
register("extra-java-module-info") {
|
||||||
implementationClass = "org.openjfx.gradle.JavaFXPlugin"
|
id = "extra-java-module-info"
|
||||||
|
implementationClass = "org.gradle.sample.transform.javamodules.ExtraModuleInfoPlugin"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,54 @@
|
||||||
|
package org.gradle.sample.transform.javamodules;
|
||||||
|
|
||||||
|
import org.gradle.api.Plugin;
|
||||||
|
import org.gradle.api.Project;
|
||||||
|
import org.gradle.api.artifacts.Configuration;
|
||||||
|
import org.gradle.api.attributes.Attribute;
|
||||||
|
import org.gradle.api.plugins.JavaPlugin;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Entry point of our plugin that should be applied in the root project.
|
||||||
|
*/
|
||||||
|
public class ExtraModuleInfoPlugin implements Plugin<Project> {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void apply(Project project) {
|
||||||
|
// register the plugin extension as 'extraJavaModuleInfo {}' configuration block
|
||||||
|
ExtraModuleInfoPluginExtension extension = project.getObjects().newInstance(ExtraModuleInfoPluginExtension.class);
|
||||||
|
project.getExtensions().add(ExtraModuleInfoPluginExtension.class, "extraJavaModuleInfo", extension);
|
||||||
|
|
||||||
|
// setup the transform for all projects in the build
|
||||||
|
project.getPlugins().withType(JavaPlugin.class).configureEach(javaPlugin -> configureTransform(project, extension));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void configureTransform(Project project, ExtraModuleInfoPluginExtension extension) {
|
||||||
|
Attribute<String> artifactType = Attribute.of("artifactType", String.class);
|
||||||
|
Attribute<Boolean> javaModule = Attribute.of("javaModule", Boolean.class);
|
||||||
|
|
||||||
|
// compile and runtime classpath express that they only accept modules by requesting the javaModule=true attribute
|
||||||
|
project.getConfigurations().matching(this::isResolvingJavaPluginConfiguration).all(
|
||||||
|
c -> c.getAttributes().attribute(javaModule, true));
|
||||||
|
|
||||||
|
// all Jars have a javaModule=false attribute by default; the transform also recognizes modules and returns them without modification
|
||||||
|
project.getDependencies().getArtifactTypes().getByName("jar").getAttributes().attribute(javaModule, false);
|
||||||
|
|
||||||
|
// register the transform for Jars and "javaModule=false -> javaModule=true"; the plugin extension object fills the input parameter
|
||||||
|
project.getDependencies().registerTransform(ExtraModuleInfoTransform.class, t -> {
|
||||||
|
t.parameters(p -> {
|
||||||
|
p.setModuleInfo(extension.getModuleInfo());
|
||||||
|
p.setAutomaticModules(extension.getAutomaticModules());
|
||||||
|
});
|
||||||
|
t.getFrom().attribute(artifactType, "jar").attribute(javaModule, false);
|
||||||
|
t.getTo().attribute(artifactType, "jar").attribute(javaModule, true);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isResolvingJavaPluginConfiguration(Configuration configuration) {
|
||||||
|
if (!configuration.isCanBeResolved()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return configuration.getName().endsWith(JavaPlugin.COMPILE_CLASSPATH_CONFIGURATION_NAME.substring(1))
|
||||||
|
|| configuration.getName().endsWith(JavaPlugin.RUNTIME_CLASSPATH_CONFIGURATION_NAME.substring(1))
|
||||||
|
|| configuration.getName().endsWith(JavaPlugin.ANNOTATION_PROCESSOR_CONFIGURATION_NAME.substring(1));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,52 @@
|
||||||
|
package org.gradle.sample.transform.javamodules;
|
||||||
|
|
||||||
|
|
||||||
|
import org.gradle.api.Action;
|
||||||
|
|
||||||
|
import javax.annotation.Nullable;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A data class to collect all the module information we want to add.
|
||||||
|
* Here the class is used as extension that can be configured in the build script
|
||||||
|
* and as input to the ExtraModuleInfoTransform that add the information to Jars.
|
||||||
|
*/
|
||||||
|
public class ExtraModuleInfoPluginExtension {
|
||||||
|
|
||||||
|
private final Map<String, ModuleInfo> moduleInfo = new HashMap<>();
|
||||||
|
private final Map<String, String> automaticModules = new HashMap<>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add full module information for a given Jar file.
|
||||||
|
*/
|
||||||
|
public void module(String jarName, String moduleName, String moduleVersion) {
|
||||||
|
module(jarName, moduleName, moduleVersion, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add full module information, including exported packages and dependencies, for a given Jar file.
|
||||||
|
*/
|
||||||
|
public void module(String jarName, String moduleName, String moduleVersion, @Nullable Action<? super ModuleInfo> conf) {
|
||||||
|
ModuleInfo moduleInfo = new ModuleInfo(moduleName, moduleVersion);
|
||||||
|
if (conf != null) {
|
||||||
|
conf.execute(moduleInfo);
|
||||||
|
}
|
||||||
|
this.moduleInfo.put(jarName, moduleInfo);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add only an automatic module name to a given jar file.
|
||||||
|
*/
|
||||||
|
public void automaticModule(String jarName, String moduleName) {
|
||||||
|
automaticModules.put(jarName, moduleName);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected Map<String, ModuleInfo> getModuleInfo() {
|
||||||
|
return moduleInfo;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected Map<String, String> getAutomaticModules() {
|
||||||
|
return automaticModules;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,176 @@
|
||||||
|
package org.gradle.sample.transform.javamodules;
|
||||||
|
|
||||||
|
import org.gradle.api.artifacts.transform.InputArtifact;
|
||||||
|
import org.gradle.api.artifacts.transform.TransformAction;
|
||||||
|
import org.gradle.api.artifacts.transform.TransformOutputs;
|
||||||
|
import org.gradle.api.artifacts.transform.TransformParameters;
|
||||||
|
import org.gradle.api.file.FileSystemLocation;
|
||||||
|
import org.gradle.api.provider.Provider;
|
||||||
|
import org.gradle.api.tasks.Input;
|
||||||
|
import org.objectweb.asm.ClassWriter;
|
||||||
|
import org.objectweb.asm.ModuleVisitor;
|
||||||
|
import org.objectweb.asm.Opcodes;
|
||||||
|
|
||||||
|
import java.io.*;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.jar.*;
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
import java.util.zip.ZipEntry;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An artifact transform that applies additional information to Jars without module information.
|
||||||
|
* The transformation fails the build if a Jar does not contain information and no extra information
|
||||||
|
* was defined for it. This way we make sure that all Jars are turned into modules.
|
||||||
|
*/
|
||||||
|
abstract public class ExtraModuleInfoTransform implements TransformAction<ExtraModuleInfoTransform.Parameter> {
|
||||||
|
|
||||||
|
public static class Parameter implements TransformParameters, Serializable {
|
||||||
|
private Map<String, ModuleInfo> moduleInfo = Collections.emptyMap();
|
||||||
|
private Map<String, String> automaticModules = Collections.emptyMap();
|
||||||
|
|
||||||
|
@Input
|
||||||
|
public Map<String, ModuleInfo> getModuleInfo() {
|
||||||
|
return moduleInfo;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Input
|
||||||
|
public Map<String, String> getAutomaticModules() {
|
||||||
|
return automaticModules;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setModuleInfo(Map<String, ModuleInfo> moduleInfo) {
|
||||||
|
this.moduleInfo = moduleInfo;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setAutomaticModules(Map<String, String> automaticModules) {
|
||||||
|
this.automaticModules = automaticModules;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@InputArtifact
|
||||||
|
protected abstract Provider<FileSystemLocation> getInputArtifact();
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void transform(TransformOutputs outputs) {
|
||||||
|
Map<String, ModuleInfo> moduleInfo = getParameters().moduleInfo;
|
||||||
|
Map<String, String> automaticModules = getParameters().automaticModules;
|
||||||
|
File originalJar = getInputArtifact().get().getAsFile();
|
||||||
|
String originalJarName = originalJar.getName();
|
||||||
|
|
||||||
|
//Recreate jackson jars as open, non-synthetic modules
|
||||||
|
if ((isModule(originalJar) && !originalJarName.contains("jackson")) || originalJarName.startsWith("javafx-")) {
|
||||||
|
outputs.file(originalJar);
|
||||||
|
} else if (moduleInfo.containsKey(originalJarName)) {
|
||||||
|
addModuleDescriptor(originalJar, getModuleJar(outputs, originalJar), moduleInfo.get(originalJarName));
|
||||||
|
} else if (isAutoModule(originalJar)) {
|
||||||
|
outputs.file(originalJar);
|
||||||
|
} else if (automaticModules.containsKey(originalJarName)) {
|
||||||
|
addAutomaticModuleName(originalJar, getModuleJar(outputs, originalJar), automaticModules.get(originalJarName));
|
||||||
|
} else if(originalJarName.startsWith("kotlin-stdlib-common")) {
|
||||||
|
//ignore
|
||||||
|
} else {
|
||||||
|
throw new RuntimeException("Not a module and no mapping defined: " + originalJarName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isModule(File jar) {
|
||||||
|
Pattern moduleInfoClassMrjarPath = Pattern.compile("META-INF/versions/\\d+/module-info.class");
|
||||||
|
try (JarInputStream inputStream = new JarInputStream(new FileInputStream(jar))) {
|
||||||
|
boolean isMultiReleaseJar = containsMultiReleaseJarEntry(inputStream);
|
||||||
|
ZipEntry next = inputStream.getNextEntry();
|
||||||
|
while (next != null) {
|
||||||
|
if ("module-info.class".equals(next.getName())) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (isMultiReleaseJar && moduleInfoClassMrjarPath.matcher(next.getName()).matches()) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
next = inputStream.getNextEntry();
|
||||||
|
}
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean containsMultiReleaseJarEntry(JarInputStream jarStream) {
|
||||||
|
Manifest manifest = jarStream.getManifest();
|
||||||
|
return manifest != null && Boolean.parseBoolean(manifest.getMainAttributes().getValue("Multi-Release"));
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isAutoModule(File jar) {
|
||||||
|
try (JarInputStream inputStream = new JarInputStream(new FileInputStream(jar))) {
|
||||||
|
return inputStream.getManifest().getMainAttributes().getValue("Automatic-Module-Name") != null;
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private File getModuleJar(TransformOutputs outputs, File originalJar) {
|
||||||
|
return outputs.file(originalJar.getName().substring(0, originalJar.getName().lastIndexOf('.')) + "-module.jar");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void addAutomaticModuleName(File originalJar, File moduleJar, String moduleName) {
|
||||||
|
try (JarInputStream inputStream = new JarInputStream(new FileInputStream(originalJar))) {
|
||||||
|
Manifest manifest = inputStream.getManifest();
|
||||||
|
manifest.getMainAttributes().put(new Attributes.Name("Automatic-Module-Name"), moduleName);
|
||||||
|
try (JarOutputStream outputStream = new JarOutputStream(new FileOutputStream(moduleJar), inputStream.getManifest())) {
|
||||||
|
copyEntries(inputStream, outputStream);
|
||||||
|
}
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void addModuleDescriptor(File originalJar, File moduleJar, ModuleInfo moduleInfo) {
|
||||||
|
try (JarInputStream inputStream = new JarInputStream(new FileInputStream(originalJar))) {
|
||||||
|
Manifest manifest = inputStream.getManifest();
|
||||||
|
if(manifest == null) {
|
||||||
|
manifest = new Manifest();
|
||||||
|
}
|
||||||
|
try (JarOutputStream outputStream = new JarOutputStream(new FileOutputStream(moduleJar), manifest)) {
|
||||||
|
copyEntries(inputStream, outputStream);
|
||||||
|
outputStream.putNextEntry(new JarEntry("module-info.class"));
|
||||||
|
outputStream.write(addModuleInfo(moduleInfo));
|
||||||
|
outputStream.closeEntry();
|
||||||
|
}
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void copyEntries(JarInputStream inputStream, JarOutputStream outputStream) throws IOException {
|
||||||
|
JarEntry jarEntry = inputStream.getNextJarEntry();
|
||||||
|
while (jarEntry != null) {
|
||||||
|
if(!jarEntry.getName().equals("module-info.class") && !jarEntry.getName().equals("org/bouncycastle/CachingBcPublicKeyDataDecryptorFactory.class")) {
|
||||||
|
outputStream.putNextEntry(jarEntry);
|
||||||
|
outputStream.write(inputStream.readAllBytes());
|
||||||
|
outputStream.closeEntry();
|
||||||
|
}
|
||||||
|
jarEntry = inputStream.getNextJarEntry();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static byte[] addModuleInfo(ModuleInfo moduleInfo) {
|
||||||
|
ClassWriter classWriter = new ClassWriter(0);
|
||||||
|
classWriter.visit(Opcodes.V9, Opcodes.ACC_MODULE, "module-info", null, null, null);
|
||||||
|
ModuleVisitor moduleVisitor = classWriter.visitModule(moduleInfo.getModuleName(), Opcodes.ACC_OPEN, moduleInfo.getModuleVersion());
|
||||||
|
for (String packageName : moduleInfo.getExports()) {
|
||||||
|
moduleVisitor.visitExport(packageName.replace('.', '/'), 0);
|
||||||
|
}
|
||||||
|
moduleVisitor.visitRequire("java.base", 0, null);
|
||||||
|
for (String requireName : moduleInfo.getRequires()) {
|
||||||
|
moduleVisitor.visitRequire(requireName, 0, null);
|
||||||
|
}
|
||||||
|
for (String requireName : moduleInfo.getRequiresTransitive()) {
|
||||||
|
moduleVisitor.visitRequire(requireName, Opcodes.ACC_TRANSITIVE, null);
|
||||||
|
}
|
||||||
|
for (String usesName : moduleInfo.getUses()) {
|
||||||
|
moduleVisitor.visitUse(usesName.replace('.', '/'));
|
||||||
|
}
|
||||||
|
moduleVisitor.visitEnd();
|
||||||
|
classWriter.visitEnd();
|
||||||
|
return classWriter.toByteArray();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,62 @@
|
||||||
|
package org.gradle.sample.transform.javamodules;
|
||||||
|
|
||||||
|
import java.io.Serializable;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Data class to hold the information that should be added as module-info.class to an existing Jar file.
|
||||||
|
*/
|
||||||
|
public class ModuleInfo implements Serializable {
|
||||||
|
private String moduleName;
|
||||||
|
private String moduleVersion;
|
||||||
|
private List<String> exports = new ArrayList<>();
|
||||||
|
private List<String> requires = new ArrayList<>();
|
||||||
|
private List<String> requiresTransitive = new ArrayList<>();
|
||||||
|
private List<String> uses = new ArrayList<>();
|
||||||
|
|
||||||
|
ModuleInfo(String moduleName, String moduleVersion) {
|
||||||
|
this.moduleName = moduleName;
|
||||||
|
this.moduleVersion = moduleVersion;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void exports(String exports) {
|
||||||
|
this.exports.add(exports);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void requires(String requires) {
|
||||||
|
this.requires.add(requires);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void requiresTransitive(String requiresTransitive) {
|
||||||
|
this.requiresTransitive.add(requiresTransitive);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void uses(String uses) {
|
||||||
|
this.uses.add(uses);
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getModuleName() {
|
||||||
|
return moduleName;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected String getModuleVersion() {
|
||||||
|
return moduleVersion;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected List<String> getExports() {
|
||||||
|
return exports;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected List<String> getRequires() {
|
||||||
|
return requires;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected List<String> getRequiresTransitive() {
|
||||||
|
return requiresTransitive;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected List<String> getUses() {
|
||||||
|
return uses;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,114 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright (c) 2018, 2020, Gluon
|
|
||||||
* All rights reserved.
|
|
||||||
*
|
|
||||||
* Redistribution and use in source and binary forms, with or without
|
|
||||||
* modification, are permitted provided that the following conditions are met:
|
|
||||||
*
|
|
||||||
* * Redistributions of source code must retain the above copyright notice, this
|
|
||||||
* list of conditions and the following disclaimer.
|
|
||||||
*
|
|
||||||
* * Redistributions in binary form must reproduce the above copyright notice,
|
|
||||||
* this list of conditions and the following disclaimer in the documentation
|
|
||||||
* and/or other materials provided with the distribution.
|
|
||||||
*
|
|
||||||
* * Neither the name of the copyright holder nor the names of its
|
|
||||||
* contributors may be used to endorse or promote products derived from
|
|
||||||
* this software without specific prior written permission.
|
|
||||||
*
|
|
||||||
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
|
||||||
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
|
||||||
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
||||||
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
|
||||||
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
|
||||||
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
|
||||||
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
|
||||||
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
|
||||||
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
||||||
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
||||||
*/
|
|
||||||
package org.openjfx.gradle;
|
|
||||||
|
|
||||||
import org.gradle.api.GradleException;
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Locale;
|
|
||||||
import java.util.Optional;
|
|
||||||
import java.util.Set;
|
|
||||||
import java.util.regex.Pattern;
|
|
||||||
import java.util.stream.Collectors;
|
|
||||||
import java.util.stream.Stream;
|
|
||||||
|
|
||||||
public enum JavaFXModule {
|
|
||||||
|
|
||||||
BASE,
|
|
||||||
GRAPHICS(BASE),
|
|
||||||
CONTROLS(BASE, GRAPHICS),
|
|
||||||
FXML(BASE, GRAPHICS),
|
|
||||||
MEDIA(BASE, GRAPHICS),
|
|
||||||
SWING(BASE, GRAPHICS),
|
|
||||||
WEB(BASE, CONTROLS, GRAPHICS, MEDIA);
|
|
||||||
|
|
||||||
static final String PREFIX_MODULE = "javafx.";
|
|
||||||
private static final String PREFIX_ARTIFACT = "javafx-";
|
|
||||||
|
|
||||||
private List<JavaFXModule> dependentModules;
|
|
||||||
|
|
||||||
JavaFXModule(JavaFXModule...dependentModules) {
|
|
||||||
this.dependentModules = List.of(dependentModules);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static Optional<JavaFXModule> fromModuleName(String moduleName) {
|
|
||||||
return Stream.of(JavaFXModule.values())
|
|
||||||
.filter(javaFXModule -> moduleName.equals(javaFXModule.getModuleName()))
|
|
||||||
.findFirst();
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getModuleName() {
|
|
||||||
return PREFIX_MODULE + name().toLowerCase(Locale.ROOT);
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getModuleJarFileName() {
|
|
||||||
return getModuleName() + ".jar";
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getArtifactName() {
|
|
||||||
return PREFIX_ARTIFACT + name().toLowerCase(Locale.ROOT);
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean compareJarFileName(JavaFXPlatform platform, String jarFileName) {
|
|
||||||
Pattern p = Pattern.compile(getArtifactName() + "-.+-" + platform.getClassifier() + "\\.jar");
|
|
||||||
return p.matcher(jarFileName).matches();
|
|
||||||
}
|
|
||||||
|
|
||||||
public static Set<JavaFXModule> getJavaFXModules(List<String> moduleNames) {
|
|
||||||
validateModules(moduleNames);
|
|
||||||
|
|
||||||
return moduleNames.stream()
|
|
||||||
.map(JavaFXModule::fromModuleName)
|
|
||||||
.flatMap(Optional::stream)
|
|
||||||
.flatMap(javaFXModule -> javaFXModule.getMavenDependencies().stream())
|
|
||||||
.collect(Collectors.toSet());
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void validateModules(List<String> moduleNames) {
|
|
||||||
var invalidModules = moduleNames.stream()
|
|
||||||
.filter(module -> JavaFXModule.fromModuleName(module).isEmpty())
|
|
||||||
.collect(Collectors.toList());
|
|
||||||
|
|
||||||
if (! invalidModules.isEmpty()) {
|
|
||||||
throw new GradleException("Found one or more invalid JavaFX module names: " + invalidModules);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public List<JavaFXModule> getDependentModules() {
|
|
||||||
return dependentModules;
|
|
||||||
}
|
|
||||||
|
|
||||||
public List<JavaFXModule> getMavenDependencies() {
|
|
||||||
List<JavaFXModule> dependencies = new ArrayList<>(dependentModules);
|
|
||||||
dependencies.add(0, this);
|
|
||||||
return dependencies;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,164 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright (c) 2018, Gluon
|
|
||||||
* All rights reserved.
|
|
||||||
*
|
|
||||||
* Redistribution and use in source and binary forms, with or without
|
|
||||||
* modification, are permitted provided that the following conditions are met:
|
|
||||||
*
|
|
||||||
* * Redistributions of source code must retain the above copyright notice, this
|
|
||||||
* list of conditions and the following disclaimer.
|
|
||||||
*
|
|
||||||
* * Redistributions in binary form must reproduce the above copyright notice,
|
|
||||||
* this list of conditions and the following disclaimer in the documentation
|
|
||||||
* and/or other materials provided with the distribution.
|
|
||||||
*
|
|
||||||
* * Neither the name of the copyright holder nor the names of its
|
|
||||||
* contributors may be used to endorse or promote products derived from
|
|
||||||
* this software without specific prior written permission.
|
|
||||||
*
|
|
||||||
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
|
||||||
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
|
||||||
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
||||||
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
|
||||||
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
|
||||||
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
|
||||||
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
|
||||||
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
|
||||||
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
||||||
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
||||||
*/
|
|
||||||
package org.openjfx.gradle;
|
|
||||||
|
|
||||||
import org.gradle.api.Project;
|
|
||||||
import org.gradle.api.artifacts.repositories.FlatDirectoryArtifactRepository;
|
|
||||||
|
|
||||||
import java.io.File;
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
import static org.openjfx.gradle.JavaFXModule.PREFIX_MODULE;
|
|
||||||
|
|
||||||
public class JavaFXOptions {
|
|
||||||
|
|
||||||
private static final String MAVEN_JAVAFX_ARTIFACT_GROUP_ID = "org.openjfx";
|
|
||||||
private static final String JAVAFX_SDK_LIB_FOLDER = "lib";
|
|
||||||
|
|
||||||
private final Project project;
|
|
||||||
private final JavaFXPlatform platform;
|
|
||||||
|
|
||||||
private String version = "16";
|
|
||||||
private String sdk;
|
|
||||||
private String configuration = "implementation";
|
|
||||||
private String lastUpdatedConfiguration;
|
|
||||||
private List<String> modules = new ArrayList<>();
|
|
||||||
private FlatDirectoryArtifactRepository customSDKArtifactRepository;
|
|
||||||
|
|
||||||
public JavaFXOptions(Project project) {
|
|
||||||
this.project = project;
|
|
||||||
this.platform = JavaFXPlatform.detect(project);
|
|
||||||
}
|
|
||||||
|
|
||||||
public JavaFXPlatform getPlatform() {
|
|
||||||
return platform;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getVersion() {
|
|
||||||
return version;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setVersion(String version) {
|
|
||||||
this.version = version;
|
|
||||||
updateJavaFXDependencies();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* If set, the JavaFX modules will be taken from this local
|
|
||||||
* repository, and not from Maven Central
|
|
||||||
* @param sdk, the path to the local JavaFX SDK folder
|
|
||||||
*/
|
|
||||||
public void setSdk(String sdk) {
|
|
||||||
this.sdk = sdk;
|
|
||||||
updateJavaFXDependencies();
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getSdk() {
|
|
||||||
return sdk;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Set the configuration name for dependencies, e.g.
|
|
||||||
* 'implementation', 'compileOnly' etc.
|
|
||||||
* @param configuration The configuration name for dependencies
|
|
||||||
*/
|
|
||||||
public void setConfiguration(String configuration) {
|
|
||||||
this.configuration = configuration;
|
|
||||||
updateJavaFXDependencies();
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getConfiguration() {
|
|
||||||
return configuration;
|
|
||||||
}
|
|
||||||
|
|
||||||
public List<String> getModules() {
|
|
||||||
return modules;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setModules(List<String> modules) {
|
|
||||||
this.modules = modules;
|
|
||||||
updateJavaFXDependencies();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void modules(String...moduleNames) {
|
|
||||||
setModules(List.of(moduleNames));
|
|
||||||
}
|
|
||||||
|
|
||||||
private void updateJavaFXDependencies() {
|
|
||||||
clearJavaFXDependencies();
|
|
||||||
|
|
||||||
String configuration = getConfiguration();
|
|
||||||
JavaFXModule.getJavaFXModules(this.modules).stream()
|
|
||||||
.sorted()
|
|
||||||
.forEach(javaFXModule -> {
|
|
||||||
if (customSDKArtifactRepository != null) {
|
|
||||||
project.getDependencies().add(configuration, Map.of("name", javaFXModule.getModuleName()));
|
|
||||||
} else {
|
|
||||||
project.getDependencies().add(configuration,
|
|
||||||
String.format("%s:%s:%s:%s", MAVEN_JAVAFX_ARTIFACT_GROUP_ID, javaFXModule.getArtifactName(),
|
|
||||||
getVersion(), getPlatform().getClassifier()));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
lastUpdatedConfiguration = configuration;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void clearJavaFXDependencies() {
|
|
||||||
if (customSDKArtifactRepository != null) {
|
|
||||||
project.getRepositories().remove(customSDKArtifactRepository);
|
|
||||||
customSDKArtifactRepository = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (sdk != null && ! sdk.isEmpty()) {
|
|
||||||
Map<String, String> dirs = new HashMap<>();
|
|
||||||
dirs.put("name", "customSDKArtifactRepository");
|
|
||||||
if (sdk.endsWith(File.separator)) {
|
|
||||||
dirs.put("dirs", sdk + JAVAFX_SDK_LIB_FOLDER);
|
|
||||||
} else {
|
|
||||||
dirs.put("dirs", sdk + File.separator + JAVAFX_SDK_LIB_FOLDER);
|
|
||||||
}
|
|
||||||
customSDKArtifactRepository = project.getRepositories().flatDir(dirs);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (lastUpdatedConfiguration == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
var configuration = project.getConfigurations().findByName(lastUpdatedConfiguration);
|
|
||||||
if (configuration != null) {
|
|
||||||
if (customSDKArtifactRepository != null) {
|
|
||||||
configuration.getDependencies()
|
|
||||||
.removeIf(dependency -> dependency.getName().startsWith(PREFIX_MODULE));
|
|
||||||
}
|
|
||||||
configuration.getDependencies()
|
|
||||||
.removeIf(dependency -> MAVEN_JAVAFX_ARTIFACT_GROUP_ID.equals(dependency.getGroup()));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,91 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright (c) 2018, Gluon
|
|
||||||
* All rights reserved.
|
|
||||||
*
|
|
||||||
* Redistribution and use in source and binary forms, with or without
|
|
||||||
* modification, are permitted provided that the following conditions are met:
|
|
||||||
*
|
|
||||||
* * Redistributions of source code must retain the above copyright notice, this
|
|
||||||
* list of conditions and the following disclaimer.
|
|
||||||
*
|
|
||||||
* * Redistributions in binary form must reproduce the above copyright notice,
|
|
||||||
* this list of conditions and the following disclaimer in the documentation
|
|
||||||
* and/or other materials provided with the distribution.
|
|
||||||
*
|
|
||||||
* * Neither the name of the copyright holder nor the names of its
|
|
||||||
* contributors may be used to endorse or promote products derived from
|
|
||||||
* this software without specific prior written permission.
|
|
||||||
*
|
|
||||||
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
|
||||||
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
|
||||||
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
||||||
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
|
||||||
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
|
||||||
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
|
||||||
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
|
||||||
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
|
||||||
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
||||||
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
||||||
*/
|
|
||||||
package org.openjfx.gradle;
|
|
||||||
|
|
||||||
import com.google.gradle.osdetector.OsDetector;
|
|
||||||
import org.gradle.api.GradleException;
|
|
||||||
import org.gradle.api.Project;
|
|
||||||
|
|
||||||
import java.awt.*;
|
|
||||||
import java.util.Arrays;
|
|
||||||
import java.util.stream.Collectors;
|
|
||||||
|
|
||||||
public enum JavaFXPlatform {
|
|
||||||
|
|
||||||
LINUX("linux", "linux-x86_64"),
|
|
||||||
LINUX_MONOCLE("linux-monocle", "linux-x86_64-monocle"),
|
|
||||||
LINUX_AARCH64("linux-aarch64", "linux-aarch_64"),
|
|
||||||
LINUX_AARCH64_MONOCLE("linux-aarch64-monocle", "linux-aarch_64-monocle"),
|
|
||||||
WINDOWS("win", "windows-x86_64"),
|
|
||||||
WINDOWS_MONOCLE("win-monocle", "windows-x86_64-monocle"),
|
|
||||||
OSX("mac", "osx-x86_64"),
|
|
||||||
OSX_MONOCLE("mac-monocle", "osx-x86_64-monocle"),
|
|
||||||
OSX_AARCH64("mac-aarch64", "osx-aarch_64"),
|
|
||||||
OSX_AARCH64_MONOCLE("mac-aarch64-monocle", "osx-aarch_64-monocle");
|
|
||||||
|
|
||||||
private final String classifier;
|
|
||||||
private final String osDetectorClassifier;
|
|
||||||
|
|
||||||
JavaFXPlatform( String classifier, String osDetectorClassifier ) {
|
|
||||||
this.classifier = classifier;
|
|
||||||
this.osDetectorClassifier = osDetectorClassifier;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getClassifier() {
|
|
||||||
return classifier;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static JavaFXPlatform detect(Project project) {
|
|
||||||
|
|
||||||
String osClassifier = project.getExtensions().getByType(OsDetector.class).getClassifier();
|
|
||||||
|
|
||||||
if("true".equals(System.getProperty("java.awt.headless"))) {
|
|
||||||
osClassifier += "-monocle";
|
|
||||||
}
|
|
||||||
|
|
||||||
for ( JavaFXPlatform platform: values()) {
|
|
||||||
if ( platform.osDetectorClassifier.equals(osClassifier)) {
|
|
||||||
return platform;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
String supportedPlatforms = Arrays.stream(values())
|
|
||||||
.map(p->p.osDetectorClassifier)
|
|
||||||
.collect(Collectors.joining("', '", "'", "'"));
|
|
||||||
|
|
||||||
throw new GradleException(
|
|
||||||
String.format(
|
|
||||||
"Unsupported JavaFX platform found: '%s'! " +
|
|
||||||
"This plugin is designed to work on supported platforms only." +
|
|
||||||
"Current supported platforms are %s.", osClassifier, supportedPlatforms )
|
|
||||||
);
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,47 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright (c) 2018, Gluon
|
|
||||||
* All rights reserved.
|
|
||||||
*
|
|
||||||
* Redistribution and use in source and binary forms, with or without
|
|
||||||
* modification, are permitted provided that the following conditions are met:
|
|
||||||
*
|
|
||||||
* * Redistributions of source code must retain the above copyright notice, this
|
|
||||||
* list of conditions and the following disclaimer.
|
|
||||||
*
|
|
||||||
* * Redistributions in binary form must reproduce the above copyright notice,
|
|
||||||
* this list of conditions and the following disclaimer in the documentation
|
|
||||||
* and/or other materials provided with the distribution.
|
|
||||||
*
|
|
||||||
* * Neither the name of the copyright holder nor the names of its
|
|
||||||
* contributors may be used to endorse or promote products derived from
|
|
||||||
* this software without specific prior written permission.
|
|
||||||
*
|
|
||||||
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
|
||||||
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
|
||||||
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
||||||
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
|
||||||
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
|
||||||
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
|
||||||
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
|
||||||
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
|
||||||
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
||||||
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
||||||
*/
|
|
||||||
package org.openjfx.gradle;
|
|
||||||
|
|
||||||
import com.google.gradle.osdetector.OsDetectorPlugin;
|
|
||||||
import org.gradle.api.Plugin;
|
|
||||||
import org.gradle.api.Project;
|
|
||||||
import org.openjfx.gradle.tasks.ExecTask;
|
|
||||||
|
|
||||||
public class JavaFXPlugin implements Plugin<Project> {
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void apply(Project project) {
|
|
||||||
project.getPlugins().apply(OsDetectorPlugin.class);
|
|
||||||
|
|
||||||
project.getExtensions().create("javafx", JavaFXOptions.class, project);
|
|
||||||
|
|
||||||
project.getTasks().register("configJavafxRun", ExecTask.class, project);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,90 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright (c) 2019, 2021, Gluon
|
|
||||||
* All rights reserved.
|
|
||||||
*
|
|
||||||
* Redistribution and use in source and binary forms, with or without
|
|
||||||
* modification, are permitted provided that the following conditions are met:
|
|
||||||
*
|
|
||||||
* * Redistributions of source code must retain the above copyright notice, this
|
|
||||||
* list of conditions and the following disclaimer.
|
|
||||||
*
|
|
||||||
* * Redistributions in binary form must reproduce the above copyright notice,
|
|
||||||
* this list of conditions and the following disclaimer in the documentation
|
|
||||||
* and/or other materials provided with the distribution.
|
|
||||||
*
|
|
||||||
* * Neither the name of the copyright holder nor the names of its
|
|
||||||
* contributors may be used to endorse or promote products derived from
|
|
||||||
* this software without specific prior written permission.
|
|
||||||
*
|
|
||||||
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
|
||||||
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
|
||||||
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
||||||
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
|
||||||
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
|
||||||
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
|
||||||
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
|
||||||
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
|
||||||
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
||||||
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
||||||
*/
|
|
||||||
package org.openjfx.gradle.tasks;
|
|
||||||
|
|
||||||
import org.gradle.api.DefaultTask;
|
|
||||||
import org.gradle.api.GradleException;
|
|
||||||
import org.gradle.api.Project;
|
|
||||||
import org.gradle.api.file.FileCollection;
|
|
||||||
import org.gradle.api.plugins.ApplicationPlugin;
|
|
||||||
import org.gradle.api.tasks.JavaExec;
|
|
||||||
import org.gradle.api.tasks.TaskAction;
|
|
||||||
import org.openjfx.gradle.JavaFXModule;
|
|
||||||
import org.openjfx.gradle.JavaFXOptions;
|
|
||||||
import org.openjfx.gradle.JavaFXPlatform;
|
|
||||||
|
|
||||||
import javax.inject.Inject;
|
|
||||||
import java.io.File;
|
|
||||||
import java.util.Arrays;
|
|
||||||
import java.util.TreeSet;
|
|
||||||
|
|
||||||
public class ExecTask extends DefaultTask {
|
|
||||||
private final Project project;
|
|
||||||
private JavaExec execTask;
|
|
||||||
|
|
||||||
@Inject
|
|
||||||
public ExecTask(Project project) {
|
|
||||||
this.project = project;
|
|
||||||
project.getPluginManager().withPlugin(ApplicationPlugin.APPLICATION_PLUGIN_NAME, e -> {
|
|
||||||
execTask = (JavaExec) project.getTasks().findByName(ApplicationPlugin.TASK_RUN_NAME);
|
|
||||||
if (execTask != null) {
|
|
||||||
execTask.dependsOn(this);
|
|
||||||
} else {
|
|
||||||
throw new GradleException("Run task not found.");
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
@TaskAction
|
|
||||||
public void action() {
|
|
||||||
if (execTask != null) {
|
|
||||||
JavaFXOptions javaFXOptions = project.getExtensions().getByType(JavaFXOptions.class);
|
|
||||||
JavaFXModule.validateModules(javaFXOptions.getModules());
|
|
||||||
|
|
||||||
var definedJavaFXModuleNames = new TreeSet<>(javaFXOptions.getModules());
|
|
||||||
if (!definedJavaFXModuleNames.isEmpty()) {
|
|
||||||
final FileCollection classpathWithoutJavaFXJars = execTask.getClasspath().filter(
|
|
||||||
jar -> Arrays.stream(JavaFXModule.values()).noneMatch(javaFXModule -> jar.getName().contains(javaFXModule.getArtifactName()))
|
|
||||||
);
|
|
||||||
final FileCollection javaFXPlatformJars = execTask.getClasspath().filter(jar -> isJavaFXJar(jar, javaFXOptions.getPlatform()));
|
|
||||||
execTask.setClasspath(classpathWithoutJavaFXJars.plus(javaFXPlatformJars));
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
throw new GradleException("Run task not found. Please, make sure the Application plugin is applied");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static boolean isJavaFXJar(File jar, JavaFXPlatform platform) {
|
|
||||||
return jar.isFile() &&
|
|
||||||
Arrays.stream(JavaFXModule.values()).anyMatch(javaFXModule ->
|
|
||||||
javaFXModule.compareJarFileName(platform, jar.getName()) ||
|
|
||||||
javaFXModule.getModuleJarFileName().equals(jar.getName()));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -12,18 +12,17 @@ Work on resolving both of these issues is ongoing.
|
||||||
### Install Java
|
### Install Java
|
||||||
|
|
||||||
Because Sparrow bundles a Java runtime in the release binaries, it is essential to have the same version of Java installed when creating the release.
|
Because Sparrow bundles a Java runtime in the release binaries, it is essential to have the same version of Java installed when creating the release.
|
||||||
For v1.6.6 to v1.9.1, this was Eclipse Temurin 18.0.1+10. For v2.0.0 and later, Eclipse Temurin 22.0.2+9 is used.
|
For v1.6.6 and later, this is Eclipse Temurin 18.0.1+10.
|
||||||
|
|
||||||
#### Java from Adoptium github repo
|
#### Java from Adoptium github repo
|
||||||
|
|
||||||
It is available for all supported platforms from [Eclipse Temurin 22.0.2+9](https://github.com/adoptium/temurin22-binaries/releases/tag/jdk-22.0.2%2B9).
|
It is available for all supported platforms from [Eclipse Temurin 18.0.1+10](https://github.com/adoptium/temurin18-binaries/releases/tag/jdk-18.0.1%2B10).
|
||||||
|
|
||||||
For reference, the downloads are as follows:
|
For reference, the downloads are as follows:
|
||||||
- [Linux x64](https://github.com/adoptium/temurin22-binaries/releases/download/jdk-22.0.2%2B9/OpenJDK22U-jdk_x64_linux_hotspot_22.0.2_9.tar.gz)
|
- [Linux x64](https://github.com/adoptium/temurin18-binaries/releases/download/jdk-18.0.1%2B10/OpenJDK18U-jdk_x64_linux_hotspot_18.0.1_10.tar.gz)
|
||||||
- [Linux aarch64](https://github.com/adoptium/temurin22-binaries/releases/download/jdk-22.0.2%2B9/OpenJDK22U-jdk_aarch64_linux_hotspot_22.0.2_9.tar.gz)
|
- [MacOS x64](https://github.com/adoptium/temurin18-binaries/releases/download/jdk-18.0.1%2B10/OpenJDK18U-jdk_x64_mac_hotspot_18.0.1_10.tar.gz)
|
||||||
- [MacOS x64](https://github.com/adoptium/temurin22-binaries/releases/download/jdk-22.0.2%2B9/OpenJDK22U-jdk_x64_mac_hotspot_22.0.2_9.tar.gz)
|
- [MacOS aarch64](https://github.com/adoptium/temurin18-binaries/releases/download/jdk-18.0.1%2B10/OpenJDK18U-jdk_aarch64_mac_hotspot_18.0.1_10.tar.gz)
|
||||||
- [MacOS aarch64](https://github.com/adoptium/temurin22-binaries/releases/download/jdk-22.0.2%2B9/OpenJDK22U-jdk_aarch64_mac_hotspot_22.0.2_9.tar.gz)
|
- [Windows x64](https://github.com/adoptium/temurin18-binaries/releases/download/jdk-18.0.1%2B10/OpenJDK18U-jdk_x64_windows_hotspot_18.0.1_10.zip)
|
||||||
- [Windows x64](https://github.com/adoptium/temurin22-binaries/releases/download/jdk-22.0.2%2B9/OpenJDK22U-jdk_x64_windows_hotspot_22.0.2_9.zip)
|
|
||||||
|
|
||||||
#### Java from Adoptium deb repo
|
#### Java from Adoptium deb repo
|
||||||
|
|
||||||
|
|
@ -58,7 +57,7 @@ echo "deb [signed-by=/usr/share/keyrings/adoptium.asc] https://packages.adoptium
|
||||||
Update cache, install the desired temurin version and configure java to be linked to this same version:
|
Update cache, install the desired temurin version and configure java to be linked to this same version:
|
||||||
```
|
```
|
||||||
sudo apt update -y
|
sudo apt update -y
|
||||||
sudo apt-get install -y temurin-22-jdk=22.0.2+9
|
sudo apt-get install -y temurin-18-jdk=18.0.1+10
|
||||||
sudo update-alternatives --config java
|
sudo update-alternatives --config java
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
@ -68,7 +67,7 @@ A alternative option for all platforms is to use the [sdkman.io](https://sdkman.
|
||||||
See the installation [instructions here](https://sdkman.io/install).
|
See the installation [instructions here](https://sdkman.io/install).
|
||||||
Once installed, run
|
Once installed, run
|
||||||
```shell
|
```shell
|
||||||
sdk install java 22.0.2-tem
|
sdk install java 18.0.1-tem
|
||||||
```
|
```
|
||||||
|
|
||||||
### Other requirements
|
### Other requirements
|
||||||
|
|
@ -83,7 +82,7 @@ sudo apt install -y rpm fakeroot binutils
|
||||||
First, assign a temporary variable in your shell for the specific release you want to build. For the current one specify:
|
First, assign a temporary variable in your shell for the specific release you want to build. For the current one specify:
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
GIT_TAG="2.3.0"
|
GIT_TAG="1.8.2"
|
||||||
```
|
```
|
||||||
|
|
||||||
The project can then be initially cloned as follows:
|
The project can then be initially cloned as follows:
|
||||||
|
|
|
||||||
2
drongo
2
drongo
|
|
@ -1 +1 @@
|
||||||
Subproject commit e975cbe6f8d8574785124e6db5780d0541e20024
|
Subproject commit 6868b026fbc1c5093bbad7db32b14e00c78717f2
|
||||||
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
Binary file not shown.
2
gradle/wrapper/gradle-wrapper.properties
vendored
2
gradle/wrapper/gradle-wrapper.properties
vendored
|
|
@ -1,6 +1,6 @@
|
||||||
distributionBase=GRADLE_USER_HOME
|
distributionBase=GRADLE_USER_HOME
|
||||||
distributionPath=wrapper/dists
|
distributionPath=wrapper/dists
|
||||||
distributionUrl=https\://services.gradle.org/distributions/gradle-9.1.0-bin.zip
|
distributionUrl=https\://services.gradle.org/distributions/gradle-8.5-bin.zip
|
||||||
networkTimeout=10000
|
networkTimeout=10000
|
||||||
validateDistributionUrl=true
|
validateDistributionUrl=true
|
||||||
zipStoreBase=GRADLE_USER_HOME
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
|
|
|
||||||
15
gradlew
vendored
15
gradlew
vendored
|
|
@ -1,7 +1,7 @@
|
||||||
#!/bin/sh
|
#!/bin/sh
|
||||||
|
|
||||||
#
|
#
|
||||||
# Copyright © 2015 the original authors.
|
# Copyright © 2015-2021 the original authors.
|
||||||
#
|
#
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
# you may not use this file except in compliance with the License.
|
# you may not use this file except in compliance with the License.
|
||||||
|
|
@ -15,8 +15,6 @@
|
||||||
# See the License for the specific language governing permissions and
|
# See the License for the specific language governing permissions and
|
||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
#
|
#
|
||||||
# SPDX-License-Identifier: Apache-2.0
|
|
||||||
#
|
|
||||||
|
|
||||||
##############################################################################
|
##############################################################################
|
||||||
#
|
#
|
||||||
|
|
@ -57,7 +55,7 @@
|
||||||
# Darwin, MinGW, and NonStop.
|
# Darwin, MinGW, and NonStop.
|
||||||
#
|
#
|
||||||
# (3) This script is generated from the Groovy template
|
# (3) This script is generated from the Groovy template
|
||||||
# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
|
# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
|
||||||
# within the Gradle project.
|
# within the Gradle project.
|
||||||
#
|
#
|
||||||
# You can find Gradle at https://github.com/gradle/gradle/.
|
# You can find Gradle at https://github.com/gradle/gradle/.
|
||||||
|
|
@ -86,7 +84,7 @@ done
|
||||||
# shellcheck disable=SC2034
|
# shellcheck disable=SC2034
|
||||||
APP_BASE_NAME=${0##*/}
|
APP_BASE_NAME=${0##*/}
|
||||||
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
|
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
|
||||||
APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit
|
APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit
|
||||||
|
|
||||||
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
||||||
MAX_FD=maximum
|
MAX_FD=maximum
|
||||||
|
|
@ -114,6 +112,7 @@ case "$( uname )" in #(
|
||||||
NONSTOP* ) nonstop=true ;;
|
NONSTOP* ) nonstop=true ;;
|
||||||
esac
|
esac
|
||||||
|
|
||||||
|
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
|
||||||
|
|
||||||
|
|
||||||
# Determine the Java command to use to start the JVM.
|
# Determine the Java command to use to start the JVM.
|
||||||
|
|
@ -171,6 +170,7 @@ fi
|
||||||
# For Cygwin or MSYS, switch paths to Windows format before running java
|
# For Cygwin or MSYS, switch paths to Windows format before running java
|
||||||
if "$cygwin" || "$msys" ; then
|
if "$cygwin" || "$msys" ; then
|
||||||
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
|
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
|
||||||
|
CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
|
||||||
|
|
||||||
JAVACMD=$( cygpath --unix "$JAVACMD" )
|
JAVACMD=$( cygpath --unix "$JAVACMD" )
|
||||||
|
|
||||||
|
|
@ -203,14 +203,15 @@ fi
|
||||||
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
|
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
|
||||||
|
|
||||||
# Collect all arguments for the java command:
|
# Collect all arguments for the java command:
|
||||||
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
|
# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
|
||||||
# and any embedded shellness will be escaped.
|
# and any embedded shellness will be escaped.
|
||||||
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
|
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
|
||||||
# treated as '${Hostname}' itself on the command line.
|
# treated as '${Hostname}' itself on the command line.
|
||||||
|
|
||||||
set -- \
|
set -- \
|
||||||
"-Dorg.gradle.appname=$APP_BASE_NAME" \
|
"-Dorg.gradle.appname=$APP_BASE_NAME" \
|
||||||
-jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \
|
-classpath "$CLASSPATH" \
|
||||||
|
org.gradle.wrapper.GradleWrapperMain \
|
||||||
"$@"
|
"$@"
|
||||||
|
|
||||||
# Stop when "xargs" is not available.
|
# Stop when "xargs" is not available.
|
||||||
|
|
|
||||||
25
gradlew.bat
vendored
25
gradlew.bat
vendored
|
|
@ -13,8 +13,6 @@
|
||||||
@rem See the License for the specific language governing permissions and
|
@rem See the License for the specific language governing permissions and
|
||||||
@rem limitations under the License.
|
@rem limitations under the License.
|
||||||
@rem
|
@rem
|
||||||
@rem SPDX-License-Identifier: Apache-2.0
|
|
||||||
@rem
|
|
||||||
|
|
||||||
@if "%DEBUG%"=="" @echo off
|
@if "%DEBUG%"=="" @echo off
|
||||||
@rem ##########################################################################
|
@rem ##########################################################################
|
||||||
|
|
@ -45,11 +43,11 @@ set JAVA_EXE=java.exe
|
||||||
%JAVA_EXE% -version >NUL 2>&1
|
%JAVA_EXE% -version >NUL 2>&1
|
||||||
if %ERRORLEVEL% equ 0 goto execute
|
if %ERRORLEVEL% equ 0 goto execute
|
||||||
|
|
||||||
echo. 1>&2
|
echo.
|
||||||
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
|
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||||
echo. 1>&2
|
echo.
|
||||||
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
|
echo Please set the JAVA_HOME variable in your environment to match the
|
||||||
echo location of your Java installation. 1>&2
|
echo location of your Java installation.
|
||||||
|
|
||||||
goto fail
|
goto fail
|
||||||
|
|
||||||
|
|
@ -59,21 +57,22 @@ set JAVA_EXE=%JAVA_HOME%/bin/java.exe
|
||||||
|
|
||||||
if exist "%JAVA_EXE%" goto execute
|
if exist "%JAVA_EXE%" goto execute
|
||||||
|
|
||||||
echo. 1>&2
|
echo.
|
||||||
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
|
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
|
||||||
echo. 1>&2
|
echo.
|
||||||
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
|
echo Please set the JAVA_HOME variable in your environment to match the
|
||||||
echo location of your Java installation. 1>&2
|
echo location of your Java installation.
|
||||||
|
|
||||||
goto fail
|
goto fail
|
||||||
|
|
||||||
:execute
|
:execute
|
||||||
@rem Setup the command line
|
@rem Setup the command line
|
||||||
|
|
||||||
|
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
|
||||||
|
|
||||||
|
|
||||||
@rem Execute Gradle
|
@rem Execute Gradle
|
||||||
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %*
|
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
|
||||||
|
|
||||||
:end
|
:end
|
||||||
@rem End local scope for the variables with windows NT shell
|
@rem End local scope for the variables with windows NT shell
|
||||||
|
|
|
||||||
1
lark
1
lark
|
|
@ -1 +0,0 @@
|
||||||
Subproject commit 10e8d9cd4bbe9fde4dd93c059e2a9faeec6be3e0
|
|
||||||
48
repackage.sh
48
repackage.sh
|
|
@ -1,48 +0,0 @@
|
||||||
#!/bin/bash
|
|
||||||
set -e # Exit on any error
|
|
||||||
|
|
||||||
# Define paths
|
|
||||||
BUILD_DIR="build"
|
|
||||||
JPACKAGE_DIR="$BUILD_DIR/jpackage"
|
|
||||||
TEMP_DIR="$BUILD_DIR/repackage"
|
|
||||||
|
|
||||||
# Find the .deb file in build/jpackage (assuming there is only one)
|
|
||||||
DEB_FILE=$(find "$JPACKAGE_DIR" -type f -name "*.deb" -print -quit)
|
|
||||||
|
|
||||||
# Check if a .deb file was found
|
|
||||||
if [ -z "$DEB_FILE" ]; then
|
|
||||||
echo "Error: No .deb file found in $JPACKAGE_DIR"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Extract the filename from the path for later use
|
|
||||||
DEB_FILENAME=$(basename "$DEB_FILE")
|
|
||||||
|
|
||||||
echo "Found .deb file: $DEB_FILENAME"
|
|
||||||
|
|
||||||
# Create a temp directory inside build to avoid file conflicts
|
|
||||||
mkdir -p "$TEMP_DIR"
|
|
||||||
cd "$TEMP_DIR"
|
|
||||||
|
|
||||||
# Extract the .deb file contents
|
|
||||||
ar x "../../$DEB_FILE"
|
|
||||||
|
|
||||||
# Decompress zst files to tar
|
|
||||||
unzstd control.tar.zst
|
|
||||||
unzstd data.tar.zst
|
|
||||||
|
|
||||||
# Compress tar files to xz
|
|
||||||
xz -c control.tar > control.tar.xz
|
|
||||||
xz -c data.tar > data.tar.xz
|
|
||||||
|
|
||||||
# Remove the original .deb file
|
|
||||||
rm "../../$DEB_FILE"
|
|
||||||
|
|
||||||
# Create the new .deb file with xz compression in the original location
|
|
||||||
ar cr "../../$DEB_FILE" debian-binary control.tar.xz data.tar.xz
|
|
||||||
|
|
||||||
# Clean up temp files
|
|
||||||
cd ../..
|
|
||||||
rm -rf "$TEMP_DIR"
|
|
||||||
|
|
||||||
echo "Repackaging complete: $DEB_FILENAME"
|
|
||||||
|
|
@ -1,4 +1,3 @@
|
||||||
rootProject.name = 'sparrow'
|
rootProject.name = 'sparrow'
|
||||||
include 'drongo'
|
include 'drongo'
|
||||||
include 'lark'
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,12 +0,0 @@
|
||||||
Package: sparrowserver
|
|
||||||
Version: ${version}-1
|
|
||||||
Section: utils
|
|
||||||
Maintainer: Craig Raw <mail@sparrowwallet.com>
|
|
||||||
Priority: optional
|
|
||||||
Architecture: ${arch}
|
|
||||||
Conflicts: sparrow (<= 2.1.4)
|
|
||||||
Replaces: sparrow (<= 2.1.4)
|
|
||||||
Provides: sparrowserver
|
|
||||||
Description: Sparrow Server
|
|
||||||
Depends: libc6, zlib1g
|
|
||||||
Installed-Size: ${size}
|
|
||||||
|
|
@ -1,85 +0,0 @@
|
||||||
Summary: Sparrow Server
|
|
||||||
Name: sparrowserver
|
|
||||||
Version: ${version}
|
|
||||||
Release: 1
|
|
||||||
License: ASL 2.0
|
|
||||||
Vendor: Unknown
|
|
||||||
|
|
||||||
%if "x" != "x"
|
|
||||||
URL: https://sparrowwallet.com
|
|
||||||
%endif
|
|
||||||
|
|
||||||
%if "x/opt" != "x"
|
|
||||||
Prefix: /opt
|
|
||||||
%endif
|
|
||||||
|
|
||||||
Provides: sparrowserver
|
|
||||||
Obsoletes: sparrow <= 2.1.4
|
|
||||||
|
|
||||||
%if "xutils" != "x"
|
|
||||||
Group: utils
|
|
||||||
%endif
|
|
||||||
|
|
||||||
Autoprov: 0
|
|
||||||
Autoreq: 0
|
|
||||||
|
|
||||||
#comment line below to enable effective jar compression
|
|
||||||
#it could easily get your package size from 40 to 15Mb but
|
|
||||||
#build time will substantially increase and it may require unpack200/system java to install
|
|
||||||
%define __jar_repack %{nil}
|
|
||||||
|
|
||||||
# on RHEL we got unwanted improved debugging enhancements
|
|
||||||
%define _build_id_links none
|
|
||||||
|
|
||||||
%define package_filelist %{_builddir}/%{name}.files
|
|
||||||
%define app_filelist %{_builddir}/%{name}.app.files
|
|
||||||
%define filesystem_filelist %{_builddir}/%{name}.filesystem.files
|
|
||||||
|
|
||||||
%define default_filesystem / /opt /usr /usr/bin /usr/lib /usr/local /usr/local/bin /usr/local/lib
|
|
||||||
|
|
||||||
%description
|
|
||||||
Sparrow Server
|
|
||||||
|
|
||||||
%global __os_install_post %{nil}
|
|
||||||
|
|
||||||
%prep
|
|
||||||
|
|
||||||
%build
|
|
||||||
|
|
||||||
%install
|
|
||||||
rm -rf %{buildroot}
|
|
||||||
install -d -m 755 %{buildroot}/opt/sparrowserver
|
|
||||||
cp -r %{_sourcedir}/opt/sparrowserver/* %{buildroot}/opt/sparrowserver
|
|
||||||
if [ "$(echo %{_sourcedir}/lib/systemd/system/*.service)" != '%{_sourcedir}/lib/systemd/system/*.service' ]; then
|
|
||||||
install -d -m 755 %{buildroot}/lib/systemd/system
|
|
||||||
cp %{_sourcedir}/lib/systemd/system/*.service %{buildroot}/lib/systemd/system
|
|
||||||
fi
|
|
||||||
%if "x%{_rpmdir}/../../LICENSE" != "x"
|
|
||||||
%define license_install_file %{_defaultlicensedir}/%{name}-%{version}/%{basename:%{_rpmdir}/../../LICENSE}
|
|
||||||
install -d -m 755 "%{buildroot}%{dirname:%{license_install_file}}"
|
|
||||||
install -m 644 "%{_rpmdir}/../../LICENSE" "%{buildroot}%{license_install_file}"
|
|
||||||
%endif
|
|
||||||
(cd %{buildroot} && find . -path ./lib/systemd -prune -o -type d -print) | sed -e 's/^\.//' -e '/^$/d' | sort > %{app_filelist}
|
|
||||||
{ rpm -ql filesystem || echo %{default_filesystem}; } | sort > %{filesystem_filelist}
|
|
||||||
comm -23 %{app_filelist} %{filesystem_filelist} > %{package_filelist}
|
|
||||||
sed -i -e 's/.*/%dir "&"/' %{package_filelist}
|
|
||||||
(cd %{buildroot} && find . -not -type d) | sed -e 's/^\.//' -e 's/.*/"&"/' >> %{package_filelist}
|
|
||||||
%if "x%{_rpmdir}/../../LICENSE" != "x"
|
|
||||||
sed -i -e 's|"%{license_install_file}"||' -e '/^$/d' %{package_filelist}
|
|
||||||
%endif
|
|
||||||
|
|
||||||
%files -f %{package_filelist}
|
|
||||||
%if "x%{_rpmdir}/../../LICENSE" != "x"
|
|
||||||
%license "%{license_install_file}"
|
|
||||||
%endif
|
|
||||||
|
|
||||||
%post
|
|
||||||
package_type=rpm
|
|
||||||
|
|
||||||
%pre
|
|
||||||
package_type=rpm
|
|
||||||
|
|
||||||
%preun
|
|
||||||
package_type=rpm
|
|
||||||
|
|
||||||
%clean
|
|
||||||
|
|
@ -1,11 +1,10 @@
|
||||||
[Desktop Entry]
|
[Desktop Entry]
|
||||||
Name=Sparrow
|
Name=Sparrow
|
||||||
Comment=Sparrow
|
Comment=Sparrow
|
||||||
Exec=/opt/sparrowwallet/bin/Sparrow %U
|
Exec=/opt/sparrow/bin/Sparrow %U
|
||||||
Icon=/opt/sparrowwallet/lib/Sparrow.png
|
Icon=/opt/sparrow/lib/Sparrow.png
|
||||||
Terminal=false
|
Terminal=false
|
||||||
Type=Application
|
Type=Application
|
||||||
Categories=Finance;Network;
|
Categories=Finance;Network;
|
||||||
MimeType=application/psbt;application/bitcoin-transaction;application/pgp-signature;x-scheme-handler/bitcoin;x-scheme-handler/auth47;x-scheme-handler/lightning
|
MimeType=application/psbt;application/bitcoin-transaction;application/pgp-signature;x-scheme-handler/bitcoin;x-scheme-handler/auth47;x-scheme-handler/lightning
|
||||||
StartupWMClass=Sparrow
|
StartupWMClass=Sparrow
|
||||||
SingleMainWindow=true
|
|
||||||
|
|
|
||||||
|
|
@ -1,12 +0,0 @@
|
||||||
Package: sparrowwallet
|
|
||||||
Version: ${version}-1
|
|
||||||
Section: utils
|
|
||||||
Maintainer: Craig Raw <mail@sparrowwallet.com>
|
|
||||||
Priority: optional
|
|
||||||
Architecture: ${arch}
|
|
||||||
Provides: sparrowwallet
|
|
||||||
Conflicts: sparrow (<= 2.1.4)
|
|
||||||
Replaces: sparrow (<= 2.1.4)
|
|
||||||
Description: Sparrow Wallet
|
|
||||||
Depends: libasound2, libbsd0, libc6, libmd0, libx11-6, libxau6, libxcb1, libxdmcp6, libxext6, libxi6, libxrender1, libxtst6, xdg-utils
|
|
||||||
Installed-Size: ${size}
|
|
||||||
|
|
@ -1,49 +0,0 @@
|
||||||
#!/bin/sh
|
|
||||||
# postinst script for sparrowwallet
|
|
||||||
#
|
|
||||||
# see: dh_installdeb(1)
|
|
||||||
|
|
||||||
set -e
|
|
||||||
|
|
||||||
# summary of how this script can be called:
|
|
||||||
# * <postinst> `configure' <most-recently-configured-version>
|
|
||||||
# * <old-postinst> `abort-upgrade' <new version>
|
|
||||||
# * <conflictor's-postinst> `abort-remove' `in-favour' <package>
|
|
||||||
# <new-version>
|
|
||||||
# * <postinst> `abort-remove'
|
|
||||||
# * <deconfigured's-postinst> `abort-deconfigure' `in-favour'
|
|
||||||
# <failed-install-package> <version> `removing'
|
|
||||||
# <conflicting-package> <version>
|
|
||||||
# for details, see https://www.debian.org/doc/debian-policy/ or
|
|
||||||
# the debian-policy package
|
|
||||||
|
|
||||||
package_type=deb
|
|
||||||
|
|
||||||
|
|
||||||
case "$1" in
|
|
||||||
configure)
|
|
||||||
xdg-desktop-menu install /opt/sparrowwallet/lib/sparrowwallet-Sparrow.desktop
|
|
||||||
xdg-mime install /opt/sparrowwallet/lib/sparrowwallet-Sparrow-MimeInfo.xml
|
|
||||||
install -D -m 644 /opt/sparrowwallet/lib/runtime/conf/udev/*.rules /etc/udev/rules.d
|
|
||||||
if ! getent group plugdev > /dev/null; then
|
|
||||||
groupadd plugdev
|
|
||||||
fi
|
|
||||||
if ! groups "${SUDO_USER:-$(whoami)}" | grep -q plugdev; then
|
|
||||||
usermod -aG plugdev "${SUDO_USER:-$(whoami)}"
|
|
||||||
fi
|
|
||||||
if [ -w /sys/devices ] && [ -w /sys/kernel/uevent_seqnum ] && [ -x /bin/udevadm ]; then
|
|
||||||
/bin/udevadm control --reload
|
|
||||||
/bin/udevadm trigger
|
|
||||||
fi
|
|
||||||
;;
|
|
||||||
|
|
||||||
abort-upgrade|abort-remove|abort-deconfigure)
|
|
||||||
;;
|
|
||||||
|
|
||||||
*)
|
|
||||||
echo "postinst called with unknown argument \`$1'" >&2
|
|
||||||
exit 1
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
|
|
||||||
exit 0
|
|
||||||
|
|
@ -1,260 +0,0 @@
|
||||||
Summary: Sparrow
|
|
||||||
Name: sparrowwallet
|
|
||||||
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: sparrowwallet
|
|
||||||
Obsoletes: sparrow <= 2.1.4
|
|
||||||
|
|
||||||
%if "xutils" != "x"
|
|
||||||
Group: utils
|
|
||||||
%endif
|
|
||||||
|
|
||||||
Autoprov: 0
|
|
||||||
Autoreq: 0
|
|
||||||
%if "xxdg-utils" != "x" || "x" != "x"
|
|
||||||
Requires: xdg-utils
|
|
||||||
%endif
|
|
||||||
|
|
||||||
#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 Wallet
|
|
||||||
|
|
||||||
%global __os_install_post %{nil}
|
|
||||||
|
|
||||||
%prep
|
|
||||||
|
|
||||||
%build
|
|
||||||
|
|
||||||
%install
|
|
||||||
rm -rf %{buildroot}
|
|
||||||
install -d -m 755 %{buildroot}/opt/sparrowwallet
|
|
||||||
cp -r %{_sourcedir}/opt/sparrowwallet/* %{buildroot}/opt/sparrowwallet
|
|
||||||
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
|
|
||||||
xdg-desktop-menu install /opt/sparrowwallet/lib/sparrowwallet-Sparrow.desktop
|
|
||||||
xdg-mime install /opt/sparrowwallet/lib/sparrowwallet-Sparrow-MimeInfo.xml
|
|
||||||
install -D -m 644 /opt/sparrowwallet/lib/runtime/conf/udev/*.rules /etc/udev/rules.d
|
|
||||||
if ! getent group plugdev > /dev/null; then
|
|
||||||
groupadd plugdev
|
|
||||||
fi
|
|
||||||
if ! groups "${SUDO_USER:-$(whoami)}" | grep -q plugdev; then
|
|
||||||
usermod -aG plugdev "${SUDO_USER:-$(whoami)}"
|
|
||||||
fi
|
|
||||||
if [ -w /sys/devices ] && [ -w /sys/kernel/uevent_seqnum ] && [ -x /bin/udevadm ]; then
|
|
||||||
/bin/udevadm control --reload
|
|
||||||
/bin/udevadm trigger
|
|
||||||
fi
|
|
||||||
|
|
||||||
%pre
|
|
||||||
package_type=rpm
|
|
||||||
file_belongs_to_single_package ()
|
|
||||||
{
|
|
||||||
if [ ! -e "$1" ]; then
|
|
||||||
false
|
|
||||||
elif [ "$package_type" = rpm ]; then
|
|
||||||
test `rpm -q --whatprovides "$1" | wc -l` = 1
|
|
||||||
elif [ "$package_type" = deb ]; then
|
|
||||||
test `dpkg -S "$1" | wc -l` = 1
|
|
||||||
else
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
do_if_file_belongs_to_single_package ()
|
|
||||||
{
|
|
||||||
local file="$1"
|
|
||||||
shift
|
|
||||||
|
|
||||||
if file_belongs_to_single_package "$file"; then
|
|
||||||
"$@"
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
if [ "$1" -gt 1 ]; then
|
|
||||||
:;
|
|
||||||
fi
|
|
||||||
|
|
||||||
%preun
|
|
||||||
package_type=rpm
|
|
||||||
file_belongs_to_single_package ()
|
|
||||||
{
|
|
||||||
if [ ! -e "$1" ]; then
|
|
||||||
false
|
|
||||||
elif [ "$package_type" = rpm ]; then
|
|
||||||
test `rpm -q --whatprovides "$1" | wc -l` = 1
|
|
||||||
elif [ "$package_type" = deb ]; then
|
|
||||||
test `dpkg -S "$1" | wc -l` = 1
|
|
||||||
else
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
do_if_file_belongs_to_single_package ()
|
|
||||||
{
|
|
||||||
local file="$1"
|
|
||||||
shift
|
|
||||||
|
|
||||||
if file_belongs_to_single_package "$file"; then
|
|
||||||
"$@"
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
#
|
|
||||||
# Remove $1 desktop file from the list of default handlers for $2 mime type
|
|
||||||
# in $3 file dumping output to stdout.
|
|
||||||
#
|
|
||||||
desktop_filter_out_default_mime_handler ()
|
|
||||||
{
|
|
||||||
local defaults_list="$3"
|
|
||||||
|
|
||||||
local desktop_file="$1"
|
|
||||||
local mime_type="$2"
|
|
||||||
|
|
||||||
awk -f- "$defaults_list" <<EOF
|
|
||||||
BEGIN {
|
|
||||||
mime_type="$mime_type"
|
|
||||||
mime_type_regexp="~" mime_type "="
|
|
||||||
desktop_file="$desktop_file"
|
|
||||||
}
|
|
||||||
\$0 ~ mime_type {
|
|
||||||
\$0 = substr(\$0, length(mime_type) + 2);
|
|
||||||
split(\$0, desktop_files, ";")
|
|
||||||
remaining_desktop_files
|
|
||||||
counter=0
|
|
||||||
for (idx in desktop_files) {
|
|
||||||
if (desktop_files[idx] != desktop_file) {
|
|
||||||
++counter;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (counter) {
|
|
||||||
printf mime_type "="
|
|
||||||
for (idx in desktop_files) {
|
|
||||||
if (desktop_files[idx] != desktop_file) {
|
|
||||||
printf desktop_files[idx]
|
|
||||||
if (--counter) {
|
|
||||||
printf ";"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
printf "\n"
|
|
||||||
}
|
|
||||||
next
|
|
||||||
}
|
|
||||||
|
|
||||||
{ print }
|
|
||||||
EOF
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
#
|
|
||||||
# Remove $2 desktop file from the list of default handlers for $@ mime types
|
|
||||||
# in $1 file.
|
|
||||||
# Result is saved in $1 file.
|
|
||||||
#
|
|
||||||
desktop_uninstall_default_mime_handler_0 ()
|
|
||||||
{
|
|
||||||
local defaults_list=$1
|
|
||||||
shift
|
|
||||||
[ -f "$defaults_list" ] || return 0
|
|
||||||
|
|
||||||
local desktop_file="$1"
|
|
||||||
shift
|
|
||||||
|
|
||||||
tmpfile1=$(mktemp)
|
|
||||||
tmpfile2=$(mktemp)
|
|
||||||
cat "$defaults_list" > "$tmpfile1"
|
|
||||||
|
|
||||||
local v
|
|
||||||
local update=
|
|
||||||
for mime in "$@"; do
|
|
||||||
desktop_filter_out_default_mime_handler "$desktop_file" "$mime" "$tmpfile1" > "$tmpfile2"
|
|
||||||
v="$tmpfile2"
|
|
||||||
tmpfile2="$tmpfile1"
|
|
||||||
tmpfile1="$v"
|
|
||||||
|
|
||||||
if ! diff -q "$tmpfile1" "$tmpfile2" > /dev/null; then
|
|
||||||
update=yes
|
|
||||||
desktop_trace Remove $desktop_file default handler for $mime mime type from $defaults_list file
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
|
|
||||||
if [ -n "$update" ]; then
|
|
||||||
cat "$tmpfile1" > "$defaults_list"
|
|
||||||
desktop_trace "$defaults_list" file updated
|
|
||||||
fi
|
|
||||||
|
|
||||||
rm -f "$tmpfile1" "$tmpfile2"
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
#
|
|
||||||
# Remove $1 desktop file from the list of default handlers for $@ mime types
|
|
||||||
# in all known system defaults lists.
|
|
||||||
#
|
|
||||||
desktop_uninstall_default_mime_handler ()
|
|
||||||
{
|
|
||||||
for f in /usr/share/applications/defaults.list /usr/local/share/applications/defaults.list; do
|
|
||||||
desktop_uninstall_default_mime_handler_0 "$f" "$@"
|
|
||||||
done
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
desktop_trace ()
|
|
||||||
{
|
|
||||||
echo "$@"
|
|
||||||
}
|
|
||||||
|
|
||||||
do_if_file_belongs_to_single_package /opt/sparrowwallet/lib/sparrowwallet-Sparrow.desktop xdg-desktop-menu uninstall /opt/sparrowwallet/lib/sparrowwallet-Sparrow.desktop
|
|
||||||
do_if_file_belongs_to_single_package /opt/sparrowwallet/lib/sparrowwallet-Sparrow-MimeInfo.xml xdg-mime uninstall /opt/sparrowwallet/lib/sparrowwallet-Sparrow-MimeInfo.xml
|
|
||||||
do_if_file_belongs_to_single_package /opt/sparrowwallet/lib/sparrowwallet-Sparrow.desktop desktop_uninstall_default_mime_handler sparrowwallet-Sparrow.desktop application/psbt application/bitcoin-transaction application/pgp-signature x-scheme-handler/bitcoin x-scheme-handler/auth47 x-scheme-handler/lightning
|
|
||||||
|
|
||||||
|
|
||||||
%clean
|
|
||||||
|
|
@ -21,7 +21,7 @@
|
||||||
<key>CFBundlePackageType</key>
|
<key>CFBundlePackageType</key>
|
||||||
<string>APPL</string>
|
<string>APPL</string>
|
||||||
<key>CFBundleShortVersionString</key>
|
<key>CFBundleShortVersionString</key>
|
||||||
<string>2.3.1</string>
|
<string>1.8.3</string>
|
||||||
<key>CFBundleSignature</key>
|
<key>CFBundleSignature</key>
|
||||||
<string>????</string>
|
<string>????</string>
|
||||||
<!-- See https://developer.apple.com/app-store/categories/ for list of AppStore categories -->
|
<!-- See https://developer.apple.com/app-store/categories/ for list of AppStore categories -->
|
||||||
|
|
@ -33,12 +33,8 @@
|
||||||
<string>Copyright (C) 2021</string>
|
<string>Copyright (C) 2021</string>
|
||||||
<key>NSHighResolutionCapable</key>
|
<key>NSHighResolutionCapable</key>
|
||||||
<string>true</string>
|
<string>true</string>
|
||||||
<key>NSCameraUseContinuityCameraDeviceType</key>
|
|
||||||
<true/>
|
|
||||||
<key>NSCameraUsageDescription</key>
|
<key>NSCameraUsageDescription</key>
|
||||||
<string>Sparrow requires access to the camera in order to scan QR codes</string>
|
<string>Sparrow requires access to the camera in order to scan QR codes</string>
|
||||||
<key>NSLocalNetworkUsageDescription</key>
|
|
||||||
<string>Sparrow requires access to the local network in order to connect to your configured server</string>
|
|
||||||
<key>CFBundleURLTypes</key>
|
<key>CFBundleURLTypes</key>
|
||||||
<array>
|
<array>
|
||||||
<dict>
|
<dict>
|
||||||
|
|
|
||||||
|
|
@ -1,16 +1,19 @@
|
||||||
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.ScriptType;
|
||||||
import com.sparrowwallet.drongo.psbt.*;
|
import com.sparrowwallet.drongo.protocol.Sha256Hash;
|
||||||
import com.sparrowwallet.drongo.silentpayments.SilentPaymentAddress;
|
import com.sparrowwallet.drongo.protocol.Transaction;
|
||||||
|
import com.sparrowwallet.drongo.psbt.PSBT;
|
||||||
|
import com.sparrowwallet.drongo.psbt.PSBTInput;
|
||||||
|
import com.sparrowwallet.drongo.psbt.PSBTParseException;
|
||||||
|
import com.sparrowwallet.drongo.psbt.PSBTSignatureException;
|
||||||
import com.sparrowwallet.drongo.wallet.*;
|
import com.sparrowwallet.drongo.wallet.*;
|
||||||
import com.sparrowwallet.hummingbird.UR;
|
import com.sparrowwallet.hummingbird.UR;
|
||||||
import com.sparrowwallet.hummingbird.registry.CryptoPSBT;
|
import com.sparrowwallet.hummingbird.registry.CryptoPSBT;
|
||||||
|
|
@ -22,16 +25,19 @@ import com.sparrowwallet.sparrow.io.bbqr.BBQR;
|
||||||
import com.sparrowwallet.sparrow.io.bbqr.BBQRType;
|
import com.sparrowwallet.sparrow.io.bbqr.BBQRType;
|
||||||
import com.sparrowwallet.sparrow.net.ElectrumServer;
|
import com.sparrowwallet.sparrow.net.ElectrumServer;
|
||||||
import com.sparrowwallet.sparrow.net.ServerType;
|
import com.sparrowwallet.sparrow.net.ServerType;
|
||||||
import com.sparrowwallet.sparrow.settings.SettingsGroup;
|
import com.sparrowwallet.sparrow.preferences.PreferenceGroup;
|
||||||
import com.sparrowwallet.sparrow.settings.SettingsDialog;
|
import com.sparrowwallet.sparrow.preferences.PreferencesDialog;
|
||||||
|
import com.sparrowwallet.sparrow.soroban.CounterpartyDialog;
|
||||||
import com.sparrowwallet.sparrow.paynym.PayNymDialog;
|
import com.sparrowwallet.sparrow.paynym.PayNymDialog;
|
||||||
|
import com.sparrowwallet.sparrow.soroban.Soroban;
|
||||||
|
import com.sparrowwallet.sparrow.soroban.SorobanServices;
|
||||||
import com.sparrowwallet.sparrow.transaction.TransactionController;
|
import com.sparrowwallet.sparrow.transaction.TransactionController;
|
||||||
import com.sparrowwallet.sparrow.transaction.TransactionData;
|
import com.sparrowwallet.sparrow.transaction.TransactionData;
|
||||||
import com.sparrowwallet.sparrow.transaction.TransactionView;
|
import com.sparrowwallet.sparrow.transaction.TransactionView;
|
||||||
import com.sparrowwallet.sparrow.wallet.Entry;
|
import com.sparrowwallet.sparrow.wallet.Entry;
|
||||||
import com.sparrowwallet.sparrow.wallet.WalletController;
|
import com.sparrowwallet.sparrow.wallet.WalletController;
|
||||||
import com.sparrowwallet.sparrow.wallet.WalletForm;
|
import com.sparrowwallet.sparrow.wallet.WalletForm;
|
||||||
import de.jangassen.MenuToolkit;
|
import de.codecentric.centerdevice.MenuToolkit;
|
||||||
import javafx.animation.*;
|
import javafx.animation.*;
|
||||||
import javafx.application.Platform;
|
import javafx.application.Platform;
|
||||||
import javafx.beans.binding.Bindings;
|
import javafx.beans.binding.Bindings;
|
||||||
|
|
@ -50,14 +56,12 @@ import javafx.geometry.Side;
|
||||||
import javafx.scene.Node;
|
import javafx.scene.Node;
|
||||||
import javafx.scene.Scene;
|
import javafx.scene.Scene;
|
||||||
import javafx.scene.control.*;
|
import javafx.scene.control.*;
|
||||||
import javafx.scene.control.Label;
|
import javafx.scene.image.Image;
|
||||||
import javafx.scene.control.Menu;
|
import javafx.scene.image.ImageView;
|
||||||
import javafx.scene.control.MenuItem;
|
|
||||||
import javafx.scene.input.*;
|
import javafx.scene.input.*;
|
||||||
import javafx.scene.layout.*;
|
import javafx.scene.layout.*;
|
||||||
import javafx.scene.paint.Color;
|
import javafx.scene.paint.Color;
|
||||||
import javafx.stage.*;
|
import javafx.stage.*;
|
||||||
import javafx.stage.Window;
|
|
||||||
import javafx.util.Duration;
|
import javafx.util.Duration;
|
||||||
import org.controlsfx.control.Notifications;
|
import org.controlsfx.control.Notifications;
|
||||||
import org.controlsfx.control.StatusBar;
|
import org.controlsfx.control.StatusBar;
|
||||||
|
|
@ -72,7 +76,6 @@ import java.nio.charset.StandardCharsets;
|
||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
import java.text.ParseException;
|
import java.text.ParseException;
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
import java.util.List;
|
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
import static com.sparrowwallet.sparrow.AppServices.*;
|
import static com.sparrowwallet.sparrow.AppServices.*;
|
||||||
|
|
@ -82,7 +85,6 @@ public class AppController implements Initializable {
|
||||||
private static final Logger log = LoggerFactory.getLogger(AppController.class);
|
private static final Logger log = LoggerFactory.getLogger(AppController.class);
|
||||||
|
|
||||||
public static final String DRAG_OVER_CLASS = "drag-over";
|
public static final String DRAG_OVER_CLASS = "drag-over";
|
||||||
public static final int TAB_LABEL_MAX_WIDTH = 300;
|
|
||||||
public static final double TAB_LABEL_GRAPHIC_OPACITY_INACTIVE = 0.8;
|
public static final double TAB_LABEL_GRAPHIC_OPACITY_INACTIVE = 0.8;
|
||||||
public static final double TAB_LABEL_GRAPHIC_OPACITY_ACTIVE = 0.95;
|
public static final double TAB_LABEL_GRAPHIC_OPACITY_ACTIVE = 0.95;
|
||||||
public static final String LOADING_TRANSACTIONS_MESSAGE = "Loading wallet, select Transactions tab to view...";
|
public static final String LOADING_TRANSACTIONS_MESSAGE = "Loading wallet, select Transactions tab to view...";
|
||||||
|
|
@ -153,10 +155,6 @@ public class AppController implements Initializable {
|
||||||
private CheckMenuItem useHdCameraResolution;
|
private CheckMenuItem useHdCameraResolution;
|
||||||
private static final BooleanProperty useHdCameraResolutionProperty = new SimpleBooleanProperty();
|
private static final BooleanProperty useHdCameraResolutionProperty = new SimpleBooleanProperty();
|
||||||
|
|
||||||
@FXML
|
|
||||||
private CheckMenuItem mirrorCameraImage;
|
|
||||||
private static final BooleanProperty mirrorCameraImageProperty = new SimpleBooleanProperty();
|
|
||||||
|
|
||||||
@FXML
|
@FXML
|
||||||
private CheckMenuItem showLoadingLog;
|
private CheckMenuItem showLoadingLog;
|
||||||
private static final BooleanProperty showLoadingLogProperty = new SimpleBooleanProperty();
|
private static final BooleanProperty showLoadingLogProperty = new SimpleBooleanProperty();
|
||||||
|
|
@ -189,6 +187,9 @@ public class AppController implements Initializable {
|
||||||
@FXML
|
@FXML
|
||||||
private MenuItem sweepPrivateKey;
|
private MenuItem sweepPrivateKey;
|
||||||
|
|
||||||
|
@FXML
|
||||||
|
private MenuItem findMixingPartner;
|
||||||
|
|
||||||
@FXML
|
@FXML
|
||||||
private MenuItem showPayNym;
|
private MenuItem showPayNym;
|
||||||
|
|
||||||
|
|
@ -214,14 +215,10 @@ public class AppController implements Initializable {
|
||||||
@FXML
|
@FXML
|
||||||
private UnlabeledToggleSwitch serverToggle;
|
private UnlabeledToggleSwitch serverToggle;
|
||||||
|
|
||||||
private Storage.KeyDerivationService keyDerivationService;
|
|
||||||
|
|
||||||
private PauseTransition wait;
|
private PauseTransition wait;
|
||||||
|
|
||||||
private Timeline statusTimeline;
|
private Timeline statusTimeline;
|
||||||
|
|
||||||
private SearchWalletDialog searchWalletDialog;
|
|
||||||
|
|
||||||
private SendToManyDialog sendToManyDialog;
|
private SendToManyDialog sendToManyDialog;
|
||||||
|
|
||||||
private DownloadVerifierDialog downloadVerifierDialog;
|
private DownloadVerifierDialog downloadVerifierDialog;
|
||||||
|
|
@ -281,16 +278,7 @@ public class AppController implements Initializable {
|
||||||
}
|
}
|
||||||
|
|
||||||
void initializeView() {
|
void initializeView() {
|
||||||
Platform.runLater(this::setPlatformApplicationMenu);
|
setPlatformApplicationMenu();
|
||||||
|
|
||||||
rootStack.getScene().getWindow().setOnHiding(windowEvent -> {
|
|
||||||
if(searchWalletDialog != null && searchWalletDialog.isShowing()) {
|
|
||||||
searchWalletDialog.close();
|
|
||||||
}
|
|
||||||
if(sendToManyDialog != null && sendToManyDialog.isShowing()) {
|
|
||||||
sendToManyDialog.close();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
rootStack.setOnDragOver(event -> {
|
rootStack.setOnDragOver(event -> {
|
||||||
if(event.getGestureSource() != rootStack && event.getDragboard().hasFiles()) {
|
if(event.getGestureSource() != rootStack && event.getDragboard().hasFiles()) {
|
||||||
|
|
@ -345,8 +333,6 @@ public class AppController implements Initializable {
|
||||||
EventManager.get().post(new OpenWalletsEvent(tabs.getScene().getWindow(), Collections.emptyList()));
|
EventManager.get().post(new OpenWalletsEvent(tabs.getScene().getWindow(), Collections.emptyList()));
|
||||||
});
|
});
|
||||||
|
|
||||||
tabs.setPickOnBounds(false);
|
|
||||||
|
|
||||||
registerShortcuts();
|
registerShortcuts();
|
||||||
|
|
||||||
BitcoinUnit unit = Config.get().getBitcoinUnit();
|
BitcoinUnit unit = Config.get().getBitcoinUnit();
|
||||||
|
|
@ -384,10 +370,8 @@ public class AppController implements Initializable {
|
||||||
openWalletsInNewWindows.selectedProperty().bindBidirectional(openWalletsInNewWindowsProperty);
|
openWalletsInNewWindows.selectedProperty().bindBidirectional(openWalletsInNewWindowsProperty);
|
||||||
hideEmptyUsedAddressesProperty.set(Config.get().isHideEmptyUsedAddresses());
|
hideEmptyUsedAddressesProperty.set(Config.get().isHideEmptyUsedAddresses());
|
||||||
hideEmptyUsedAddresses.selectedProperty().bindBidirectional(hideEmptyUsedAddressesProperty);
|
hideEmptyUsedAddresses.selectedProperty().bindBidirectional(hideEmptyUsedAddressesProperty);
|
||||||
useHdCameraResolutionProperty.set(Config.get().getWebcamResolution() == null || Config.get().getWebcamResolution().isWidescreenAspect());
|
useHdCameraResolutionProperty.set(Config.get().isHdCapture());
|
||||||
useHdCameraResolution.selectedProperty().bindBidirectional(useHdCameraResolutionProperty);
|
useHdCameraResolution.selectedProperty().bindBidirectional(useHdCameraResolutionProperty);
|
||||||
mirrorCameraImageProperty.set(Config.get().isMirrorCapture());
|
|
||||||
mirrorCameraImage.selectedProperty().bindBidirectional(mirrorCameraImageProperty);
|
|
||||||
showTxHexProperty.set(Config.get().isShowTransactionHex());
|
showTxHexProperty.set(Config.get().isShowTransactionHex());
|
||||||
showTxHex.selectedProperty().bindBidirectional(showTxHexProperty);
|
showTxHex.selectedProperty().bindBidirectional(showTxHexProperty);
|
||||||
showLoadingLogProperty.set(Config.get().isShowLoadingLog());
|
showLoadingLogProperty.set(Config.get().isShowLoadingLog());
|
||||||
|
|
@ -395,10 +379,7 @@ public class AppController implements Initializable {
|
||||||
preventSleepProperty.set(Config.get().isPreventSleep());
|
preventSleepProperty.set(Config.get().isPreventSleep());
|
||||||
preventSleep.selectedProperty().bindBidirectional(preventSleepProperty);
|
preventSleep.selectedProperty().bindBidirectional(preventSleepProperty);
|
||||||
|
|
||||||
MenuItem homeItem = new MenuItem("Home Folder...");
|
List<Network> networks = new ArrayList<>(List.of(Network.MAINNET, Network.TESTNET, Network.SIGNET));
|
||||||
homeItem.setOnAction(this::restartInHome);
|
|
||||||
restart.getItems().add(homeItem);
|
|
||||||
List<Network> networks = new ArrayList<>(List.of(Network.MAINNET, Network.TESTNET, Network.TESTNET4, Network.SIGNET));
|
|
||||||
networks.remove(Network.get());
|
networks.remove(Network.get());
|
||||||
for(Network network : networks) {
|
for(Network network : networks) {
|
||||||
MenuItem networkItem = new MenuItem(network.toDisplayString());
|
MenuItem networkItem = new MenuItem(network.toDisplayString());
|
||||||
|
|
@ -424,6 +405,10 @@ public class AppController implements Initializable {
|
||||||
sendToMany.disableProperty().bind(exportWallet.disableProperty());
|
sendToMany.disableProperty().bind(exportWallet.disableProperty());
|
||||||
sweepPrivateKey.disableProperty().bind(Bindings.or(serverToggle.disableProperty(), AppServices.onlineProperty().not()));
|
sweepPrivateKey.disableProperty().bind(Bindings.or(serverToggle.disableProperty(), AppServices.onlineProperty().not()));
|
||||||
showPayNym.setDisable(true);
|
showPayNym.setDisable(true);
|
||||||
|
findMixingPartner.setDisable(true);
|
||||||
|
AppServices.onlineProperty().addListener((observable, oldValue, newValue) -> {
|
||||||
|
findMixingPartner.setDisable(exportWallet.isDisable() || getSelectedWalletForm() == null || !SorobanServices.canWalletMix(getSelectedWalletForm().getWallet()) || !newValue);
|
||||||
|
});
|
||||||
|
|
||||||
configureSwitchServer();
|
configureSwitchServer();
|
||||||
setServerType(Config.get().getServerType());
|
setServerType(Config.get().getServerType());
|
||||||
|
|
@ -440,8 +425,8 @@ public class AppController implements Initializable {
|
||||||
}
|
}
|
||||||
|
|
||||||
private void registerShortcuts() {
|
private void registerShortcuts() {
|
||||||
OsType osType = OsType.getCurrent();
|
org.controlsfx.tools.Platform platform = org.controlsfx.tools.Platform.getCurrent();
|
||||||
if(osType == OsType.MACOS) {
|
if(platform == org.controlsfx.tools.Platform.OSX) {
|
||||||
tabs.getScene().addEventFilter(KeyEvent.KEY_PRESSED, event -> {
|
tabs.getScene().addEventFilter(KeyEvent.KEY_PRESSED, event -> {
|
||||||
if(event.isShortcutDown() && event.isAltDown() && (event.getCode() == KeyCode.LEFT || event.getCode() == KeyCode.RIGHT)) {
|
if(event.isShortcutDown() && event.isAltDown() && (event.getCode() == KeyCode.LEFT || event.getCode() == KeyCode.RIGHT)) {
|
||||||
int currentIndex = tabs.getSelectionModel().getSelectedIndex();
|
int currentIndex = tabs.getSelectionModel().getSelectedIndex();
|
||||||
|
|
@ -456,14 +441,14 @@ public class AppController implements Initializable {
|
||||||
}
|
}
|
||||||
|
|
||||||
private void setPlatformApplicationMenu() {
|
private void setPlatformApplicationMenu() {
|
||||||
OsType osType = OsType.getCurrent();
|
org.controlsfx.tools.Platform platform = org.controlsfx.tools.Platform.getCurrent();
|
||||||
if(osType == OsType.MACOS) {
|
if(platform == org.controlsfx.tools.Platform.OSX) {
|
||||||
MenuToolkit tk = MenuToolkit.toolkit();
|
MenuToolkit tk = MenuToolkit.toolkit();
|
||||||
MenuItem settings = new MenuItem("Settings...");
|
MenuItem preferences = new MenuItem("Preferences...");
|
||||||
settings.setOnAction(this::openSettings);
|
preferences.setOnAction(this::openPreferences);
|
||||||
settings.setAccelerator(new KeyCodeCombination(KeyCode.COMMA, KeyCombination.META_DOWN));
|
preferences.setAccelerator(new KeyCodeCombination(KeyCode.COMMA, KeyCombination.META_DOWN));
|
||||||
Menu defaultApplicationMenu = new Menu("Apple", null, tk.createAboutMenuItem(SparrowWallet.APP_NAME, getAboutStage()), new SeparatorMenuItem(),
|
Menu defaultApplicationMenu = new Menu("Apple", null, tk.createAboutMenuItem(SparrowWallet.APP_NAME, getAboutStage()), new SeparatorMenuItem(),
|
||||||
settings, new SeparatorMenuItem(),
|
preferences, new SeparatorMenuItem(),
|
||||||
tk.createHideMenuItem(SparrowWallet.APP_NAME), tk.createHideOthersMenuItem(), tk.createUnhideAllMenuItem(), new SeparatorMenuItem(),
|
tk.createHideMenuItem(SparrowWallet.APP_NAME), tk.createHideOthersMenuItem(), tk.createUnhideAllMenuItem(), new SeparatorMenuItem(),
|
||||||
tk.createQuitMenuItem(SparrowWallet.APP_NAME));
|
tk.createQuitMenuItem(SparrowWallet.APP_NAME));
|
||||||
tk.setApplicationMenu(defaultApplicationMenu);
|
tk.setApplicationMenu(defaultApplicationMenu);
|
||||||
|
|
@ -471,11 +456,11 @@ public class AppController implements Initializable {
|
||||||
fileMenu.getItems().removeIf(item -> item.getStyleClass().contains("osxHide"));
|
fileMenu.getItems().removeIf(item -> item.getStyleClass().contains("osxHide"));
|
||||||
toolsMenu.getItems().removeIf(item -> item.getStyleClass().contains("osxHide"));
|
toolsMenu.getItems().removeIf(item -> item.getStyleClass().contains("osxHide"));
|
||||||
helpMenu.getItems().removeIf(item -> item.getStyleClass().contains("osxHide"));
|
helpMenu.getItems().removeIf(item -> item.getStyleClass().contains("osxHide"));
|
||||||
} else if(osType == OsType.WINDOWS) {
|
} else if(platform == org.controlsfx.tools.Platform.WINDOWS) {
|
||||||
toolsMenu.getItems().removeIf(item -> item.getStyleClass().contains("windowsHide"));
|
toolsMenu.getItems().removeIf(item -> item.getStyleClass().contains("windowsHide"));
|
||||||
}
|
}
|
||||||
|
|
||||||
if(osType == OsType.UNIX || !TrayManager.isSupported()) {
|
if(platform == org.controlsfx.tools.Platform.UNIX || !TrayManager.isSupported()) {
|
||||||
viewMenu.getItems().remove(minimizeToTray);
|
viewMenu.getItems().remove(minimizeToTray);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -501,7 +486,7 @@ public class AppController implements Initializable {
|
||||||
welcomeDialog.initOwner(rootStack.getScene().getWindow());
|
welcomeDialog.initOwner(rootStack.getScene().getWindow());
|
||||||
Optional<Mode> optionalMode = welcomeDialog.showAndWait();
|
Optional<Mode> optionalMode = welcomeDialog.showAndWait();
|
||||||
if(optionalMode.isPresent() && optionalMode.get().equals(Mode.ONLINE)) {
|
if(optionalMode.isPresent() && optionalMode.get().equals(Mode.ONLINE)) {
|
||||||
openSettings(SettingsGroup.SERVER);
|
openPreferences(PreferenceGroup.SERVER);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -547,7 +532,7 @@ public class AppController implements Initializable {
|
||||||
StackPane root = loader.load();
|
StackPane root = loader.load();
|
||||||
AboutController controller = loader.getController();
|
AboutController controller = loader.getController();
|
||||||
|
|
||||||
if(OsType.getCurrent() == OsType.WINDOWS) {
|
if(org.controlsfx.tools.Platform.getCurrent() == org.controlsfx.tools.Platform.WINDOWS) {
|
||||||
root.setBorder(new Border(new BorderStroke(Color.DARKGRAY, BorderStrokeStyle.SOLID, CornerRadii.EMPTY, BorderWidths.DEFAULT)));
|
root.setBorder(new Border(new BorderStroke(Color.DARKGRAY, BorderStrokeStyle.SOLID, CornerRadii.EMPTY, BorderWidths.DEFAULT)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -575,27 +560,21 @@ public class AppController implements Initializable {
|
||||||
}
|
}
|
||||||
|
|
||||||
public void installUdevRules(ActionEvent event) {
|
public void installUdevRules(ActionEvent event) {
|
||||||
String commands = """
|
Hwi.EnumerateService enumerateService = new Hwi.EnumerateService(null);
|
||||||
sudo install -m 644 /opt/sparrowwallet/lib/runtime/conf/udev/*.rules /etc/udev/rules.d
|
enumerateService.setOnSucceeded(workerStateEvent -> {
|
||||||
sudo udevadm control --reload
|
Platform.runLater(this::showInstallUdevMessage);
|
||||||
sudo udevadm trigger
|
});
|
||||||
sudo groupadd -f plugdev
|
enumerateService.setOnFailed(workerStateEvent -> {
|
||||||
sudo usermod -aG plugdev `whoami`
|
Platform.runLater(this::showInstallUdevMessage);
|
||||||
""";
|
});
|
||||||
String home = System.getProperty(JPACKAGE_APP_PATH);
|
enumerateService.start();
|
||||||
if(home != null && !home.startsWith("/opt/sparrowwallet") && home.endsWith("bin/Sparrow")) {
|
}
|
||||||
home = home.replace("bin/Sparrow", "");
|
|
||||||
commands = commands.replace("/opt/sparrowwallet/", home);
|
|
||||||
}
|
|
||||||
|
|
||||||
TextAreaDialog dialog = new TextAreaDialog(commands, false);
|
public void showInstallUdevMessage() {
|
||||||
|
TextAreaDialog dialog = new TextAreaDialog("sudo " + Config.get().getHwi().getAbsolutePath() + " installudevrules", false);
|
||||||
dialog.initOwner(rootStack.getScene().getWindow());
|
dialog.initOwner(rootStack.getScene().getWindow());
|
||||||
dialog.setTitle("Install udev Rules");
|
dialog.setTitle("Install Udev Rules");
|
||||||
dialog.getDialogPane().setHeaderText("""
|
dialog.getDialogPane().setHeaderText("Installing udev rules ensures devices can connect over USB.\nThis command requires root privileges.\nOpen a shell and enter the following:");
|
||||||
Installing udev rules ensures devices can connect over USB.
|
|
||||||
These commands require root privileges.
|
|
||||||
Open a shell and enter the following.
|
|
||||||
""");
|
|
||||||
dialog.showAndWait();
|
dialog.showAndWait();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -605,7 +584,7 @@ public class AppController implements Initializable {
|
||||||
FileChooser fileChooser = new FileChooser();
|
FileChooser fileChooser = new FileChooser();
|
||||||
fileChooser.setTitle("Open Transaction");
|
fileChooser.setTitle("Open Transaction");
|
||||||
fileChooser.getExtensionFilters().addAll(
|
fileChooser.getExtensionFilters().addAll(
|
||||||
new FileChooser.ExtensionFilter("All Files", OsType.getCurrent().equals(OsType.UNIX) ? "*" : "*.*"),
|
new FileChooser.ExtensionFilter("All Files", org.controlsfx.tools.Platform.getCurrent().equals(org.controlsfx.tools.Platform.UNIX) ? "*" : "*.*"),
|
||||||
new FileChooser.ExtensionFilter("PSBT", "*.psbt"),
|
new FileChooser.ExtensionFilter("PSBT", "*.psbt"),
|
||||||
new FileChooser.ExtensionFilter("TXN", "*.txn")
|
new FileChooser.ExtensionFilter("TXN", "*.txn")
|
||||||
);
|
);
|
||||||
|
|
@ -636,10 +615,19 @@ public class AppController implements Initializable {
|
||||||
byte[] bytes = Files.readAllBytes(file.toPath());
|
byte[] bytes = Files.readAllBytes(file.toPath());
|
||||||
String name = file.getName();
|
String name = file.getName();
|
||||||
|
|
||||||
if(Utils.isHex(bytes) || Utils.isBase64(bytes)) {
|
try {
|
||||||
addTransactionTab(name, file, new String(bytes, StandardCharsets.UTF_8).trim());
|
|
||||||
} else {
|
|
||||||
addTransactionTab(name, file, bytes);
|
addTransactionTab(name, file, bytes);
|
||||||
|
} catch(ParseException e) {
|
||||||
|
ByteArrayInputStream inputStream = new ByteArrayInputStream(bytes);
|
||||||
|
ByteSource byteSource = new ByteSource() {
|
||||||
|
@Override
|
||||||
|
public InputStream openStream() {
|
||||||
|
return inputStream;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
String text = byteSource.asCharSource(Charsets.UTF_8).read().trim();
|
||||||
|
addTransactionTab(name, file, text);
|
||||||
}
|
}
|
||||||
} catch(IOException e) {
|
} catch(IOException e) {
|
||||||
showErrorDialog("Error opening file", e.getMessage());
|
showErrorDialog("Error opening file", e.getMessage());
|
||||||
|
|
@ -804,7 +792,6 @@ public class AppController implements Initializable {
|
||||||
|
|
||||||
String fileName = ((Label)selectedTab.getGraphic()).getText();
|
String fileName = ((Label)selectedTab.getGraphic()).getText();
|
||||||
if(fileName != null && !fileName.isEmpty()) {
|
if(fileName != null && !fileName.isEmpty()) {
|
||||||
fileName = fileName.replace('/', '_');
|
|
||||||
if(!fileName.endsWith(".psbt")) {
|
if(!fileName.endsWith(".psbt")) {
|
||||||
fileName += ".psbt";
|
fileName += ".psbt";
|
||||||
}
|
}
|
||||||
|
|
@ -826,10 +813,10 @@ public class AppController implements Initializable {
|
||||||
try(FileOutputStream outputStream = new FileOutputStream(file)) {
|
try(FileOutputStream outputStream = new FileOutputStream(file)) {
|
||||||
if(asText) {
|
if(asText) {
|
||||||
PrintWriter writer = new PrintWriter(new OutputStreamWriter(outputStream, StandardCharsets.UTF_8));
|
PrintWriter writer = new PrintWriter(new OutputStreamWriter(outputStream, StandardCharsets.UTF_8));
|
||||||
writer.print(transactionTabData.getPsbt().getForExport().toBase64String(includeXpubs));
|
writer.print(transactionTabData.getPsbt().toBase64String(includeXpubs));
|
||||||
writer.flush();
|
writer.flush();
|
||||||
} else {
|
} else {
|
||||||
outputStream.write(transactionTabData.getPsbt().getForExport().serialize(includeXpubs, true));
|
outputStream.write(transactionTabData.getPsbt().serialize(includeXpubs, true));
|
||||||
}
|
}
|
||||||
} catch(IOException e) {
|
} catch(IOException e) {
|
||||||
log.error("Error saving PSBT", e);
|
log.error("Error saving PSBT", e);
|
||||||
|
|
@ -852,7 +839,7 @@ public class AppController implements Initializable {
|
||||||
TabData tabData = (TabData)selectedTab.getUserData();
|
TabData tabData = (TabData)selectedTab.getUserData();
|
||||||
if(tabData.getType() == TabData.TabType.TRANSACTION) {
|
if(tabData.getType() == TabData.TabType.TRANSACTION) {
|
||||||
TransactionTabData transactionTabData = (TransactionTabData)tabData;
|
TransactionTabData transactionTabData = (TransactionTabData)tabData;
|
||||||
String data = asBase64 ? transactionTabData.getPsbt().getForExport().toBase64String() : transactionTabData.getPsbt().getForExport().toString();
|
String data = asBase64 ? transactionTabData.getPsbt().toBase64String() : transactionTabData.getPsbt().toString();
|
||||||
|
|
||||||
ClipboardContent content = new ClipboardContent();
|
ClipboardContent content = new ClipboardContent();
|
||||||
content.putString(data);
|
content.putString(data);
|
||||||
|
|
@ -866,7 +853,7 @@ public class AppController implements Initializable {
|
||||||
if(tabData.getType() == TabData.TabType.TRANSACTION) {
|
if(tabData.getType() == TabData.TabType.TRANSACTION) {
|
||||||
TransactionTabData transactionTabData = (TransactionTabData)tabData;
|
TransactionTabData transactionTabData = (TransactionTabData)tabData;
|
||||||
|
|
||||||
byte[] psbtBytes = transactionTabData.getPsbt().getForExport().serialize();
|
byte[] psbtBytes = transactionTabData.getPsbt().serialize();
|
||||||
CryptoPSBT cryptoPSBT = new CryptoPSBT(psbtBytes);
|
CryptoPSBT cryptoPSBT = new CryptoPSBT(psbtBytes);
|
||||||
BBQR bbqr = new BBQR(BBQRType.PSBT, psbtBytes);
|
BBQR bbqr = new BBQR(BBQRType.PSBT, psbtBytes);
|
||||||
QRDisplayDialog qrDisplayDialog = new QRDisplayDialog(cryptoPSBT.toUR(), bbqr, false, true, false);
|
QRDisplayDialog qrDisplayDialog = new QRDisplayDialog(cryptoPSBT.toUR(), bbqr, false, true, false);
|
||||||
|
|
@ -949,16 +936,7 @@ public class AppController implements Initializable {
|
||||||
|
|
||||||
public void useHdCameraResolution(ActionEvent event) {
|
public void useHdCameraResolution(ActionEvent event) {
|
||||||
CheckMenuItem item = (CheckMenuItem)event.getSource();
|
CheckMenuItem item = (CheckMenuItem)event.getSource();
|
||||||
if(Config.get().getWebcamResolution().isStandardAspect() && item.isSelected()) {
|
Config.get().setHdCapture(item.isSelected());
|
||||||
Config.get().setWebcamResolution(WebcamResolution.HD);
|
|
||||||
} else if(Config.get().getWebcamResolution().isWidescreenAspect() && !item.isSelected()) {
|
|
||||||
Config.get().setWebcamResolution(WebcamResolution.VGA);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void mirrorCameraImage(ActionEvent event) {
|
|
||||||
CheckMenuItem item = (CheckMenuItem)event.getSource();
|
|
||||||
Config.get().setMirrorCapture(item.isSelected());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void showLoadingLog(ActionEvent event) {
|
public void showLoadingLog(ActionEvent event) {
|
||||||
|
|
@ -993,55 +971,24 @@ public class AppController implements Initializable {
|
||||||
AppServices.get().setPreventSleep(item.isSelected());
|
AppServices.get().setPreventSleep(item.isSelected());
|
||||||
}
|
}
|
||||||
|
|
||||||
public void restartInHome(ActionEvent event) {
|
|
||||||
Args args = getRestartArgs();
|
|
||||||
File initialDir = null;
|
|
||||||
if(args.dir != null) {
|
|
||||||
initialDir = new File(args.dir);
|
|
||||||
}
|
|
||||||
|
|
||||||
Stage window = new Stage();
|
|
||||||
DirectoryChooser directoryChooser = new DirectoryChooser();
|
|
||||||
directoryChooser.setTitle("Choose Sparrow Home Folder");
|
|
||||||
directoryChooser.setInitialDirectory(initialDir == null || !initialDir.exists() ? Storage.getSparrowHome() : initialDir);
|
|
||||||
File newHome = directoryChooser.showDialog(window);
|
|
||||||
|
|
||||||
if(newHome != null) {
|
|
||||||
args.dir = newHome.getAbsolutePath();
|
|
||||||
restart(event, args);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void restart(ActionEvent event, Network network) {
|
public void restart(ActionEvent event, Network network) {
|
||||||
if(System.getProperty(JPACKAGE_APP_PATH) == null) {
|
if(System.getProperty(JPACKAGE_APP_PATH) == null) {
|
||||||
throw new IllegalStateException("Property " + JPACKAGE_APP_PATH + " is not present");
|
throw new IllegalStateException("Property " + JPACKAGE_APP_PATH + " is not present");
|
||||||
}
|
}
|
||||||
|
|
||||||
Args args = getRestartArgs();
|
|
||||||
args.network = network;
|
|
||||||
restart(event, args);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static Args getRestartArgs() {
|
|
||||||
Args args = new Args();
|
Args args = new Args();
|
||||||
ProcessHandle.current().info().arguments().ifPresent(argv -> {
|
ProcessHandle.current().info().arguments().ifPresent(argv -> {
|
||||||
JCommander jCommander = JCommander.newBuilder().addObject(args).acceptUnknownOptions(true).build();
|
JCommander jCommander = JCommander.newBuilder().addObject(args).acceptUnknownOptions(true).build();
|
||||||
jCommander.parse(argv);
|
jCommander.parse(argv);
|
||||||
});
|
});
|
||||||
|
|
||||||
return args;
|
args.network = network;
|
||||||
}
|
|
||||||
|
|
||||||
private void restart(ActionEvent event, Args args) {
|
|
||||||
try {
|
try {
|
||||||
List<String> cmd = new ArrayList<>();
|
List<String> cmd = new ArrayList<>();
|
||||||
cmd.add(System.getProperty(JPACKAGE_APP_PATH));
|
cmd.add(System.getProperty(JPACKAGE_APP_PATH));
|
||||||
cmd.addAll(args.toParams());
|
cmd.addAll(args.toParams());
|
||||||
final ProcessBuilder builder = new ProcessBuilder(cmd);
|
final ProcessBuilder builder = new ProcessBuilder(cmd);
|
||||||
if(OsType.getCurrent() == OsType.UNIX) {
|
|
||||||
Map<String, String> env = builder.environment();
|
|
||||||
env.remove("LD_LIBRARY_PATH");
|
|
||||||
}
|
|
||||||
builder.start();
|
builder.start();
|
||||||
quit(event);
|
quit(event);
|
||||||
} catch(Exception e) {
|
} catch(Exception e) {
|
||||||
|
|
@ -1265,10 +1212,6 @@ public class AppController implements Initializable {
|
||||||
}
|
}
|
||||||
|
|
||||||
private void addImportedWallet(Wallet wallet) {
|
private void addImportedWallet(Wallet wallet) {
|
||||||
if(AppServices.disallowAnyInvalidDerivationPaths(wallet)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
WalletNameDialog nameDlg = new WalletNameDialog(wallet.getName(), true, wallet.getBirthDate());
|
WalletNameDialog nameDlg = new WalletNameDialog(wallet.getName(), true, wallet.getBirthDate());
|
||||||
nameDlg.initOwner(rootStack.getScene().getWindow());
|
nameDlg.initOwner(rootStack.getScene().getWindow());
|
||||||
Optional<WalletNameDialog.NameAndBirthDate> optNameAndBirthDate = nameDlg.showAndWait();
|
Optional<WalletNameDialog.NameAndBirthDate> optNameAndBirthDate = nameDlg.showAndWait();
|
||||||
|
|
@ -1336,7 +1279,7 @@ public class AppController implements Initializable {
|
||||||
log.error("Error saving imported wallet", e);
|
log.error("Error saving imported wallet", e);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
keyDerivationService = new Storage.KeyDerivationService(storage, password.get());
|
Storage.KeyDerivationService keyDerivationService = new Storage.KeyDerivationService(storage, password.get());
|
||||||
keyDerivationService.setOnSucceeded(workerStateEvent -> {
|
keyDerivationService.setOnSucceeded(workerStateEvent -> {
|
||||||
EventManager.get().post(new StorageEvent(Storage.getWalletFile(wallet.getName()).getAbsolutePath(), TimedEvent.Action.END, "Done"));
|
EventManager.get().post(new StorageEvent(Storage.getWalletFile(wallet.getName()).getAbsolutePath(), TimedEvent.Action.END, "Done"));
|
||||||
ECKey encryptionFullKey = keyDerivationService.getValue();
|
ECKey encryptionFullKey = keyDerivationService.getValue();
|
||||||
|
|
@ -1367,13 +1310,11 @@ public class AppController implements Initializable {
|
||||||
if(key != null) {
|
if(key != null) {
|
||||||
key.clear();
|
key.clear();
|
||||||
}
|
}
|
||||||
keyDerivationService = null;
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
keyDerivationService.setOnFailed(workerStateEvent -> {
|
keyDerivationService.setOnFailed(workerStateEvent -> {
|
||||||
EventManager.get().post(new StorageEvent(Storage.getWalletFile(wallet.getName()).getAbsolutePath(), TimedEvent.Action.END, "Failed"));
|
EventManager.get().post(new StorageEvent(Storage.getWalletFile(wallet.getName()).getAbsolutePath(), TimedEvent.Action.END, "Failed"));
|
||||||
showErrorDialog("Error encrypting wallet", keyDerivationService.getException().getMessage());
|
showErrorDialog("Error encrypting wallet", keyDerivationService.getException().getMessage());
|
||||||
keyDerivationService = null;
|
|
||||||
});
|
});
|
||||||
EventManager.get().post(new StorageEvent(Storage.getWalletFile(wallet.getName()).getAbsolutePath(), TimedEvent.Action.START, "Encrypting wallet..."));
|
EventManager.get().post(new StorageEvent(Storage.getWalletFile(wallet.getName()).getAbsolutePath(), TimedEvent.Action.START, "Encrypting wallet..."));
|
||||||
keyDerivationService.start();
|
keyDerivationService.start();
|
||||||
|
|
@ -1384,7 +1325,7 @@ public class AppController implements Initializable {
|
||||||
public void exportWallet(ActionEvent event) {
|
public void exportWallet(ActionEvent event) {
|
||||||
WalletForm selectedWalletForm = getSelectedWalletForm();
|
WalletForm selectedWalletForm = getSelectedWalletForm();
|
||||||
if(selectedWalletForm != null) {
|
if(selectedWalletForm != null) {
|
||||||
WalletExportDialog dlg = new WalletExportDialog(selectedWalletForm, getSelectedWalletForms());
|
WalletExportDialog dlg = new WalletExportDialog(selectedWalletForm);
|
||||||
dlg.initOwner(rootStack.getScene().getWindow());
|
dlg.initOwner(rootStack.getScene().getWindow());
|
||||||
Optional<Wallet> wallet = dlg.showAndWait();
|
Optional<Wallet> wallet = dlg.showAndWait();
|
||||||
if(wallet.isPresent()) {
|
if(wallet.isPresent()) {
|
||||||
|
|
@ -1393,18 +1334,18 @@ public class AppController implements Initializable {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void openSettings(ActionEvent event) {
|
public void openPreferences(ActionEvent event) {
|
||||||
openSettings(SettingsGroup.GENERAL);
|
openPreferences(PreferenceGroup.GENERAL);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void openServerSettings(ActionEvent event) {
|
public void openServerPreferences(ActionEvent event) {
|
||||||
openSettings(SettingsGroup.SERVER);
|
openPreferences(PreferenceGroup.SERVER);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void openSettings(SettingsGroup settingsGroup) {
|
private void openPreferences(PreferenceGroup preferenceGroup) {
|
||||||
SettingsDialog settingsDialog = new SettingsDialog(settingsGroup);
|
PreferencesDialog preferencesDialog = new PreferencesDialog(preferenceGroup);
|
||||||
settingsDialog.initOwner(rootStack.getScene().getWindow());
|
preferencesDialog.initOwner(rootStack.getScene().getWindow());
|
||||||
settingsDialog.showAndWait();
|
preferencesDialog.showAndWait();
|
||||||
configureSwitchServer();
|
configureSwitchServer();
|
||||||
serverToggle.setDisable(!Config.get().hasServer());
|
serverToggle.setDisable(!Config.get().hasServer());
|
||||||
}
|
}
|
||||||
|
|
@ -1430,10 +1371,6 @@ public class AppController implements Initializable {
|
||||||
}
|
}
|
||||||
|
|
||||||
public void sendToMany(ActionEvent event) {
|
public void sendToMany(ActionEvent event) {
|
||||||
sendToMany(Collections.emptyList());
|
|
||||||
}
|
|
||||||
|
|
||||||
private void sendToMany(List<Payment> initialPayments) {
|
|
||||||
if(sendToManyDialog != null) {
|
if(sendToManyDialog != null) {
|
||||||
Stage stage = (Stage)sendToManyDialog.getDialogPane().getScene().getWindow();
|
Stage stage = (Stage)sendToManyDialog.getDialogPane().getScene().getWindow();
|
||||||
stage.setAlwaysOnTop(true);
|
stage.setAlwaysOnTop(true);
|
||||||
|
|
@ -1449,7 +1386,8 @@ public class AppController implements Initializable {
|
||||||
bitcoinUnit = wallet.getAutoUnit();
|
bitcoinUnit = wallet.getAutoUnit();
|
||||||
}
|
}
|
||||||
|
|
||||||
sendToManyDialog = new SendToManyDialog(bitcoinUnit, initialPayments);
|
sendToManyDialog = new SendToManyDialog(bitcoinUnit);
|
||||||
|
sendToManyDialog.initOwner(rootStack.getScene().getWindow());
|
||||||
sendToManyDialog.initModality(Modality.NONE);
|
sendToManyDialog.initModality(Modality.NONE);
|
||||||
Optional<List<Payment>> optPayments = sendToManyDialog.showAndWait();
|
Optional<List<Payment>> optPayments = sendToManyDialog.showAndWait();
|
||||||
sendToManyDialog = null;
|
sendToManyDialog = null;
|
||||||
|
|
@ -1475,6 +1413,74 @@ public class AppController implements Initializable {
|
||||||
optTransaction.ifPresent(transaction -> addTransactionTab(null, null, transaction));
|
optTransaction.ifPresent(transaction -> addTransactionTab(null, null, transaction));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void findMixingPartner(ActionEvent event) {
|
||||||
|
WalletForm selectedWalletForm = getSelectedWalletForm();
|
||||||
|
if(selectedWalletForm != null) {
|
||||||
|
Wallet wallet = selectedWalletForm.getWallet();
|
||||||
|
Soroban soroban = AppServices.getSorobanServices().getSoroban(selectedWalletForm.getWalletId());
|
||||||
|
if(soroban.getHdWallet() == null) {
|
||||||
|
if(wallet.isEncrypted()) {
|
||||||
|
Wallet copy = wallet.copy();
|
||||||
|
WalletPasswordDialog dlg = new WalletPasswordDialog(copy.getMasterName(), WalletPasswordDialog.PasswordRequirement.LOAD);
|
||||||
|
dlg.initOwner(rootStack.getScene().getWindow());
|
||||||
|
Optional<SecureString> password = dlg.showAndWait();
|
||||||
|
if(password.isPresent()) {
|
||||||
|
Storage storage = selectedWalletForm.getStorage();
|
||||||
|
Storage.KeyDerivationService keyDerivationService = new Storage.KeyDerivationService(storage, password.get(), true);
|
||||||
|
keyDerivationService.setOnSucceeded(workerStateEvent -> {
|
||||||
|
EventManager.get().post(new StorageEvent(selectedWalletForm.getWalletId(), TimedEvent.Action.END, "Done"));
|
||||||
|
ECKey encryptionFullKey = keyDerivationService.getValue();
|
||||||
|
Key key = new Key(encryptionFullKey.getPrivKeyBytes(), storage.getKeyDeriver().getSalt(), EncryptionType.Deriver.ARGON2);
|
||||||
|
copy.decrypt(key);
|
||||||
|
|
||||||
|
try {
|
||||||
|
soroban.setHDWallet(copy);
|
||||||
|
CounterpartyDialog counterpartyDialog = new CounterpartyDialog(selectedWalletForm.getWalletId(), selectedWalletForm.getWallet());
|
||||||
|
counterpartyDialog.initOwner(rootStack.getScene().getWindow());
|
||||||
|
if(Config.get().isSameAppMixing()) {
|
||||||
|
counterpartyDialog.initModality(Modality.NONE);
|
||||||
|
}
|
||||||
|
counterpartyDialog.showAndWait();
|
||||||
|
} finally {
|
||||||
|
key.clear();
|
||||||
|
encryptionFullKey.clear();
|
||||||
|
password.get().clear();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
keyDerivationService.setOnFailed(workerStateEvent -> {
|
||||||
|
EventManager.get().post(new StorageEvent(selectedWalletForm.getWalletId(), TimedEvent.Action.END, "Failed"));
|
||||||
|
if(keyDerivationService.getException() instanceof InvalidPasswordException) {
|
||||||
|
Optional<ButtonType> optResponse = showErrorDialog("Invalid Password", "The wallet password was invalid. Try again?", ButtonType.CANCEL, ButtonType.OK);
|
||||||
|
if(optResponse.isPresent() && optResponse.get().equals(ButtonType.OK)) {
|
||||||
|
Platform.runLater(() -> findMixingPartner(null));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
log.error("Error deriving wallet key", keyDerivationService.getException());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
EventManager.get().post(new StorageEvent(selectedWalletForm.getWalletId(), TimedEvent.Action.START, "Decrypting wallet..."));
|
||||||
|
keyDerivationService.start();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
soroban.setHDWallet(wallet);
|
||||||
|
CounterpartyDialog counterpartyDialog = new CounterpartyDialog(selectedWalletForm.getWalletId(), selectedWalletForm.getWallet());
|
||||||
|
counterpartyDialog.initOwner(rootStack.getScene().getWindow());
|
||||||
|
if(Config.get().isSameAppMixing()) {
|
||||||
|
counterpartyDialog.initModality(Modality.NONE);
|
||||||
|
}
|
||||||
|
counterpartyDialog.showAndWait();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
CounterpartyDialog counterpartyDialog = new CounterpartyDialog(selectedWalletForm.getWalletId(), selectedWalletForm.getWallet());
|
||||||
|
counterpartyDialog.initOwner(rootStack.getScene().getWindow());
|
||||||
|
if(Config.get().isSameAppMixing()) {
|
||||||
|
counterpartyDialog.initModality(Modality.NONE);
|
||||||
|
}
|
||||||
|
counterpartyDialog.showAndWait();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public void showPayNym(ActionEvent event) {
|
public void showPayNym(ActionEvent event) {
|
||||||
WalletForm selectedWalletForm = getSelectedWalletForm();
|
WalletForm selectedWalletForm = getSelectedWalletForm();
|
||||||
if(selectedWalletForm != null) {
|
if(selectedWalletForm != null) {
|
||||||
|
|
@ -1490,7 +1496,6 @@ public class AppController implements Initializable {
|
||||||
stage.setAlwaysOnTop(true);
|
stage.setAlwaysOnTop(true);
|
||||||
stage.setAlwaysOnTop(false);
|
stage.setAlwaysOnTop(false);
|
||||||
if(event.getSource() instanceof File file) {
|
if(event.getSource() instanceof File file) {
|
||||||
downloadVerifierDialog.setInitialFile(file);
|
|
||||||
downloadVerifierDialog.setSignatureFile(file);
|
downloadVerifierDialog.setSignatureFile(file);
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
|
|
@ -1568,46 +1573,13 @@ public class AppController implements Initializable {
|
||||||
}
|
}
|
||||||
|
|
||||||
private void searchWallets(List<WalletForm> walletForms) {
|
private void searchWallets(List<WalletForm> walletForms) {
|
||||||
if(searchWalletDialog != null) {
|
SearchWalletDialog searchWalletDialog = new SearchWalletDialog(walletForms);
|
||||||
if(!searchWalletDialog.getWalletForms().equals(walletForms)) {
|
searchWalletDialog.initOwner(rootStack.getScene().getWindow());
|
||||||
searchWalletDialog.close();
|
Optional<Entry> optEntry = searchWalletDialog.showAndWait();
|
||||||
} else {
|
if(optEntry.isPresent()) {
|
||||||
Stage stage = (Stage)searchWalletDialog.getDialogPane().getScene().getWindow();
|
Entry entry = optEntry.get();
|
||||||
stage.setAlwaysOnTop(true);
|
EventManager.get().post(new FunctionActionEvent(entry.getWalletFunction(), entry.getWallet()));
|
||||||
stage.setAlwaysOnTop(false);
|
Platform.runLater(() -> EventManager.get().post(new SelectEntryEvent(entry)));
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Platform.runLater(() -> {
|
|
||||||
searchWalletDialog = new SearchWalletDialog(walletForms);
|
|
||||||
searchWalletDialog.initModality(Modality.NONE);
|
|
||||||
Optional<Entry> optEntry = searchWalletDialog.showAndWait();
|
|
||||||
if(optEntry.isPresent()) {
|
|
||||||
Entry entry = optEntry.get();
|
|
||||||
EventManager.get().post(new FunctionActionEvent(entry.getWalletFunction(), entry.getWallet()));
|
|
||||||
Platform.runLater(() -> EventManager.get().post(new SelectEntryEvent(entry)));
|
|
||||||
}
|
|
||||||
searchWalletDialog = null;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public void showAllWalletsSummary(ActionEvent event) {
|
|
||||||
List<List<WalletForm>> allWalletForms = new ArrayList<>();
|
|
||||||
for(Tab tab : tabs.getTabs()) {
|
|
||||||
if(tab.getUserData() instanceof WalletTabData) {
|
|
||||||
TabPane subTabs = (TabPane)tab.getContent();
|
|
||||||
allWalletForms.add(subTabs.getTabs().stream().map(subTab -> ((WalletTabData)subTab.getUserData()).getWalletForm())
|
|
||||||
.filter(walletForm -> walletForm.getWallet().isValid() && !walletForm.isLocked()).collect(Collectors.toList()));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if(allWalletForms.isEmpty() || allWalletForms.stream().allMatch(List::isEmpty)) {
|
|
||||||
showErrorDialog("No wallets", "There are no open and unlocked wallets to summarize.");
|
|
||||||
} else {
|
|
||||||
WalletSummaryDialog walletSummaryDialog = new WalletSummaryDialog(allWalletForms);
|
|
||||||
walletSummaryDialog.initOwner(rootStack.getScene().getWindow());
|
|
||||||
walletSummaryDialog.showAndWait();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1619,7 +1591,7 @@ public class AppController implements Initializable {
|
||||||
TabPane subTabs = (TabPane) selectedTab.getContent();
|
TabPane subTabs = (TabPane) selectedTab.getContent();
|
||||||
List<WalletForm> walletForms = subTabs.getTabs().stream().map(subTab -> ((WalletTabData)subTab.getUserData()).getWalletForm()).collect(Collectors.toList());
|
List<WalletForm> walletForms = subTabs.getTabs().stream().map(subTab -> ((WalletTabData)subTab.getUserData()).getWalletForm()).collect(Collectors.toList());
|
||||||
if(!walletForms.isEmpty()) {
|
if(!walletForms.isEmpty()) {
|
||||||
WalletSummaryDialog walletSummaryDialog = new WalletSummaryDialog(List.of(walletForms));
|
WalletSummaryDialog walletSummaryDialog = new WalletSummaryDialog(walletForms);
|
||||||
walletSummaryDialog.initOwner(rootStack.getScene().getWindow());
|
walletSummaryDialog.initOwner(rootStack.getScene().getWindow());
|
||||||
walletSummaryDialog.showAndWait();
|
walletSummaryDialog.showAndWait();
|
||||||
}
|
}
|
||||||
|
|
@ -1675,6 +1647,14 @@ public class AppController implements Initializable {
|
||||||
tabLabel.setGraphicTextGap(5.0);
|
tabLabel.setGraphicTextGap(5.0);
|
||||||
tab.setGraphic(tabLabel);
|
tab.setGraphic(tabLabel);
|
||||||
tab.setClosable(true);
|
tab.setClosable(true);
|
||||||
|
tab.setOnCloseRequest(event -> {
|
||||||
|
if(AppServices.getWhirlpoolServices().getWhirlpoolForMixToWallet(((WalletTabData)tab.getUserData()).getWalletForm().getWalletId()) != null) {
|
||||||
|
Optional<ButtonType> optType = AppServices.showWarningDialog("Close mix to wallet?", "This wallet has been configured as the final destination for mixes, and needs to be open for this to occur.\n\nAre you sure you want to close?", ButtonType.NO, ButtonType.YES);
|
||||||
|
if(optType.isPresent() && optType.get() == ButtonType.NO) {
|
||||||
|
event.consume();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
TabPane subTabs = new TabPane();
|
TabPane subTabs = new TabPane();
|
||||||
subTabs.setSide(Side.LEFT);
|
subTabs.setSide(Side.LEFT);
|
||||||
|
|
@ -1901,63 +1881,6 @@ public class AppController implements Initializable {
|
||||||
}
|
}
|
||||||
|
|
||||||
private void addTransactionTab(String name, File file, PSBT psbt) {
|
private void addTransactionTab(String name, File file, PSBT psbt) {
|
||||||
//Convert to PSBTv0 first
|
|
||||||
if(psbt.getVersion() != null && psbt.getVersion() >= 2) {
|
|
||||||
psbt.convertVersion(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
//Add any missing previous outputs if available in open wallets
|
|
||||||
for(PSBTInput psbtInput : psbt.getPsbtInputs()) {
|
|
||||||
if(psbtInput.getUtxo() == null) {
|
|
||||||
for(Wallet wallet : AppServices.get().getOpenWallets().keySet().stream().filter(Wallet::isValid).toList()) {
|
|
||||||
TransactionOutPoint outpoint = psbtInput.getInput().getOutpoint();
|
|
||||||
BlockTransaction blockTransaction = wallet.getWalletTransaction(outpoint.getHash());
|
|
||||||
if(blockTransaction != null && blockTransaction.getTransaction().getOutputs().size() > outpoint.getIndex()) {
|
|
||||||
psbtInput.setNonWitnessUtxo(blockTransaction.getTransaction());
|
|
||||||
ScriptType type = psbtInput.getScriptType();
|
|
||||||
if(type != null && Arrays.asList(ScriptType.WITNESS_TYPES).contains(type)) {
|
|
||||||
psbtInput.setWitnessUtxo(blockTransaction.getTransaction().getOutputs().get((int)outpoint.getIndex()));
|
|
||||||
psbtInput.setNonWitnessUtxo(null);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//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));
|
||||||
|
|
@ -1975,7 +1898,7 @@ public class AppController implements Initializable {
|
||||||
}
|
}
|
||||||
|
|
||||||
private void addTransactionTab(BlockTransaction blockTransaction, TransactionView initialView, Integer initialIndex) {
|
private void addTransactionTab(BlockTransaction blockTransaction, TransactionView initialView, Integer initialIndex) {
|
||||||
addTransactionTab(blockTransaction.getLabel(), null, blockTransaction.getTransaction(), null, blockTransaction, initialView, initialIndex);
|
addTransactionTab(null, null, blockTransaction.getTransaction(), null, blockTransaction, initialView, initialIndex);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void addTransactionTab(String name, File file, Transaction transaction, PSBT psbt, BlockTransaction blockTransaction, TransactionView initialView, Integer initialIndex) {
|
private void addTransactionTab(String name, File file, Transaction transaction, PSBT psbt, BlockTransaction blockTransaction, TransactionView initialView, Integer initialIndex) {
|
||||||
|
|
@ -2041,13 +1964,8 @@ public class AppController implements Initializable {
|
||||||
glyph.setFontSize(10.0);
|
glyph.setFontSize(10.0);
|
||||||
glyph.setOpacity(TAB_LABEL_GRAPHIC_OPACITY_ACTIVE);
|
glyph.setOpacity(TAB_LABEL_GRAPHIC_OPACITY_ACTIVE);
|
||||||
Label tabLabel = new Label(tabName);
|
Label tabLabel = new Label(tabName);
|
||||||
tabLabel.setMaxWidth(TAB_LABEL_MAX_WIDTH);
|
|
||||||
tabLabel.setGraphic(glyph);
|
tabLabel.setGraphic(glyph);
|
||||||
tabLabel.setGraphicTextGap(5.0);
|
tabLabel.setGraphicTextGap(5.0);
|
||||||
if(TextUtils.computeTextWidth(tabLabel.getFont(), tabName, 0.0D) > TAB_LABEL_MAX_WIDTH) {
|
|
||||||
Tooltip tooltip = new Tooltip(tabName);
|
|
||||||
tabLabel.setTooltip(tooltip);
|
|
||||||
}
|
|
||||||
tab.setGraphic(tabLabel);
|
tab.setGraphic(tabLabel);
|
||||||
tab.setContextMenu(getTabContextMenu(tab));
|
tab.setContextMenu(getTabContextMenu(tab));
|
||||||
tab.setClosable(true);
|
tab.setClosable(true);
|
||||||
|
|
@ -2096,33 +2014,23 @@ public class AppController implements Initializable {
|
||||||
}
|
}
|
||||||
|
|
||||||
MenuItem moveRight = new MenuItem("Move Right");
|
MenuItem moveRight = new MenuItem("Move Right");
|
||||||
moveRight.setAccelerator(new KeyCodeCombination(KeyCode.RIGHT, KeyCombination.CONTROL_DOWN, KeyCombination.SHIFT_DOWN));
|
|
||||||
moveRight.setOnAction(event -> {
|
moveRight.setOnAction(event -> {
|
||||||
int currentIndex = tabs.getSelectionModel().getSelectedIndex();
|
int index = tabs.getTabs().indexOf(tab);
|
||||||
if(currentIndex + 1 >= tabs.getTabs().size()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
Tab selectedTab = tabs.getSelectionModel().getSelectedItem();
|
|
||||||
tabs.getTabs().removeListener(tabsChangeListener);
|
tabs.getTabs().removeListener(tabsChangeListener);
|
||||||
tabs.getTabs().remove(selectedTab);
|
tabs.getTabs().remove(tab);
|
||||||
tabs.getTabs().add(currentIndex + 1, selectedTab);
|
tabs.getTabs().add(index + 1, tab);
|
||||||
tabs.getTabs().addListener(tabsChangeListener);
|
tabs.getTabs().addListener(tabsChangeListener);
|
||||||
tabs.getSelectionModel().select(selectedTab);
|
tabs.getSelectionModel().select(tab);
|
||||||
EventManager.get().post(new RequestOpenWalletsEvent()); //Rearrange recent files list
|
EventManager.get().post(new RequestOpenWalletsEvent()); //Rearrange recent files list
|
||||||
});
|
});
|
||||||
MenuItem moveLeft = new MenuItem("Move Left");
|
MenuItem moveLeft = new MenuItem("Move Left");
|
||||||
moveLeft.setAccelerator(new KeyCodeCombination(KeyCode.LEFT, KeyCombination.CONTROL_DOWN, KeyCombination.SHIFT_DOWN));
|
|
||||||
moveLeft.setOnAction(event -> {
|
moveLeft.setOnAction(event -> {
|
||||||
int currentIndex = tabs.getSelectionModel().getSelectedIndex();
|
int index = tabs.getTabs().indexOf(tab);
|
||||||
if(currentIndex == 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
Tab selectedTab = tabs.getSelectionModel().getSelectedItem();
|
|
||||||
tabs.getTabs().removeListener(tabsChangeListener);
|
tabs.getTabs().removeListener(tabsChangeListener);
|
||||||
tabs.getTabs().remove(selectedTab);
|
tabs.getTabs().remove(tab);
|
||||||
tabs.getTabs().add(currentIndex - 1, selectedTab);
|
tabs.getTabs().add(index - 1, tab);
|
||||||
tabs.getTabs().addListener(tabsChangeListener);
|
tabs.getTabs().addListener(tabsChangeListener);
|
||||||
tabs.getSelectionModel().select(selectedTab);
|
tabs.getSelectionModel().select(tab);
|
||||||
EventManager.get().post(new RequestOpenWalletsEvent()); //Rearrange recent files list
|
EventManager.get().post(new RequestOpenWalletsEvent()); //Rearrange recent files list
|
||||||
});
|
});
|
||||||
contextMenu.getItems().addAll(moveRight, moveLeft);
|
contextMenu.getItems().addAll(moveRight, moveLeft);
|
||||||
|
|
@ -2179,7 +2087,7 @@ public class AppController implements Initializable {
|
||||||
FileChooser fileChooser = new FileChooser();
|
FileChooser fileChooser = new FileChooser();
|
||||||
fileChooser.setTitle("Open Image");
|
fileChooser.setTitle("Open Image");
|
||||||
fileChooser.getExtensionFilters().addAll(
|
fileChooser.getExtensionFilters().addAll(
|
||||||
new FileChooser.ExtensionFilter("All Files", OsType.getCurrent().equals(OsType.UNIX) ? "*" : "*.*"),
|
new FileChooser.ExtensionFilter("All Files", org.controlsfx.tools.Platform.getCurrent().equals(org.controlsfx.tools.Platform.UNIX) ? "*" : "*.*"),
|
||||||
new FileChooser.ExtensionFilter("Images", "*.png", "*.jpg", "*.jpeg", "*.gif")
|
new FileChooser.ExtensionFilter("Images", "*.png", "*.jpg", "*.jpeg", "*.gif")
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -2240,7 +2148,7 @@ public class AppController implements Initializable {
|
||||||
dlg.initOwner(rootStack.getScene().getWindow());
|
dlg.initOwner(rootStack.getScene().getWindow());
|
||||||
Optional<SecureString> password = dlg.showAndWait();
|
Optional<SecureString> password = dlg.showAndWait();
|
||||||
if(password.isPresent()) {
|
if(password.isPresent()) {
|
||||||
keyDerivationService = new Storage.KeyDerivationService(storage, password.get(), true);
|
Storage.KeyDerivationService keyDerivationService = new Storage.KeyDerivationService(storage, password.get(), true);
|
||||||
keyDerivationService.setOnSucceeded(workerStateEvent -> {
|
keyDerivationService.setOnSucceeded(workerStateEvent -> {
|
||||||
EventManager.get().post(new StorageEvent(selectedWalletForm.getWalletId(), TimedEvent.Action.END, "Done"));
|
EventManager.get().post(new StorageEvent(selectedWalletForm.getWalletId(), TimedEvent.Action.END, "Done"));
|
||||||
ECKey encryptionFullKey = keyDerivationService.getValue();
|
ECKey encryptionFullKey = keyDerivationService.getValue();
|
||||||
|
|
@ -2250,7 +2158,7 @@ public class AppController implements Initializable {
|
||||||
deleteStorage(storage, true);
|
deleteStorage(storage, true);
|
||||||
} finally {
|
} finally {
|
||||||
encryptionFullKey.clear();
|
encryptionFullKey.clear();
|
||||||
keyDerivationService = null;
|
password.get().clear();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
keyDerivationService.setOnFailed(workerStateEvent -> {
|
keyDerivationService.setOnFailed(workerStateEvent -> {
|
||||||
|
|
@ -2263,7 +2171,6 @@ public class AppController implements Initializable {
|
||||||
} else {
|
} else {
|
||||||
log.error("Error deriving wallet key", keyDerivationService.getException());
|
log.error("Error deriving wallet key", keyDerivationService.getException());
|
||||||
}
|
}
|
||||||
keyDerivationService = null;
|
|
||||||
});
|
});
|
||||||
EventManager.get().post(new StorageEvent(selectedWalletForm.getWalletId(), TimedEvent.Action.START, "Decrypting wallet..."));
|
EventManager.get().post(new StorageEvent(selectedWalletForm.getWalletId(), TimedEvent.Action.START, "Decrypting wallet..."));
|
||||||
keyDerivationService.start();
|
keyDerivationService.start();
|
||||||
|
|
@ -2566,6 +2473,7 @@ public class AppController implements Initializable {
|
||||||
showLoadingLog.setDisable(true);
|
showLoadingLog.setDisable(true);
|
||||||
showTxHex.setDisable(false);
|
showTxHex.setDisable(false);
|
||||||
showPayNym.setDisable(true);
|
showPayNym.setDisable(true);
|
||||||
|
findMixingPartner.setDisable(true);
|
||||||
} else if(event instanceof WalletTabSelectedEvent) {
|
} else if(event instanceof WalletTabSelectedEvent) {
|
||||||
WalletTabSelectedEvent walletTabEvent = (WalletTabSelectedEvent)event;
|
WalletTabSelectedEvent walletTabEvent = (WalletTabSelectedEvent)event;
|
||||||
WalletTabData walletTabData = walletTabEvent.getWalletTabData();
|
WalletTabData walletTabData = walletTabEvent.getWalletTabData();
|
||||||
|
|
@ -2577,6 +2485,7 @@ public class AppController implements Initializable {
|
||||||
showLoadingLog.setDisable(false);
|
showLoadingLog.setDisable(false);
|
||||||
showTxHex.setDisable(true);
|
showTxHex.setDisable(true);
|
||||||
showPayNym.setDisable(exportWallet.isDisable() || !walletTabData.getWallet().hasPaymentCode());
|
showPayNym.setDisable(exportWallet.isDisable() || !walletTabData.getWallet().hasPaymentCode());
|
||||||
|
findMixingPartner.setDisable(exportWallet.isDisable() || !SorobanServices.canWalletMix(walletTabData.getWallet()) || !AppServices.onlineProperty().get());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -2602,6 +2511,7 @@ public class AppController implements Initializable {
|
||||||
if(selectedWalletForm.getWalletId().equals(event.getWalletId())) {
|
if(selectedWalletForm.getWalletId().equals(event.getWalletId())) {
|
||||||
exportWallet.setDisable(!event.getWallet().isValid() || selectedWalletForm.isLocked());
|
exportWallet.setDisable(!event.getWallet().isValid() || selectedWalletForm.isLocked());
|
||||||
showPayNym.setDisable(exportWallet.isDisable() || !event.getWallet().hasPaymentCode());
|
showPayNym.setDisable(exportWallet.isDisable() || !event.getWallet().hasPaymentCode());
|
||||||
|
findMixingPartner.setDisable(exportWallet.isDisable() || !SorobanServices.canWalletMix(event.getWallet()) || !AppServices.onlineProperty().get());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -2694,6 +2604,7 @@ public class AppController implements Initializable {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
Image image = new Image("image/sparrow-small.png", 50, 50, false, false);
|
||||||
String walletName = event.getWallet().getFullDisplayName();
|
String walletName = event.getWallet().getFullDisplayName();
|
||||||
if(walletName.length() > 40) {
|
if(walletName.length() > 40) {
|
||||||
walletName = walletName.substring(0, 40) + "...";
|
walletName = walletName.substring(0, 40) + "...";
|
||||||
|
|
@ -2702,10 +2613,10 @@ public class AppController implements Initializable {
|
||||||
Notifications notificationBuilder = Notifications.create()
|
Notifications notificationBuilder = Notifications.create()
|
||||||
.title("Sparrow - " + walletName)
|
.title("Sparrow - " + walletName)
|
||||||
.text(text)
|
.text(text)
|
||||||
.graphic(new DialogImage(DialogImage.Type.SPARROW))
|
.graphic(new ImageView(image))
|
||||||
.hideAfter(Duration.seconds(15))
|
.hideAfter(Duration.seconds(15))
|
||||||
.position(Pos.TOP_RIGHT)
|
.position(Pos.TOP_RIGHT)
|
||||||
.threshold(5, Notifications.create().title("Sparrow").text("Multiple new wallet transactions").graphic(new DialogImage(DialogImage.Type.SPARROW)))
|
.threshold(5, Notifications.create().title("Sparrow").text("Multiple new wallet transactions").graphic(new ImageView(image)))
|
||||||
.onAction(e -> selectTab(event.getWallet()));
|
.onAction(e -> selectTab(event.getWallet()));
|
||||||
|
|
||||||
//If controlsfx can't find our window, we must set the window ourselves (unfortunately notification is then shown within this window)
|
//If controlsfx can't find our window, we must set the window ourselves (unfortunately notification is then shown within this window)
|
||||||
|
|
@ -2821,8 +2732,8 @@ public class AppController implements Initializable {
|
||||||
@Subscribe
|
@Subscribe
|
||||||
public void connectionFailed(ConnectionFailedEvent event) {
|
public void connectionFailed(ConnectionFailedEvent event) {
|
||||||
String status = CONNECTION_FAILED_PREFIX + event.getMessage();
|
String status = CONNECTION_FAILED_PREFIX + event.getMessage();
|
||||||
Hyperlink hyperlink = new Hyperlink("Server Settings");
|
Hyperlink hyperlink = new Hyperlink("Server Preferences");
|
||||||
hyperlink.setOnAction(this::openServerSettings);
|
hyperlink.setOnAction(this::openServerPreferences);
|
||||||
statusUpdated(new StatusEvent(status, hyperlink));
|
statusUpdated(new StatusEvent(status, hyperlink));
|
||||||
serverToggleStopAnimation();
|
serverToggleStopAnimation();
|
||||||
setTorIcon();
|
setTorIcon();
|
||||||
|
|
@ -2842,7 +2753,7 @@ public class AppController implements Initializable {
|
||||||
public void disconnection(DisconnectionEvent event) {
|
public void disconnection(DisconnectionEvent event) {
|
||||||
serverToggle.setDisable(false);
|
serverToggle.setDisable(false);
|
||||||
if(!AppServices.isConnecting() && !AppServices.isConnected() && !statusBar.getText().startsWith(CONNECTION_FAILED_PREFIX) && !statusBar.getText().contains(TRYING_ANOTHER_SERVER_MESSAGE)) {
|
if(!AppServices.isConnecting() && !AppServices.isConnected() && !statusBar.getText().startsWith(CONNECTION_FAILED_PREFIX) && !statusBar.getText().contains(TRYING_ANOTHER_SERVER_MESSAGE)) {
|
||||||
statusUpdated(new StatusEvent("Disconnected (click toggle on the right to connect)", 240));
|
statusUpdated(new StatusEvent("Disconnected"));
|
||||||
}
|
}
|
||||||
if(statusTimeline == null || statusTimeline.getStatus() != Animation.Status.RUNNING) {
|
if(statusTimeline == null || statusTimeline.getStatus() != Animation.Status.RUNNING) {
|
||||||
statusBar.setProgress(0);
|
statusBar.setProgress(0);
|
||||||
|
|
@ -2946,7 +2857,6 @@ public class AppController implements Initializable {
|
||||||
}
|
}
|
||||||
} else if(event.isCompleted()) {
|
} else if(event.isCompleted()) {
|
||||||
serverToggle.setDisable(false);
|
serverToggle.setDisable(false);
|
||||||
statusBar.setProgress(0);
|
|
||||||
if(statusBar.getText().startsWith("Scanning...")) {
|
if(statusBar.getText().startsWith("Scanning...")) {
|
||||||
statusBar.setText("");
|
statusBar.setText("");
|
||||||
}
|
}
|
||||||
|
|
@ -3133,8 +3043,6 @@ public class AppController implements Initializable {
|
||||||
public void requestWalletOpen(RequestWalletOpenEvent event) {
|
public void requestWalletOpen(RequestWalletOpenEvent event) {
|
||||||
if(tabs.getScene().getWindow().equals(event.getWindow())) {
|
if(tabs.getScene().getWindow().equals(event.getWindow())) {
|
||||||
if(event.getFile() != null) {
|
if(event.getFile() != null) {
|
||||||
Optional<Tab> optExisting = tabs.getTabs().stream().filter(tab -> tab.getUserData() instanceof WalletTabData walletTabData && walletTabData.getStorage().getWalletFile().equals(event.getFile())).findFirst();
|
|
||||||
optExisting.ifPresent(tab -> tabs.getTabs().remove(tab));
|
|
||||||
openWalletFile(event.getFile(), true);
|
openWalletFile(event.getFile(), true);
|
||||||
} else {
|
} else {
|
||||||
openWallet(true);
|
openWallet(true);
|
||||||
|
|
@ -3167,11 +3075,6 @@ public class AppController implements Initializable {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Subscribe
|
|
||||||
public void requestSendToMany(RequestSendToManyEvent event) {
|
|
||||||
sendToMany(event.getPayments());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Subscribe
|
@Subscribe
|
||||||
public void functionAction(FunctionActionEvent event) {
|
public void functionAction(FunctionActionEvent event) {
|
||||||
selectTab(event.getWallet());
|
selectTab(event.getWallet());
|
||||||
|
|
@ -3222,14 +3125,4 @@ public class AppController implements Initializable {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Subscribe
|
|
||||||
public void webcamResolutionChanged(WebcamResolutionChangedEvent event) {
|
|
||||||
useHdCameraResolutionProperty.set(event.getResolution().isWidescreenAspect());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Subscribe
|
|
||||||
public void webcamMirroredChanged(WebcamMirroredChangedEvent event) {
|
|
||||||
mirrorCameraImageProperty.set(event.isMirrored());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,6 @@ package com.sparrowwallet.sparrow;
|
||||||
import com.google.common.eventbus.Subscribe;
|
import com.google.common.eventbus.Subscribe;
|
||||||
import com.google.common.net.HostAndPort;
|
import com.google.common.net.HostAndPort;
|
||||||
import com.sparrowwallet.drongo.Network;
|
import com.sparrowwallet.drongo.Network;
|
||||||
import com.sparrowwallet.drongo.OsType;
|
|
||||||
import com.sparrowwallet.drongo.SecureString;
|
import com.sparrowwallet.drongo.SecureString;
|
||||||
import com.sparrowwallet.drongo.address.Address;
|
import com.sparrowwallet.drongo.address.Address;
|
||||||
import com.sparrowwallet.drongo.bip47.PaymentCode;
|
import com.sparrowwallet.drongo.bip47.PaymentCode;
|
||||||
|
|
@ -13,7 +12,6 @@ import com.sparrowwallet.drongo.crypto.InvalidPasswordException;
|
||||||
import com.sparrowwallet.drongo.crypto.Key;
|
import com.sparrowwallet.drongo.crypto.Key;
|
||||||
import com.sparrowwallet.drongo.policy.PolicyType;
|
import com.sparrowwallet.drongo.policy.PolicyType;
|
||||||
import com.sparrowwallet.drongo.wallet.*;
|
import com.sparrowwallet.drongo.wallet.*;
|
||||||
import com.sparrowwallet.sparrow.control.DialogImage;
|
|
||||||
import com.sparrowwallet.sparrow.control.WalletPasswordDialog;
|
import com.sparrowwallet.sparrow.control.WalletPasswordDialog;
|
||||||
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
|
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
|
||||||
import com.sparrowwallet.sparrow.net.Auth47;
|
import com.sparrowwallet.sparrow.net.Auth47;
|
||||||
|
|
@ -26,8 +24,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 com.sparrowwallet.sparrow.soroban.SorobanServices;
|
||||||
import io.reactivex.subjects.PublishSubject;
|
import com.sparrowwallet.sparrow.whirlpool.WhirlpoolServices;
|
||||||
import javafx.application.Application;
|
import javafx.application.Application;
|
||||||
import javafx.application.Platform;
|
import javafx.application.Platform;
|
||||||
import javafx.beans.property.BooleanProperty;
|
import javafx.beans.property.BooleanProperty;
|
||||||
|
|
@ -45,6 +43,7 @@ import javafx.scene.Scene;
|
||||||
import javafx.scene.control.*;
|
import javafx.scene.control.*;
|
||||||
import javafx.scene.control.Dialog;
|
import javafx.scene.control.Dialog;
|
||||||
import javafx.scene.image.Image;
|
import javafx.scene.image.Image;
|
||||||
|
import javafx.scene.image.ImageView;
|
||||||
import javafx.scene.input.KeyCode;
|
import javafx.scene.input.KeyCode;
|
||||||
import javafx.scene.text.Font;
|
import javafx.scene.text.Font;
|
||||||
import javafx.stage.Screen;
|
import javafx.stage.Screen;
|
||||||
|
|
@ -68,8 +67,6 @@ import java.time.ZonedDateTime;
|
||||||
import java.time.temporal.ChronoUnit;
|
import java.time.temporal.ChronoUnit;
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.concurrent.ConcurrentHashMap;
|
|
||||||
import java.util.concurrent.TimeUnit;
|
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
import static com.sparrowwallet.sparrow.control.DownloadVerifierDialog.*;
|
import static com.sparrowwallet.sparrow.control.DownloadVerifierDialog.*;
|
||||||
|
|
@ -91,13 +88,18 @@ public class AppServices {
|
||||||
private static final String TOR_DEFAULT_PROXY_CIRCUIT_ID = "default";
|
private static final String TOR_DEFAULT_PROXY_CIRCUIT_ID = "default";
|
||||||
|
|
||||||
public static final List<Integer> TARGET_BLOCKS_RANGE = List.of(1, 2, 3, 4, 5, 10, 25, 50);
|
public static final List<Integer> TARGET_BLOCKS_RANGE = List.of(1, 2, 3, 4, 5, 10, 25, 50);
|
||||||
private static final List<Double> LONG_FEE_RATES_RANGE = List.of(1d, 2d, 4d, 8d, 16d, 32d, 64d, 128d, 256d, 512d, 1024d, 2048d, 4096d, 8192d);
|
public static final List<Long> LONG_FEE_RATES_RANGE = List.of(1L, 2L, 4L, 8L, 16L, 32L, 64L, 128L, 256L, 512L, 1024L, 2048L, 4096L, 8192L);
|
||||||
|
public static final List<Long> FEE_RATES_RANGE = LONG_FEE_RATES_RANGE.subList(0, LONG_FEE_RATES_RANGE.size() - 3);
|
||||||
public static final double FALLBACK_FEE_RATE = 20000d / 1000;
|
public static final double FALLBACK_FEE_RATE = 20000d / 1000;
|
||||||
public static final double TESTNET_FALLBACK_FEE_RATE = 1000d / 1000;
|
public static final double TESTNET_FALLBACK_FEE_RATE = 1000d / 1000;
|
||||||
|
|
||||||
private static AppServices INSTANCE;
|
private static AppServices INSTANCE;
|
||||||
|
|
||||||
private final InteractionServices interactionServices;
|
private final WhirlpoolServices whirlpoolServices = new WhirlpoolServices();
|
||||||
|
|
||||||
|
private final SorobanServices sorobanServices = new SorobanServices();
|
||||||
|
|
||||||
|
private InteractionServices interactionServices;
|
||||||
|
|
||||||
private static HttpClientService httpClientService;
|
private static HttpClientService httpClientService;
|
||||||
|
|
||||||
|
|
@ -107,8 +109,6 @@ public class AppServices {
|
||||||
|
|
||||||
private TrayManager trayManager;
|
private TrayManager trayManager;
|
||||||
|
|
||||||
private final PublishSubject<NewBlockEvent> newBlockSubject = PublishSubject.create();
|
|
||||||
|
|
||||||
private static Image windowIcon;
|
private static Image windowIcon;
|
||||||
|
|
||||||
private static final BooleanProperty onlineProperty = new SimpleBooleanProperty(false);
|
private static final BooleanProperty onlineProperty = new SimpleBooleanProperty(false);
|
||||||
|
|
@ -117,8 +117,6 @@ public class AppServices {
|
||||||
|
|
||||||
private ElectrumServer.ConnectionService connectionService;
|
private ElectrumServer.ConnectionService connectionService;
|
||||||
|
|
||||||
private ElectrumServer.FeeRatesService feeRatesService;
|
|
||||||
|
|
||||||
private Hwi.ScheduledEnumerateService deviceEnumerateService;
|
private Hwi.ScheduledEnumerateService deviceEnumerateService;
|
||||||
|
|
||||||
private VersionCheckService versionCheckService;
|
private VersionCheckService versionCheckService;
|
||||||
|
|
@ -131,18 +129,12 @@ public class AppServices {
|
||||||
|
|
||||||
private static BlockHeader latestBlockHeader;
|
private static BlockHeader latestBlockHeader;
|
||||||
|
|
||||||
private static final Map<Integer, BlockSummary> blockSummaries = new ConcurrentHashMap<>();
|
|
||||||
|
|
||||||
private static Map<Integer, Double> targetBlockFeeRates;
|
private static Map<Integer, Double> targetBlockFeeRates;
|
||||||
|
|
||||||
private static Double nextBlockMedianFeeRate;
|
|
||||||
|
|
||||||
private static final TreeMap<Date, Set<MempoolRateSize>> mempoolHistogram = new TreeMap<>();
|
private static final TreeMap<Date, Set<MempoolRateSize>> mempoolHistogram = new TreeMap<>();
|
||||||
|
|
||||||
private static Double minimumRelayFeeRate;
|
private static Double minimumRelayFeeRate;
|
||||||
|
|
||||||
private static Double serverMinimumRelayFeeRate;
|
|
||||||
|
|
||||||
private static CurrencyRate fiatCurrencyExchangeRate;
|
private static CurrencyRate fiatCurrencyExchangeRate;
|
||||||
|
|
||||||
private static List<Device> devices;
|
private static List<Device> devices;
|
||||||
|
|
@ -173,11 +165,6 @@ public class AppServices {
|
||||||
connectionService.cancel();
|
connectionService.cancel();
|
||||||
ratesService.cancel();
|
ratesService.cancel();
|
||||||
versionCheckService.cancel();
|
versionCheckService.cancel();
|
||||||
|
|
||||||
if(httpClientService != null) {
|
|
||||||
HttpClientService.ShutdownService shutdownService = new HttpClientService.ShutdownService(httpClientService);
|
|
||||||
shutdownService.start();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
@ -193,26 +180,20 @@ 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);
|
||||||
|
EventManager.get().register(whirlpoolServices);
|
||||||
|
EventManager.get().register(sorobanServices);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void start() {
|
public void start() {
|
||||||
Config config = Config.get();
|
Config config = Config.get();
|
||||||
connectionService = createConnectionService();
|
connectionService = createConnectionService();
|
||||||
feeRatesService = createFeeRatesService();
|
|
||||||
ratesService = createRatesService(config.getExchangeSource(), config.getFiatCurrency());
|
ratesService = createRatesService(config.getExchangeSource(), config.getFiatCurrency());
|
||||||
versionCheckService = createVersionCheckService();
|
versionCheckService = createVersionCheckService();
|
||||||
torService = createTorService();
|
torService = createTorService();
|
||||||
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()) {
|
||||||
|
|
@ -220,8 +201,6 @@ public class AppServices {
|
||||||
} else {
|
} else {
|
||||||
restartServices();
|
restartServices();
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
EventManager.get().post(new DisconnectionEvent());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
addURIHandlers();
|
addURIHandlers();
|
||||||
|
|
@ -279,7 +258,7 @@ public class AppServices {
|
||||||
}
|
}
|
||||||
|
|
||||||
if(Tor.getDefault() != null) {
|
if(Tor.getDefault() != null) {
|
||||||
Tor.getDefault().close();
|
Tor.getDefault().getTorManager().destroy(true, success -> {});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -305,9 +284,8 @@ public class AppServices {
|
||||||
onlineProperty.setValue(true);
|
onlineProperty.setValue(true);
|
||||||
onlineProperty.addListener(onlineServicesListener);
|
onlineProperty.addListener(onlineServicesListener);
|
||||||
|
|
||||||
FeeRatesUpdatedEvent event = connectionService.getValue();
|
if(connectionService.getValue() != null) {
|
||||||
if(event != null) {
|
EventManager.get().post(connectionService.getValue());
|
||||||
EventManager.get().post(event);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
connectionService.setOnFailed(failEvent -> {
|
connectionService.setOnFailed(failEvent -> {
|
||||||
|
|
@ -378,21 +356,12 @@ public class AppServices {
|
||||||
return connectionService;
|
return connectionService;
|
||||||
}
|
}
|
||||||
|
|
||||||
private ElectrumServer.FeeRatesService createFeeRatesService() {
|
|
||||||
ElectrumServer.FeeRatesService feeRatesService = new ElectrumServer.FeeRatesService();
|
|
||||||
feeRatesService.setOnSucceeded(workerStateEvent -> {
|
|
||||||
EventManager.get().post(feeRatesService.getValue());
|
|
||||||
});
|
|
||||||
|
|
||||||
return feeRatesService;
|
|
||||||
}
|
|
||||||
|
|
||||||
private ExchangeSource.RatesService createRatesService(ExchangeSource exchangeSource, Currency currency) {
|
private ExchangeSource.RatesService createRatesService(ExchangeSource exchangeSource, Currency currency) {
|
||||||
ExchangeSource.RatesService ratesService = new ExchangeSource.RatesService(
|
ExchangeSource.RatesService ratesService = new ExchangeSource.RatesService(
|
||||||
exchangeSource == null ? DEFAULT_EXCHANGE_SOURCE : exchangeSource,
|
exchangeSource == null ? DEFAULT_EXCHANGE_SOURCE : exchangeSource,
|
||||||
currency == null ? DEFAULT_FIAT_CURRENCY : currency);
|
currency == null ? DEFAULT_FIAT_CURRENCY : currency);
|
||||||
//Delay startup on first run, Windows requires a longer delay
|
//Delay startup on first run, Windows requires a longer delay
|
||||||
ratesService.setDelay(OsType.getCurrent() == OsType.WINDOWS ? Duration.seconds(RATES_DELAY_SECS_WINDOWS) : Duration.seconds(RATES_DELAY_SECS_DEFAULT));
|
ratesService.setDelay(org.controlsfx.tools.Platform.getCurrent() == org.controlsfx.tools.Platform.WINDOWS ? Duration.seconds(RATES_DELAY_SECS_WINDOWS) : Duration.seconds(RATES_DELAY_SECS_DEFAULT));
|
||||||
ratesService.setPeriod(Duration.seconds(RATES_PERIOD_SECS));
|
ratesService.setPeriod(Duration.seconds(RATES_PERIOD_SECS));
|
||||||
ratesService.setRestartOnFailure(true);
|
ratesService.setRestartOnFailure(true);
|
||||||
|
|
||||||
|
|
@ -492,26 +461,6 @@ public class AppServices {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void fetchFeeRates() {
|
|
||||||
if(feeRatesService != null && !feeRatesService.isRunning() && Config.get().getMode() != Mode.OFFLINE) {
|
|
||||||
feeRatesService = createFeeRatesService();
|
|
||||||
feeRatesService.start();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void fetchBlockSummaries(List<NewBlockEvent> newBlockEvents) {
|
|
||||||
if(isConnected()) {
|
|
||||||
ElectrumServer.BlockSummaryService blockSummaryService = new ElectrumServer.BlockSummaryService(newBlockEvents);
|
|
||||||
blockSummaryService.setOnSucceeded(_ -> {
|
|
||||||
EventManager.get().post(blockSummaryService.getValue());
|
|
||||||
});
|
|
||||||
blockSummaryService.setOnFailed(failedState -> {
|
|
||||||
log.error("Error fetching block summaries", failedState.getSource().getException());
|
|
||||||
});
|
|
||||||
blockSummaryService.start();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static boolean isTorRunning() {
|
public static boolean isTorRunning() {
|
||||||
return Tor.getDefault() != null;
|
return Tor.getDefault() != null;
|
||||||
}
|
}
|
||||||
|
|
@ -527,7 +476,7 @@ public class AppServices {
|
||||||
public static Proxy getProxy(String proxyCircuitId) {
|
public static Proxy getProxy(String proxyCircuitId) {
|
||||||
Config config = Config.get();
|
Config config = Config.get();
|
||||||
Proxy proxy = null;
|
Proxy proxy = null;
|
||||||
if(config.isUseProxy() && config.getProxyServer() != null) {
|
if(config.isUseProxy()) {
|
||||||
HostAndPort proxyHostAndPort = HostAndPort.fromString(config.getProxyServer());
|
HostAndPort proxyHostAndPort = HostAndPort.fromString(config.getProxyServer());
|
||||||
InetSocketAddress proxyAddress = new InetSocketAddress(proxyHostAndPort.getHost(), proxyHostAndPort.getPortOrDefault(ProxyTcpOverTlsTransport.DEFAULT_PROXY_PORT));
|
InetSocketAddress proxyAddress = new InetSocketAddress(proxyHostAndPort.getHost(), proxyHostAndPort.getPortOrDefault(ProxyTcpOverTlsTransport.DEFAULT_PROXY_PORT));
|
||||||
proxy = new Proxy(Proxy.Type.SOCKS, proxyAddress);
|
proxy = new Proxy(Proxy.Type.SOCKS, proxyAddress);
|
||||||
|
|
@ -559,15 +508,24 @@ public class AppServices {
|
||||||
return INSTANCE;
|
return INSTANCE;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static WhirlpoolServices getWhirlpoolServices() {
|
||||||
|
return get().whirlpoolServices;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static SorobanServices getSorobanServices() {
|
||||||
|
return get().sorobanServices;
|
||||||
|
}
|
||||||
|
|
||||||
public static InteractionServices getInteractionServices() {
|
public static InteractionServices getInteractionServices() {
|
||||||
return get().interactionServices;
|
return get().interactionServices;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static HttpClientService getHttpClientService() {
|
public static HttpClientService getHttpClientService() {
|
||||||
HostAndPort torProxy = getTorProxy();
|
|
||||||
if(httpClientService == null) {
|
if(httpClientService == null) {
|
||||||
|
HostAndPort torProxy = getTorProxy();
|
||||||
httpClientService = new HttpClientService(torProxy);
|
httpClientService = new HttpClientService(torProxy);
|
||||||
} else {
|
} else {
|
||||||
|
HostAndPort torProxy = getTorProxy();
|
||||||
if(!Objects.equals(httpClientService.getTorProxy(), torProxy)) {
|
if(!Objects.equals(httpClientService.getTorProxy(), torProxy)) {
|
||||||
httpClientService.setTorProxy(getTorProxy());
|
httpClientService.setTorProxy(getTorProxy());
|
||||||
}
|
}
|
||||||
|
|
@ -606,34 +564,6 @@ public class AppServices {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void runAfterDelay(long delay, Runnable runnable) {
|
|
||||||
if(delay <= 0) {
|
|
||||||
if(Platform.isFxApplicationThread()) {
|
|
||||||
runnable.run();
|
|
||||||
} else {
|
|
||||||
Platform.runLater(runnable);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
ScheduledService<Void> delayService = new ScheduledService<>() {
|
|
||||||
@Override
|
|
||||||
protected Task<Void> createTask() {
|
|
||||||
return new Task<>() {
|
|
||||||
@Override
|
|
||||||
protected Void call() {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
};
|
|
||||||
delayService.setOnSucceeded(_ -> {
|
|
||||||
delayService.cancel();
|
|
||||||
runnable.run();
|
|
||||||
});
|
|
||||||
delayService.setDelay(Duration.millis(delay));
|
|
||||||
delayService.start();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static Image getWindowIcon() {
|
private static Image getWindowIcon() {
|
||||||
if(windowIcon == null) {
|
if(windowIcon == null) {
|
||||||
windowIcon = new Image(SparrowWallet.class.getResourceAsStream("/image/sparrow-icon.png"));
|
windowIcon = new Image(SparrowWallet.class.getResourceAsStream("/image/sparrow-icon.png"));
|
||||||
|
|
@ -652,7 +582,7 @@ public class AppServices {
|
||||||
}
|
}
|
||||||
|
|
||||||
private static double getReducedWindowHeight() {
|
private static double getReducedWindowHeight() {
|
||||||
return OsType.getCurrent() != OsType.MACOS ? 802d : 768d; //Check for menu bar of ~34px
|
return org.controlsfx.tools.Platform.getCurrent() != org.controlsfx.tools.Platform.OSX ? 802d : 768d; //Check for menu bar of ~34px
|
||||||
}
|
}
|
||||||
|
|
||||||
public Application getApplication() {
|
public Application getApplication() {
|
||||||
|
|
@ -737,10 +667,6 @@ public class AppServices {
|
||||||
return latestBlockHeader;
|
return latestBlockHeader;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static Map<Integer, BlockSummary> getBlockSummaries() {
|
|
||||||
return blockSummaries;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static Double getDefaultFeeRate() {
|
public static Double getDefaultFeeRate() {
|
||||||
int defaultTarget = TARGET_BLOCKS_RANGE.get((TARGET_BLOCKS_RANGE.size() / 2) - 1);
|
int defaultTarget = TARGET_BLOCKS_RANGE.get((TARGET_BLOCKS_RANGE.size() / 2) - 1);
|
||||||
return getTargetBlockFeeRates() == null ? getFallbackFeeRate() : getTargetBlockFeeRates().get(defaultTarget);
|
return getTargetBlockFeeRates() == null ? getFallbackFeeRate() : getTargetBlockFeeRates().get(defaultTarget);
|
||||||
|
|
@ -752,30 +678,6 @@ public class AppServices {
|
||||||
return Math.max(minRate, Transaction.DUST_RELAY_TX_FEE);
|
return Math.max(minRate, Transaction.DUST_RELAY_TX_FEE);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static List<Double> getLongFeeRatesRange() {
|
|
||||||
if(minimumRelayFeeRate == null || minimumRelayFeeRate >= Transaction.DEFAULT_MIN_RELAY_FEE) {
|
|
||||||
return LONG_FEE_RATES_RANGE;
|
|
||||||
} else {
|
|
||||||
List<Double> longFeeRatesRange = new ArrayList<>();
|
|
||||||
longFeeRatesRange.add(minimumRelayFeeRate);
|
|
||||||
longFeeRatesRange.addAll(LONG_FEE_RATES_RANGE);
|
|
||||||
return longFeeRatesRange;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static List<Double> getFeeRatesRange() {
|
|
||||||
if(minimumRelayFeeRate == null || minimumRelayFeeRate >= Transaction.DEFAULT_MIN_RELAY_FEE) {
|
|
||||||
return LONG_FEE_RATES_RANGE.subList(0, LONG_FEE_RATES_RANGE.size() - 3);
|
|
||||||
} else {
|
|
||||||
List<Double> longFeeRatesRange = getLongFeeRatesRange();
|
|
||||||
return longFeeRatesRange.subList(0, longFeeRatesRange.size() - 4);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static Double getNextBlockMedianFeeRate() {
|
|
||||||
return nextBlockMedianFeeRate == null ? getDefaultFeeRate() : nextBlockMedianFeeRate;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static double getFallbackFeeRate() {
|
public static double getFallbackFeeRate() {
|
||||||
return Network.get() == Network.MAINNET ? FALLBACK_FEE_RATE : TESTNET_FALLBACK_FEE_RATE;
|
return Network.get() == Network.MAINNET ? FALLBACK_FEE_RATE : TESTNET_FALLBACK_FEE_RATE;
|
||||||
}
|
}
|
||||||
|
|
@ -810,18 +712,10 @@ public class AppServices {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public static Double getConfiguredMinimumRelayFeeRate(Config config) {
|
|
||||||
return config.getMinRelayFeeRate() >= 0d && config.getMinRelayFeeRate() < Transaction.DEFAULT_MIN_RELAY_FEE ? config.getMinRelayFeeRate() : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static Double getMinimumRelayFeeRate() {
|
public static Double getMinimumRelayFeeRate() {
|
||||||
return minimumRelayFeeRate == null ? Transaction.DEFAULT_MIN_RELAY_FEE : minimumRelayFeeRate;
|
return minimumRelayFeeRate == null ? Transaction.DEFAULT_MIN_RELAY_FEE : minimumRelayFeeRate;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static Double getServerMinimumRelayFeeRate() {
|
|
||||||
return serverMinimumRelayFeeRate;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static CurrencyRate getFiatCurrencyExchangeRate() {
|
public static CurrencyRate getFiatCurrencyExchangeRate() {
|
||||||
return fiatCurrencyExchangeRate;
|
return fiatCurrencyExchangeRate;
|
||||||
}
|
}
|
||||||
|
|
@ -835,8 +729,8 @@ public class AppServices {
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void addPayjoinURI(BitcoinURI bitcoinURI) {
|
public static void addPayjoinURI(BitcoinURI bitcoinURI) {
|
||||||
if(bitcoinURI.getPayjoinUrl() == null || bitcoinURI.getAddress() == null) {
|
if(bitcoinURI.getPayjoinUrl() == null) {
|
||||||
throw new IllegalArgumentException("Not a valid payjoin URI");
|
throw new IllegalArgumentException("Not a payjoin URI");
|
||||||
}
|
}
|
||||||
payjoinURIs.put(bitcoinURI.getAddress(), bitcoinURI);
|
payjoinURIs.put(bitcoinURI.getAddress(), bitcoinURI);
|
||||||
}
|
}
|
||||||
|
|
@ -1004,7 +898,6 @@ public class AppServices {
|
||||||
}
|
}
|
||||||
|
|
||||||
if(openWindow instanceof Stage) {
|
if(openWindow instanceof Stage) {
|
||||||
((Stage)openWindow).setIconified(false);
|
|
||||||
((Stage)openWindow).setAlwaysOnTop(true);
|
((Stage)openWindow).setAlwaysOnTop(true);
|
||||||
((Stage)openWindow).setAlwaysOnTop(false);
|
((Stage)openWindow).setAlwaysOnTop(false);
|
||||||
}
|
}
|
||||||
|
|
@ -1163,7 +1056,8 @@ public class AppServices {
|
||||||
walletChoiceDialog.initOwner(getActiveWindow());
|
walletChoiceDialog.initOwner(getActiveWindow());
|
||||||
walletChoiceDialog.setTitle("Choose Wallet");
|
walletChoiceDialog.setTitle("Choose Wallet");
|
||||||
walletChoiceDialog.setHeaderText("Choose a wallet to " + actionDescription);
|
walletChoiceDialog.setHeaderText("Choose a wallet to " + actionDescription);
|
||||||
walletChoiceDialog.getDialogPane().setGraphic(new DialogImage(DialogImage.Type.SPARROW));
|
Image image = new Image("/image/sparrow-small.png");
|
||||||
|
walletChoiceDialog.getDialogPane().setGraphic(new ImageView(image));
|
||||||
setStageIcon(walletChoiceDialog.getDialogPane().getScene().getWindow());
|
setStageIcon(walletChoiceDialog.getDialogPane().getScene().getWindow());
|
||||||
moveToActiveWindowScreen(walletChoiceDialog);
|
moveToActiveWindowScreen(walletChoiceDialog);
|
||||||
Optional<Wallet> optWallet = walletChoiceDialog.showAndWait();
|
Optional<Wallet> optWallet = walletChoiceDialog.showAndWait();
|
||||||
|
|
@ -1175,96 +1069,17 @@ 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 boolean isWhirlpoolCompatible(Wallet wallet) {
|
|
||||||
return WHIRLPOOL_NETWORKS.contains(Network.get())
|
|
||||||
&& wallet.getScriptType() != ScriptType.P2TR //Taproot not yet supported
|
|
||||||
&& wallet.getKeystores().size() == 1
|
|
||||||
&& wallet.getKeystores().get(0).hasSeed()
|
|
||||||
&& wallet.getKeystores().get(0).getSeed().getType() == DeterministicSeed.Type.BIP39
|
|
||||||
&& wallet.getStandardAccountType() != null
|
|
||||||
&& StandardAccount.isMixableAccount(wallet.getStandardAccountType());
|
|
||||||
}
|
|
||||||
|
|
||||||
public static boolean isWhirlpoolPostmixCompatible(Wallet wallet) {
|
|
||||||
return WHIRLPOOL_NETWORKS.contains(Network.get())
|
|
||||||
&& wallet.getScriptType() != ScriptType.P2TR //Taproot not yet supported
|
|
||||||
&& 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) {
|
|
||||||
List<Wallet> childWallets = new ArrayList<>();
|
|
||||||
for(StandardAccount whirlpoolAccount : StandardAccount.WHIRLPOOL_ACCOUNTS) {
|
|
||||||
if(decryptedWallet.getChildWallet(whirlpoolAccount) == null) {
|
|
||||||
Wallet childWallet = decryptedWallet.addChildWallet(whirlpoolAccount);
|
|
||||||
childWallets.add(childWallet);
|
|
||||||
EventManager.get().post(new ChildWalletsAddedEvent(storage, decryptedWallet, childWallet));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return childWallets;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static Font getMonospaceFont() {
|
public static Font getMonospaceFont() {
|
||||||
return Font.font("Fragment Mono Regular", 13);
|
return Font.font("Roboto Mono", 13);
|
||||||
}
|
|
||||||
|
|
||||||
public static boolean isOnWayland() {
|
|
||||||
if(OsType.getCurrent() != OsType.UNIX) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
String waylandDisplay = System.getenv("WAYLAND_DISPLAY");
|
|
||||||
return waylandDisplay != null && !waylandDisplay.isEmpty();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Subscribe
|
@Subscribe
|
||||||
public void newConnection(ConnectionEvent event) {
|
public void newConnection(ConnectionEvent event) {
|
||||||
currentBlockHeight = event.getBlockHeight();
|
currentBlockHeight = event.getBlockHeight();
|
||||||
System.setProperty(Network.BLOCK_HEIGHT_PROPERTY, Integer.toString(currentBlockHeight));
|
System.setProperty(Network.BLOCK_HEIGHT_PROPERTY, Integer.toString(currentBlockHeight));
|
||||||
if(getConfiguredMinimumRelayFeeRate(Config.get()) == null) {
|
minimumRelayFeeRate = Math.max(event.getMinimumRelayFeeRate(), Transaction.DEFAULT_MIN_RELAY_FEE);
|
||||||
minimumRelayFeeRate = event.getMinimumRelayFeeRate() == null ? Transaction.DEFAULT_MIN_RELAY_FEE : event.getMinimumRelayFeeRate();
|
|
||||||
}
|
|
||||||
serverMinimumRelayFeeRate = event.getMinimumRelayFeeRate();
|
|
||||||
latestBlockHeader = event.getBlockHeader();
|
latestBlockHeader = event.getBlockHeader();
|
||||||
Config.get().addRecentServer();
|
Config.get().addRecentServer();
|
||||||
|
|
||||||
FeeRatesSource feeRatesSource = Config.get().getFeeRatesSource();
|
|
||||||
feeRatesSource = (feeRatesSource == null ? FeeRatesSource.MEMPOOL_SPACE : feeRatesSource);
|
|
||||||
if(feeRatesSource.supportsNetwork(Network.get()) && feeRatesSource.isExternal()) {
|
|
||||||
fetchFeeRates();
|
|
||||||
}
|
|
||||||
|
|
||||||
if(!blockSummaries.containsKey(currentBlockHeight)) {
|
|
||||||
fetchBlockSummaries(Collections.emptyList());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Subscribe
|
@Subscribe
|
||||||
|
|
@ -1279,36 +1094,26 @@ 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
|
||||||
public void mempoolRateSizes(MempoolRateSizesUpdatedEvent event) {
|
public void mempoolRateSizes(MempoolRateSizesUpdatedEvent event) {
|
||||||
if(event.getMempoolRateSizes() != null) {
|
addMempoolRateSizes(event.getMempoolRateSizes());
|
||||||
addMempoolRateSizes(event.getMempoolRateSizes());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Subscribe
|
@Subscribe
|
||||||
public void feeRateSourceChanged(FeeRatesSourceChangedEvent event) {
|
public void feeRateSourceChanged(FeeRatesSourceChangedEvent event) {
|
||||||
|
ElectrumServer.FeeRatesService feeRatesService = new ElectrumServer.FeeRatesService();
|
||||||
|
feeRatesService.setOnSucceeded(workerStateEvent -> {
|
||||||
|
EventManager.get().post(feeRatesService.getValue());
|
||||||
|
});
|
||||||
//Perform once-off fee rates retrieval to immediately change displayed rates
|
//Perform once-off fee rates retrieval to immediately change displayed rates
|
||||||
fetchFeeRates();
|
feeRatesService.start();
|
||||||
fetchBlockSummaries(Collections.emptyList());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Subscribe
|
@Subscribe
|
||||||
|
|
|
||||||
|
|
@ -1,76 +0,0 @@
|
||||||
package com.sparrowwallet.sparrow;
|
|
||||||
|
|
||||||
import java.time.Instant;
|
|
||||||
import java.time.temporal.ChronoUnit;
|
|
||||||
import java.util.Date;
|
|
||||||
import java.util.Optional;
|
|
||||||
|
|
||||||
public class BlockSummary implements Comparable<BlockSummary> {
|
|
||||||
private final Integer height;
|
|
||||||
private final Date timestamp;
|
|
||||||
private final Double medianFee;
|
|
||||||
private final Integer transactionCount;
|
|
||||||
private final Integer weight;
|
|
||||||
|
|
||||||
public BlockSummary(Integer height, Date timestamp) {
|
|
||||||
this(height, timestamp, null, null, null);
|
|
||||||
}
|
|
||||||
|
|
||||||
public BlockSummary(Integer height, Date timestamp, Double medianFee, Integer transactionCount, Integer weight) {
|
|
||||||
this.height = height;
|
|
||||||
this.timestamp = timestamp;
|
|
||||||
this.medianFee = medianFee;
|
|
||||||
this.transactionCount = transactionCount;
|
|
||||||
this.weight = weight;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Integer getHeight() {
|
|
||||||
return height;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Date getTimestamp() {
|
|
||||||
return timestamp;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Optional<Double> getMedianFee() {
|
|
||||||
return medianFee == null ? Optional.empty() : Optional.of(medianFee);
|
|
||||||
}
|
|
||||||
|
|
||||||
public Optional<Integer> getTransactionCount() {
|
|
||||||
return transactionCount == null ? Optional.empty() : Optional.of(transactionCount);
|
|
||||||
}
|
|
||||||
|
|
||||||
public Optional<Integer> getWeight() {
|
|
||||||
return weight == null ? Optional.empty() : Optional.of(weight);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static long calculateElapsedSeconds(long timestampUtc) {
|
|
||||||
Instant timestampInstant = Instant.ofEpochMilli(timestampUtc);
|
|
||||||
Instant nowInstant = Instant.now();
|
|
||||||
return ChronoUnit.SECONDS.between(timestampInstant, nowInstant);
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getElapsed() {
|
|
||||||
long elapsed = calculateElapsedSeconds(getTimestamp().getTime());
|
|
||||||
if(elapsed < 0) {
|
|
||||||
return "now";
|
|
||||||
} else if(elapsed < 60) {
|
|
||||||
return elapsed + "s";
|
|
||||||
} else if(elapsed < 3600) {
|
|
||||||
return elapsed / 60 + "m";
|
|
||||||
} else if(elapsed < 86400) {
|
|
||||||
return elapsed / 3600 + "h";
|
|
||||||
} else {
|
|
||||||
return elapsed / 86400 + "d";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public String toString() {
|
|
||||||
return getElapsed() + ":" + getMedianFee();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int compareTo(BlockSummary o) {
|
|
||||||
return o.height - height;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
package com.sparrowwallet.sparrow;
|
package com.sparrowwallet.sparrow;
|
||||||
|
|
||||||
import com.sparrowwallet.drongo.OsType;
|
|
||||||
import com.sparrowwallet.drongo.wallet.Keystore;
|
import com.sparrowwallet.drongo.wallet.Keystore;
|
||||||
import com.sparrowwallet.sparrow.control.KeystorePassphraseDialog;
|
import com.sparrowwallet.sparrow.control.KeystorePassphraseDialog;
|
||||||
import com.sparrowwallet.sparrow.control.TextUtils;
|
import com.sparrowwallet.sparrow.control.TextUtils;
|
||||||
|
|
@ -49,7 +48,7 @@ public class DefaultInteractionServices implements InteractionServices {
|
||||||
}
|
}
|
||||||
|
|
||||||
String[] lines = content.split("\r\n|\r|\n");
|
String[] lines = content.split("\r\n|\r|\n");
|
||||||
if(lines.length > 3 || OsType.getCurrent() == OsType.WINDOWS) {
|
if(lines.length > 3 || org.controlsfx.tools.Platform.getCurrent() == org.controlsfx.tools.Platform.WINDOWS) {
|
||||||
double numLines = Arrays.stream(lines).mapToDouble(line -> Math.ceil(TextUtils.computeTextWidth(Font.getDefault(), line, 0) / 300)).sum();
|
double numLines = Arrays.stream(lines).mapToDouble(line -> Math.ceil(TextUtils.computeTextWidth(Font.getDefault(), line, 0) / 300)).sum();
|
||||||
alert.getDialogPane().setPrefHeight(200 + numLines * 20);
|
alert.getDialogPane().setPrefHeight(200 + numLines * 20);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
package com.sparrowwallet.sparrow;
|
package com.sparrowwallet.sparrow;
|
||||||
|
|
||||||
import com.sparrowwallet.drongo.Network;
|
import com.sparrowwallet.drongo.Network;
|
||||||
import com.sparrowwallet.drongo.OsType;
|
|
||||||
import com.sparrowwallet.drongo.wallet.Wallet;
|
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||||
import com.sparrowwallet.sparrow.control.WalletIcon;
|
import com.sparrowwallet.sparrow.control.WalletIcon;
|
||||||
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
|
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
|
||||||
|
|
@ -10,12 +9,13 @@ import com.sparrowwallet.sparrow.io.Config;
|
||||||
import com.sparrowwallet.sparrow.io.Storage;
|
import com.sparrowwallet.sparrow.io.Storage;
|
||||||
import com.sparrowwallet.sparrow.net.PublicElectrumServer;
|
import com.sparrowwallet.sparrow.net.PublicElectrumServer;
|
||||||
import com.sparrowwallet.sparrow.net.ServerType;
|
import com.sparrowwallet.sparrow.net.ServerType;
|
||||||
import com.sparrowwallet.sparrow.settings.SettingsGroup;
|
import com.sparrowwallet.sparrow.preferences.PreferenceGroup;
|
||||||
import com.sparrowwallet.sparrow.settings.SettingsDialog;
|
import com.sparrowwallet.sparrow.preferences.PreferencesDialog;
|
||||||
import javafx.application.Application;
|
import javafx.application.Application;
|
||||||
import javafx.scene.text.Font;
|
import javafx.scene.text.Font;
|
||||||
import javafx.stage.Stage;
|
import javafx.stage.Stage;
|
||||||
import org.controlsfx.glyphfont.GlyphFontRegistry;
|
import org.controlsfx.glyphfont.GlyphFontRegistry;
|
||||||
|
import org.controlsfx.tools.Platform;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
|
|
@ -42,7 +42,10 @@ public class SparrowDesktop extends Application {
|
||||||
public void start(Stage stage) throws Exception {
|
public void start(Stage stage) throws Exception {
|
||||||
this.mainStage = stage;
|
this.mainStage = stage;
|
||||||
|
|
||||||
initializeFonts();
|
GlyphFontRegistry.register(new FontAwesome5());
|
||||||
|
GlyphFontRegistry.register(new FontAwesome5Brands());
|
||||||
|
Font.loadFont(AppServices.class.getResourceAsStream("/font/RobotoMono-Regular.ttf"), 13);
|
||||||
|
Font.loadFont(AppServices.class.getResourceAsStream("/font/RobotoMono-Italic.ttf"), 11);
|
||||||
URL.setURLStreamHandlerFactory(protocol -> WalletIcon.PROTOCOL.equals(protocol) ? new WalletIcon.WalletIconStreamHandler() : null);
|
URL.setURLStreamHandlerFactory(protocol -> WalletIcon.PROTOCOL.equals(protocol) ? new WalletIcon.WalletIconStreamHandler() : null);
|
||||||
|
|
||||||
AppServices.initialize(this);
|
AppServices.initialize(this);
|
||||||
|
|
@ -57,8 +60,8 @@ public class SparrowDesktop extends Application {
|
||||||
Config.get().setMode(mode);
|
Config.get().setMode(mode);
|
||||||
|
|
||||||
if(mode.equals(Mode.ONLINE)) {
|
if(mode.equals(Mode.ONLINE)) {
|
||||||
SettingsDialog settingsDialog = new SettingsDialog(SettingsGroup.SERVER, true);
|
PreferencesDialog preferencesDialog = new PreferencesDialog(PreferenceGroup.SERVER, true);
|
||||||
Optional<Boolean> optNewWallet = settingsDialog.showAndWait();
|
Optional<Boolean> optNewWallet = preferencesDialog.showAndWait();
|
||||||
createNewWallet = optNewWallet.isPresent() && optNewWallet.get();
|
createNewWallet = optNewWallet.isPresent() && optNewWallet.get();
|
||||||
} else if(Network.get() == Network.MAINNET) {
|
} else if(Network.get() == Network.MAINNET) {
|
||||||
Config.get().setServerType(ServerType.PUBLIC_ELECTRUM_SERVER);
|
Config.get().setServerType(ServerType.PUBLIC_ELECTRUM_SERVER);
|
||||||
|
|
@ -72,6 +75,10 @@ public class SparrowDesktop extends Application {
|
||||||
Config.get().setServerType(ServerType.ELECTRUM_SERVER);
|
Config.get().setServerType(ServerType.ELECTRUM_SERVER);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if(Config.get().getHdCapture() == null && Platform.getCurrent() == Platform.OSX) {
|
||||||
|
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()));
|
||||||
|
|
||||||
|
|
@ -82,42 +89,28 @@ public class SparrowDesktop extends Application {
|
||||||
|
|
||||||
AppController appController = AppServices.newAppWindow(stage);
|
AppController appController = AppServices.newAppWindow(stage);
|
||||||
|
|
||||||
final boolean showNewWallet = createNewWallet;
|
if(createNewWallet) {
|
||||||
//Delay opening new dialogs on Wayland
|
appController.newWallet(null);
|
||||||
AppServices.runAfterDelay(AppServices.isOnWayland() ? 1000 : 0, () -> {
|
}
|
||||||
if(showNewWallet) {
|
|
||||||
appController.newWallet(null);
|
|
||||||
}
|
|
||||||
|
|
||||||
List<File> recentWalletFiles = Config.get().getRecentWalletFiles();
|
List<File> recentWalletFiles = Config.get().getRecentWalletFiles();
|
||||||
if(recentWalletFiles != null) {
|
if(recentWalletFiles != null) {
|
||||||
//Preserve wallet order as far as possible. Unencrypted wallets will still be opened first.
|
//Preserve wallet order as far as possible. Unencrypted wallets will still be opened first.
|
||||||
List<File> encryptedWalletFiles = recentWalletFiles.stream().filter(Storage::isEncrypted).collect(Collectors.toList());
|
List<File> encryptedWalletFiles = recentWalletFiles.stream().filter(Storage::isEncrypted).collect(Collectors.toList());
|
||||||
List<File> sortedWalletFiles = new ArrayList<>(recentWalletFiles);
|
List<File> sortedWalletFiles = new ArrayList<>(recentWalletFiles);
|
||||||
sortedWalletFiles.removeAll(encryptedWalletFiles);
|
sortedWalletFiles.removeAll(encryptedWalletFiles);
|
||||||
sortedWalletFiles.addAll(encryptedWalletFiles);
|
sortedWalletFiles.addAll(encryptedWalletFiles);
|
||||||
|
|
||||||
for(File walletFile : sortedWalletFiles) {
|
for(File walletFile : sortedWalletFiles) {
|
||||||
if(walletFile.exists()) {
|
if(walletFile.exists()) {
|
||||||
appController.openWalletFile(walletFile, false);
|
appController.openWalletFile(walletFile, false);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
AppServices.openFileUriArgumentsAfterWalletLoading(stage);
|
|
||||||
|
|
||||||
AppServices.get().start();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private void initializeFonts() {
|
|
||||||
GlyphFontRegistry.register(new FontAwesome5());
|
|
||||||
GlyphFontRegistry.register(new FontAwesome5Brands());
|
|
||||||
Font.loadFont(AppServices.class.getResourceAsStream("/font/FragmentMono-Regular.ttf"), 13);
|
|
||||||
Font.loadFont(AppServices.class.getResourceAsStream("/font/FragmentMono-Italic.ttf"), 11);
|
|
||||||
if(OsType.getCurrent() == OsType.MACOS) {
|
|
||||||
Font.loadFont(AppServices.class.getResourceAsStream("/font/LiberationSans-Regular.ttf"), 13);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
AppServices.openFileUriArgumentsAfterWalletLoading(stage);
|
||||||
|
|
||||||
|
AppServices.get().start();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
|
||||||
|
|
@ -16,9 +16,9 @@ import java.io.File;
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
|
|
||||||
public class SparrowWallet {
|
public class SparrowWallet {
|
||||||
public static final String APP_ID = "sparrow";
|
public static final String APP_ID = "com.sparrowwallet.sparrow";
|
||||||
public static final String APP_NAME = "Sparrow";
|
public static final String APP_NAME = "Sparrow";
|
||||||
public static final String APP_VERSION = "2.3.1";
|
public static final String APP_VERSION = "1.8.3";
|
||||||
public static final String APP_VERSION_SUFFIX = "";
|
public static final String APP_VERSION_SUFFIX = "";
|
||||||
public static final String APP_HOME_PROPERTY = "sparrow.home";
|
public static final String APP_HOME_PROPERTY = "sparrow.home";
|
||||||
public static final String NETWORK_ENV_PROPERTY = "SPARROW_NETWORK";
|
public static final String NETWORK_ENV_PROPERTY = "SPARROW_NETWORK";
|
||||||
|
|
@ -66,11 +66,6 @@ public class SparrowWallet {
|
||||||
Network.set(Network.TESTNET);
|
Network.set(Network.TESTNET);
|
||||||
}
|
}
|
||||||
|
|
||||||
File testnet4Flag = new File(Storage.getSparrowHome(), "network-" + Network.TESTNET4.getName());
|
|
||||||
if(testnet4Flag.exists()) {
|
|
||||||
Network.set(Network.TESTNET4);
|
|
||||||
}
|
|
||||||
|
|
||||||
File signetFlag = new File(Storage.getSparrowHome(), "network-" + Network.SIGNET.getName());
|
File signetFlag = new File(Storage.getSparrowHome(), "network-" + Network.SIGNET.getName());
|
||||||
if(signetFlag.exists()) {
|
if(signetFlag.exists()) {
|
||||||
Network.set(Network.SIGNET);
|
Network.set(Network.SIGNET);
|
||||||
|
|
@ -84,7 +79,7 @@ public class SparrowWallet {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
instance = new Instance(fileUriArguments);
|
instance = new Instance(fileUriArguments);
|
||||||
instance.acquireLock(!fileUriArguments.isEmpty()); //If fileUriArguments is not empty, will exit app after sending fileUriArguments if lock cannot be acquired
|
instance.acquireLock(); //If fileUriArguments is not empty, will exit app after sending fileUriArguments if lock cannot be acquired
|
||||||
} catch(InstanceException e) {
|
} catch(InstanceException e) {
|
||||||
getLogger().error("Could not access application lock", e);
|
getLogger().error("Could not access application lock", e);
|
||||||
}
|
}
|
||||||
|
|
@ -135,13 +130,13 @@ public class SparrowWallet {
|
||||||
private final List<String> fileUriArguments;
|
private final List<String> fileUriArguments;
|
||||||
|
|
||||||
public Instance(List<String> fileUriArguments) {
|
public Instance(List<String> fileUriArguments) {
|
||||||
super(SparrowWallet.APP_ID, true);
|
super(SparrowWallet.APP_ID + "." + Network.get(), !fileUriArguments.isEmpty());
|
||||||
this.fileUriArguments = fileUriArguments;
|
this.fileUriArguments = fileUriArguments;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void receiveMessageList(List<String> messageList) {
|
protected void receiveMessageList(List<String> messageList) {
|
||||||
if(messageList != null) {
|
if(messageList != null && !messageList.isEmpty()) {
|
||||||
AppServices.parseFileUriArguments(messageList);
|
AppServices.parseFileUriArguments(messageList);
|
||||||
AppServices.openFileUriArguments(null);
|
AppServices.openFileUriArguments(null);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,7 @@ public class WelcomeDialog extends Dialog<Mode> {
|
||||||
welcomeController.initializeView();
|
welcomeController.initializeView();
|
||||||
|
|
||||||
dialogPane.setPrefWidth(600);
|
dialogPane.setPrefWidth(600);
|
||||||
dialogPane.setPrefHeight(540);
|
dialogPane.setPrefHeight(520);
|
||||||
dialogPane.setMinHeight(dialogPane.getPrefHeight());
|
dialogPane.setMinHeight(dialogPane.getPrefHeight());
|
||||||
AppServices.moveToActiveWindowScreen(this);
|
AppServices.moveToActiveWindowScreen(this);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -5,13 +5,17 @@ import com.sparrowwallet.drongo.wallet.StandardAccount;
|
||||||
import com.sparrowwallet.drongo.wallet.Wallet;
|
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||||
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.Config;
|
||||||
|
import com.sparrowwallet.sparrow.net.ServerType;
|
||||||
|
import com.sparrowwallet.sparrow.whirlpool.WhirlpoolServices;
|
||||||
import javafx.collections.FXCollections;
|
import javafx.collections.FXCollections;
|
||||||
import javafx.scene.control.*;
|
import javafx.scene.control.*;
|
||||||
import javafx.scene.layout.VBox;
|
import javafx.scene.layout.VBox;
|
||||||
import javafx.util.StringConverter;
|
import javafx.util.StringConverter;
|
||||||
import org.controlsfx.glyphfont.Glyph;
|
import org.controlsfx.glyphfont.Glyph;
|
||||||
|
|
||||||
import java.util.*;
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
import static com.sparrowwallet.drongo.wallet.StandardAccount.*;
|
import static com.sparrowwallet.drongo.wallet.StandardAccount.*;
|
||||||
|
|
||||||
|
|
@ -42,14 +46,12 @@ public class AddAccountDialog extends Dialog<List<StandardAccount>> {
|
||||||
standardAccountCombo = new ComboBox<>();
|
standardAccountCombo = new ComboBox<>();
|
||||||
standardAccountCombo.setMaxWidth(Double.MAX_VALUE);
|
standardAccountCombo.setMaxWidth(Double.MAX_VALUE);
|
||||||
|
|
||||||
Set<Integer> existingIndexes = new LinkedHashSet<>();
|
List<Integer> existingIndexes = new ArrayList<>();
|
||||||
Wallet masterWallet = wallet.isMasterWallet() ? wallet : wallet.getMasterWallet();
|
Wallet masterWallet = wallet.isMasterWallet() ? wallet : wallet.getMasterWallet();
|
||||||
existingIndexes.add(masterWallet.getAccountIndex());
|
existingIndexes.add(masterWallet.getAccountIndex());
|
||||||
for(Wallet childWallet : masterWallet.getChildWallets()) {
|
for(Wallet childWallet : masterWallet.getChildWallets()) {
|
||||||
if(!childWallet.isNested()) {
|
if(!childWallet.isNested()) {
|
||||||
existingIndexes.add(childWallet.getAccountIndex());
|
existingIndexes.add(childWallet.getAccountIndex());
|
||||||
Optional<StandardAccount> optStdAcc = Arrays.stream(StandardAccount.values()).filter(stdacc -> stdacc.getName().equals(childWallet.getName())).findFirst();
|
|
||||||
optStdAcc.ifPresent(standardAccount -> existingIndexes.add(standardAccount.getAccountNumber()));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -60,14 +62,15 @@ public class AddAccountDialog extends Dialog<List<StandardAccount>> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if(AppServices.isWhirlpoolCompatible(masterWallet) && !masterWallet.isWhirlpoolMasterWallet()) {
|
if(WhirlpoolServices.canWalletMix(masterWallet) && !masterWallet.isWhirlpoolMasterWallet()) {
|
||||||
availableAccounts.add(WHIRLPOOL_PREMIX);
|
availableAccounts.add(WHIRLPOOL_PREMIX);
|
||||||
} else if(AppServices.isWhirlpoolPostmixCompatible(masterWallet) && !existingIndexes.contains(WHIRLPOOL_POSTMIX.getAccountNumber())) {
|
} else if(WhirlpoolServices.canWatchPostmix(masterWallet) && !existingIndexes.contains(WHIRLPOOL_POSTMIX.getAccountNumber())) {
|
||||||
availableAccounts.add(WHIRLPOOL_POSTMIX);
|
availableAccounts.add(WHIRLPOOL_POSTMIX);
|
||||||
}
|
}
|
||||||
|
|
||||||
final ButtonType discoverButtonType = new javafx.scene.control.ButtonType("Discover", ButtonBar.ButtonData.LEFT);
|
final ButtonType discoverButtonType = new javafx.scene.control.ButtonType("Discover", ButtonBar.ButtonData.LEFT);
|
||||||
if(!availableAccounts.isEmpty() && (masterWallet.getKeystores().stream().allMatch(ks -> ks.getSource() == KeystoreSource.SW_SEED)
|
if(!availableAccounts.isEmpty() && Config.get().getServerType() != ServerType.BITCOIN_CORE &&
|
||||||
|
(masterWallet.getKeystores().stream().allMatch(ks -> ks.getSource() == KeystoreSource.SW_SEED)
|
||||||
|| (masterWallet.getKeystores().size() == 1 && masterWallet.getKeystores().stream().allMatch(ks -> ks.getSource() == KeystoreSource.HW_USB)))) {
|
|| (masterWallet.getKeystores().size() == 1 && masterWallet.getKeystores().stream().allMatch(ks -> ks.getSource() == KeystoreSource.HW_USB)))) {
|
||||||
dialogPane.getButtonTypes().add(discoverButtonType);
|
dialogPane.getButtonTypes().add(discoverButtonType);
|
||||||
Button discoverButton = (Button)dialogPane.lookupButton(discoverButtonType);
|
Button discoverButton = (Button)dialogPane.lookupButton(discoverButtonType);
|
||||||
|
|
|
||||||
|
|
@ -45,7 +45,6 @@ public class AddressCell extends TreeTableCell<Entry, UtxoEntry.AddressStatus> {
|
||||||
tooltip.setShowDelay(Duration.millis(250));
|
tooltip.setShowDelay(Duration.millis(250));
|
||||||
tooltip.setText(getTooltipText(utxoEntry, addressStatus.isDuplicate(), addressStatus.isDustAttack()));
|
tooltip.setText(getTooltipText(utxoEntry, addressStatus.isDuplicate(), addressStatus.isDustAttack()));
|
||||||
setTooltip(tooltip);
|
setTooltip(tooltip);
|
||||||
getStyleClass().add("address-cell");
|
|
||||||
|
|
||||||
if(addressStatus.isDustAttack()) {
|
if(addressStatus.isDustAttack()) {
|
||||||
setGraphic(getDustAttackHyperlink(utxoEntry));
|
setGraphic(getDustAttackHyperlink(utxoEntry));
|
||||||
|
|
|
||||||
|
|
@ -77,7 +77,7 @@ public class AddressTreeTable extends CoinTreeTable {
|
||||||
getColumns().forEach(col -> col.setContextMenu(contextMenu));
|
getColumns().forEach(col -> col.setContextMenu(contextMenu));
|
||||||
|
|
||||||
setEditable(true);
|
setEditable(true);
|
||||||
setupColumnWidths();
|
setColumnResizePolicy(TreeTableView.CONSTRAINED_RESIZE_POLICY);
|
||||||
|
|
||||||
addressCol.setSortType(TreeTableColumn.SortType.ASCENDING);
|
addressCol.setSortType(TreeTableColumn.SortType.ASCENDING);
|
||||||
getSortOrder().add(addressCol);
|
getSortOrder().add(addressCol);
|
||||||
|
|
|
||||||
|
|
@ -1,33 +0,0 @@
|
||||||
package com.sparrowwallet.sparrow.control;
|
|
||||||
|
|
||||||
import com.sparrowwallet.sparrow.AppServices;
|
|
||||||
import javafx.geometry.Insets;
|
|
||||||
import javafx.geometry.Pos;
|
|
||||||
import javafx.scene.control.Alert;
|
|
||||||
import javafx.scene.control.ButtonType;
|
|
||||||
import javafx.scene.control.Label;
|
|
||||||
import javafx.scene.layout.VBox;
|
|
||||||
|
|
||||||
import static com.sparrowwallet.sparrow.AppServices.*;
|
|
||||||
|
|
||||||
public class BitBoxPairingDialog extends Alert {
|
|
||||||
public BitBoxPairingDialog(String code) {
|
|
||||||
super(AlertType.INFORMATION);
|
|
||||||
initOwner(getActiveWindow());
|
|
||||||
setStageIcon(getDialogPane().getScene().getWindow());
|
|
||||||
getDialogPane().getStylesheets().add(AppServices.class.getResource("general.css").toExternalForm());
|
|
||||||
setTitle("Confirm BitBox02 Pairing");
|
|
||||||
setHeaderText(getTitle());
|
|
||||||
VBox vBox = new VBox(20);
|
|
||||||
vBox.setAlignment(Pos.CENTER);
|
|
||||||
vBox.setPadding(new Insets(10, 20, 10, 20));
|
|
||||||
Label instructions = new Label("Confirm the following code is shown on BitBox02");
|
|
||||||
Label codeLabel = new Label(code);
|
|
||||||
codeLabel.getStyleClass().add("fixed-width");
|
|
||||||
vBox.getChildren().addAll(instructions, codeLabel);
|
|
||||||
getDialogPane().setContent(vBox);
|
|
||||||
moveToActiveWindowScreen(this);
|
|
||||||
getDialogPane().getButtonTypes().clear();
|
|
||||||
getDialogPane().getButtonTypes().add(ButtonType.CLOSE);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,372 +0,0 @@
|
||||||
package com.sparrowwallet.sparrow.control;
|
|
||||||
|
|
||||||
import com.sparrowwallet.drongo.Network;
|
|
||||||
import com.sparrowwallet.drongo.protocol.Transaction;
|
|
||||||
import com.sparrowwallet.sparrow.BlockSummary;
|
|
||||||
import com.sparrowwallet.sparrow.io.Config;
|
|
||||||
import com.sparrowwallet.sparrow.net.FeeRatesSource;
|
|
||||||
import javafx.animation.KeyFrame;
|
|
||||||
import javafx.animation.KeyValue;
|
|
||||||
import javafx.animation.Timeline;
|
|
||||||
import javafx.beans.property.*;
|
|
||||||
import javafx.scene.Group;
|
|
||||||
import javafx.scene.shape.Polygon;
|
|
||||||
import javafx.scene.shape.Rectangle;
|
|
||||||
import javafx.scene.text.Font;
|
|
||||||
import javafx.scene.text.FontWeight;
|
|
||||||
import javafx.scene.text.Text;
|
|
||||||
import javafx.scene.text.TextFlow;
|
|
||||||
import javafx.util.Duration;
|
|
||||||
import org.girod.javafx.svgimage.SVGImage;
|
|
||||||
|
|
||||||
import java.time.Instant;
|
|
||||||
import java.time.temporal.ChronoUnit;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
public class BlockCube extends Group {
|
|
||||||
public static final List<Integer> MEMPOOL_FEE_RATES_INTERVALS = List.of(1, 2, 3, 4, 5, 6, 8, 10, 12, 15, 20, 30, 40, 50, 60, 70, 80, 90, 100, 125, 150, 175, 200, 250, 300, 350, 400, 500, 600, 700, 800, 900, 1000, 1200, 1400, 1600, 1800, 2000);
|
|
||||||
|
|
||||||
public static final double CUBE_SIZE = 60;
|
|
||||||
|
|
||||||
private final IntegerProperty weightProperty = new SimpleIntegerProperty(0);
|
|
||||||
private final DoubleProperty medianFeeProperty = new SimpleDoubleProperty(-Double.MAX_VALUE);
|
|
||||||
private final IntegerProperty heightProperty = new SimpleIntegerProperty(0);
|
|
||||||
private final IntegerProperty txCountProperty = new SimpleIntegerProperty(0);
|
|
||||||
private final LongProperty timestampProperty = new SimpleLongProperty(System.currentTimeMillis());
|
|
||||||
private final StringProperty elapsedProperty = new SimpleStringProperty("");
|
|
||||||
private final BooleanProperty confirmedProperty = new SimpleBooleanProperty(false);
|
|
||||||
private final ObjectProperty<FeeRatesSource> feeRatesSource = new SimpleObjectProperty<>(null);
|
|
||||||
|
|
||||||
private Polygon front;
|
|
||||||
private Rectangle unusedArea;
|
|
||||||
private Rectangle usedArea;
|
|
||||||
|
|
||||||
private final Text heightText = new Text();
|
|
||||||
private final Text medianFeeText = new Text();
|
|
||||||
private final Text unitsText = new Text();
|
|
||||||
private final TextFlow medianFeeTextFlow = new TextFlow();
|
|
||||||
private final Text txCountText = new Text();
|
|
||||||
private final Text elapsedText = new Text();
|
|
||||||
private final Group feeRateIcon = new Group();
|
|
||||||
|
|
||||||
public BlockCube(Integer weight, Double medianFee, Integer height, Integer txCount, Long timestamp, boolean confirmed) {
|
|
||||||
getStyleClass().addAll("block-" + Network.getCanonical().getName(), "block-cube");
|
|
||||||
this.confirmedProperty.set(confirmed);
|
|
||||||
|
|
||||||
FeeRatesSource feeRatesSource = Config.get().getFeeRatesSource();
|
|
||||||
feeRatesSource = (feeRatesSource == null ? FeeRatesSource.MEMPOOL_SPACE : feeRatesSource);
|
|
||||||
this.feeRatesSource.set(feeRatesSource);
|
|
||||||
|
|
||||||
this.weightProperty.addListener((_, _, _) -> {
|
|
||||||
if(front != null) {
|
|
||||||
updateFill();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
this.medianFeeProperty.addListener((_, _, newValue) -> {
|
|
||||||
medianFeeText.setText(newValue.doubleValue() < 0.0d ? "" : "~" + Math.round(Math.max(newValue.doubleValue(), 1.0d)));
|
|
||||||
unitsText.setText(newValue.doubleValue() < 0.0d ? "" : " s/vb");
|
|
||||||
double medianFeeWidth = TextUtils.computeTextWidth(medianFeeText.getFont(), medianFeeText.getText(), 0.0d);
|
|
||||||
double unitsWidth = TextUtils.computeTextWidth(unitsText.getFont(), unitsText.getText(), 0.0d);
|
|
||||||
medianFeeTextFlow.setTranslateX((CUBE_SIZE - (medianFeeWidth + unitsWidth)) / 2);
|
|
||||||
});
|
|
||||||
this.txCountProperty.addListener((_, _, newValue) -> {
|
|
||||||
txCountText.setText(newValue.intValue() == 0 ? "" : newValue + " txes");
|
|
||||||
txCountText.setX((CUBE_SIZE - txCountText.getLayoutBounds().getWidth()) / 2);
|
|
||||||
});
|
|
||||||
this.timestampProperty.addListener((_, _, newValue) -> {
|
|
||||||
elapsedProperty.set(getElapsed(newValue.longValue()));
|
|
||||||
});
|
|
||||||
this.elapsedProperty.addListener((_, _, newValue) -> {
|
|
||||||
elapsedText.setText(isConfirmed() ? newValue : "In ~10m");
|
|
||||||
elapsedText.setX((CUBE_SIZE - elapsedText.getLayoutBounds().getWidth()) / 2);
|
|
||||||
});
|
|
||||||
this.heightProperty.addListener((_, _, newValue) -> {
|
|
||||||
heightText.setText(newValue.intValue() == 0 ? "" : String.valueOf(newValue));
|
|
||||||
heightText.setX(((CUBE_SIZE * 0.7) - heightText.getLayoutBounds().getWidth()) / 2);
|
|
||||||
});
|
|
||||||
this.confirmedProperty.addListener((_, _, _) -> {
|
|
||||||
if(front != null) {
|
|
||||||
updateFill();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
this.feeRatesSource.addListener((_, _, _) -> {
|
|
||||||
if(front != null) {
|
|
||||||
updateFill();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
this.medianFeeText.textProperty().addListener((_, _, _) -> {
|
|
||||||
pulse();
|
|
||||||
});
|
|
||||||
|
|
||||||
if(weight != null) {
|
|
||||||
this.weightProperty.set(weight);
|
|
||||||
}
|
|
||||||
if(medianFee != null) {
|
|
||||||
this.medianFeeProperty.set(medianFee);
|
|
||||||
}
|
|
||||||
if(height != null) {
|
|
||||||
this.heightProperty.set(height);
|
|
||||||
}
|
|
||||||
if(txCount != null) {
|
|
||||||
this.txCountProperty.set(txCount);
|
|
||||||
}
|
|
||||||
if(timestamp != null) {
|
|
||||||
this.timestampProperty.set(timestamp);
|
|
||||||
}
|
|
||||||
|
|
||||||
drawCube();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void drawCube() {
|
|
||||||
double depth = CUBE_SIZE * 0.2;
|
|
||||||
double perspective = CUBE_SIZE * 0.04;
|
|
||||||
|
|
||||||
front = new Polygon(0, 0, CUBE_SIZE, 0, CUBE_SIZE, CUBE_SIZE, 0, CUBE_SIZE);
|
|
||||||
front.getStyleClass().add("block-front");
|
|
||||||
front.setFill(null);
|
|
||||||
unusedArea = new Rectangle(0, 0, CUBE_SIZE, CUBE_SIZE);
|
|
||||||
unusedArea.getStyleClass().add("block-unused");
|
|
||||||
usedArea = new Rectangle(0, 0, CUBE_SIZE, CUBE_SIZE);
|
|
||||||
usedArea.getStyleClass().add("block-used");
|
|
||||||
|
|
||||||
Group frontFaceGroup = new Group(front, unusedArea, usedArea);
|
|
||||||
|
|
||||||
Polygon top = new Polygon(0, 0, CUBE_SIZE, 0, CUBE_SIZE - depth - perspective, -depth, -depth, -depth);
|
|
||||||
top.getStyleClass().add("block-top");
|
|
||||||
top.setStroke(null);
|
|
||||||
|
|
||||||
Polygon left = new Polygon(0, 0, -depth, -depth, -depth, CUBE_SIZE - depth - perspective, 0, CUBE_SIZE);
|
|
||||||
left.getStyleClass().add("block-left");
|
|
||||||
left.setStroke(null);
|
|
||||||
|
|
||||||
updateFill();
|
|
||||||
|
|
||||||
heightText.getStyleClass().add("block-height");
|
|
||||||
heightText.setFont(new Font(11));
|
|
||||||
heightText.setX(((CUBE_SIZE * 0.7) - heightText.getLayoutBounds().getWidth()) / 2);
|
|
||||||
heightText.setY(-24);
|
|
||||||
|
|
||||||
medianFeeText.getStyleClass().add("block-text");
|
|
||||||
medianFeeText.setFont(Font.font(null, FontWeight.BOLD, 11));
|
|
||||||
unitsText.getStyleClass().add("block-text");
|
|
||||||
unitsText.setFont(new Font(10));
|
|
||||||
medianFeeTextFlow.getChildren().addAll(medianFeeText, unitsText);
|
|
||||||
medianFeeTextFlow.setTranslateX((CUBE_SIZE - (medianFeeText.getLayoutBounds().getWidth() + unitsText.getLayoutBounds().getWidth())) / 2);
|
|
||||||
medianFeeTextFlow.setTranslateY(7);
|
|
||||||
|
|
||||||
txCountText.getStyleClass().add("block-text");
|
|
||||||
txCountText.setFont(new Font(10));
|
|
||||||
txCountText.setOpacity(0.7);
|
|
||||||
txCountText.setX((CUBE_SIZE - txCountText.getLayoutBounds().getWidth()) / 2);
|
|
||||||
txCountText.setY(34);
|
|
||||||
|
|
||||||
feeRateIcon.setTranslateX(((CUBE_SIZE * 0.7) - 14) / 2);
|
|
||||||
feeRateIcon.setTranslateY(-36);
|
|
||||||
|
|
||||||
elapsedText.getStyleClass().add("block-text");
|
|
||||||
elapsedText.setFont(new Font(10));
|
|
||||||
elapsedText.setX((CUBE_SIZE - elapsedText.getLayoutBounds().getWidth()) / 2);
|
|
||||||
elapsedText.setY(50);
|
|
||||||
|
|
||||||
getChildren().addAll(frontFaceGroup, top, left, heightText, medianFeeTextFlow, txCountText, feeRateIcon, elapsedText);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void updateFill() {
|
|
||||||
if(isConfirmed()) {
|
|
||||||
getStyleClass().removeAll("block-unconfirmed");
|
|
||||||
if(!getStyleClass().contains("block-confirmed")) {
|
|
||||||
getStyleClass().add("block-confirmed");
|
|
||||||
}
|
|
||||||
double startY = 1 - weightProperty.doubleValue() / (Transaction.MAX_BLOCK_SIZE_VBYTES * Transaction.WITNESS_SCALE_FACTOR);
|
|
||||||
double startYAbsolute = startY * BlockCube.CUBE_SIZE;
|
|
||||||
unusedArea.setHeight(startYAbsolute);
|
|
||||||
unusedArea.setStyle(null);
|
|
||||||
usedArea.setY(startYAbsolute);
|
|
||||||
usedArea.setHeight(CUBE_SIZE - startYAbsolute);
|
|
||||||
usedArea.setVisible(true);
|
|
||||||
heightText.setVisible(true);
|
|
||||||
feeRateIcon.getChildren().clear();
|
|
||||||
} else {
|
|
||||||
getStyleClass().removeAll("block-confirmed");
|
|
||||||
if(!getStyleClass().contains("block-unconfirmed")) {
|
|
||||||
getStyleClass().add("block-unconfirmed");
|
|
||||||
}
|
|
||||||
usedArea.setVisible(false);
|
|
||||||
unusedArea.setStyle("-fx-fill: " + getFeeRateStyleName() + ";");
|
|
||||||
heightText.setVisible(false);
|
|
||||||
if(feeRatesSource.get() != null) {
|
|
||||||
SVGImage svgImage = feeRatesSource.get().getSVGImage();
|
|
||||||
if(svgImage != null) {
|
|
||||||
feeRateIcon.getChildren().setAll(feeRatesSource.get().getSVGImage());
|
|
||||||
} else {
|
|
||||||
feeRateIcon.getChildren().clear();
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
feeRateIcon.getChildren().clear();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void pulse() {
|
|
||||||
if(isConfirmed()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if(unusedArea != null) {
|
|
||||||
unusedArea.setStyle("-fx-fill: " + getFeeRateStyleName() + ";");
|
|
||||||
}
|
|
||||||
|
|
||||||
Timeline timeline = new Timeline(
|
|
||||||
new KeyFrame(Duration.ZERO, new KeyValue(opacityProperty(), 1.0)),
|
|
||||||
new KeyFrame(Duration.millis(500), new KeyValue(opacityProperty(), 0.7)),
|
|
||||||
new KeyFrame(Duration.millis(1000), new KeyValue(opacityProperty(), 1.0))
|
|
||||||
);
|
|
||||||
|
|
||||||
timeline.setCycleCount(1);
|
|
||||||
timeline.play();
|
|
||||||
}
|
|
||||||
|
|
||||||
private static long calculateElapsedSeconds(long timestampUtc) {
|
|
||||||
Instant timestampInstant = Instant.ofEpochMilli(timestampUtc);
|
|
||||||
Instant nowInstant = Instant.now();
|
|
||||||
return ChronoUnit.SECONDS.between(timestampInstant, nowInstant);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static String getElapsed(long timestampUtc) {
|
|
||||||
long elapsed = calculateElapsedSeconds(timestampUtc);
|
|
||||||
if(elapsed < 60) {
|
|
||||||
return "Just now";
|
|
||||||
} else if(elapsed < 3600) {
|
|
||||||
return Math.round(elapsed / 60f) + "m ago";
|
|
||||||
} else if(elapsed < 86400) {
|
|
||||||
return Math.round(elapsed / 3600f) + "h ago";
|
|
||||||
} else {
|
|
||||||
return Math.round(elapsed / 86400d) + "d ago";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private String getFeeRateStyleName() {
|
|
||||||
double rate = getMedianFee();
|
|
||||||
int[] feeRateInterval = getFeeRateInterval(rate);
|
|
||||||
if(feeRateInterval[1] == Integer.MAX_VALUE) {
|
|
||||||
return "VSIZE2000-2200_COLOR";
|
|
||||||
}
|
|
||||||
int[] nextRateInterval = getFeeRateInterval(rate * 2);
|
|
||||||
String from = "VSIZE" + feeRateInterval[0] + "-" + feeRateInterval[1] + "_COLOR";
|
|
||||||
String to = "VSIZE" + nextRateInterval[0] + "-" + (nextRateInterval[1] == Integer.MAX_VALUE ? "2200" : nextRateInterval[1]) + "_COLOR";
|
|
||||||
return "linear-gradient(from 75% 0% to 100% 0%, " + from + " 0%, " + to + " 100%, " + from +")";
|
|
||||||
}
|
|
||||||
|
|
||||||
private int[] getFeeRateInterval(double medianFee) {
|
|
||||||
for(int i = 0; i < MEMPOOL_FEE_RATES_INTERVALS.size(); i++) {
|
|
||||||
int feeRate = MEMPOOL_FEE_RATES_INTERVALS.get(i);
|
|
||||||
int nextFeeRate = (i == MEMPOOL_FEE_RATES_INTERVALS.size() - 1 ? Integer.MAX_VALUE : MEMPOOL_FEE_RATES_INTERVALS.get(i + 1));
|
|
||||||
if(feeRate <= medianFee && nextFeeRate > medianFee) {
|
|
||||||
return new int[] { feeRate, nextFeeRate };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return new int[] { 1, 2 };
|
|
||||||
}
|
|
||||||
|
|
||||||
public int getWeight() {
|
|
||||||
return weightProperty.get();
|
|
||||||
}
|
|
||||||
|
|
||||||
public IntegerProperty weightProperty() {
|
|
||||||
return weightProperty;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setWeight(int weight) {
|
|
||||||
weightProperty.set(weight);
|
|
||||||
}
|
|
||||||
|
|
||||||
public double getMedianFee() {
|
|
||||||
return medianFeeProperty.get();
|
|
||||||
}
|
|
||||||
|
|
||||||
public DoubleProperty medianFee() {
|
|
||||||
return medianFeeProperty;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setMedianFee(double medianFee) {
|
|
||||||
medianFeeProperty.set(medianFee);
|
|
||||||
}
|
|
||||||
|
|
||||||
public int getHeight() {
|
|
||||||
return heightProperty.get();
|
|
||||||
}
|
|
||||||
|
|
||||||
public IntegerProperty heightProperty() {
|
|
||||||
return heightProperty;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setHeight(int height) {
|
|
||||||
heightProperty.set(height);
|
|
||||||
}
|
|
||||||
|
|
||||||
public int getTxCount() {
|
|
||||||
return txCountProperty.get();
|
|
||||||
}
|
|
||||||
|
|
||||||
public IntegerProperty txCountProperty() {
|
|
||||||
return txCountProperty;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setTxCount(int txCount) {
|
|
||||||
txCountProperty.set(txCount);
|
|
||||||
}
|
|
||||||
|
|
||||||
public long getTimestamp() {
|
|
||||||
return timestampProperty.get();
|
|
||||||
}
|
|
||||||
|
|
||||||
public LongProperty timestampProperty() {
|
|
||||||
return timestampProperty;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setTimestamp(long timestamp) {
|
|
||||||
timestampProperty.set(timestamp);
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getElapsed() {
|
|
||||||
return elapsedProperty.get();
|
|
||||||
}
|
|
||||||
|
|
||||||
public StringProperty elapsedProperty() {
|
|
||||||
return elapsedProperty;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setElapsed(String elapsed) {
|
|
||||||
elapsedProperty.set(elapsed);
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean isConfirmed() {
|
|
||||||
return confirmedProperty.get();
|
|
||||||
}
|
|
||||||
|
|
||||||
public BooleanProperty confirmedProperty() {
|
|
||||||
return confirmedProperty;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setConfirmed(boolean confirmed) {
|
|
||||||
confirmedProperty.set(confirmed);
|
|
||||||
}
|
|
||||||
|
|
||||||
public FeeRatesSource getFeeRatesSource() {
|
|
||||||
return feeRatesSource.get();
|
|
||||||
}
|
|
||||||
|
|
||||||
public ObjectProperty<FeeRatesSource> feeRatesSourceProperty() {
|
|
||||||
return feeRatesSource;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setFeeRatesSource(FeeRatesSource feeRatesSource) {
|
|
||||||
this.feeRatesSource.set(feeRatesSource);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static BlockCube fromBlockSummary(BlockSummary blockSummary) {
|
|
||||||
return new BlockCube(blockSummary.getWeight().orElse(0), blockSummary.getMedianFee().orElse(-1.0d), blockSummary.getHeight(),
|
|
||||||
blockSummary.getTransactionCount().orElse(0), blockSummary.getTimestamp().getTime(), true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -48,7 +48,7 @@ public class CardImportPane extends TitledDescriptionPane {
|
||||||
private final SimpleStringProperty pin = new SimpleStringProperty("");
|
private final SimpleStringProperty pin = new SimpleStringProperty("");
|
||||||
|
|
||||||
public CardImportPane(Wallet wallet, KeystoreCardImport importer, KeyDerivation requiredDerivation) {
|
public CardImportPane(Wallet wallet, KeystoreCardImport importer, KeyDerivation requiredDerivation) {
|
||||||
super(importer.getName(), "Place card on reader", importer.getKeystoreImportDescription(getAccount(wallet, requiredDerivation)), importer.getWalletModel());
|
super(importer.getName(), "Place card on reader", importer.getKeystoreImportDescription(getAccount(wallet, requiredDerivation)), "image/" + importer.getWalletModel().getType() + ".png");
|
||||||
this.importer = importer;
|
this.importer = importer;
|
||||||
this.derivation = requiredDerivation == null ? wallet.getScriptType().getDefaultDerivation() : requiredDerivation.getDerivation();
|
this.derivation = requiredDerivation == null ? wallet.getScriptType().getDefaultDerivation() : requiredDerivation.getDerivation();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
package com.sparrowwallet.sparrow.control;
|
package com.sparrowwallet.sparrow.control;
|
||||||
|
|
||||||
import com.sparrowwallet.drongo.BitcoinUnit;
|
import com.sparrowwallet.drongo.BitcoinUnit;
|
||||||
import com.sparrowwallet.drongo.OsType;
|
|
||||||
import com.sparrowwallet.drongo.protocol.Transaction;
|
import com.sparrowwallet.drongo.protocol.Transaction;
|
||||||
import com.sparrowwallet.drongo.wallet.BlockTransactionHash;
|
import com.sparrowwallet.drongo.wallet.BlockTransactionHash;
|
||||||
import com.sparrowwallet.sparrow.UnitFormat;
|
import com.sparrowwallet.sparrow.UnitFormat;
|
||||||
|
|
@ -17,6 +16,7 @@ import javafx.scene.input.Clipboard;
|
||||||
import javafx.scene.input.ClipboardContent;
|
import javafx.scene.input.ClipboardContent;
|
||||||
import javafx.scene.layout.Region;
|
import javafx.scene.layout.Region;
|
||||||
import javafx.util.Duration;
|
import javafx.util.Duration;
|
||||||
|
import org.controlsfx.tools.Platform;
|
||||||
|
|
||||||
import java.text.DecimalFormat;
|
import java.text.DecimalFormat;
|
||||||
|
|
||||||
|
|
@ -32,7 +32,7 @@ class CoinCell extends TreeTableCell<Entry, Number> implements ConfirmationsList
|
||||||
tooltip.setShowDelay(Duration.millis(500));
|
tooltip.setShowDelay(Duration.millis(500));
|
||||||
contextMenu = new CoinContextMenu();
|
contextMenu = new CoinContextMenu();
|
||||||
getStyleClass().add("coin-cell");
|
getStyleClass().add("coin-cell");
|
||||||
if(OsType.getCurrent() == OsType.MACOS) {
|
if(Platform.getCurrent() == Platform.OSX) {
|
||||||
getStyleClass().add("number-field");
|
getStyleClass().add("number-field");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -87,8 +87,6 @@ class CoinCell extends TreeTableCell<Entry, Number> implements ConfirmationsList
|
||||||
} else if(entry instanceof UtxoEntry) {
|
} else if(entry instanceof UtxoEntry) {
|
||||||
setGraphic(null);
|
setGraphic(null);
|
||||||
} else if(entry instanceof HashIndexEntry) {
|
} else if(entry instanceof HashIndexEntry) {
|
||||||
tooltip.hideConfirmations();
|
|
||||||
|
|
||||||
Region node = new Region();
|
Region node = new Region();
|
||||||
node.setPrefWidth(10);
|
node.setPrefWidth(10);
|
||||||
setGraphic(node);
|
setGraphic(node);
|
||||||
|
|
@ -150,14 +148,6 @@ class CoinCell extends TreeTableCell<Entry, Number> implements ConfirmationsList
|
||||||
setTooltipText();
|
setTooltipText();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void hideConfirmations() {
|
|
||||||
showConfirmations = false;
|
|
||||||
isCoinbase = false;
|
|
||||||
confirmationsProperty.unbind();
|
|
||||||
|
|
||||||
setTooltipText();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void setTooltipText() {
|
private void setTooltipText() {
|
||||||
setText(value + (showConfirmations ? " (" + getConfirmationsDescription() + ")" : ""));
|
setText(value + (showConfirmations ? " (" + getConfirmationsDescription() + ")" : ""));
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,9 +5,9 @@ import javafx.scene.control.TextFormatter;
|
||||||
import javafx.scene.control.TextInputControl;
|
import javafx.scene.control.TextInputControl;
|
||||||
|
|
||||||
import java.text.DecimalFormat;
|
import java.text.DecimalFormat;
|
||||||
|
import java.text.DecimalFormatSymbols;
|
||||||
import java.text.ParseException;
|
import java.text.ParseException;
|
||||||
import java.util.function.UnaryOperator;
|
import java.util.function.UnaryOperator;
|
||||||
import java.util.regex.Matcher;
|
|
||||||
import java.util.regex.Pattern;
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
public class CoinTextFormatter extends TextFormatter<String> {
|
public class CoinTextFormatter extends TextFormatter<String> {
|
||||||
|
|
@ -51,14 +51,8 @@ public class CoinTextFormatter extends TextFormatter<String> {
|
||||||
commasRemoved = newText.length() - noFractionCommaText.length();
|
commasRemoved = newText.length() - noFractionCommaText.length();
|
||||||
}
|
}
|
||||||
|
|
||||||
Matcher matcher = coinValidation.matcher(noFractionCommaText);
|
if(!coinValidation.matcher(noFractionCommaText).matches()) {
|
||||||
if(!matcher.matches()) {
|
return null;
|
||||||
matcher.reset();
|
|
||||||
if(matcher.find()) {
|
|
||||||
noFractionCommaText = matcher.group();
|
|
||||||
} else {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if(unitFormat.getGroupingSeparator().equals(change.getText())) {
|
if(unitFormat.getGroupingSeparator().equals(change.getText())) {
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,11 @@
|
||||||
package com.sparrowwallet.sparrow.control;
|
package com.sparrowwallet.sparrow.control;
|
||||||
|
|
||||||
import com.sparrowwallet.drongo.BitcoinUnit;
|
import com.sparrowwallet.drongo.BitcoinUnit;
|
||||||
import com.sparrowwallet.drongo.wallet.SortDirection;
|
|
||||||
import com.sparrowwallet.drongo.wallet.TableType;
|
|
||||||
import com.sparrowwallet.drongo.wallet.Wallet;
|
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||||
import com.sparrowwallet.drongo.wallet.WalletTable;
|
|
||||||
import com.sparrowwallet.sparrow.CurrencyRate;
|
import com.sparrowwallet.sparrow.CurrencyRate;
|
||||||
import com.sparrowwallet.sparrow.UnitFormat;
|
import com.sparrowwallet.sparrow.UnitFormat;
|
||||||
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.WalletTableChangedEvent;
|
|
||||||
import com.sparrowwallet.sparrow.event.WalletAddressesChangedEvent;
|
import com.sparrowwallet.sparrow.event.WalletAddressesChangedEvent;
|
||||||
import com.sparrowwallet.sparrow.event.WalletDataChangedEvent;
|
import com.sparrowwallet.sparrow.event.WalletDataChangedEvent;
|
||||||
import com.sparrowwallet.sparrow.event.WalletHistoryStatusEvent;
|
import com.sparrowwallet.sparrow.event.WalletHistoryStatusEvent;
|
||||||
|
|
@ -17,38 +13,24 @@ import com.sparrowwallet.sparrow.io.Config;
|
||||||
import com.sparrowwallet.sparrow.io.Storage;
|
import com.sparrowwallet.sparrow.io.Storage;
|
||||||
import com.sparrowwallet.sparrow.net.ServerType;
|
import com.sparrowwallet.sparrow.net.ServerType;
|
||||||
import com.sparrowwallet.sparrow.wallet.Entry;
|
import com.sparrowwallet.sparrow.wallet.Entry;
|
||||||
import io.reactivex.Observable;
|
|
||||||
import io.reactivex.subjects.PublishSubject;
|
|
||||||
import javafx.application.Platform;
|
import javafx.application.Platform;
|
||||||
import javafx.collections.ListChangeListener;
|
|
||||||
import javafx.geometry.Pos;
|
import javafx.geometry.Pos;
|
||||||
import javafx.scene.Node;
|
import javafx.scene.Node;
|
||||||
import javafx.scene.control.*;
|
import javafx.scene.control.Hyperlink;
|
||||||
|
import javafx.scene.control.Label;
|
||||||
|
import javafx.scene.control.TreeTableColumn;
|
||||||
|
import javafx.scene.control.TreeTableView;
|
||||||
import javafx.scene.layout.StackPane;
|
import javafx.scene.layout.StackPane;
|
||||||
|
|
||||||
import java.text.DateFormat;
|
import java.text.DateFormat;
|
||||||
import java.text.SimpleDateFormat;
|
import java.text.SimpleDateFormat;
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import java.util.concurrent.TimeUnit;
|
|
||||||
|
|
||||||
public class CoinTreeTable extends TreeTableView<Entry> {
|
public class CoinTreeTable extends TreeTableView<Entry> {
|
||||||
private TableType tableType;
|
|
||||||
private BitcoinUnit bitcoinUnit;
|
private BitcoinUnit bitcoinUnit;
|
||||||
private UnitFormat unitFormat;
|
private UnitFormat unitFormat;
|
||||||
private CurrencyRate currencyRate;
|
private CurrencyRate currencyRate;
|
||||||
protected static final double STANDARD_WIDTH = 100.0;
|
|
||||||
|
|
||||||
private final PublishSubject<WalletTableChangedEvent> walletTableSubject = PublishSubject.create();
|
|
||||||
private final Observable<WalletTableChangedEvent> walletTableEvents = walletTableSubject.debounce(1, TimeUnit.SECONDS);
|
|
||||||
|
|
||||||
public TableType getTableType() {
|
|
||||||
return tableType;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setTableType(TableType tableType) {
|
|
||||||
this.tableType = tableType;
|
|
||||||
}
|
|
||||||
|
|
||||||
public BitcoinUnit getBitcoinUnit() {
|
public BitcoinUnit getBitcoinUnit() {
|
||||||
return bitcoinUnit;
|
return bitcoinUnit;
|
||||||
|
|
@ -154,107 +136,11 @@ public class CoinTreeTable extends TreeTableView<Entry> {
|
||||||
return stackPane;
|
return stackPane;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected void setupColumnSort(int defaultColumnIndex, TreeTableColumn.SortType defaultSortType) {
|
public void setSortColumn(int columnIndex, TreeTableColumn.SortType sortType) {
|
||||||
WalletTable.Sort columnSort = getSavedColumnSort();
|
if(columnIndex >= 0 && columnIndex < getColumns().size() && getSortOrder().isEmpty() && !getRoot().getChildren().isEmpty()) {
|
||||||
if(columnSort == null) {
|
TreeTableColumn<Entry, ?> column = getColumns().get(columnIndex);
|
||||||
columnSort = new WalletTable.Sort(defaultColumnIndex, getSortDirection(defaultSortType));
|
column.setSortType(sortType == null ? TreeTableColumn.SortType.DESCENDING : sortType);
|
||||||
}
|
|
||||||
|
|
||||||
setSortColumn(columnSort);
|
|
||||||
|
|
||||||
getSortOrder().addListener((ListChangeListener<? super TreeTableColumn<Entry, ?>>) c -> {
|
|
||||||
if(c.next()) {
|
|
||||||
walletTableChanged();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
for(TreeTableColumn<Entry, ?> column : getColumns()) {
|
|
||||||
column.sortTypeProperty().addListener((_, _, _) -> walletTableChanged());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
protected void resetSortColumn() {
|
|
||||||
setSortColumn(getColumnSort());
|
|
||||||
}
|
|
||||||
|
|
||||||
protected void setSortColumn(WalletTable.Sort sort) {
|
|
||||||
if(sort.sortColumn() >= 0 && sort.sortColumn() < getColumns().size() && getSortOrder().isEmpty() && !getRoot().getChildren().isEmpty()) {
|
|
||||||
TreeTableColumn<Entry, ?> column = getColumns().get(sort.sortColumn());
|
|
||||||
column.setSortType(sort.sortDirection() == SortDirection.DESCENDING ? TreeTableColumn.SortType.DESCENDING : TreeTableColumn.SortType.ASCENDING);
|
|
||||||
getSortOrder().add(column);
|
getSortOrder().add(column);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private WalletTable.Sort getColumnSort() {
|
|
||||||
if(getSortOrder().isEmpty() || !getColumns().contains(getSortOrder().getFirst())) {
|
|
||||||
return new WalletTable.Sort(tableType == TableType.UTXOS ? getColumns().size() - 1 : 0, SortDirection.DESCENDING);
|
|
||||||
}
|
|
||||||
|
|
||||||
return new WalletTable.Sort(getColumns().indexOf(getSortOrder().getFirst()), getSortDirection(getSortOrder().getFirst().getSortType()));
|
|
||||||
}
|
|
||||||
|
|
||||||
private SortDirection getSortDirection(TreeTableColumn.SortType sortType) {
|
|
||||||
return sortType == TreeTableColumn.SortType.ASCENDING ? SortDirection.ASCENDING : SortDirection.DESCENDING;
|
|
||||||
}
|
|
||||||
|
|
||||||
private WalletTable.Sort getSavedColumnSort() {
|
|
||||||
if(getRoot() != null && getRoot().getValue() != null && getRoot().getValue().getWallet() != null) {
|
|
||||||
Wallet wallet = getRoot().getValue().getWallet();
|
|
||||||
WalletTable walletTable = wallet.getWalletTable(tableType);
|
|
||||||
if(walletTable != null) {
|
|
||||||
return walletTable.getSort();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
@SuppressWarnings("deprecation")
|
|
||||||
protected void setupColumnWidths() {
|
|
||||||
Double[] savedWidths = getSavedColumnWidths();
|
|
||||||
for(int i = 0; i < getColumns().size(); i++) {
|
|
||||||
TreeTableColumn<Entry, ?> column = getColumns().get(i);
|
|
||||||
column.setPrefWidth(savedWidths != null && getColumns().size() == savedWidths.length ? savedWidths[i] : STANDARD_WIDTH);
|
|
||||||
}
|
|
||||||
|
|
||||||
//TODO: Replace with TreeTableView.CONSTRAINED_RESIZE_POLICY_FLEX_LAST_COLUMN when JavaFX 20+ has headless support
|
|
||||||
setColumnResizePolicy(TreeTableView.CONSTRAINED_RESIZE_POLICY);
|
|
||||||
|
|
||||||
getColumns().getLast().widthProperty().addListener((_, _, _) -> walletTableChanged());
|
|
||||||
|
|
||||||
//Ignore initial resizes during layout
|
|
||||||
walletTableEvents.skip(3, TimeUnit.SECONDS).subscribe(event -> {
|
|
||||||
event.getWallet().getWalletTables().put(event.getTableType(), event.getWalletTable());
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private void walletTableChanged() {
|
|
||||||
if(getRoot() != null && getRoot().getValue() != null && getRoot().getValue().getWallet() != null) {
|
|
||||||
WalletTable walletTable = new WalletTable(tableType, getColumnWidths(), getColumnSort());
|
|
||||||
walletTableSubject.onNext(new WalletTableChangedEvent(getRoot().getValue().getWallet(), walletTable));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private Double[] getColumnWidths() {
|
|
||||||
return getColumns().stream().map(TableColumnBase::getWidth).toArray(Double[]::new);
|
|
||||||
}
|
|
||||||
|
|
||||||
private Double[] getSavedColumnWidths() {
|
|
||||||
if(getRoot() != null && getRoot().getValue() != null && getRoot().getValue().getWallet() != null) {
|
|
||||||
Wallet wallet = getRoot().getValue().getWallet();
|
|
||||||
WalletTable walletTable = wallet.getWalletTable(tableType);
|
|
||||||
if(walletTable != null) {
|
|
||||||
return walletTable.getWidths();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,16 +6,10 @@ import javafx.beans.property.SimpleObjectProperty;
|
||||||
import javafx.scene.Cursor;
|
import javafx.scene.Cursor;
|
||||||
import javafx.scene.Node;
|
import javafx.scene.Node;
|
||||||
import javafx.scene.control.ComboBox;
|
import javafx.scene.control.ComboBox;
|
||||||
import javafx.scene.control.ContextMenu;
|
|
||||||
import javafx.scene.control.MenuItem;
|
|
||||||
import javafx.scene.control.SeparatorMenuItem;
|
|
||||||
import javafx.scene.input.Clipboard;
|
|
||||||
import javafx.scene.layout.Region;
|
import javafx.scene.layout.Region;
|
||||||
import javafx.scene.layout.StackPane;
|
import javafx.scene.layout.StackPane;
|
||||||
import org.controlsfx.control.textfield.CustomTextField;
|
import org.controlsfx.control.textfield.CustomTextField;
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
public class ComboBoxTextField extends CustomTextField {
|
public class ComboBoxTextField extends CustomTextField {
|
||||||
private final ObjectProperty<ComboBox<?>> comboProperty = new SimpleObjectProperty<>();
|
private final ObjectProperty<ComboBox<?>> comboProperty = new SimpleObjectProperty<>();
|
||||||
|
|
||||||
|
|
@ -74,53 +68,4 @@ public class ComboBoxTextField extends CustomTextField {
|
||||||
public void setComboProperty(ComboBox<?> comboProperty) {
|
public void setComboProperty(ComboBox<?> comboProperty) {
|
||||||
this.comboProperty.set(comboProperty);
|
this.comboProperty.set(comboProperty);
|
||||||
}
|
}
|
||||||
|
|
||||||
public ContextMenu getCustomContextMenu(List<MenuItem> customItems) {
|
|
||||||
return new CustomContextMenu(customItems);
|
|
||||||
}
|
|
||||||
|
|
||||||
public class CustomContextMenu extends ContextMenu {
|
|
||||||
public CustomContextMenu(List<MenuItem> customItems) {
|
|
||||||
super();
|
|
||||||
setFont(null);
|
|
||||||
|
|
||||||
MenuItem undo = new MenuItem("Undo");
|
|
||||||
undo.setOnAction(_ -> undo());
|
|
||||||
|
|
||||||
MenuItem redo = new MenuItem("Redo");
|
|
||||||
redo.setOnAction(_ -> redo());
|
|
||||||
|
|
||||||
MenuItem cut = new MenuItem("Cut");
|
|
||||||
cut.setOnAction(_ -> cut());
|
|
||||||
|
|
||||||
MenuItem copy = new MenuItem("Copy");
|
|
||||||
copy.setOnAction(_ -> copy());
|
|
||||||
|
|
||||||
MenuItem paste = new MenuItem("Paste");
|
|
||||||
paste.setOnAction(_ -> paste());
|
|
||||||
|
|
||||||
MenuItem delete = new MenuItem("Delete");
|
|
||||||
delete.setOnAction(_ -> deleteText(getSelection()));
|
|
||||||
|
|
||||||
MenuItem selectAll = new MenuItem("Select All");
|
|
||||||
selectAll.setOnAction(_ -> selectAll());
|
|
||||||
|
|
||||||
getItems().addAll(undo, redo, new SeparatorMenuItem(), cut, copy, paste, delete, new SeparatorMenuItem(), selectAll);
|
|
||||||
getItems().addAll(customItems);
|
|
||||||
|
|
||||||
setOnShowing(_ -> {
|
|
||||||
boolean hasSelection = getSelection().getLength() > 0;
|
|
||||||
boolean hasText = getText() != null && !getText().isEmpty();
|
|
||||||
boolean clipboardHasContent = Clipboard.getSystemClipboard().hasString();
|
|
||||||
|
|
||||||
undo.setDisable(!isUndoable());
|
|
||||||
redo.setDisable(!isRedoable());
|
|
||||||
cut.setDisable(!isEditable() || !hasSelection);
|
|
||||||
copy.setDisable(!hasSelection);
|
|
||||||
paste.setDisable(!isEditable() || !clipboardHasContent);
|
|
||||||
delete.setDisable(!hasSelection);
|
|
||||||
selectAll.setDisable(!hasText);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,39 +0,0 @@
|
||||||
package com.sparrowwallet.sparrow.control;
|
|
||||||
|
|
||||||
import com.sparrowwallet.sparrow.AppServices;
|
|
||||||
import javafx.geometry.Insets;
|
|
||||||
import javafx.scene.control.Alert;
|
|
||||||
import javafx.scene.control.ButtonType;
|
|
||||||
import javafx.scene.control.CheckBox;
|
|
||||||
import javafx.scene.control.Label;
|
|
||||||
import javafx.scene.layout.VBox;
|
|
||||||
|
|
||||||
import static com.sparrowwallet.sparrow.AppServices.getActiveWindow;
|
|
||||||
import static com.sparrowwallet.sparrow.AppServices.setStageIcon;
|
|
||||||
|
|
||||||
public class ConfirmationAlert extends Alert {
|
|
||||||
private final CheckBox dontAskAgain;
|
|
||||||
|
|
||||||
public ConfirmationAlert(String title, String contentText, ButtonType... buttons) {
|
|
||||||
super(AlertType.CONFIRMATION, contentText, buttons);
|
|
||||||
|
|
||||||
initOwner(getActiveWindow());
|
|
||||||
setStageIcon(getDialogPane().getScene().getWindow());
|
|
||||||
getDialogPane().getScene().getStylesheets().add(AppServices.class.getResource("general.css").toExternalForm());
|
|
||||||
setTitle(title);
|
|
||||||
setHeaderText(title);
|
|
||||||
|
|
||||||
VBox contentBox = new VBox(20);
|
|
||||||
contentBox.setPadding(new Insets(10, 20, 10, 20));
|
|
||||||
Label contentLabel = new Label(contentText);
|
|
||||||
contentLabel.setWrapText(true);
|
|
||||||
dontAskAgain = new CheckBox("Don't ask again");
|
|
||||||
contentBox.getChildren().addAll(contentLabel, dontAskAgain);
|
|
||||||
|
|
||||||
getDialogPane().setContent(contentBox);
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean isDontAskAgain() {
|
|
||||||
return dontAskAgain.isSelected();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -11,7 +11,6 @@ import javafx.scene.control.Tooltip;
|
||||||
import javafx.scene.input.Clipboard;
|
import javafx.scene.input.Clipboard;
|
||||||
import javafx.scene.input.ClipboardContent;
|
import javafx.scene.input.ClipboardContent;
|
||||||
import javafx.event.EventHandler;
|
import javafx.event.EventHandler;
|
||||||
import javafx.scene.input.MouseButton;
|
|
||||||
import javafx.scene.input.MouseEvent;
|
import javafx.scene.input.MouseEvent;
|
||||||
|
|
||||||
public class CopyableCoinLabel extends CopyableLabel {
|
public class CopyableCoinLabel extends CopyableLabel {
|
||||||
|
|
@ -30,10 +29,6 @@ public class CopyableCoinLabel extends CopyableLabel {
|
||||||
valueProperty().addListener((observable, oldValue, newValue) -> setValueAsText((Long)newValue, Config.get().getUnitFormat(), Config.get().getBitcoinUnit()));
|
valueProperty().addListener((observable, oldValue, newValue) -> setValueAsText((Long)newValue, Config.get().getUnitFormat(), Config.get().getBitcoinUnit()));
|
||||||
|
|
||||||
setOnMouseClicked(event -> {
|
setOnMouseClicked(event -> {
|
||||||
if(!event.getButton().equals(MouseButton.PRIMARY)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if(bitcoinUnit == null) {
|
if(bitcoinUnit == null) {
|
||||||
bitcoinUnit = Config.get().getBitcoinUnit();
|
bitcoinUnit = Config.get().getBitcoinUnit();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,6 @@ import javafx.beans.value.ChangeListener;
|
||||||
import javafx.event.EventHandler;
|
import javafx.event.EventHandler;
|
||||||
import javafx.scene.Cursor;
|
import javafx.scene.Cursor;
|
||||||
import javafx.scene.Node;
|
import javafx.scene.Node;
|
||||||
import javafx.scene.control.ContextMenu;
|
|
||||||
import javafx.scene.control.Tooltip;
|
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;
|
||||||
|
|
@ -53,7 +52,6 @@ public class CopyableTextField extends CustomTextField {
|
||||||
selectedTextProperty().removeListener(selectionListener);
|
selectedTextProperty().removeListener(selectionListener);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
setContextMenu(new ContextMenu());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void setupCopyButtonField(ObjectProperty<Node> rightProperty) {
|
private void setupCopyButtonField(ObjectProperty<Node> rightProperty) {
|
||||||
|
|
|
||||||
|
|
@ -3,14 +3,13 @@ package com.sparrowwallet.sparrow.control;
|
||||||
import com.sparrowwallet.hummingbird.UR;
|
import com.sparrowwallet.hummingbird.UR;
|
||||||
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
|
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
|
||||||
import com.sparrowwallet.sparrow.io.PdfUtils;
|
import com.sparrowwallet.sparrow.io.PdfUtils;
|
||||||
import com.sparrowwallet.sparrow.io.bbqr.BBQR;
|
|
||||||
import javafx.event.ActionEvent;
|
import javafx.event.ActionEvent;
|
||||||
import javafx.scene.control.*;
|
import javafx.scene.control.*;
|
||||||
import javafx.scene.control.Button;
|
import javafx.scene.control.Button;
|
||||||
|
|
||||||
public class DescriptorQRDisplayDialog extends QRDisplayDialog {
|
public class DescriptorQRDisplayDialog extends QRDisplayDialog {
|
||||||
public DescriptorQRDisplayDialog(String walletName, String outputDescriptor, UR ur, BBQR bbqr, boolean selectBbqrButton) {
|
public DescriptorQRDisplayDialog(String walletName, String outputDescriptor, UR ur) {
|
||||||
super(ur, bbqr, false, false, selectBbqrButton);
|
super(ur);
|
||||||
|
|
||||||
DialogPane dialogPane = getDialogPane();
|
DialogPane dialogPane = getDialogPane();
|
||||||
final ButtonType pdfButtonType = new javafx.scene.control.ButtonType("Save PDF...", ButtonBar.ButtonData.HELP_2);
|
final ButtonType pdfButtonType = new javafx.scene.control.ButtonType("Save PDF...", ButtonBar.ButtonData.HELP_2);
|
||||||
|
|
@ -20,7 +19,7 @@ public class DescriptorQRDisplayDialog extends QRDisplayDialog {
|
||||||
pdfButton.setGraphicTextGap(5);
|
pdfButton.setGraphicTextGap(5);
|
||||||
pdfButton.setGraphic(getGlyph(FontAwesome5.Glyph.FILE_PDF));
|
pdfButton.setGraphic(getGlyph(FontAwesome5.Glyph.FILE_PDF));
|
||||||
pdfButton.addEventFilter(ActionEvent.ACTION, event -> {
|
pdfButton.addEventFilter(ActionEvent.ACTION, event -> {
|
||||||
PdfUtils.saveOutputDescriptor(walletName, outputDescriptor, ur, isUseBbqrEncoding() ? bbqr : null);
|
PdfUtils.saveOutputDescriptor(walletName, outputDescriptor, ur);
|
||||||
event.consume();
|
event.consume();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -16,8 +16,11 @@ 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.*;
|
import com.sparrowwallet.sparrow.event.*;
|
||||||
import com.sparrowwallet.sparrow.io.*;
|
import com.sparrowwallet.sparrow.io.CardApi;
|
||||||
|
import com.sparrowwallet.sparrow.io.Device;
|
||||||
|
import com.sparrowwallet.sparrow.io.Hwi;
|
||||||
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
|
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
|
||||||
|
import com.sparrowwallet.sparrow.io.CardAuthorizationException;
|
||||||
import com.sparrowwallet.sparrow.net.ElectrumServer;
|
import com.sparrowwallet.sparrow.net.ElectrumServer;
|
||||||
import javafx.application.Platform;
|
import javafx.application.Platform;
|
||||||
import javafx.beans.property.SimpleStringProperty;
|
import javafx.beans.property.SimpleStringProperty;
|
||||||
|
|
@ -75,7 +78,7 @@ public class DevicePane extends TitledDescriptionPane {
|
||||||
private boolean defaultDevice;
|
private boolean defaultDevice;
|
||||||
|
|
||||||
public DevicePane(Wallet wallet, Device device, boolean defaultDevice, KeyDerivation requiredDerivation) {
|
public DevicePane(Wallet wallet, Device device, boolean defaultDevice, KeyDerivation requiredDerivation) {
|
||||||
super(device.getModel().toDisplayString(), "", "", device.getModel());
|
super(device.getModel().toDisplayString(), "", "", "image/" + device.getType() + ".png");
|
||||||
this.deviceOperation = DeviceOperation.IMPORT;
|
this.deviceOperation = DeviceOperation.IMPORT;
|
||||||
this.wallet = wallet;
|
this.wallet = wallet;
|
||||||
this.psbt = null;
|
this.psbt = null;
|
||||||
|
|
@ -102,7 +105,7 @@ public class DevicePane extends TitledDescriptionPane {
|
||||||
}
|
}
|
||||||
|
|
||||||
public DevicePane(Wallet wallet, PSBT psbt, Device device, boolean defaultDevice) {
|
public DevicePane(Wallet wallet, PSBT psbt, Device device, boolean defaultDevice) {
|
||||||
super(device.getModel().toDisplayString(), "", "", device.getModel());
|
super(device.getModel().toDisplayString(), "", "", "image/" + device.getType() + ".png");
|
||||||
this.deviceOperation = DeviceOperation.SIGN;
|
this.deviceOperation = DeviceOperation.SIGN;
|
||||||
this.wallet = wallet;
|
this.wallet = wallet;
|
||||||
this.psbt = psbt;
|
this.psbt = psbt;
|
||||||
|
|
@ -129,7 +132,7 @@ public class DevicePane extends TitledDescriptionPane {
|
||||||
}
|
}
|
||||||
|
|
||||||
public DevicePane(Wallet wallet, OutputDescriptor outputDescriptor, Device device, boolean defaultDevice) {
|
public DevicePane(Wallet wallet, OutputDescriptor outputDescriptor, Device device, boolean defaultDevice) {
|
||||||
super(device.getModel().toDisplayString(), "", "", device.getModel());
|
super(device.getModel().toDisplayString(), "", "", "image/" + device.getType() + ".png");
|
||||||
this.deviceOperation = DeviceOperation.DISPLAY_ADDRESS;
|
this.deviceOperation = DeviceOperation.DISPLAY_ADDRESS;
|
||||||
this.wallet = wallet;
|
this.wallet = wallet;
|
||||||
this.psbt = null;
|
this.psbt = null;
|
||||||
|
|
@ -152,7 +155,7 @@ public class DevicePane extends TitledDescriptionPane {
|
||||||
}
|
}
|
||||||
|
|
||||||
public DevicePane(Wallet wallet, String message, KeyDerivation keyDerivation, Device device, boolean defaultDevice) {
|
public DevicePane(Wallet wallet, String message, KeyDerivation keyDerivation, Device device, boolean defaultDevice) {
|
||||||
super(device.getModel().toDisplayString(), "", "", device.getModel());
|
super(device.getModel().toDisplayString(), "", "", "image/" + device.getType() + ".png");
|
||||||
this.deviceOperation = DeviceOperation.SIGN_MESSAGE;
|
this.deviceOperation = DeviceOperation.SIGN_MESSAGE;
|
||||||
this.wallet = wallet;
|
this.wallet = wallet;
|
||||||
this.psbt = null;
|
this.psbt = null;
|
||||||
|
|
@ -179,7 +182,7 @@ public class DevicePane extends TitledDescriptionPane {
|
||||||
}
|
}
|
||||||
|
|
||||||
public DevicePane(Wallet wallet, List<StandardAccount> availableAccounts, Device device, boolean defaultDevice) {
|
public DevicePane(Wallet wallet, List<StandardAccount> availableAccounts, Device device, boolean defaultDevice) {
|
||||||
super(device.getModel().toDisplayString(), "", "", device.getModel());
|
super(device.getModel().toDisplayString(), "", "", "image/" + device.getType() + ".png");
|
||||||
this.deviceOperation = DeviceOperation.DISCOVER_KEYSTORES;
|
this.deviceOperation = DeviceOperation.DISCOVER_KEYSTORES;
|
||||||
this.wallet = wallet;
|
this.wallet = wallet;
|
||||||
this.psbt = null;
|
this.psbt = null;
|
||||||
|
|
@ -202,7 +205,7 @@ public class DevicePane extends TitledDescriptionPane {
|
||||||
}
|
}
|
||||||
|
|
||||||
public DevicePane(DeviceOperation deviceOperation, Device device, boolean defaultDevice) {
|
public DevicePane(DeviceOperation deviceOperation, Device device, boolean defaultDevice) {
|
||||||
super(device.getModel().toDisplayString(), "", "", device.getModel());
|
super(device.getModel().toDisplayString(), "", "", "image/" + device.getType() + ".png");
|
||||||
this.deviceOperation = deviceOperation;
|
this.deviceOperation = deviceOperation;
|
||||||
this.wallet = null;
|
this.wallet = null;
|
||||||
this.psbt = null;
|
this.psbt = null;
|
||||||
|
|
@ -453,26 +456,20 @@ public class DevicePane extends TitledDescriptionPane {
|
||||||
});
|
});
|
||||||
vBox.getChildren().addAll(pinField, enterPinButton);
|
vBox.getChildren().addAll(pinField, enterPinButton);
|
||||||
|
|
||||||
GridPane gridPane = new GridPane();
|
TilePane tilePane = new TilePane();
|
||||||
gridPane.setHgap(10);
|
tilePane.setPrefColumns(3);
|
||||||
gridPane.setVgap(10);
|
tilePane.setHgap(10);
|
||||||
gridPane.setMaxWidth(150);
|
tilePane.setVgap(10);
|
||||||
gridPane.setMaxHeight(device.getModel().hasZeroInPin() ? 160 : 120);
|
tilePane.setMaxWidth(150);
|
||||||
|
tilePane.setMaxHeight(120);
|
||||||
|
|
||||||
int[] digits = device.getModel().hasZeroInPin() ? new int[] {7, 8, 9, 4, 5, 6, 1, 2, 3, 0} : new int[] {7, 8, 9, 4, 5, 6, 1, 2, 3};
|
int[] digits = new int[] {7, 8, 9, 4, 5, 6, 1, 2, 3};
|
||||||
for(int i = 0; i < digits.length; i++) {
|
for(int i = 0; i < digits.length; i++) {
|
||||||
Button pinButton = new Button();
|
Button pinButton = new Button();
|
||||||
Glyph circle = new Glyph(FontAwesome5.FONT_NAME, "CIRCLE");
|
Glyph circle = new Glyph(FontAwesome5.FONT_NAME, "CIRCLE");
|
||||||
pinButton.setGraphic(circle);
|
pinButton.setGraphic(circle);
|
||||||
pinButton.setUserData(digits[i]);
|
pinButton.setUserData(digits[i]);
|
||||||
GridPane.setRowIndex(pinButton, i / 3);
|
tilePane.getChildren().add(pinButton);
|
||||||
GridPane.setColumnIndex(pinButton, i % 3);
|
|
||||||
if((i / 3) == 3) {
|
|
||||||
GridPane.setHgrow(pinButton, Priority.ALWAYS);
|
|
||||||
GridPane.setColumnSpan(pinButton, 3);
|
|
||||||
pinButton.setMaxWidth(Double.MAX_VALUE);
|
|
||||||
}
|
|
||||||
gridPane.getChildren().add(pinButton);
|
|
||||||
pinButton.setOnAction(event -> {
|
pinButton.setOnAction(event -> {
|
||||||
pinField.setText(pinField.getText() + pinButton.getUserData());
|
pinField.setText(pinField.getText() + pinButton.getUserData());
|
||||||
});
|
});
|
||||||
|
|
@ -480,7 +477,7 @@ public class DevicePane extends TitledDescriptionPane {
|
||||||
|
|
||||||
HBox contentBox = new HBox();
|
HBox contentBox = new HBox();
|
||||||
contentBox.setSpacing(50);
|
contentBox.setSpacing(50);
|
||||||
contentBox.getChildren().add(gridPane);
|
contentBox.getChildren().add(tilePane);
|
||||||
contentBox.getChildren().add(vBox);
|
contentBox.getChildren().add(vBox);
|
||||||
contentBox.setPadding(new Insets(10, 0, 10, 0));
|
contentBox.setPadding(new Insets(10, 0, 10, 0));
|
||||||
contentBox.setAlignment(Pos.TOP_CENTER);
|
contentBox.setAlignment(Pos.TOP_CENTER);
|
||||||
|
|
@ -781,12 +778,10 @@ public class DevicePane extends TitledDescriptionPane {
|
||||||
signButton.setDisable(false);
|
signButton.setDisable(false);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
Hwi.SignPSBTService signPSBTService = new Hwi.SignPSBTService(device, passphrase.get(), psbt,
|
Hwi.SignPSBTService signPSBTService = new Hwi.SignPSBTService(device, passphrase.get(), psbt);
|
||||||
OutputDescriptor.getOutputDescriptor(wallet), wallet.getFullName(), getDeviceRegistration());
|
|
||||||
signPSBTService.setOnSucceeded(workerStateEvent -> {
|
signPSBTService.setOnSucceeded(workerStateEvent -> {
|
||||||
PSBT signedPsbt = signPSBTService.getValue();
|
PSBT signedPsbt = signPSBTService.getValue();
|
||||||
EventManager.get().post(new PSBTSignedEvent(psbt, signedPsbt));
|
EventManager.get().post(new PSBTSignedEvent(psbt, signedPsbt));
|
||||||
updateDeviceRegistrations(signPSBTService.getNewDeviceRegistrations());
|
|
||||||
});
|
});
|
||||||
signPSBTService.setOnFailed(workerStateEvent -> {
|
signPSBTService.setOnFailed(workerStateEvent -> {
|
||||||
setError("Signing Error", signPSBTService.getException().getMessage());
|
setError("Signing Error", signPSBTService.getException().getMessage());
|
||||||
|
|
@ -825,12 +820,10 @@ public class DevicePane extends TitledDescriptionPane {
|
||||||
}
|
}
|
||||||
|
|
||||||
private void displayAddress() {
|
private void displayAddress() {
|
||||||
Hwi.DisplayAddressService displayAddressService = new Hwi.DisplayAddressService(device, passphrase.get(), wallet.getScriptType(), outputDescriptor,
|
Hwi.DisplayAddressService displayAddressService = new Hwi.DisplayAddressService(device, passphrase.get(), wallet.getScriptType(), outputDescriptor);
|
||||||
OutputDescriptor.getOutputDescriptor(wallet), wallet.getFullName(), getDeviceRegistration());
|
|
||||||
displayAddressService.setOnSucceeded(successEvent -> {
|
displayAddressService.setOnSucceeded(successEvent -> {
|
||||||
String address = displayAddressService.getValue();
|
String address = displayAddressService.getValue();
|
||||||
EventManager.get().post(new AddressDisplayedEvent(address));
|
EventManager.get().post(new AddressDisplayedEvent(address));
|
||||||
updateDeviceRegistrations(displayAddressService.getNewDeviceRegistrations());
|
|
||||||
});
|
});
|
||||||
displayAddressService.setOnFailed(failedEvent -> {
|
displayAddressService.setOnFailed(failedEvent -> {
|
||||||
setError("Could not display address", displayAddressService.getException().getMessage());
|
setError("Could not display address", displayAddressService.getException().getMessage());
|
||||||
|
|
@ -840,26 +833,6 @@ public class DevicePane extends TitledDescriptionPane {
|
||||||
displayAddressService.start();
|
displayAddressService.start();
|
||||||
}
|
}
|
||||||
|
|
||||||
private byte[] getDeviceRegistration() {
|
|
||||||
Optional<Keystore> optKeystore = wallet.getKeystores().stream()
|
|
||||||
.filter(keystore -> keystore.getKeyDerivation().getMasterFingerprint().equals(device.getFingerprint()) && keystore.getDeviceRegistration() != null).findFirst();
|
|
||||||
return optKeystore.map(Keystore::getDeviceRegistration).orElse(null);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void updateDeviceRegistrations(Set<byte[]> newDeviceRegistrations) {
|
|
||||||
if(!newDeviceRegistrations.isEmpty()) {
|
|
||||||
List<Keystore> registrationKeystores = getDeviceRegistrationKeystores();
|
|
||||||
if(!registrationKeystores.isEmpty()) {
|
|
||||||
registrationKeystores.forEach(keystore -> keystore.setDeviceRegistration(newDeviceRegistrations.iterator().next()));
|
|
||||||
EventManager.get().post(new KeystoreDeviceRegistrationsChangedEvent(wallet, registrationKeystores));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private List<Keystore> getDeviceRegistrationKeystores() {
|
|
||||||
return wallet.getKeystores().stream().filter(keystore -> keystore.getKeyDerivation().getMasterFingerprint().equals(device.getFingerprint())).toList();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void signMessage() {
|
private void signMessage() {
|
||||||
if(device.isCard()) {
|
if(device.isCard()) {
|
||||||
try {
|
try {
|
||||||
|
|
|
||||||
|
|
@ -1,85 +0,0 @@
|
||||||
package com.sparrowwallet.sparrow.control;
|
|
||||||
|
|
||||||
import com.sparrowwallet.sparrow.AppServices;
|
|
||||||
import com.sparrowwallet.sparrow.Theme;
|
|
||||||
import com.sparrowwallet.sparrow.io.Config;
|
|
||||||
import javafx.beans.NamedArg;
|
|
||||||
import javafx.beans.property.ObjectProperty;
|
|
||||||
import javafx.beans.property.SimpleObjectProperty;
|
|
||||||
import javafx.scene.layout.StackPane;
|
|
||||||
import org.girod.javafx.svgimage.SVGImage;
|
|
||||||
import org.girod.javafx.svgimage.SVGLoader;
|
|
||||||
import org.slf4j.Logger;
|
|
||||||
import org.slf4j.LoggerFactory;
|
|
||||||
|
|
||||||
import java.net.URL;
|
|
||||||
import java.util.Locale;
|
|
||||||
|
|
||||||
public class DialogImage extends StackPane {
|
|
||||||
private static final Logger log = LoggerFactory.getLogger(DialogImage.class);
|
|
||||||
|
|
||||||
public static final int WIDTH = 50;
|
|
||||||
public static final int HEIGHT = 50;
|
|
||||||
|
|
||||||
public ObjectProperty<DialogImage.Type> typeProperty = new SimpleObjectProperty<>();
|
|
||||||
|
|
||||||
public DialogImage() {
|
|
||||||
setPrefSize(WIDTH, HEIGHT);
|
|
||||||
this.typeProperty.addListener((observable, oldValue, type) -> {
|
|
||||||
refresh(type);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public DialogImage(@NamedArg("type") Type type) {
|
|
||||||
this();
|
|
||||||
this.typeProperty.set(type);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void refresh() {
|
|
||||||
Type type = getType();
|
|
||||||
refresh(type);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected void refresh(Type type) {
|
|
||||||
SVGImage svgImage;
|
|
||||||
if(Config.get().getTheme() == Theme.DARK) {
|
|
||||||
svgImage = loadSVGImage("/image/dialog/" + type.name().toLowerCase(Locale.ROOT) + "-invert.svg");
|
|
||||||
} else {
|
|
||||||
svgImage = loadSVGImage("/image/dialog/" + type.name().toLowerCase(Locale.ROOT) + ".svg");
|
|
||||||
}
|
|
||||||
|
|
||||||
if(svgImage != null) {
|
|
||||||
getChildren().clear();
|
|
||||||
getChildren().add(svgImage);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public Type getType() {
|
|
||||||
return typeProperty.get();
|
|
||||||
}
|
|
||||||
|
|
||||||
public ObjectProperty<Type> typeProperty() {
|
|
||||||
return typeProperty;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setType(Type type) {
|
|
||||||
this.typeProperty.set(type);
|
|
||||||
}
|
|
||||||
|
|
||||||
private SVGImage loadSVGImage(String imageName) {
|
|
||||||
try {
|
|
||||||
URL url = AppServices.class.getResource(imageName);
|
|
||||||
if(url != null) {
|
|
||||||
return SVGLoader.load(url);
|
|
||||||
}
|
|
||||||
} catch(Exception e) {
|
|
||||||
log.error("Could not find image " + imageName);
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
public enum Type {
|
|
||||||
SPARROW, SEED, PAYNYM, BORDERWALLETS, USERADD, WHIRLPOOL;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
package com.sparrowwallet.sparrow.control;
|
package com.sparrowwallet.sparrow.control;
|
||||||
|
|
||||||
import com.sparrowwallet.drongo.OsType;
|
|
||||||
import com.sparrowwallet.drongo.Utils;
|
import com.sparrowwallet.drongo.Utils;
|
||||||
import com.sparrowwallet.drongo.pgp.PGPKeySource;
|
import com.sparrowwallet.drongo.pgp.PGPKeySource;
|
||||||
import com.sparrowwallet.drongo.pgp.PGPUtils;
|
import com.sparrowwallet.drongo.pgp.PGPUtils;
|
||||||
|
|
@ -25,6 +24,7 @@ import javafx.scene.input.TransferMode;
|
||||||
import javafx.scene.layout.*;
|
import javafx.scene.layout.*;
|
||||||
import javafx.stage.FileChooser;
|
import javafx.stage.FileChooser;
|
||||||
import javafx.stage.Stage;
|
import javafx.stage.Stage;
|
||||||
|
import org.controlsfx.tools.Platform;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
import tornadofx.control.Field;
|
import tornadofx.control.Field;
|
||||||
|
|
@ -56,15 +56,13 @@ public class DownloadVerifierDialog extends Dialog<ButtonBar.ButtonData> {
|
||||||
private static final List<String> MANIFEST_EXTENSIONS = List.of("txt");
|
private static final List<String> MANIFEST_EXTENSIONS = List.of("txt");
|
||||||
private static final List<String> PUBLIC_KEY_EXTENSIONS = List.of("asc");
|
private static final List<String> PUBLIC_KEY_EXTENSIONS = List.of("asc");
|
||||||
private static final List<String> MACOS_RELEASE_EXTENSIONS = List.of("dmg");
|
private static final List<String> MACOS_RELEASE_EXTENSIONS = List.of("dmg");
|
||||||
private static final List<String> WINDOWS_RELEASE_EXTENSIONS = List.of("exe", "msi", "zip");
|
private static final List<String> WINDOWS_RELEASE_EXTENSIONS = List.of("exe", "zip");
|
||||||
private static final List<String> LINUX_RELEASE_EXTENSIONS = List.of("deb", "rpm", "tar.gz");
|
private static final List<String> LINUX_RELEASE_EXTENSIONS = List.of("deb", "rpm", "tar.gz");
|
||||||
private static final List<String> DISK_IMAGE_EXTENSIONS = List.of("img", "bin", "dfu");
|
private static final List<String> DISK_IMAGE_EXTENSIONS = List.of("img", "bin", "dfu");
|
||||||
private static final List<String> ARCHIVE_EXTENSIONS = List.of("zip", "tar.gz", "tar.bz2", "tar.xz", "rar", "7z");
|
private static final List<String> ARCHIVE_EXTENSIONS = List.of("zip", "tar.gz", "tar.bz2", "tar.xz", "rar", "7z");
|
||||||
|
|
||||||
private static final String SPARROW_RELEASE_PREFIX = "sparrow-";
|
private static final String SPARROW_RELEASE_PREFIX = "sparrow-";
|
||||||
private static final String SPARROW_RELEASE_ALT_PREFIX = "sparrow_";
|
private static final String SPARROW_SIGNATURE_SUFFIX = "-manifest.txt.asc";
|
||||||
private static final String SPARROW_MANIFEST_SUFFIX = "-manifest.txt";
|
|
||||||
private static final String SPARROW_SIGNATURE_SUFFIX = SPARROW_MANIFEST_SUFFIX + ".asc";
|
|
||||||
private static final Pattern SPARROW_RELEASE_VERSION = Pattern.compile("[0-9]+(\\.[0-9]+)*");
|
private static final Pattern SPARROW_RELEASE_VERSION = Pattern.compile("[0-9]+(\\.[0-9]+)*");
|
||||||
private static final long MIN_VALID_SPARROW_RELEASE_SIZE = 10 * 1024 * 1024;
|
private static final long MIN_VALID_SPARROW_RELEASE_SIZE = 10 * 1024 * 1024;
|
||||||
|
|
||||||
|
|
@ -72,9 +70,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 publicKeyDisabled = new SimpleBooleanProperty();
|
private final BooleanProperty publicKeyDisabled = new SimpleBooleanProperty();
|
||||||
|
|
||||||
private final Label signedBy;
|
private final Label signedBy;
|
||||||
|
|
@ -84,7 +80,7 @@ public class DownloadVerifierDialog extends Dialog<ButtonBar.ButtonData> {
|
||||||
|
|
||||||
private static File lastFileParent;
|
private static File lastFileParent;
|
||||||
|
|
||||||
public DownloadVerifierDialog(File initialFile) {
|
public DownloadVerifierDialog(File initialSignatureFile) {
|
||||||
final DialogPane dialogPane = getDialogPane();
|
final DialogPane dialogPane = getDialogPane();
|
||||||
dialogPane.getStylesheets().add(AppServices.class.getResource("general.css").toExternalForm());
|
dialogPane.getStylesheets().add(AppServices.class.getResource("general.css").toExternalForm());
|
||||||
dialogPane.getStylesheets().add(AppServices.class.getResource("dialog.css").toExternalForm());
|
dialogPane.getStylesheets().add(AppServices.class.getResource("dialog.css").toExternalForm());
|
||||||
|
|
@ -104,7 +100,7 @@ public class DownloadVerifierDialog extends Dialog<ButtonBar.ButtonData> {
|
||||||
String version = VersionCheckService.getVersion() != null ? VersionCheckService.getVersion() : "x.x.x";
|
String version = VersionCheckService.getVersion() != null ? VersionCheckService.getVersion() : "x.x.x";
|
||||||
|
|
||||||
Field signatureField = setupField(signature, "Signature", SIGNATURE_EXTENSIONS, false, "sparrow-" + version + "-manifest.txt", null);
|
Field signatureField = setupField(signature, "Signature", SIGNATURE_EXTENSIONS, false, "sparrow-" + version + "-manifest.txt", null);
|
||||||
Field manifestField = setupField(manifest, "Manifest", MANIFEST_EXTENSIONS, false, "sparrow-" + version + "-manifest", manifestDisabled);
|
Field manifestField = setupField(manifest, "Manifest", MANIFEST_EXTENSIONS, false, "sparrow-" + version + "-manifest", null);
|
||||||
Field publicKeyField = setupField(publicKey, "Public Key", PUBLIC_KEY_EXTENSIONS, true, "pgp_keys", publicKeyDisabled);
|
Field publicKeyField = setupField(publicKey, "Public Key", PUBLIC_KEY_EXTENSIONS, true, "pgp_keys", publicKeyDisabled);
|
||||||
Field releaseFileField = setupField(release, "Release File", getReleaseFileExtensions(), false, getReleaseFileExample(version), null);
|
Field releaseFileField = setupField(release, "Release File", getReleaseFileExtensions(), false, getReleaseFileExample(version), null);
|
||||||
|
|
||||||
|
|
@ -158,7 +154,6 @@ public class DownloadVerifierDialog extends Dialog<ButtonBar.ButtonData> {
|
||||||
release.set(null);
|
release.set(null);
|
||||||
signedBy.setText("");
|
signedBy.setText("");
|
||||||
signedBy.setGraphic(null);
|
signedBy.setGraphic(null);
|
||||||
signedBy.setTooltip(null);
|
|
||||||
releaseHash.setText("");
|
releaseHash.setText("");
|
||||||
releaseHash.setGraphic(null);
|
releaseHash.setGraphic(null);
|
||||||
releaseVerified.setText("");
|
releaseVerified.setText("");
|
||||||
|
|
@ -226,17 +221,11 @@ public class DownloadVerifierDialog extends Dialog<ButtonBar.ButtonData> {
|
||||||
});
|
});
|
||||||
|
|
||||||
release.addListener((observable, oldValue, releaseFile) -> {
|
release.addListener((observable, oldValue, releaseFile) -> {
|
||||||
if(releaseFile != null) {
|
|
||||||
initial.set(null);
|
|
||||||
}
|
|
||||||
verify();
|
verify();
|
||||||
});
|
});
|
||||||
|
|
||||||
if(initialFile != null) {
|
if(initialSignatureFile != null) {
|
||||||
javafx.application.Platform.runLater(() -> {
|
javafx.application.Platform.runLater(() -> signature.set(initialSignatureFile));
|
||||||
initial.set(initialFile);
|
|
||||||
signature.set(initialFile);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -274,7 +263,6 @@ public class DownloadVerifierDialog extends Dialog<ButtonBar.ButtonData> {
|
||||||
}
|
}
|
||||||
|
|
||||||
private void verify() {
|
private void verify() {
|
||||||
manifestDisabled.set(false);
|
|
||||||
publicKeyDisabled.set(false);
|
publicKeyDisabled.set(false);
|
||||||
|
|
||||||
if(signature.get() == null || manifest.get() == null) {
|
if(signature.get() == null || manifest.get() == null) {
|
||||||
|
|
@ -295,14 +283,12 @@ public class DownloadVerifierDialog extends Dialog<ButtonBar.ButtonData> {
|
||||||
String message = result.userId() + " on " + signatureDateFormat.format(result.signatureTimestamp()) + (result.expired() ? " (key expired)" : "");
|
String message = result.userId() + " on " + signatureDateFormat.format(result.signatureTimestamp()) + (result.expired() ? " (key expired)" : "");
|
||||||
signedBy.setText(message);
|
signedBy.setText(message);
|
||||||
signedBy.setGraphic(result.expired() ? GlyphUtils.getWarningGlyph() : GlyphUtils.getSuccessGlyph());
|
signedBy.setGraphic(result.expired() ? GlyphUtils.getWarningGlyph() : GlyphUtils.getSuccessGlyph());
|
||||||
signedBy.setTooltip(new Tooltip(result.fingerprint()));
|
|
||||||
|
|
||||||
if(!result.expired() && result.keySource() != PGPKeySource.USER) {
|
if(!result.expired() && result.keySource() != PGPKeySource.USER) {
|
||||||
publicKeyDisabled.set(true);
|
publicKeyDisabled.set(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
if(manifest.get().equals(release.get()) && !isSparrowManifest(manifest.get())) {
|
if(manifest.get().equals(release.get())) {
|
||||||
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());
|
||||||
releaseHash.setTooltip(null);
|
releaseHash.setTooltip(null);
|
||||||
|
|
@ -317,7 +303,6 @@ public class DownloadVerifierDialog extends Dialog<ButtonBar.ButtonData> {
|
||||||
Throwable e = event.getSource().getException();
|
Throwable e = event.getSource().getException();
|
||||||
signedBy.setText(getDisplayMessage(e));
|
signedBy.setText(getDisplayMessage(e));
|
||||||
signedBy.setGraphic(GlyphUtils.getFailureGlyph());
|
signedBy.setGraphic(GlyphUtils.getFailureGlyph());
|
||||||
signedBy.setTooltip(null);
|
|
||||||
clearReleaseFields();
|
clearReleaseFields();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -464,8 +449,7 @@ public class DownloadVerifierDialog extends Dialog<ButtonBar.ButtonData> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
String providedName = providedFile.getName().toLowerCase(Locale.ROOT);
|
if(providedFile.getName().toLowerCase(Locale.ROOT).startsWith(SPARROW_RELEASE_PREFIX)) {
|
||||||
if(providedName.startsWith(SPARROW_RELEASE_PREFIX) || providedName.startsWith(SPARROW_RELEASE_ALT_PREFIX)) {
|
|
||||||
Matcher matcher = SPARROW_RELEASE_VERSION.matcher(providedFile.getName());
|
Matcher matcher = SPARROW_RELEASE_VERSION.matcher(providedFile.getName());
|
||||||
if(matcher.find()) {
|
if(matcher.find()) {
|
||||||
String version = matcher.group();
|
String version = matcher.group();
|
||||||
|
|
@ -492,22 +476,6 @@ public class DownloadVerifierDialog extends Dialog<ButtonBar.ButtonData> {
|
||||||
}
|
}
|
||||||
|
|
||||||
private File findReleaseFile(File manifestFile, Map<File, String> manifestMap) {
|
private File findReleaseFile(File manifestFile, Map<File, String> manifestMap) {
|
||||||
File initialFile = initial.get();
|
|
||||||
if(initialFile != null && initialFile.exists()) {
|
|
||||||
for(File file : manifestMap.keySet()) {
|
|
||||||
if(initialFile.getName().equals(file.getName())) {
|
|
||||||
return initialFile;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
List<List<String>> allExtensionLists = List.of(MACOS_RELEASE_EXTENSIONS, WINDOWS_RELEASE_EXTENSIONS, LINUX_RELEASE_EXTENSIONS, DISK_IMAGE_EXTENSIONS, ARCHIVE_EXTENSIONS);
|
|
||||||
for(List<String> extensions : allExtensionLists) {
|
|
||||||
if(extensions.stream().anyMatch(ext -> initialFile.getName().toLowerCase(Locale.ROOT).endsWith(ext))) {
|
|
||||||
return initialFile;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
List<String> releaseExtensions = getReleaseFileExtensions();
|
List<String> releaseExtensions = getReleaseFileExtensions();
|
||||||
List<List<String>> extensionLists = List.of(releaseExtensions, DISK_IMAGE_EXTENSIONS, ARCHIVE_EXTENSIONS, List.of(""));
|
List<List<String>> extensionLists = List.of(releaseExtensions, DISK_IMAGE_EXTENSIONS, ARCHIVE_EXTENSIONS, List.of(""));
|
||||||
|
|
||||||
|
|
@ -526,9 +494,9 @@ public class DownloadVerifierDialog extends Dialog<ButtonBar.ButtonData> {
|
||||||
}
|
}
|
||||||
|
|
||||||
private List<String> getReleaseFileExtensions() {
|
private List<String> getReleaseFileExtensions() {
|
||||||
OsType osType = OsType.getCurrent();
|
Platform platform = Platform.getCurrent();
|
||||||
switch(osType) {
|
switch(platform) {
|
||||||
case MACOS -> {
|
case OSX -> {
|
||||||
return MACOS_RELEASE_EXTENSIONS;
|
return MACOS_RELEASE_EXTENSIONS;
|
||||||
}
|
}
|
||||||
case WINDOWS -> {
|
case WINDOWS -> {
|
||||||
|
|
@ -541,10 +509,10 @@ public class DownloadVerifierDialog extends Dialog<ButtonBar.ButtonData> {
|
||||||
}
|
}
|
||||||
|
|
||||||
private String getReleaseFileExample(String version) {
|
private String getReleaseFileExample(String version) {
|
||||||
OsType osType = OsType.getCurrent();
|
Platform platform = Platform.getCurrent();
|
||||||
String arch = System.getProperty("os.arch");
|
String arch = System.getProperty("os.arch");
|
||||||
switch(osType) {
|
switch(platform) {
|
||||||
case MACOS -> {
|
case OSX -> {
|
||||||
return "Sparrow-" + version + "-" + arch;
|
return "Sparrow-" + version + "-" + arch;
|
||||||
}
|
}
|
||||||
case WINDOWS -> {
|
case WINDOWS -> {
|
||||||
|
|
@ -591,7 +559,7 @@ public class DownloadVerifierDialog extends Dialog<ButtonBar.ButtonData> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if((name.startsWith(SPARROW_RELEASE_PREFIX) || name.startsWith(SPARROW_RELEASE_ALT_PREFIX)) && file.length() >= MIN_VALID_SPARROW_RELEASE_SIZE) {
|
if(name.startsWith(SPARROW_RELEASE_PREFIX) && file.length() >= MIN_VALID_SPARROW_RELEASE_SIZE) {
|
||||||
Matcher matcher = SPARROW_RELEASE_VERSION.matcher(name);
|
Matcher matcher = SPARROW_RELEASE_VERSION.matcher(name);
|
||||||
return matcher.find();
|
return matcher.find();
|
||||||
}
|
}
|
||||||
|
|
@ -600,18 +568,10 @@ public class DownloadVerifierDialog extends Dialog<ButtonBar.ButtonData> {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static boolean isSparrowManifest(File manifestFile) {
|
|
||||||
return manifestFile.getName().startsWith(SPARROW_RELEASE_PREFIX) && manifestFile.getName().endsWith(SPARROW_MANIFEST_SUFFIX);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setSignatureFile(File signatureFile) {
|
public void setSignatureFile(File signatureFile) {
|
||||||
signature.set(signatureFile);
|
signature.set(signatureFile);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setInitialFile(File initialFile) {
|
|
||||||
initial.set(initialFile);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static class Header extends GridPane {
|
private static class Header extends GridPane {
|
||||||
public Header() {
|
public Header() {
|
||||||
setMaxWidth(Double.MAX_VALUE);
|
setMaxWidth(Double.MAX_VALUE);
|
||||||
|
|
@ -632,8 +592,15 @@ public class DownloadVerifierDialog extends Dialog<ButtonBar.ButtonData> {
|
||||||
vBox.getChildren().addAll(headerLabel, descriptionLabel);
|
vBox.getChildren().addAll(headerLabel, descriptionLabel);
|
||||||
add(vBox, 0, 0);
|
add(vBox, 0, 0);
|
||||||
|
|
||||||
StackPane graphicContainer = new DialogImage(DialogImage.Type.SPARROW);
|
StackPane graphicContainer = new StackPane();
|
||||||
graphicContainer.getStyleClass().add("graphic-container");
|
graphicContainer.getStyleClass().add("graphic-container");
|
||||||
|
Image image = new Image("image/sparrow-small.png", 50, 50, false, false);
|
||||||
|
if (!image.isError()) {
|
||||||
|
ImageView imageView = new ImageView();
|
||||||
|
imageView.setSmooth(false);
|
||||||
|
imageView.setImage(image);
|
||||||
|
graphicContainer.getChildren().add(imageView);
|
||||||
|
}
|
||||||
add(graphicContainer, 1, 0);
|
add(graphicContainer, 1, 0);
|
||||||
|
|
||||||
ColumnConstraints textColumn = new ColumnConstraints();
|
ColumnConstraints textColumn = new ColumnConstraints();
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,9 @@
|
||||||
package com.sparrowwallet.sparrow.control;
|
package com.sparrowwallet.sparrow.control;
|
||||||
|
|
||||||
import com.sparrowwallet.drongo.KeyPurpose;
|
import com.sparrowwallet.drongo.KeyPurpose;
|
||||||
import com.sparrowwallet.drongo.OsType;
|
|
||||||
import com.sparrowwallet.drongo.Utils;
|
import com.sparrowwallet.drongo.Utils;
|
||||||
import com.sparrowwallet.drongo.address.Address;
|
import com.sparrowwallet.drongo.address.Address;
|
||||||
import com.sparrowwallet.drongo.protocol.*;
|
import com.sparrowwallet.drongo.protocol.*;
|
||||||
import com.sparrowwallet.drongo.silentpayments.SilentPayment;
|
|
||||||
import com.sparrowwallet.drongo.silentpayments.SilentPaymentAddress;
|
|
||||||
import com.sparrowwallet.drongo.wallet.*;
|
import com.sparrowwallet.drongo.wallet.*;
|
||||||
import com.sparrowwallet.sparrow.AppServices;
|
import com.sparrowwallet.sparrow.AppServices;
|
||||||
import com.sparrowwallet.sparrow.EventManager;
|
import com.sparrowwallet.sparrow.EventManager;
|
||||||
|
|
@ -57,7 +54,7 @@ public class EntryCell extends TreeTableCell<Entry, Entry> implements Confirmati
|
||||||
super.updateItem(entry, empty);
|
super.updateItem(entry, empty);
|
||||||
|
|
||||||
//Return immediately to avoid CPU usage when updating the same invisible cell to determine tableview size (see https://bugs.openjdk.org/browse/JDK-8280442)
|
//Return immediately to avoid CPU usage when updating the same invisible cell to determine tableview size (see https://bugs.openjdk.org/browse/JDK-8280442)
|
||||||
if(this == lastCell && !getTableRow().isVisible() && isTableSizeRecalculation()) {
|
if(this == lastCell && !getTableRow().isVisible()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
lastCell = this;
|
lastCell = this;
|
||||||
|
|
@ -68,7 +65,8 @@ public class EntryCell extends TreeTableCell<Entry, Entry> implements Confirmati
|
||||||
setText(null);
|
setText(null);
|
||||||
setGraphic(null);
|
setGraphic(null);
|
||||||
} else {
|
} else {
|
||||||
if(entry instanceof TransactionEntry transactionEntry) {
|
if(entry instanceof TransactionEntry) {
|
||||||
|
TransactionEntry transactionEntry = (TransactionEntry)entry;
|
||||||
if(transactionEntry.getBlockTransaction().getHeight() == -1) {
|
if(transactionEntry.getBlockTransaction().getHeight() == -1) {
|
||||||
setText("Unconfirmed Parent");
|
setText("Unconfirmed Parent");
|
||||||
setContextMenu(new UnconfirmedTransactionContextMenu(transactionEntry));
|
setContextMenu(new UnconfirmedTransactionContextMenu(transactionEntry));
|
||||||
|
|
@ -102,7 +100,7 @@ public class EntryCell extends TreeTableCell<Entry, Entry> implements Confirmati
|
||||||
actionBox.getChildren().add(viewTransactionButton);
|
actionBox.getChildren().add(viewTransactionButton);
|
||||||
|
|
||||||
BlockTransaction blockTransaction = transactionEntry.getBlockTransaction();
|
BlockTransaction blockTransaction = transactionEntry.getBlockTransaction();
|
||||||
if(blockTransaction.getHeight() <= 0 && canRBF(blockTransaction, transactionEntry.getWallet()) &&
|
if(blockTransaction.getHeight() <= 0 && canRBF(blockTransaction) &&
|
||||||
Config.get().isIncludeMempoolOutputs() && transactionEntry.getWallet().allInputsFromWallet(blockTransaction.getHash())) {
|
Config.get().isIncludeMempoolOutputs() && transactionEntry.getWallet().allInputsFromWallet(blockTransaction.getHash())) {
|
||||||
Button increaseFeeButton = new Button("");
|
Button increaseFeeButton = new Button("");
|
||||||
increaseFeeButton.setGraphic(getIncreaseFeeRBFGlyph());
|
increaseFeeButton.setGraphic(getIncreaseFeeRBFGlyph());
|
||||||
|
|
@ -122,7 +120,8 @@ public class EntryCell extends TreeTableCell<Entry, Entry> implements Confirmati
|
||||||
}
|
}
|
||||||
|
|
||||||
setGraphic(actionBox);
|
setGraphic(actionBox);
|
||||||
} else if(entry instanceof NodeEntry nodeEntry) {
|
} else if(entry instanceof NodeEntry) {
|
||||||
|
NodeEntry nodeEntry = (NodeEntry)entry;
|
||||||
Address address = nodeEntry.getAddress();
|
Address address = nodeEntry.getAddress();
|
||||||
setText(address.toString());
|
setText(address.toString());
|
||||||
setContextMenu(new AddressContextMenu(address, nodeEntry.getOutputDescriptor(), nodeEntry, true, getTreeTableView()));
|
setContextMenu(new AddressContextMenu(address, nodeEntry.getOutputDescriptor(), nodeEntry, true, getTreeTableView()));
|
||||||
|
|
@ -163,7 +162,8 @@ public class EntryCell extends TreeTableCell<Entry, Entry> implements Confirmati
|
||||||
setContextMenu(null);
|
setContextMenu(null);
|
||||||
setGraphic(new HBox());
|
setGraphic(new HBox());
|
||||||
}
|
}
|
||||||
} else if(entry instanceof HashIndexEntry hashIndexEntry) {
|
} else if(entry instanceof HashIndexEntry) {
|
||||||
|
HashIndexEntry hashIndexEntry = (HashIndexEntry)entry;
|
||||||
setText(hashIndexEntry.getDescription());
|
setText(hashIndexEntry.getDescription());
|
||||||
setContextMenu(getTreeTableView().getStyleClass().contains("bip47") ? null : new HashIndexEntryContextMenu(getTreeTableView(), hashIndexEntry));
|
setContextMenu(getTreeTableView().getStyleClass().contains("bip47") ? null : new HashIndexEntryContextMenu(getTreeTableView(), hashIndexEntry));
|
||||||
Tooltip tooltip = new Tooltip();
|
Tooltip tooltip = new Tooltip();
|
||||||
|
|
@ -211,14 +211,13 @@ public class EntryCell extends TreeTableCell<Entry, Entry> implements Confirmati
|
||||||
|
|
||||||
private static void increaseFee(TransactionEntry transactionEntry, boolean cancelTransaction) {
|
private static void increaseFee(TransactionEntry transactionEntry, boolean cancelTransaction) {
|
||||||
BlockTransaction blockTransaction = transactionEntry.getBlockTransaction();
|
BlockTransaction blockTransaction = transactionEntry.getBlockTransaction();
|
||||||
boolean silentPaymentTransaction = transactionEntry.getWallet().isSilentPaymentsTransaction(blockTransaction);
|
|
||||||
Map<BlockTransactionHashIndex, WalletNode> walletTxos = transactionEntry.getWallet().getWalletTxos();
|
Map<BlockTransactionHashIndex, WalletNode> walletTxos = transactionEntry.getWallet().getWalletTxos();
|
||||||
List<BlockTransactionHashIndex> utxos = transactionEntry.getChildren().stream()
|
List<BlockTransactionHashIndex> utxos = transactionEntry.getChildren().stream()
|
||||||
.filter(e -> e instanceof HashIndexEntry)
|
.filter(e -> e instanceof HashIndexEntry)
|
||||||
.map(e -> (HashIndexEntry)e)
|
.map(e -> (HashIndexEntry)e)
|
||||||
.filter(e -> e.getType().equals(HashIndexEntry.Type.INPUT) && e.isSpendable())
|
.filter(e -> e.getType().equals(HashIndexEntry.Type.INPUT) && e.isSpendable())
|
||||||
.map(e -> blockTransaction.getTransaction().getInputs().get((int)e.getHashIndex().getIndex()))
|
.map(e -> blockTransaction.getTransaction().getInputs().get((int)e.getHashIndex().getIndex()))
|
||||||
.filter(i -> Config.get().isMempoolFullRbf() || i.isReplaceByFeeEnabled() || silentPaymentTransaction)
|
.filter(i -> Config.get().isMempoolFullRbf() || i.isReplaceByFeeEnabled())
|
||||||
.map(txInput -> walletTxos.keySet().stream().filter(txo -> txo.getHash().equals(txInput.getOutpoint().getHash()) && txo.getIndex() == txInput.getOutpoint().getIndex()).findFirst().get())
|
.map(txInput -> walletTxos.keySet().stream().filter(txo -> txo.getHash().equals(txInput.getOutpoint().getHash()) && txo.getIndex() == txInput.getOutpoint().getIndex()).findFirst().get())
|
||||||
.collect(Collectors.toList());
|
.collect(Collectors.toList());
|
||||||
|
|
||||||
|
|
@ -243,7 +242,6 @@ public class EntryCell extends TreeTableCell<Entry, Entry> implements Confirmati
|
||||||
.collect(Collectors.toList());
|
.collect(Collectors.toList());
|
||||||
|
|
||||||
boolean consolidationTransaction = consolidationOutputs.size() == blockTransaction.getTransaction().getOutputs().size() && consolidationOutputs.size() == 1;
|
boolean consolidationTransaction = consolidationOutputs.size() == blockTransaction.getTransaction().getOutputs().size() && consolidationOutputs.size() == 1;
|
||||||
boolean safeToAddInputsOrOutputs = transactionEntry.getWallet().isSafeToAddInputsOrOutputs(blockTransaction);
|
|
||||||
long changeTotal = ourOutputs.stream().mapToLong(TransactionOutput::getValue).sum() - consolidationOutputs.stream().mapToLong(TransactionOutput::getValue).sum();
|
long changeTotal = ourOutputs.stream().mapToLong(TransactionOutput::getValue).sum() - consolidationOutputs.stream().mapToLong(TransactionOutput::getValue).sum();
|
||||||
Transaction tx = blockTransaction.getTransaction();
|
Transaction tx = blockTransaction.getTransaction();
|
||||||
double vSize = tx.getVirtualSize();
|
double vSize = tx.getVirtualSize();
|
||||||
|
|
@ -258,7 +256,7 @@ public class EntryCell extends TreeTableCell<Entry, Entry> implements Confirmati
|
||||||
List<OutputGroup> outputGroups = transactionEntry.getWallet().getGroupedUtxos(txoFilters, feeRate, AppServices.getMinimumRelayFeeRate(), Config.get().isGroupByAddress())
|
List<OutputGroup> outputGroups = transactionEntry.getWallet().getGroupedUtxos(txoFilters, feeRate, AppServices.getMinimumRelayFeeRate(), Config.get().isGroupByAddress())
|
||||||
.stream().filter(outputGroup -> outputGroup.getEffectiveValue() >= 0).collect(Collectors.toList());
|
.stream().filter(outputGroup -> outputGroup.getEffectiveValue() >= 0).collect(Collectors.toList());
|
||||||
Collections.shuffle(outputGroups);
|
Collections.shuffle(outputGroups);
|
||||||
while((double)changeTotal / vSize < getMaxFeeRate() && !outputGroups.isEmpty() && !cancelTransaction && !consolidationTransaction && safeToAddInputsOrOutputs) {
|
while((double)changeTotal / vSize < getMaxFeeRate() && !outputGroups.isEmpty() && !cancelTransaction && !consolidationTransaction) {
|
||||||
//If there is insufficient change output, include another random output group so the fee can be increased
|
//If there is insufficient change output, include another random output group so the fee can be increased
|
||||||
OutputGroup outputGroup = outputGroups.remove(0);
|
OutputGroup outputGroup = outputGroups.remove(0);
|
||||||
for(BlockTransactionHashIndex utxo : outputGroup.getUtxos()) {
|
for(BlockTransactionHashIndex utxo : outputGroup.getUtxos()) {
|
||||||
|
|
@ -299,13 +297,9 @@ public class EntryCell extends TreeTableCell<Entry, Entry> implements Confirmati
|
||||||
label += " (Replaced By Fee)";
|
label += " (Replaced By Fee)";
|
||||||
}
|
}
|
||||||
|
|
||||||
Address address = txOutput.getScript().getToAddress();
|
if(txOutput.getScript().getToAddress() != null) {
|
||||||
if(address != null) {
|
|
||||||
long value = txOutput.getValue();
|
|
||||||
//Disable change creation by enabling max payment when there is only one output and no additional UTXOs included
|
//Disable change creation by enabling max payment when there is only one output and no additional UTXOs included
|
||||||
boolean sendMax = blockTransaction.getTransaction().getOutputs().size() == 1 && rbfChange == 0;
|
return new Payment(txOutput.getScript().getToAddress(), label, txOutput.getValue(), blockTransaction.getTransaction().getOutputs().size() == 1 && rbfChange == 0);
|
||||||
SilentPaymentAddress silentPaymentAddress = transactionEntry.getWallet().getSilentPaymentAddress(address);
|
|
||||||
return silentPaymentAddress == null ? new Payment(address, label, value, sendMax) : new SilentPayment(silentPaymentAddress, label, value, sendMax);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
|
|
@ -342,7 +336,7 @@ public class EntryCell extends TreeTableCell<Entry, Entry> implements Confirmati
|
||||||
}
|
}
|
||||||
|
|
||||||
EventManager.get().post(new SendActionEvent(transactionEntry.getWallet(), utxos));
|
EventManager.get().post(new SendActionEvent(transactionEntry.getWallet(), utxos));
|
||||||
Platform.runLater(() -> EventManager.get().post(new SpendUtxoEvent(transactionEntry.getWallet(), utxos, payments, opReturns.isEmpty() ? null : opReturns, rbfFee, true, blockTransaction, safeToAddInputsOrOutputs)));
|
Platform.runLater(() -> EventManager.get().post(new SpendUtxoEvent(transactionEntry.getWallet(), utxos, payments, opReturns.isEmpty() ? null : opReturns, rbfFee, true, blockTransaction)));
|
||||||
}
|
}
|
||||||
|
|
||||||
private static Double getMaxFeeRate() {
|
private static Double getMaxFeeRate() {
|
||||||
|
|
@ -399,11 +393,11 @@ public class EntryCell extends TreeTableCell<Entry, Entry> implements Confirmati
|
||||||
Payment payment = new Payment(freshAddress, label, inputTotal, true);
|
Payment payment = new Payment(freshAddress, label, inputTotal, true);
|
||||||
|
|
||||||
EventManager.get().post(new SendActionEvent(transactionEntry.getWallet(), utxos));
|
EventManager.get().post(new SendActionEvent(transactionEntry.getWallet(), utxos));
|
||||||
Platform.runLater(() -> EventManager.get().post(new SpendUtxoEvent(transactionEntry.getWallet(), utxos, List.of(payment), null, blockTransaction.getFee(), true, null, true)));
|
Platform.runLater(() -> EventManager.get().post(new SpendUtxoEvent(transactionEntry.getWallet(), utxos, List.of(payment), null, blockTransaction.getFee(), true, null)));
|
||||||
}
|
}
|
||||||
|
|
||||||
private static boolean canRBF(BlockTransaction blockTransaction, Wallet wallet) {
|
private static boolean canRBF(BlockTransaction blockTransaction) {
|
||||||
return Config.get().isMempoolFullRbf() || blockTransaction.getTransaction().isReplaceByFee() || wallet.isSilentPaymentsTransaction(blockTransaction);
|
return Config.get().isMempoolFullRbf() || blockTransaction.getTransaction().isReplaceByFee();
|
||||||
}
|
}
|
||||||
|
|
||||||
private static boolean canSignMessage(WalletNode walletNode) {
|
private static boolean canSignMessage(WalletNode walletNode) {
|
||||||
|
|
@ -465,7 +459,7 @@ public class EntryCell extends TreeTableCell<Entry, Entry> implements Confirmati
|
||||||
Double feeRate = transactionEntry.getBlockTransaction().getFeeRate();
|
Double feeRate = transactionEntry.getBlockTransaction().getFeeRate();
|
||||||
Long vSizefromTip = transactionEntry.getVSizeFromTip();
|
Long vSizefromTip = transactionEntry.getVSizeFromTip();
|
||||||
if(feeRate != null && vSizefromTip != null) {
|
if(feeRate != null && vSizefromTip != null) {
|
||||||
long blocksFromTip = (long)Math.ceil((double)vSizefromTip / Transaction.MAX_BLOCK_SIZE_VBYTES);
|
long blocksFromTip = (long)Math.ceil((double)vSizefromTip / Transaction.MAX_BLOCK_SIZE);
|
||||||
|
|
||||||
String amount = vSizefromTip + " vB";
|
String amount = vSizefromTip + " vB";
|
||||||
if(vSizefromTip > 1000 * 1000) {
|
if(vSizefromTip > 1000 * 1000) {
|
||||||
|
|
@ -481,7 +475,7 @@ public class EntryCell extends TreeTableCell<Entry, Entry> implements Confirmati
|
||||||
tooltip += "\nFee rate: " + String.format("%.2f", feeRate) + " sats/vB";
|
tooltip += "\nFee rate: " + String.format("%.2f", feeRate) + " sats/vB";
|
||||||
}
|
}
|
||||||
|
|
||||||
tooltip += "\nRBF: " + (canRBF(transactionEntry.getBlockTransaction(), transactionEntry.getWallet()) ? "Enabled" : "Disabled");
|
tooltip += "\nRBF: " + (canRBF(transactionEntry.getBlockTransaction()) ? "Enabled" : "Disabled");
|
||||||
}
|
}
|
||||||
|
|
||||||
return tooltip;
|
return tooltip;
|
||||||
|
|
@ -549,7 +543,6 @@ public class EntryCell extends TreeTableCell<Entry, Entry> implements Confirmati
|
||||||
|
|
||||||
private static class UnconfirmedTransactionContextMenu extends ContextMenu {
|
private static class UnconfirmedTransactionContextMenu extends ContextMenu {
|
||||||
public UnconfirmedTransactionContextMenu(TransactionEntry transactionEntry) {
|
public UnconfirmedTransactionContextMenu(TransactionEntry transactionEntry) {
|
||||||
Wallet wallet = transactionEntry.getWallet();
|
|
||||||
BlockTransaction blockTransaction = transactionEntry.getBlockTransaction();
|
BlockTransaction blockTransaction = transactionEntry.getBlockTransaction();
|
||||||
MenuItem viewTransaction = new MenuItem("View Transaction");
|
MenuItem viewTransaction = new MenuItem("View Transaction");
|
||||||
viewTransaction.setGraphic(getViewTransactionGlyph());
|
viewTransaction.setGraphic(getViewTransactionGlyph());
|
||||||
|
|
@ -559,7 +552,7 @@ public class EntryCell extends TreeTableCell<Entry, Entry> implements Confirmati
|
||||||
});
|
});
|
||||||
getItems().add(viewTransaction);
|
getItems().add(viewTransaction);
|
||||||
|
|
||||||
if(canRBF(blockTransaction, wallet) && Config.get().isIncludeMempoolOutputs() && transactionEntry.getWallet().allInputsFromWallet(blockTransaction.getHash())) {
|
if(canRBF(blockTransaction) && Config.get().isIncludeMempoolOutputs() && transactionEntry.getWallet().allInputsFromWallet(blockTransaction.getHash())) {
|
||||||
MenuItem increaseFee = new MenuItem("Increase Fee (RBF)");
|
MenuItem increaseFee = new MenuItem("Increase Fee (RBF)");
|
||||||
increaseFee.setGraphic(getIncreaseFeeRBFGlyph());
|
increaseFee.setGraphic(getIncreaseFeeRBFGlyph());
|
||||||
increaseFee.setOnAction(AE -> {
|
increaseFee.setOnAction(AE -> {
|
||||||
|
|
@ -570,7 +563,7 @@ public class EntryCell extends TreeTableCell<Entry, Entry> implements Confirmati
|
||||||
getItems().add(increaseFee);
|
getItems().add(increaseFee);
|
||||||
}
|
}
|
||||||
|
|
||||||
if(canRBF(blockTransaction, wallet) && Config.get().isIncludeMempoolOutputs() && transactionEntry.getWallet().allInputsFromWallet(blockTransaction.getHash())) {
|
if(canRBF(blockTransaction) && Config.get().isIncludeMempoolOutputs() && transactionEntry.getWallet().allInputsFromWallet(blockTransaction.getHash())) {
|
||||||
MenuItem cancelTx = new MenuItem("Cancel Transaction (RBF)");
|
MenuItem cancelTx = new MenuItem("Cancel Transaction (RBF)");
|
||||||
cancelTx.setGraphic(getCancelTransactionRBFGlyph());
|
cancelTx.setGraphic(getCancelTransactionRBFGlyph());
|
||||||
cancelTx.setOnAction(AE -> {
|
cancelTx.setOnAction(AE -> {
|
||||||
|
|
@ -592,14 +585,12 @@ public class EntryCell extends TreeTableCell<Entry, Entry> implements Confirmati
|
||||||
getItems().add(createCpfp);
|
getItems().add(createCpfp);
|
||||||
}
|
}
|
||||||
|
|
||||||
if(!Config.get().isBlockExplorerDisabled()) {
|
MenuItem openBlockExplorer = new MenuItem("Open in Block Explorer");
|
||||||
MenuItem openBlockExplorer = new MenuItem("Open in Block Explorer");
|
openBlockExplorer.setOnAction(AE -> {
|
||||||
openBlockExplorer.setOnAction(AE -> {
|
hide();
|
||||||
hide();
|
AppServices.openBlockExplorer(blockTransaction.getHashAsString());
|
||||||
AppServices.openBlockExplorer(blockTransaction.getHashAsString());
|
});
|
||||||
});
|
getItems().add(openBlockExplorer);
|
||||||
getItems().add(openBlockExplorer);
|
|
||||||
}
|
|
||||||
|
|
||||||
MenuItem copyTxid = new MenuItem("Copy Transaction ID");
|
MenuItem copyTxid = new MenuItem("Copy Transaction ID");
|
||||||
copyTxid.setOnAction(AE -> {
|
copyTxid.setOnAction(AE -> {
|
||||||
|
|
@ -613,7 +604,7 @@ public class EntryCell extends TreeTableCell<Entry, Entry> implements Confirmati
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected static class TransactionContextMenu extends ContextMenu {
|
private static class TransactionContextMenu extends ContextMenu {
|
||||||
public TransactionContextMenu(String date, BlockTransaction blockTransaction) {
|
public TransactionContextMenu(String date, BlockTransaction blockTransaction) {
|
||||||
MenuItem viewTransaction = new MenuItem("View Transaction");
|
MenuItem viewTransaction = new MenuItem("View Transaction");
|
||||||
viewTransaction.setGraphic(getViewTransactionGlyph());
|
viewTransaction.setGraphic(getViewTransactionGlyph());
|
||||||
|
|
@ -621,16 +612,12 @@ public class EntryCell extends TreeTableCell<Entry, Entry> implements Confirmati
|
||||||
hide();
|
hide();
|
||||||
EventManager.get().post(new ViewTransactionEvent(this.getOwnerWindow(), blockTransaction));
|
EventManager.get().post(new ViewTransactionEvent(this.getOwnerWindow(), blockTransaction));
|
||||||
});
|
});
|
||||||
getItems().add(viewTransaction);
|
|
||||||
|
|
||||||
if(!Config.get().isBlockExplorerDisabled()) {
|
MenuItem openBlockExplorer = new MenuItem("Open in Block Explorer");
|
||||||
MenuItem openBlockExplorer = new MenuItem("Open in Block Explorer");
|
openBlockExplorer.setOnAction(AE -> {
|
||||||
openBlockExplorer.setOnAction(AE -> {
|
hide();
|
||||||
hide();
|
AppServices.openBlockExplorer(blockTransaction.getHashAsString());
|
||||||
AppServices.openBlockExplorer(blockTransaction.getHashAsString());
|
});
|
||||||
});
|
|
||||||
getItems().add(openBlockExplorer);
|
|
||||||
}
|
|
||||||
|
|
||||||
MenuItem copyDate = new MenuItem("Copy Date");
|
MenuItem copyDate = new MenuItem("Copy Date");
|
||||||
copyDate.setOnAction(AE -> {
|
copyDate.setOnAction(AE -> {
|
||||||
|
|
@ -639,7 +626,6 @@ public class EntryCell extends TreeTableCell<Entry, Entry> implements Confirmati
|
||||||
content.putString(date);
|
content.putString(date);
|
||||||
Clipboard.getSystemClipboard().setContent(content);
|
Clipboard.getSystemClipboard().setContent(content);
|
||||||
});
|
});
|
||||||
getItems().add(copyDate);
|
|
||||||
|
|
||||||
MenuItem copyTxid = new MenuItem("Copy Transaction ID");
|
MenuItem copyTxid = new MenuItem("Copy Transaction ID");
|
||||||
copyTxid.setOnAction(AE -> {
|
copyTxid.setOnAction(AE -> {
|
||||||
|
|
@ -648,7 +634,6 @@ public class EntryCell extends TreeTableCell<Entry, Entry> implements Confirmati
|
||||||
content.putString(blockTransaction.getHashAsString());
|
content.putString(blockTransaction.getHashAsString());
|
||||||
Clipboard.getSystemClipboard().setContent(content);
|
Clipboard.getSystemClipboard().setContent(content);
|
||||||
});
|
});
|
||||||
getItems().add(copyTxid);
|
|
||||||
|
|
||||||
MenuItem copyHeight = new MenuItem("Copy Block Height");
|
MenuItem copyHeight = new MenuItem("Copy Block Height");
|
||||||
copyHeight.setOnAction(AE -> {
|
copyHeight.setOnAction(AE -> {
|
||||||
|
|
@ -657,7 +642,8 @@ public class EntryCell extends TreeTableCell<Entry, Entry> implements Confirmati
|
||||||
content.putString(blockTransaction.getHeight() > 0 ? Integer.toString(blockTransaction.getHeight()) : "Mempool");
|
content.putString(blockTransaction.getHeight() > 0 ? Integer.toString(blockTransaction.getHeight()) : "Mempool");
|
||||||
Clipboard.getSystemClipboard().setContent(content);
|
Clipboard.getSystemClipboard().setContent(content);
|
||||||
});
|
});
|
||||||
getItems().add(copyHeight);
|
|
||||||
|
getItems().addAll(viewTransaction, openBlockExplorer, copyDate, copyTxid, copyHeight);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -812,17 +798,18 @@ public class EntryCell extends TreeTableCell<Entry, Entry> implements Confirmati
|
||||||
cell.getStyleClass().remove("utxo-row");
|
cell.getStyleClass().remove("utxo-row");
|
||||||
cell.getStyleClass().remove("unconfirmed-row");
|
cell.getStyleClass().remove("unconfirmed-row");
|
||||||
cell.getStyleClass().remove("summary-row");
|
cell.getStyleClass().remove("summary-row");
|
||||||
boolean addressCell = cell.getStyleClass().remove("address-cell");
|
cell.getStyleClass().remove("address-cell");
|
||||||
cell.getStyleClass().remove("hashindex-row");
|
cell.getStyleClass().remove("hashindex-row");
|
||||||
cell.getStyleClass().remove("confirming");
|
cell.getStyleClass().remove("confirming");
|
||||||
cell.getStyleClass().remove("negative-amount");
|
cell.getStyleClass().remove("negative-amount");
|
||||||
cell.getStyleClass().remove("spent");
|
cell.getStyleClass().remove("spent");
|
||||||
cell.getStyleClass().remove("unspendable");
|
cell.getStyleClass().remove("unspendable");
|
||||||
cell.getStyleClass().remove("number-field");
|
|
||||||
|
|
||||||
if(entry != null) {
|
if(entry != null) {
|
||||||
if(entry instanceof TransactionEntry transactionEntry) {
|
if(entry instanceof TransactionEntry) {
|
||||||
cell.getStyleClass().add("transaction-row");
|
cell.getStyleClass().add("transaction-row");
|
||||||
|
TransactionEntry transactionEntry = (TransactionEntry)entry;
|
||||||
|
|
||||||
if(cell instanceof ConfirmationsListener confirmationsListener) {
|
if(cell instanceof ConfirmationsListener confirmationsListener) {
|
||||||
if(transactionEntry.isConfirming()) {
|
if(transactionEntry.isConfirming()) {
|
||||||
cell.getStyleClass().add("confirming");
|
cell.getStyleClass().add("confirming");
|
||||||
|
|
@ -831,36 +818,25 @@ public class EntryCell extends TreeTableCell<Entry, Entry> implements Confirmati
|
||||||
confirmationsListener.getConfirmationsProperty().unbind();
|
confirmationsListener.getConfirmationsProperty().unbind();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if(OsType.getCurrent() == OsType.MACOS && transactionEntry.getBlockTransaction().getHeight() > 0 && !cell.getStyleClass().contains("label-cell")) {
|
|
||||||
cell.getStyleClass().add("number-field");
|
|
||||||
}
|
|
||||||
} else if(entry instanceof NodeEntry) {
|
} else if(entry instanceof NodeEntry) {
|
||||||
cell.getStyleClass().add("node-row");
|
cell.getStyleClass().add("node-row");
|
||||||
} else if(entry instanceof UtxoEntry utxoEntry) {
|
} else if(entry instanceof UtxoEntry) {
|
||||||
cell.getStyleClass().add("utxo-row");
|
cell.getStyleClass().add("utxo-row");
|
||||||
|
UtxoEntry utxoEntry = (UtxoEntry)entry;
|
||||||
if(!utxoEntry.isSpendable()) {
|
if(!utxoEntry.isSpendable()) {
|
||||||
cell.getStyleClass().add("unspendable");
|
cell.getStyleClass().add("unspendable");
|
||||||
}
|
}
|
||||||
if(OsType.getCurrent() == OsType.MACOS && utxoEntry.getHashIndex().getHeight() > 0 && !addressCell && !cell.getStyleClass().contains("label-cell")) {
|
} else if(entry instanceof HashIndexEntry) {
|
||||||
cell.getStyleClass().add("number-field");
|
|
||||||
}
|
|
||||||
} else if(entry instanceof HashIndexEntry hashIndexEntry) {
|
|
||||||
cell.getStyleClass().add("hashindex-row");
|
cell.getStyleClass().add("hashindex-row");
|
||||||
|
HashIndexEntry hashIndexEntry = (HashIndexEntry)entry;
|
||||||
if(hashIndexEntry.isSpent()) {
|
if(hashIndexEntry.isSpent()) {
|
||||||
cell.getStyleClass().add("spent");
|
cell.getStyleClass().add("spent");
|
||||||
}
|
}
|
||||||
} else if(entry instanceof WalletSummaryDialog.UnconfirmedEntry) {
|
} else if(entry instanceof WalletSummaryDialog.UnconfirmedEntry) {
|
||||||
cell.getStyleClass().add("unconfirmed-row");
|
cell.getStyleClass().add("unconfirmed-row");
|
||||||
} else if(entry instanceof WalletSummaryDialog.SummaryEntry || entry instanceof WalletSummaryDialog.AllSummaryEntry) {
|
} else if(entry instanceof WalletSummaryDialog.SummaryEntry) {
|
||||||
cell.getStyleClass().add("summary-row");
|
cell.getStyleClass().add("summary-row");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean isTableSizeRecalculation() {
|
|
||||||
//As per https://bugs.openjdk.org/browse/JDK-8265669 we check for cell visibility to avoid unnecessary recalculation, but this can result in false positives
|
|
||||||
//The method releaseCell in VirtualFlow is responsible for setting accumCell visibility to false after use, so check this method is calling updateItem
|
|
||||||
return StackWalker.getInstance().walk(frames -> frames.anyMatch(frame -> frame.getClassName().equals("javafx.scene.control.skin.VirtualFlow")
|
|
||||||
&& frame.getMethodName().equals("releaseCell")));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
package com.sparrowwallet.sparrow.control;
|
package com.sparrowwallet.sparrow.control;
|
||||||
|
|
||||||
import com.sparrowwallet.drongo.protocol.Transaction;
|
|
||||||
import com.sparrowwallet.sparrow.AppServices;
|
import com.sparrowwallet.sparrow.AppServices;
|
||||||
import com.sparrowwallet.sparrow.net.FeeRatesSource;
|
import com.sparrowwallet.sparrow.net.FeeRatesSource;
|
||||||
import javafx.application.Platform;
|
import javafx.application.Platform;
|
||||||
|
|
@ -8,19 +7,14 @@ 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;
|
||||||
|
|
||||||
import static com.sparrowwallet.sparrow.AppServices.*;
|
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 DecimalFormat INTEGER_FEE_RATE_FORMAT = new DecimalFormat("0");
|
|
||||||
private static final DecimalFormat FRACTIONAL_FEE_RATE_FORMAT = new DecimalFormat("0.###");
|
|
||||||
|
|
||||||
public FeeRangeSlider() {
|
public FeeRangeSlider() {
|
||||||
super(0, AppServices.getFeeRatesRange().size() - 1, 0);
|
super(0, FEE_RATES_RANGE.size() - 1, 0);
|
||||||
setMajorTickUnit(1);
|
setMajorTickUnit(1);
|
||||||
setMinorTickCount(0);
|
setMinorTickCount(0);
|
||||||
setSnapToTicks(false);
|
setSnapToTicks(false);
|
||||||
|
|
@ -31,11 +25,11 @@ public class FeeRangeSlider extends Slider {
|
||||||
setLabelFormatter(new StringConverter<>() {
|
setLabelFormatter(new StringConverter<>() {
|
||||||
@Override
|
@Override
|
||||||
public String toString(Double object) {
|
public String toString(Double object) {
|
||||||
Double feeRate = AppServices.getLongFeeRatesRange().get(object.intValue());
|
Long feeRate = LONG_FEE_RATES_RANGE.get(object.intValue());
|
||||||
if(isLongFeeRange() && feeRate >= 1000) {
|
if(isLongFeeRange() && feeRate >= 1000) {
|
||||||
return INTEGER_FEE_RATE_FORMAT.format(feeRate / 1000) + "k";
|
return feeRate / 1000 + "k";
|
||||||
}
|
}
|
||||||
return feeRate > 0d && feeRate < Transaction.DEFAULT_MIN_RELAY_FEE ? FRACTIONAL_FEE_RATE_FORMAT.format(feeRate) : INTEGER_FEE_RATE_FORMAT.format(feeRate);
|
return Long.toString(feeRate);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
@ -51,94 +45,30 @@ public class FeeRangeSlider extends Slider {
|
||||||
updateMaxFeeRange(newValue.doubleValue());
|
updateMaxFeeRange(newValue.doubleValue());
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
setOnScroll(event -> {
|
|
||||||
if(event.getDeltaY() != 0) {
|
|
||||||
double newFeeRate = getFeeRate() + (event.getDeltaY() > 0 ? FEE_RATE_SCROLL_INCREMENT : -FEE_RATE_SCROLL_INCREMENT);
|
|
||||||
if(newFeeRate < AppServices.getLongFeeRatesRange().getFirst()) {
|
|
||||||
newFeeRate = AppServices.getLongFeeRatesRange().getFirst();
|
|
||||||
} else if(newFeeRate > AppServices.getLongFeeRatesRange().getLast()) {
|
|
||||||
newFeeRate = AppServices.getLongFeeRatesRange().getLast();
|
|
||||||
}
|
|
||||||
setFeeRate(newFeeRate);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public double getFeeRate() {
|
public double getFeeRate() {
|
||||||
return getFeeRate(AppServices.getMinimumRelayFeeRate());
|
return Math.pow(2.0, getValue());
|
||||||
}
|
|
||||||
|
|
||||||
public double getFeeRate(Double minRelayFeeRate) {
|
|
||||||
if(minRelayFeeRate >= Transaction.DEFAULT_MIN_RELAY_FEE) {
|
|
||||||
return Math.pow(2.0, getValue());
|
|
||||||
}
|
|
||||||
|
|
||||||
if(getValue() < 1.0d) {
|
|
||||||
if(minRelayFeeRate == 0.0d) {
|
|
||||||
return getValue();
|
|
||||||
}
|
|
||||||
return Math.pow(minRelayFeeRate, 1.0d - getValue());
|
|
||||||
}
|
|
||||||
|
|
||||||
return Math.pow(2.0, getValue() - 1.0d);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setFeeRate(double feeRate) {
|
public void setFeeRate(double feeRate) {
|
||||||
setFeeRate(feeRate, AppServices.getMinimumRelayFeeRate());
|
double value = Math.log(feeRate) / Math.log(2);
|
||||||
}
|
|
||||||
|
|
||||||
public void setFeeRate(double feeRate, Double minRelayFeeRate) {
|
|
||||||
double value = getValue(feeRate, minRelayFeeRate);
|
|
||||||
updateMaxFeeRange(value);
|
updateMaxFeeRange(value);
|
||||||
setValue(value);
|
setValue(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
private double getValue(double feeRate, Double minRelayFeeRate) {
|
|
||||||
double value;
|
|
||||||
|
|
||||||
if(minRelayFeeRate >= Transaction.DEFAULT_MIN_RELAY_FEE) {
|
|
||||||
value = Math.log(feeRate) / Math.log(2);
|
|
||||||
} else {
|
|
||||||
if(feeRate < Transaction.DEFAULT_MIN_RELAY_FEE) {
|
|
||||||
if(minRelayFeeRate == 0.0d) {
|
|
||||||
return feeRate;
|
|
||||||
}
|
|
||||||
value = 1.0d - (Math.log(feeRate) / Math.log(minRelayFeeRate));
|
|
||||||
} else {
|
|
||||||
value = (Math.log(feeRate) / Math.log(2.0)) + 1.0d;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void updateFeeRange(Double minRelayFeeRate, Double previousMinRelayFeeRate) {
|
|
||||||
if(minRelayFeeRate != null && previousMinRelayFeeRate != null) {
|
|
||||||
setFeeRate(getFeeRate(previousMinRelayFeeRate), minRelayFeeRate);
|
|
||||||
}
|
|
||||||
setMinorTickCount(1);
|
|
||||||
setMinorTickCount(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void updateMaxFeeRange(double value) {
|
private void updateMaxFeeRange(double value) {
|
||||||
if(value >= getMax() && !isLongFeeRange()) {
|
if(value >= getMax() && !isLongFeeRange()) {
|
||||||
if(AppServices.getMinimumRelayFeeRate() < Transaction.DEFAULT_MIN_RELAY_FEE) {
|
setMax(LONG_FEE_RATES_RANGE.size() - 1);
|
||||||
setMin(1.0d);
|
|
||||||
}
|
|
||||||
setMax(AppServices.getLongFeeRatesRange().size() - 1);
|
|
||||||
updateTrackHighlight();
|
updateTrackHighlight();
|
||||||
} else if(value == getMin() && isLongFeeRange()) {
|
} else if(value == getMin() && isLongFeeRange()) {
|
||||||
if(AppServices.getMinimumRelayFeeRate() < Transaction.DEFAULT_MIN_RELAY_FEE) {
|
setMax(FEE_RATES_RANGE.size() - 1);
|
||||||
setMin(0.0d);
|
|
||||||
}
|
|
||||||
setMax(AppServices.getFeeRatesRange().size() - 1);
|
|
||||||
updateTrackHighlight();
|
updateTrackHighlight();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean isLongFeeRange() {
|
private boolean isLongFeeRange() {
|
||||||
return getMax() > AppServices.getFeeRatesRange().size() - 1;
|
return getMax() > FEE_RATES_RANGE.size() - 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void updateTrackHighlight() {
|
public void updateTrackHighlight() {
|
||||||
|
|
@ -193,9 +123,9 @@ public class FeeRangeSlider extends Slider {
|
||||||
}
|
}
|
||||||
|
|
||||||
private int getPercentageOfFeeRange(Double feeRate) {
|
private int getPercentageOfFeeRange(Double feeRate) {
|
||||||
double index = getValue(feeRate, AppServices.getMinimumRelayFeeRate());
|
double index = Math.log(feeRate) / Math.log(2);
|
||||||
if(isLongFeeRange()) {
|
if(isLongFeeRange()) {
|
||||||
index *= ((double)AppServices.getFeeRatesRange().size() / (AppServices.getLongFeeRatesRange().size())) * 0.99;
|
index *= ((double)FEE_RATES_RANGE.size() / (LONG_FEE_RATES_RANGE.size())) * 0.99;
|
||||||
}
|
}
|
||||||
return (int)Math.round(index * 10.0);
|
return (int)Math.round(index * 10.0);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
package com.sparrowwallet.sparrow.control;
|
package com.sparrowwallet.sparrow.control;
|
||||||
|
|
||||||
import com.sparrowwallet.drongo.OsType;
|
|
||||||
import com.sparrowwallet.drongo.protocol.Transaction;
|
import com.sparrowwallet.drongo.protocol.Transaction;
|
||||||
import com.sparrowwallet.sparrow.CurrencyRate;
|
import com.sparrowwallet.sparrow.CurrencyRate;
|
||||||
import com.sparrowwallet.sparrow.UnitFormat;
|
import com.sparrowwallet.sparrow.UnitFormat;
|
||||||
|
|
@ -11,6 +10,7 @@ import javafx.scene.control.Tooltip;
|
||||||
import javafx.scene.control.TreeTableCell;
|
import javafx.scene.control.TreeTableCell;
|
||||||
import javafx.scene.input.Clipboard;
|
import javafx.scene.input.Clipboard;
|
||||||
import javafx.scene.input.ClipboardContent;
|
import javafx.scene.input.ClipboardContent;
|
||||||
|
import org.controlsfx.tools.Platform;
|
||||||
|
|
||||||
import java.math.BigDecimal;
|
import java.math.BigDecimal;
|
||||||
import java.util.Currency;
|
import java.util.Currency;
|
||||||
|
|
@ -24,7 +24,7 @@ public class FiatCell extends TreeTableCell<Entry, Number> {
|
||||||
tooltip = new Tooltip();
|
tooltip = new Tooltip();
|
||||||
contextMenu = new FiatContextMenu();
|
contextMenu = new FiatContextMenu();
|
||||||
getStyleClass().add("coin-cell");
|
getStyleClass().add("coin-cell");
|
||||||
if(OsType.getCurrent() == OsType.MACOS) {
|
if(Platform.getCurrent() == Platform.OSX) {
|
||||||
getStyleClass().add("number-field");
|
getStyleClass().add("number-field");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,11 @@
|
||||||
package com.sparrowwallet.sparrow.control;
|
package com.sparrowwallet.sparrow.control;
|
||||||
|
|
||||||
import com.google.gson.JsonParseException;
|
import com.google.gson.JsonParseException;
|
||||||
import com.sparrowwallet.drongo.OsType;
|
|
||||||
import com.sparrowwallet.drongo.crypto.InvalidPasswordException;
|
import com.sparrowwallet.drongo.crypto.InvalidPasswordException;
|
||||||
import com.sparrowwallet.drongo.protocol.ScriptType;
|
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;
|
||||||
|
|
@ -26,7 +24,9 @@ import javafx.stage.FileChooser;
|
||||||
import javafx.stage.Stage;
|
import javafx.stage.Stage;
|
||||||
import org.controlsfx.control.SegmentedButton;
|
import org.controlsfx.control.SegmentedButton;
|
||||||
import org.controlsfx.control.textfield.CustomPasswordField;
|
import org.controlsfx.control.textfield.CustomPasswordField;
|
||||||
|
import org.controlsfx.control.textfield.TextFields;
|
||||||
import org.controlsfx.glyphfont.Glyph;
|
import org.controlsfx.glyphfont.Glyph;
|
||||||
|
import org.controlsfx.tools.Platform;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
|
@ -45,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, WalletModel walletModel, boolean scannable, boolean fileFormatAvailable) {
|
public FileImportPane(FileImport importer, String title, String description, String content, String imageUrl, boolean scannable, boolean fileFormatAvailable) {
|
||||||
super(title, description, content, walletModel);
|
super(title, description, content, imageUrl);
|
||||||
this.importer = importer;
|
this.importer = importer;
|
||||||
this.scannable = scannable;
|
this.scannable = scannable;
|
||||||
this.fileFormatAvailable = fileFormatAvailable;
|
this.fileFormatAvailable = fileFormatAvailable;
|
||||||
|
|
@ -104,7 +104,7 @@ public abstract class FileImportPane extends TitledDescriptionPane {
|
||||||
FileChooser fileChooser = new FileChooser();
|
FileChooser fileChooser = new FileChooser();
|
||||||
fileChooser.setTitle("Open " + importer.getWalletModel().toDisplayString() + " File");
|
fileChooser.setTitle("Open " + importer.getWalletModel().toDisplayString() + " File");
|
||||||
fileChooser.getExtensionFilters().addAll(
|
fileChooser.getExtensionFilters().addAll(
|
||||||
new FileChooser.ExtensionFilter("All Files", OsType.getCurrent().equals(OsType.UNIX) ? "*" : "*.*"),
|
new FileChooser.ExtensionFilter("All Files", Platform.getCurrent().equals(Platform.UNIX) ? "*" : "*.*"),
|
||||||
new FileChooser.ExtensionFilter("JSON", "*.json"),
|
new FileChooser.ExtensionFilter("JSON", "*.json"),
|
||||||
new FileChooser.ExtensionFilter("TXT", "*.txt")
|
new FileChooser.ExtensionFilter("TXT", "*.txt")
|
||||||
);
|
);
|
||||||
|
|
@ -240,8 +240,6 @@ public abstract class FileImportPane extends TitledDescriptionPane {
|
||||||
contentBox.setPadding(new Insets(10, 30, 10, 30));
|
contentBox.setPadding(new Insets(10, 30, 10, 30));
|
||||||
contentBox.setPrefHeight(60);
|
contentBox.setPrefHeight(60);
|
||||||
|
|
||||||
javafx.application.Platform.runLater(passwordField::requestFocus);
|
|
||||||
|
|
||||||
return contentBox;
|
return contentBox;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -37,7 +37,7 @@ public class FileKeystoreExportPane extends TitledDescriptionPane {
|
||||||
private final boolean file;
|
private final boolean file;
|
||||||
|
|
||||||
public FileKeystoreExportPane(Keystore keystore, KeystoreFileExport exporter) {
|
public FileKeystoreExportPane(Keystore keystore, KeystoreFileExport exporter) {
|
||||||
super(exporter.getName(), "Keystore export", exporter.getKeystoreExportDescription(), exporter.getWalletModel());
|
super(exporter.getName(), "Keystore export", exporter.getKeystoreExportDescription(), "image/" + exporter.getWalletModel().getType() + ".png");
|
||||||
this.keystore = keystore;
|
this.keystore = keystore;
|
||||||
this.exporter = exporter;
|
this.exporter = exporter;
|
||||||
this.scannable = exporter.isKeystoreExportScannable();
|
this.scannable = exporter.isKeystoreExportScannable();
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,7 @@ public class FileKeystoreImportPane extends FileImportPane {
|
||||||
private final KeyDerivation requiredDerivation;
|
private final KeyDerivation requiredDerivation;
|
||||||
|
|
||||||
public FileKeystoreImportPane(Wallet wallet, KeystoreFileImport importer, KeyDerivation requiredDerivation) {
|
public FileKeystoreImportPane(Wallet wallet, KeystoreFileImport importer, KeyDerivation requiredDerivation) {
|
||||||
super(importer, importer.getName(), "Key import", importer.getKeystoreImportDescription(getAccount(wallet, requiredDerivation)), importer.getWalletModel(), importer.isKeystoreImportScannable(), importer.isFileFormatAvailable());
|
super(importer, importer.getName(), "Keystore import", importer.getKeystoreImportDescription(getAccount(wallet, requiredDerivation)), "image/" + importer.getWalletModel().getType() + ".png", importer.isKeystoreImportScannable(), importer.isFileFormatAvailable());
|
||||||
this.wallet = wallet;
|
this.wallet = wallet;
|
||||||
this.importer = importer;
|
this.importer = importer;
|
||||||
this.requiredDerivation = requiredDerivation;
|
this.requiredDerivation = requiredDerivation;
|
||||||
|
|
@ -29,7 +29,7 @@ public class FileKeystoreImportPane extends FileImportPane {
|
||||||
}
|
}
|
||||||
|
|
||||||
if(requiredDerivation != null && !requiredDerivation.getDerivation().equals(keystore.getKeyDerivation().getDerivation())) {
|
if(requiredDerivation != null && !requiredDerivation.getDerivation().equals(keystore.getKeyDerivation().getDerivation())) {
|
||||||
setError("Incorrect derivation", "This account requires a derivation of " + requiredDerivation.getDerivationPath() + ", but the imported keystore has a derivation of " + KeyDerivation.writePath(keystore.getKeyDerivation().getDerivation()) + ".");
|
setError("Incorrect derivation", "This account requires a derivation of " + requiredDerivation.getDerivationPath() + ", but the imported keystore has a derivation of " + keystore.getKeyDerivation().getDerivationPath() + ".");
|
||||||
} else {
|
} else {
|
||||||
EventManager.get().post(new KeystoreImportEvent(keystore));
|
EventManager.get().post(new KeystoreImportEvent(keystore));
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -41,7 +41,7 @@ public class FileWalletExportPane extends TitledDescriptionPane {
|
||||||
private final boolean file;
|
private final boolean file;
|
||||||
|
|
||||||
public FileWalletExportPane(Wallet wallet, WalletExport exporter) {
|
public FileWalletExportPane(Wallet wallet, WalletExport exporter) {
|
||||||
super(exporter.getName(), "Wallet export", exporter.getWalletExportDescription(), exporter.getWalletModel());
|
super(exporter.getName(), "Wallet export", exporter.getWalletExportDescription(), "image/" + exporter.getWalletModel().getType() + ".png");
|
||||||
this.wallet = wallet;
|
this.wallet = wallet;
|
||||||
this.exporter = exporter;
|
this.exporter = exporter;
|
||||||
this.scannable = exporter.isWalletExportScannable();
|
this.scannable = exporter.isWalletExportScannable();
|
||||||
|
|
@ -168,21 +168,14 @@ public class FileWalletExportPane extends TitledDescriptionPane {
|
||||||
qrDisplayDialog = new QRDisplayDialog(RegistryType.BYTES.toString(), outputStream.toByteArray(), true);
|
qrDisplayDialog = new QRDisplayDialog(RegistryType.BYTES.toString(), outputStream.toByteArray(), true);
|
||||||
} else if(exporter instanceof PassportMultisig || exporter instanceof KeystoneMultisig || exporter instanceof JadeMultisig) {
|
} else if(exporter instanceof PassportMultisig || exporter instanceof KeystoneMultisig || exporter instanceof JadeMultisig) {
|
||||||
qrDisplayDialog = new QRDisplayDialog(RegistryType.BYTES.toString(), outputStream.toByteArray(), false);
|
qrDisplayDialog = new QRDisplayDialog(RegistryType.BYTES.toString(), outputStream.toByteArray(), false);
|
||||||
} else if(exporter instanceof Bip129 || exporter instanceof WalletLabels) {
|
} else if(exporter instanceof Bip129) {
|
||||||
UR ur = UR.fromBytes(outputStream.toByteArray());
|
UR ur = UR.fromBytes(outputStream.toByteArray());
|
||||||
BBQR bbqr = new BBQR(BBQRType.UNICODE, outputStream.toByteArray());
|
BBQR bbqr = new BBQR(BBQRType.UNICODE, outputStream.toByteArray());
|
||||||
qrDisplayDialog = new QRDisplayDialog(ur, bbqr, false, false, false);
|
qrDisplayDialog = new QRDisplayDialog(ur, bbqr, false, true, false);
|
||||||
} else if(exporter instanceof Descriptor) {
|
} else if(exporter instanceof Descriptor) {
|
||||||
boolean addBbqrOption = exportWallet.getKeystores().stream().anyMatch(keystore -> keystore.getWalletModel().showBbqr());
|
|
||||||
boolean selectBbqrOption = exportWallet.getKeystores().stream().allMatch(keystore -> keystore.getWalletModel().selectBbqr());
|
|
||||||
OutputDescriptor outputDescriptor = OutputDescriptor.getOutputDescriptor(exportWallet, KeyPurpose.DEFAULT_PURPOSES, null);
|
OutputDescriptor outputDescriptor = OutputDescriptor.getOutputDescriptor(exportWallet, KeyPurpose.DEFAULT_PURPOSES, null);
|
||||||
CryptoOutput cryptoOutput = getCryptoOutput(exportWallet);
|
CryptoOutput cryptoOutput = getCryptoOutput(exportWallet);
|
||||||
BBQR bbqr = addBbqrOption ? new BBQR(BBQRType.UNICODE, outputDescriptor.toString(true).getBytes(StandardCharsets.UTF_8)) : null;
|
qrDisplayDialog = new DescriptorQRDisplayDialog(exportWallet.getFullDisplayName(), outputDescriptor.toString(true), cryptoOutput.toUR());
|
||||||
qrDisplayDialog = new DescriptorQRDisplayDialog(exportWallet.getFullDisplayName(), outputDescriptor.toString(true), cryptoOutput.toUR(), bbqr, selectBbqrOption);
|
|
||||||
} else if(exporter.getClass().equals(ColdcardMultisig.class)) {
|
|
||||||
UR ur = UR.fromBytes(outputStream.toByteArray());
|
|
||||||
BBQR bbqr = new BBQR(BBQRType.UNICODE, outputStream.toByteArray());
|
|
||||||
qrDisplayDialog = new QRDisplayDialog(ur, bbqr, false, false, true);
|
|
||||||
} else {
|
} else {
|
||||||
qrDisplayDialog = new QRDisplayDialog(outputStream.toString(StandardCharsets.UTF_8));
|
qrDisplayDialog = new QRDisplayDialog(outputStream.toString(StandardCharsets.UTF_8));
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@ public class FileWalletImportPane extends FileImportPane {
|
||||||
private final WalletImport importer;
|
private final WalletImport importer;
|
||||||
|
|
||||||
public FileWalletImportPane(WalletImport importer) {
|
public FileWalletImportPane(WalletImport importer) {
|
||||||
super(importer, importer.getName(), "Wallet import", importer.getWalletImportDescription(), importer.getWalletModel(), importer.isWalletImportScannable(), importer.isWalletImportFileFormatAvailable());
|
super(importer, importer.getName(), "Wallet import", importer.getWalletImportDescription(), "image/" + importer.getWalletModel().getType() + ".png", importer.isWalletImportScannable(), importer.isWalletImportFileFormatAvailable());
|
||||||
this.importer = importer;
|
this.importer = importer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,6 @@ import com.sparrowwallet.sparrow.EventManager;
|
||||||
import com.sparrowwallet.sparrow.event.WalletImportEvent;
|
import com.sparrowwallet.sparrow.event.WalletImportEvent;
|
||||||
import com.sparrowwallet.sparrow.io.ImportException;
|
import com.sparrowwallet.sparrow.io.ImportException;
|
||||||
import com.sparrowwallet.sparrow.io.KeystoreFileImport;
|
import com.sparrowwallet.sparrow.io.KeystoreFileImport;
|
||||||
import javafx.application.Platform;
|
|
||||||
import javafx.collections.FXCollections;
|
import javafx.collections.FXCollections;
|
||||||
import javafx.geometry.Insets;
|
import javafx.geometry.Insets;
|
||||||
import javafx.geometry.Pos;
|
import javafx.geometry.Pos;
|
||||||
|
|
@ -39,16 +38,14 @@ public class FileWalletKeystoreImportPane extends FileImportPane {
|
||||||
private final KeystoreFileImport importer;
|
private final KeystoreFileImport importer;
|
||||||
private String fileName;
|
private String fileName;
|
||||||
private byte[] fileBytes;
|
private byte[] fileBytes;
|
||||||
private String password;
|
|
||||||
|
|
||||||
public FileWalletKeystoreImportPane(KeystoreFileImport importer) {
|
public FileWalletKeystoreImportPane(KeystoreFileImport importer) {
|
||||||
super(importer, importer.getName(), "Wallet import", importer.getKeystoreImportDescription(), importer.getWalletModel(), importer.isKeystoreImportScannable(), importer.isFileFormatAvailable());
|
super(importer, importer.getName(), "Wallet import", importer.getKeystoreImportDescription(), "image/" + importer.getWalletModel().getType() + ".png", importer.isKeystoreImportScannable(), importer.isFileFormatAvailable());
|
||||||
this.importer = importer;
|
this.importer = importer;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected void importFile(String fileName, InputStream inputStream, String password) throws ImportException {
|
protected void importFile(String fileName, InputStream inputStream, String password) throws ImportException {
|
||||||
this.fileName = fileName;
|
this.fileName = fileName;
|
||||||
this.password = password;
|
|
||||||
|
|
||||||
List<ScriptType> scriptTypes = ScriptType.getAddressableScriptTypes(PolicyType.SINGLE);
|
List<ScriptType> scriptTypes = ScriptType.getAddressableScriptTypes(PolicyType.SINGLE);
|
||||||
if(wallets != null && !wallets.isEmpty()) {
|
if(wallets != null && !wallets.isEmpty()) {
|
||||||
|
|
@ -86,7 +83,7 @@ public class FileWalletKeystoreImportPane extends FileImportPane {
|
||||||
EventManager.get().post(new WalletImportEvent(wallet));
|
EventManager.get().post(new WalletImportEvent(wallet));
|
||||||
} else {
|
} else {
|
||||||
ByteArrayInputStream bais = new ByteArrayInputStream(fileBytes);
|
ByteArrayInputStream bais = new ByteArrayInputStream(fileBytes);
|
||||||
Keystore keystore = importer.getKeystore(scriptType, bais, password);
|
Keystore keystore = importer.getKeystore(scriptType, bais, "");
|
||||||
|
|
||||||
Wallet wallet = new Wallet();
|
Wallet wallet = new Wallet();
|
||||||
wallet.setName(Files.getNameWithoutExtension(fileName));
|
wallet.setName(Files.getNameWithoutExtension(fileName));
|
||||||
|
|
@ -154,8 +151,6 @@ public class FileWalletKeystoreImportPane extends FileImportPane {
|
||||||
contentBox.setPadding(new Insets(10, 30, 10, 30));
|
contentBox.setPadding(new Insets(10, 30, 10, 30));
|
||||||
contentBox.setPrefHeight(60);
|
contentBox.setPrefHeight(60);
|
||||||
|
|
||||||
Platform.runLater(scriptTypeComboBox::requestFocus);
|
|
||||||
|
|
||||||
return contentBox;
|
return contentBox;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,20 +1,16 @@
|
||||||
package com.sparrowwallet.sparrow.control;
|
package com.sparrowwallet.sparrow.control;
|
||||||
|
|
||||||
import com.sparrowwallet.drongo.wallet.BlockTransactionHash;
|
import com.sparrowwallet.drongo.wallet.BlockTransactionHash;
|
||||||
import com.sparrowwallet.drongo.wallet.Persistable;
|
|
||||||
import com.sparrowwallet.sparrow.wallet.Entry;
|
import com.sparrowwallet.sparrow.wallet.Entry;
|
||||||
import javafx.animation.PauseTransition;
|
|
||||||
import javafx.application.Platform;
|
import javafx.application.Platform;
|
||||||
import javafx.beans.property.IntegerProperty;
|
import javafx.beans.property.IntegerProperty;
|
||||||
import javafx.beans.property.SimpleIntegerProperty;
|
import javafx.beans.property.SimpleIntegerProperty;
|
||||||
import javafx.event.Event;
|
import javafx.event.Event;
|
||||||
import javafx.geometry.Point2D;
|
|
||||||
import javafx.scene.control.*;
|
import javafx.scene.control.*;
|
||||||
import javafx.scene.control.cell.TextFieldTreeTableCell;
|
import javafx.scene.control.cell.TextFieldTreeTableCell;
|
||||||
import javafx.scene.input.Clipboard;
|
import javafx.scene.input.Clipboard;
|
||||||
import javafx.scene.input.ClipboardContent;
|
import javafx.scene.input.ClipboardContent;
|
||||||
import javafx.scene.input.DataFormat;
|
import javafx.scene.input.DataFormat;
|
||||||
import javafx.util.Duration;
|
|
||||||
import javafx.util.converter.DefaultStringConverter;
|
import javafx.util.converter.DefaultStringConverter;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
@ -38,23 +34,12 @@ class LabelCell extends TextFieldTreeTableCell<Entry, String> implements Confirm
|
||||||
if(empty) {
|
if(empty) {
|
||||||
setText(null);
|
setText(null);
|
||||||
setGraphic(null);
|
setGraphic(null);
|
||||||
setTooltip(null);
|
|
||||||
} else {
|
} else {
|
||||||
Entry entry = getTreeTableView().getTreeItem(getIndex()).getValue();
|
Entry entry = getTreeTableView().getTreeItem(getIndex()).getValue();
|
||||||
EntryCell.applyRowStyles(this, entry);
|
EntryCell.applyRowStyles(this, entry);
|
||||||
|
|
||||||
setText(label);
|
setText(label);
|
||||||
setContextMenu(new LabelContextMenu(entry, label));
|
setContextMenu(new LabelContextMenu(entry, label));
|
||||||
|
|
||||||
double width = label == null || label.length() < 20 ? 0.0 : TextUtils.computeTextWidth(getFont(), label, 0.0D);
|
|
||||||
if(width > getTableColumn().getWidth()) {
|
|
||||||
Tooltip tooltip = new Tooltip(label);
|
|
||||||
tooltip.setMaxWidth(getTreeTableView().getWidth());
|
|
||||||
tooltip.setWrapText(true);
|
|
||||||
setTooltip(tooltip);
|
|
||||||
} else {
|
|
||||||
setTooltip(null);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -62,20 +47,6 @@ class LabelCell extends TextFieldTreeTableCell<Entry, String> implements Confirm
|
||||||
public void commitEdit(String label) {
|
public void commitEdit(String label) {
|
||||||
if(label != null) {
|
if(label != null) {
|
||||||
label = label.trim();
|
label = label.trim();
|
||||||
if(label.length() > Persistable.MAX_LABEL_LENGTH) {
|
|
||||||
label = label.substring(0, Persistable.MAX_LABEL_LENGTH);
|
|
||||||
Platform.runLater(() -> {
|
|
||||||
Point2D p = this.localToScene(0.0, 0.0);
|
|
||||||
final Tooltip truncateTooltip = new Tooltip();
|
|
||||||
truncateTooltip.setText("Labels are truncated at " + Persistable.MAX_LABEL_LENGTH + " characters");
|
|
||||||
truncateTooltip.setAutoHide(true);
|
|
||||||
truncateTooltip.show(this, p.getX() + this.getScene().getX() + this.getScene().getWindow().getX() + this.getHeight(),
|
|
||||||
p.getY() + this.getScene().getY() + this.getScene().getWindow().getY() + this.getHeight());
|
|
||||||
PauseTransition pt = new PauseTransition(Duration.millis(2000));
|
|
||||||
pt.setOnFinished(_ -> truncateTooltip.hide());
|
|
||||||
pt.play();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// This block is necessary to support commit on losing focus, because
|
// This block is necessary to support commit on losing focus, because
|
||||||
|
|
@ -132,7 +103,7 @@ class LabelCell extends TextFieldTreeTableCell<Entry, String> implements Confirm
|
||||||
return confirmationsProperty;
|
return confirmationsProperty;
|
||||||
}
|
}
|
||||||
|
|
||||||
private class LabelContextMenu extends ContextMenu {
|
private static class LabelContextMenu extends ContextMenu {
|
||||||
public LabelContextMenu(Entry entry, String label) {
|
public LabelContextMenu(Entry entry, String label) {
|
||||||
MenuItem copyLabel = new MenuItem("Copy Label");
|
MenuItem copyLabel = new MenuItem("Copy Label");
|
||||||
copyLabel.setOnAction(AE -> {
|
copyLabel.setOnAction(AE -> {
|
||||||
|
|
@ -152,13 +123,6 @@ class LabelCell extends TextFieldTreeTableCell<Entry, String> implements Confirm
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
getItems().add(pasteLabel);
|
getItems().add(pasteLabel);
|
||||||
|
|
||||||
MenuItem editLabel = new MenuItem("Edit Label...");
|
|
||||||
editLabel.setOnAction(AE -> {
|
|
||||||
hide();
|
|
||||||
startEdit();
|
|
||||||
});
|
|
||||||
getItems().add(editLabel);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
package com.sparrowwallet.sparrow.control;
|
package com.sparrowwallet.sparrow.control;
|
||||||
|
|
||||||
import com.sparrowwallet.drongo.OsType;
|
|
||||||
import com.sparrowwallet.sparrow.AppServices;
|
import com.sparrowwallet.sparrow.AppServices;
|
||||||
import com.sparrowwallet.sparrow.Theme;
|
import com.sparrowwallet.sparrow.Theme;
|
||||||
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
|
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
|
||||||
|
|
@ -58,7 +57,7 @@ public class MempoolSizeFeeRatesChart extends StackedAreaChart<String, Number> {
|
||||||
stage.setResizable(false);
|
stage.setResizable(false);
|
||||||
|
|
||||||
StackPane scenePane = new StackPane();
|
StackPane scenePane = new StackPane();
|
||||||
if(OsType.getCurrent() == OsType.WINDOWS) {
|
if(org.controlsfx.tools.Platform.getCurrent() == org.controlsfx.tools.Platform.WINDOWS) {
|
||||||
scenePane.setBorder(new Border(new BorderStroke(Color.DARKGRAY, BorderStrokeStyle.SOLID, CornerRadii.EMPTY, BorderWidths.DEFAULT)));
|
scenePane.setBorder(new Border(new BorderStroke(Color.DARKGRAY, BorderStrokeStyle.SOLID, CornerRadii.EMPTY, BorderWidths.DEFAULT)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,6 @@ package com.sparrowwallet.sparrow.control;
|
||||||
|
|
||||||
import com.google.common.eventbus.Subscribe;
|
import com.google.common.eventbus.Subscribe;
|
||||||
import com.sparrowwallet.drongo.KeyDerivation;
|
import com.sparrowwallet.drongo.KeyDerivation;
|
||||||
import com.sparrowwallet.drongo.OsType;
|
|
||||||
import com.sparrowwallet.drongo.SecureString;
|
import com.sparrowwallet.drongo.SecureString;
|
||||||
import com.sparrowwallet.drongo.address.Address;
|
import com.sparrowwallet.drongo.address.Address;
|
||||||
import com.sparrowwallet.drongo.address.InvalidAddressException;
|
import com.sparrowwallet.drongo.address.InvalidAddressException;
|
||||||
|
|
@ -18,13 +17,10 @@ import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
|
||||||
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5Brands;
|
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5Brands;
|
||||||
import com.sparrowwallet.sparrow.io.Storage;
|
import com.sparrowwallet.sparrow.io.Storage;
|
||||||
import javafx.application.Platform;
|
import javafx.application.Platform;
|
||||||
import javafx.scene.Node;
|
|
||||||
import javafx.scene.control.*;
|
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.VBox;
|
import javafx.scene.layout.VBox;
|
||||||
import javafx.stage.FileChooser;
|
|
||||||
import javafx.stage.Stage;
|
|
||||||
import org.controlsfx.control.SegmentedButton;
|
import org.controlsfx.control.SegmentedButton;
|
||||||
import org.controlsfx.glyphfont.Glyph;
|
import org.controlsfx.glyphfont.Glyph;
|
||||||
import org.controlsfx.validation.ValidationResult;
|
import org.controlsfx.validation.ValidationResult;
|
||||||
|
|
@ -36,21 +32,17 @@ import tornadofx.control.Field;
|
||||||
import tornadofx.control.Fieldset;
|
import tornadofx.control.Fieldset;
|
||||||
import tornadofx.control.Form;
|
import tornadofx.control.Form;
|
||||||
|
|
||||||
import java.io.*;
|
|
||||||
import java.nio.charset.StandardCharsets;
|
|
||||||
import java.nio.file.Files;
|
|
||||||
import java.security.SignatureException;
|
import java.security.SignatureException;
|
||||||
import java.util.*;
|
import java.util.Arrays;
|
||||||
import java.util.regex.Matcher;
|
import java.util.List;
|
||||||
import java.util.regex.Pattern;
|
import java.util.Locale;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
import static com.sparrowwallet.sparrow.AppServices.showErrorDialog;
|
import static com.sparrowwallet.sparrow.AppServices.showErrorDialog;
|
||||||
|
|
||||||
public class MessageSignDialog extends Dialog<ButtonBar.ButtonData> {
|
public class MessageSignDialog extends Dialog<ButtonBar.ButtonData> {
|
||||||
private static final Logger log = LoggerFactory.getLogger(MessageSignDialog.class);
|
private static final Logger log = LoggerFactory.getLogger(MessageSignDialog.class);
|
||||||
|
|
||||||
private static final Pattern signedMessagePattern = Pattern.compile("-----BEGIN BITCOIN SIGNED MESSAGE-----\\r?\\n(.*)\\r?\\n-----BEGIN BITCOIN SIGNATURE-----\\r?\\n(.*)\\r?\\n(.*)\\r?\\n-----END BITCOIN SIGNATURE-----\r?\n?");
|
|
||||||
|
|
||||||
private final TextField address;
|
private final TextField address;
|
||||||
private final TextArea message;
|
private final TextArea message;
|
||||||
private final TextArea signature;
|
private final TextArea signature;
|
||||||
|
|
@ -112,13 +104,19 @@ public class MessageSignDialog extends Dialog<ButtonBar.ButtonData> {
|
||||||
this.wallet = wallet;
|
this.wallet = wallet;
|
||||||
this.walletNode = walletNode;
|
this.walletNode = walletNode;
|
||||||
|
|
||||||
final DialogPane dialogPane = new MessageSignDialogPane();
|
final DialogPane dialogPane = getDialogPane();
|
||||||
setDialogPane(dialogPane);
|
|
||||||
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());
|
||||||
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);
|
||||||
|
|
@ -201,7 +199,13 @@ public class MessageSignDialog extends Dialog<ButtonBar.ButtonData> {
|
||||||
} else {
|
} else {
|
||||||
dialogPane.getButtonTypes().addAll(showQrButtonType, signButtonType, verifyButtonType, doneButtonType);
|
dialogPane.getButtonTypes().addAll(showQrButtonType, signButtonType, verifyButtonType, doneButtonType);
|
||||||
|
|
||||||
Node showQrButton = dialogPane.lookupButton(showQrButtonType);
|
Button showQrButton = (Button) dialogPane.lookupButton(showQrButtonType);
|
||||||
|
showQrButton.setDisable(wallet == null);
|
||||||
|
showQrButton.setGraphic(getGlyph(new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.QRCODE)));
|
||||||
|
showQrButton.setGraphicTextGap(5);
|
||||||
|
showQrButton.setOnAction(event -> {
|
||||||
|
showQr();
|
||||||
|
});
|
||||||
|
|
||||||
Button signButton = (Button) dialogPane.lookupButton(signButtonType);
|
Button signButton = (Button) dialogPane.lookupButton(signButtonType);
|
||||||
signButton.setDisable(!canSign);
|
signButton.setDisable(!canSign);
|
||||||
|
|
@ -240,9 +244,6 @@ public class MessageSignDialog extends Dialog<ButtonBar.ButtonData> {
|
||||||
setFormatFromScriptType(address.getScriptType());
|
setFormatFromScriptType(address.getScriptType());
|
||||||
if(wallet != null) {
|
if(wallet != null) {
|
||||||
setWalletNodeFromAddress(wallet, address);
|
setWalletNodeFromAddress(wallet, address);
|
||||||
if(walletNode != null) {
|
|
||||||
setFormatFromScriptType(getSigningScriptType(walletNode));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} catch(InvalidAddressException e) {
|
} catch(InvalidAddressException e) {
|
||||||
//can't happen
|
//can't happen
|
||||||
|
|
@ -266,7 +267,7 @@ public class MessageSignDialog extends Dialog<ButtonBar.ButtonData> {
|
||||||
|
|
||||||
AppServices.onEscapePressed(dialogPane.getScene(), () -> setResult(ButtonBar.ButtonData.CANCEL_CLOSE));
|
AppServices.onEscapePressed(dialogPane.getScene(), () -> setResult(ButtonBar.ButtonData.CANCEL_CLOSE));
|
||||||
AppServices.moveToActiveWindowScreen(this);
|
AppServices.moveToActiveWindowScreen(this);
|
||||||
setResultConverter(dialogButton -> dialogButton == signButtonType || dialogButton == verifyButtonType ? ButtonBar.ButtonData.APPLY : dialogButton.getButtonData());
|
setResultConverter(dialogButton -> dialogButton == showQrButtonType || dialogButton == signButtonType || dialogButton == verifyButtonType ? ButtonBar.ButtonData.APPLY : dialogButton.getButtonData());
|
||||||
|
|
||||||
Platform.runLater(() -> {
|
Platform.runLater(() -> {
|
||||||
if(address.getText().isEmpty()) {
|
if(address.getText().isEmpty()) {
|
||||||
|
|
@ -276,7 +277,7 @@ public class MessageSignDialog extends Dialog<ButtonBar.ButtonData> {
|
||||||
}
|
}
|
||||||
|
|
||||||
if(wallet != null && walletNode != null) {
|
if(wallet != null && walletNode != null) {
|
||||||
setFormatFromScriptType(getSigningScriptType(walletNode));
|
setFormatFromScriptType(wallet.getScriptType());
|
||||||
} else {
|
} else {
|
||||||
formatGroup.selectToggle(formatElectrum);
|
formatGroup.selectToggle(formatElectrum);
|
||||||
}
|
}
|
||||||
|
|
@ -290,13 +291,9 @@ public class MessageSignDialog extends Dialog<ButtonBar.ButtonData> {
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean canSign(Wallet wallet) {
|
private boolean canSign(Wallet wallet) {
|
||||||
return wallet.getKeystores().getFirst().hasPrivateKey()
|
return wallet.getKeystores().get(0).hasPrivateKey()
|
||||||
|| wallet.getKeystores().getFirst().getSource() == KeystoreSource.HW_USB
|
|| wallet.getKeystores().get(0).getSource() == KeystoreSource.HW_USB
|
||||||
|| wallet.getKeystores().getFirst().getWalletModel().isCard();
|
|| wallet.getKeystores().get(0).getWalletModel().isCard();
|
||||||
}
|
|
||||||
|
|
||||||
private boolean canSignBip322(Wallet wallet) {
|
|
||||||
return wallet.getKeystores().getFirst().hasPrivateKey();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private Address getAddress()throws InvalidAddressException {
|
private Address getAddress()throws InvalidAddressException {
|
||||||
|
|
@ -320,11 +317,6 @@ public class MessageSignDialog extends Dialog<ButtonBar.ButtonData> {
|
||||||
walletNode = wallet.getWalletAddresses().get(address);
|
walletNode = wallet.getWalletAddresses().get(address);
|
||||||
}
|
}
|
||||||
|
|
||||||
private ScriptType getSigningScriptType(WalletNode walletNode) {
|
|
||||||
ScriptType scriptType = walletNode.getWallet().getScriptType();
|
|
||||||
return canSign(walletNode.getWallet()) && !canSignBip322(walletNode.getWallet()) ? ScriptType.P2PKH : scriptType;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void setFormatFromScriptType(ScriptType scriptType) {
|
private void setFormatFromScriptType(ScriptType scriptType) {
|
||||||
formatElectrum.setDisable(scriptType == ScriptType.P2TR);
|
formatElectrum.setDisable(scriptType == ScriptType.P2TR);
|
||||||
formatTrezor.setDisable(scriptType == ScriptType.P2TR || scriptType == ScriptType.P2PKH);
|
formatTrezor.setDisable(scriptType == ScriptType.P2TR || scriptType == ScriptType.P2PKH);
|
||||||
|
|
@ -357,7 +349,7 @@ public class MessageSignDialog extends Dialog<ButtonBar.ButtonData> {
|
||||||
|
|
||||||
//Note we can expect a single keystore due to the check in the constructor
|
//Note we can expect a single keystore due to the check in the constructor
|
||||||
Wallet signingWallet = walletNode.getWallet();
|
Wallet signingWallet = walletNode.getWallet();
|
||||||
if(signingWallet.getKeystores().getFirst().hasPrivateKey()) {
|
if(signingWallet.getKeystores().get(0).hasPrivateKey()) {
|
||||||
if(signingWallet.isEncrypted()) {
|
if(signingWallet.isEncrypted()) {
|
||||||
EventManager.get().post(new RequestOpenWalletsEvent());
|
EventManager.get().post(new RequestOpenWalletsEvent());
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -370,7 +362,7 @@ public class MessageSignDialog extends Dialog<ButtonBar.ButtonData> {
|
||||||
|
|
||||||
private void signUnencryptedKeystore(Wallet decryptedWallet) {
|
private void signUnencryptedKeystore(Wallet decryptedWallet) {
|
||||||
try {
|
try {
|
||||||
Keystore keystore = decryptedWallet.getKeystores().getFirst();
|
Keystore keystore = decryptedWallet.getKeystores().get(0);
|
||||||
ECKey privKey = keystore.getKey(walletNode);
|
ECKey privKey = keystore.getKey(walletNode);
|
||||||
String signatureText;
|
String signatureText;
|
||||||
if(isBip322()) {
|
if(isBip322()) {
|
||||||
|
|
@ -390,8 +382,8 @@ public class MessageSignDialog extends Dialog<ButtonBar.ButtonData> {
|
||||||
}
|
}
|
||||||
|
|
||||||
private void signDeviceKeystore(Wallet deviceWallet) {
|
private void signDeviceKeystore(Wallet deviceWallet) {
|
||||||
List<String> fingerprints = List.of(deviceWallet.getKeystores().getFirst().getKeyDerivation().getMasterFingerprint());
|
List<String> fingerprints = List.of(deviceWallet.getKeystores().get(0).getKeyDerivation().getMasterFingerprint());
|
||||||
KeyDerivation fullDerivation = deviceWallet.getKeystores().getFirst().getKeyDerivation().extend(walletNode.getDerivation());
|
KeyDerivation fullDerivation = deviceWallet.getKeystores().get(0).getKeyDerivation().extend(walletNode.getDerivation());
|
||||||
DeviceSignMessageDialog deviceSignMessageDialog = new DeviceSignMessageDialog(fingerprints, deviceWallet, message.getText().trim(), fullDerivation);
|
DeviceSignMessageDialog deviceSignMessageDialog = new DeviceSignMessageDialog(fingerprints, deviceWallet, message.getText().trim(), fullDerivation);
|
||||||
deviceSignMessageDialog.initOwner(getDialogPane().getScene().getWindow());
|
deviceSignMessageDialog.initOwner(getDialogPane().getScene().getWindow());
|
||||||
Optional<String> optSignature = deviceSignMessageDialog.showAndWait();
|
Optional<String> optSignature = deviceSignMessageDialog.showAndWait();
|
||||||
|
|
@ -480,7 +472,7 @@ public class MessageSignDialog extends Dialog<ButtonBar.ButtonData> {
|
||||||
QRDisplayDialog qrDisplayDialog = new QRDisplayDialog(qrText, true);
|
QRDisplayDialog qrDisplayDialog = new QRDisplayDialog(qrText, true);
|
||||||
qrDisplayDialog.initOwner(getDialogPane().getScene().getWindow());
|
qrDisplayDialog.initOwner(getDialogPane().getScene().getWindow());
|
||||||
Optional<ButtonType> optButtonType = qrDisplayDialog.showAndWait();
|
Optional<ButtonType> optButtonType = qrDisplayDialog.showAndWait();
|
||||||
if(optButtonType.isPresent() && optButtonType.get().getButtonData() == ButtonBar.ButtonData.OK_DONE) {
|
if(optButtonType.isPresent() && optButtonType.get().getButtonData() == ButtonBar.ButtonData.NEXT_FORWARD) {
|
||||||
scanQr();
|
scanQr();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -503,82 +495,6 @@ public class MessageSignDialog extends Dialog<ButtonBar.ButtonData> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void exportFile() {
|
|
||||||
if(walletNode == null) {
|
|
||||||
AppServices.showErrorDialog("Address not in wallet", "The provided address is not present in the currently selected wallet.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
StringJoiner joiner = new StringJoiner("\n");
|
|
||||||
joiner.add(message.getText().trim().replaceAll("\r*\n*", ""));
|
|
||||||
//Note we can expect a single keystore due to the check in the constructor
|
|
||||||
KeyDerivation firstDerivation = walletNode.getWallet().getKeystores().get(0).getKeyDerivation();
|
|
||||||
joiner.add(KeyDerivation.writePath(firstDerivation.extend(walletNode.getDerivation()).getDerivation(), true));
|
|
||||||
joiner.add(walletNode.getWallet().getScriptType().toString());
|
|
||||||
|
|
||||||
Stage window = new Stage();
|
|
||||||
|
|
||||||
FileChooser fileChooser = new FileChooser();
|
|
||||||
fileChooser.setTitle("Save Text File");
|
|
||||||
fileChooser.setInitialFileName("signmessage.txt");
|
|
||||||
AppServices.moveToActiveWindowScreen(window, 800, 450);
|
|
||||||
File file = fileChooser.showSaveDialog(window);
|
|
||||||
if(file != null) {
|
|
||||||
if(!file.getName().toLowerCase(Locale.ROOT).endsWith(".txt")) {
|
|
||||||
file = new File(file.getAbsolutePath() + ".txt");
|
|
||||||
}
|
|
||||||
|
|
||||||
try(BufferedWriter writer = new BufferedWriter(new FileWriter(file, StandardCharsets.UTF_8))) {
|
|
||||||
writer.write(joiner.toString());
|
|
||||||
} catch(IOException e) {
|
|
||||||
log.error("Error saving signing message", e);
|
|
||||||
AppServices.showErrorDialog("Error saving signing message", "Cannot write to " + file.getAbsolutePath());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void importFile() {
|
|
||||||
Stage window = new Stage();
|
|
||||||
|
|
||||||
FileChooser fileChooser = new FileChooser();
|
|
||||||
fileChooser.setTitle("Open Signed Text File");
|
|
||||||
fileChooser.getExtensionFilters().addAll(
|
|
||||||
new FileChooser.ExtensionFilter("All Files", OsType.getCurrent().equals(OsType.UNIX) ? "*" : "*.*"),
|
|
||||||
new FileChooser.ExtensionFilter("Text Files", "*.txt")
|
|
||||||
);
|
|
||||||
|
|
||||||
AppServices.moveToActiveWindowScreen(window, 800, 450);
|
|
||||||
File file = fileChooser.showOpenDialog(window);
|
|
||||||
|
|
||||||
if(file != null) {
|
|
||||||
try {
|
|
||||||
String content = Files.readString(file.toPath(), StandardCharsets.UTF_8);
|
|
||||||
Matcher matcher = signedMessagePattern.matcher(content);
|
|
||||||
if(matcher.matches()) {
|
|
||||||
String signedMessage = matcher.group(1);
|
|
||||||
String signedAddress = matcher.group(2);
|
|
||||||
String signedSignature = matcher.group(3);
|
|
||||||
|
|
||||||
if(!message.getText().isEmpty() && !signedMessage.trim().equals(message.getText().trim().replaceAll("\r*\n*", ""))) {
|
|
||||||
AppServices.showErrorDialog("Incorrect Message", "The file contained a different message of:\n\n" + signedMessage);
|
|
||||||
return;
|
|
||||||
} else if(!signedAddress.trim().equals(address.getText().trim())) {
|
|
||||||
AppServices.showErrorDialog("Incorrect Address", "The file contained a different address of:\n\n" + signedAddress);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
message.setText(signedMessage);
|
|
||||||
signature.setText(signedSignature);
|
|
||||||
} else {
|
|
||||||
signature.setText(content);
|
|
||||||
}
|
|
||||||
} catch(IOException e) {
|
|
||||||
log.error("Error loading signed message", e);
|
|
||||||
AppServices.showErrorDialog("Error loading signed message", e.getMessage());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
protected Glyph getSignGlyph() {
|
protected Glyph getSignGlyph() {
|
||||||
if(wallet != null) {
|
if(wallet != null) {
|
||||||
if(wallet.containsSource(KeystoreSource.HW_USB)) {
|
if(wallet.containsSource(KeystoreSource.HW_USB)) {
|
||||||
|
|
@ -623,37 +539,4 @@ public class MessageSignDialog extends Dialog<ButtonBar.ButtonData> {
|
||||||
decryptWalletService.start();
|
decryptWalletService.start();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private class MessageSignDialogPane extends DialogPane {
|
|
||||||
@Override
|
|
||||||
protected Node createButton(ButtonType buttonType) {
|
|
||||||
if(buttonType.getButtonData() == ButtonBar.ButtonData.LEFT) {
|
|
||||||
SplitMenuButton signByButton = new SplitMenuButton();
|
|
||||||
signByButton.setText("Sign by QR");
|
|
||||||
signByButton.setDisable(wallet == null);
|
|
||||||
signByButton.setGraphic(getGlyph(new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.QRCODE)));
|
|
||||||
signByButton.setGraphicTextGap(5);
|
|
||||||
signByButton.setOnAction(event -> {
|
|
||||||
showQr();
|
|
||||||
});
|
|
||||||
MenuItem exportFile = new MenuItem("Sign by File...");
|
|
||||||
exportFile.setGraphic(getGlyph(new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.FILE_EXPORT)));
|
|
||||||
exportFile.setOnAction(event -> {
|
|
||||||
exportFile();
|
|
||||||
});
|
|
||||||
MenuItem importFile = new MenuItem("Load Signed File...");
|
|
||||||
importFile.setGraphic(getGlyph(new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.FILE_IMPORT)));
|
|
||||||
importFile.setOnAction(event -> {
|
|
||||||
importFile();
|
|
||||||
});
|
|
||||||
signByButton.getItems().addAll(exportFile, importFile);
|
|
||||||
final ButtonBar.ButtonData buttonData = buttonType.getButtonData();
|
|
||||||
ButtonBar.setButtonData(signByButton, buttonData);
|
|
||||||
|
|
||||||
return signByButton;
|
|
||||||
}
|
|
||||||
|
|
||||||
return super.createButton(buttonType);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,27 @@
|
||||||
package com.sparrowwallet.sparrow.control;
|
package com.sparrowwallet.sparrow.control;
|
||||||
|
|
||||||
|
import com.samourai.whirlpool.client.mix.listener.MixFailReason;
|
||||||
|
import com.samourai.whirlpool.client.mix.listener.MixStep;
|
||||||
|
import com.samourai.whirlpool.client.wallet.beans.MixProgress;
|
||||||
|
import com.samourai.whirlpool.protocol.beans.Utxo;
|
||||||
|
import com.sparrowwallet.sparrow.AppServices;
|
||||||
|
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
|
||||||
import com.sparrowwallet.sparrow.wallet.Entry;
|
import com.sparrowwallet.sparrow.wallet.Entry;
|
||||||
import com.sparrowwallet.sparrow.wallet.UtxoEntry;
|
import com.sparrowwallet.sparrow.wallet.UtxoEntry;
|
||||||
|
import com.sparrowwallet.sparrow.whirlpool.Whirlpool;
|
||||||
|
import com.sparrowwallet.sparrow.whirlpool.WhirlpoolException;
|
||||||
|
import javafx.animation.Timeline;
|
||||||
import javafx.geometry.Pos;
|
import javafx.geometry.Pos;
|
||||||
import javafx.scene.control.*;
|
import javafx.scene.control.*;
|
||||||
|
import javafx.util.Duration;
|
||||||
|
import org.controlsfx.glyphfont.Glyph;
|
||||||
|
import org.controlsfx.tools.Platform;
|
||||||
|
|
||||||
|
import java.util.Locale;
|
||||||
|
|
||||||
public class MixStatusCell extends TreeTableCell<Entry, UtxoEntry.MixStatus> {
|
public class MixStatusCell extends TreeTableCell<Entry, UtxoEntry.MixStatus> {
|
||||||
|
private static final int ERROR_DISPLAY_MILLIS = 5 * 60 * 1000;
|
||||||
|
|
||||||
public MixStatusCell() {
|
public MixStatusCell() {
|
||||||
super();
|
super();
|
||||||
setAlignment(Pos.CENTER_RIGHT);
|
setAlignment(Pos.CENTER_RIGHT);
|
||||||
|
|
@ -25,9 +41,174 @@ public class MixStatusCell extends TreeTableCell<Entry, UtxoEntry.MixStatus> {
|
||||||
setGraphic(null);
|
setGraphic(null);
|
||||||
} else {
|
} else {
|
||||||
setText(Integer.toString(mixStatus.getMixesDone()));
|
setText(Integer.toString(mixStatus.getMixesDone()));
|
||||||
setContextMenu(null);
|
if(mixStatus.getNextMixUtxo() == null) {
|
||||||
|
setContextMenu(new MixStatusContextMenu(mixStatus.getUtxoEntry(), mixStatus.getMixProgress() != null && mixStatus.getMixProgress().getMixStep() != MixStep.FAIL));
|
||||||
|
} else {
|
||||||
|
setContextMenu(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
if(mixStatus.getNextMixUtxo() != null) {
|
||||||
|
setMixSuccess(mixStatus.getNextMixUtxo());
|
||||||
|
} else if(mixStatus.getMixFailReason() != null) {
|
||||||
|
setMixFail(mixStatus.getMixFailReason(), mixStatus.getMixError(), mixStatus.getMixErrorTimestamp());
|
||||||
|
} else if(mixStatus.getMixProgress() != null) {
|
||||||
|
setMixProgress(mixStatus.getUtxoEntry(), mixStatus.getMixProgress());
|
||||||
|
} else {
|
||||||
|
setGraphic(null);
|
||||||
|
setTooltip(null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void setMixSuccess(Utxo nextMixUtxo) {
|
||||||
|
ProgressIndicator progressIndicator = getProgressIndicator();
|
||||||
|
progressIndicator.setProgress(-1);
|
||||||
|
setGraphic(progressIndicator);
|
||||||
|
Tooltip tt = new Tooltip();
|
||||||
|
tt.setText("Waiting for broadcast of " + nextMixUtxo.getHash().substring(0, 8) + "..." + ":" + nextMixUtxo.getIndex() );
|
||||||
|
setTooltip(tt);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void setMixFail(MixFailReason mixFailReason, String mixError, Long mixErrorTimestamp) {
|
||||||
|
if(mixFailReason != MixFailReason.CANCEL) {
|
||||||
|
long elapsed = mixErrorTimestamp == null ? 0L : System.currentTimeMillis() - mixErrorTimestamp;
|
||||||
|
if(elapsed >= ERROR_DISPLAY_MILLIS) {
|
||||||
|
//Old error, don't set again.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Glyph failGlyph = getFailGlyph();
|
||||||
|
setGraphic(failGlyph);
|
||||||
|
Tooltip tt = new Tooltip();
|
||||||
|
tt.setText(mixFailReason.getMessage() + (mixError == null ? "" : ": " + mixError) +
|
||||||
|
"\nMix failures are generally caused by peers disconnecting during a mix." +
|
||||||
|
"\nMake sure your internet connection is stable and the computer is configured to prevent sleeping." +
|
||||||
|
"\nTo prevent sleeping, use the " + getPlatformSleepConfig() + " or enable the function in the Tools menu.");
|
||||||
|
setTooltip(tt);
|
||||||
|
|
||||||
|
Duration fadeDuration = Duration.millis(ERROR_DISPLAY_MILLIS - elapsed);
|
||||||
|
double fadeFromValue = 1.0 - ((double)elapsed / ERROR_DISPLAY_MILLIS);
|
||||||
|
Timeline timeline = AnimationUtil.getSlowFadeOut(failGlyph, fadeDuration, fadeFromValue, 10);
|
||||||
|
timeline.setOnFinished(event -> {
|
||||||
|
setTooltip(null);
|
||||||
|
});
|
||||||
|
timeline.play();
|
||||||
|
} else {
|
||||||
setGraphic(null);
|
setGraphic(null);
|
||||||
setTooltip(null);
|
setTooltip(null);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private String getPlatformSleepConfig() {
|
||||||
|
Platform platform = Platform.getCurrent();
|
||||||
|
if(platform == Platform.OSX) {
|
||||||
|
return "OSX System Preferences";
|
||||||
|
} else if(platform == Platform.WINDOWS) {
|
||||||
|
return "Windows Control Panel";
|
||||||
|
}
|
||||||
|
|
||||||
|
return "system power settings";
|
||||||
|
}
|
||||||
|
|
||||||
|
private void setMixProgress(UtxoEntry utxoEntry, MixProgress mixProgress) {
|
||||||
|
if(mixProgress.getMixStep() != MixStep.FAIL) {
|
||||||
|
ProgressIndicator progressIndicator = getProgressIndicator();
|
||||||
|
progressIndicator.setProgress(mixProgress.getMixStep().getProgressPercent() == 100 ? -1 : mixProgress.getMixStep().getProgressPercent() / 100.0);
|
||||||
|
setGraphic(progressIndicator);
|
||||||
|
Tooltip tt = new Tooltip();
|
||||||
|
String status = mixProgress.getMixStep().getMessage().substring(0, 1).toUpperCase(Locale.ROOT) + mixProgress.getMixStep().getMessage().substring(1);
|
||||||
|
tt.setText(status);
|
||||||
|
setTooltip(tt);
|
||||||
|
|
||||||
|
if(mixProgress.getMixStep() == MixStep.REGISTERED_INPUT) {
|
||||||
|
tt.setOnShowing(event -> {
|
||||||
|
Whirlpool whirlpool = AppServices.getWhirlpoolServices().getWhirlpool(utxoEntry.getWallet());
|
||||||
|
Whirlpool.RegisteredInputsService registeredInputsService = new Whirlpool.RegisteredInputsService(whirlpool, mixProgress.getPoolId());
|
||||||
|
registeredInputsService.setOnSucceeded(eventStateHandler -> {
|
||||||
|
if(registeredInputsService.getValue() != null) {
|
||||||
|
tt.setText(status + " (1 of " + registeredInputsService.getValue() + ")");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
registeredInputsService.start();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
setGraphic(null);
|
||||||
|
setTooltip(null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private ProgressIndicator getProgressIndicator() {
|
||||||
|
ProgressIndicator progressIndicator;
|
||||||
|
if(getGraphic() instanceof ProgressIndicator) {
|
||||||
|
progressIndicator = (ProgressIndicator)getGraphic();
|
||||||
|
} else {
|
||||||
|
progressIndicator = new ProgressBar();
|
||||||
|
}
|
||||||
|
|
||||||
|
return progressIndicator;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Glyph getMixGlyph() {
|
||||||
|
Glyph copyGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.RANDOM);
|
||||||
|
copyGlyph.setFontSize(12);
|
||||||
|
return copyGlyph;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Glyph getStopGlyph() {
|
||||||
|
Glyph copyGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.STOP_CIRCLE);
|
||||||
|
copyGlyph.setFontSize(12);
|
||||||
|
return copyGlyph;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Glyph getFailGlyph() {
|
||||||
|
Glyph failGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.EXCLAMATION_CIRCLE);
|
||||||
|
failGlyph.getStyleClass().add("fail-warning");
|
||||||
|
failGlyph.setFontSize(12);
|
||||||
|
return failGlyph;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class MixStatusContextMenu extends ContextMenu {
|
||||||
|
public MixStatusContextMenu(UtxoEntry utxoEntry, boolean isMixing) {
|
||||||
|
Whirlpool pool = AppServices.getWhirlpoolServices().getWhirlpool(utxoEntry.getWallet());
|
||||||
|
if(isMixing) {
|
||||||
|
MenuItem mixStop = new MenuItem("Stop Mixing");
|
||||||
|
if(pool != null) {
|
||||||
|
mixStop.disableProperty().bind(pool.mixingProperty().not());
|
||||||
|
}
|
||||||
|
mixStop.setGraphic(getStopGlyph());
|
||||||
|
mixStop.setOnAction(event -> {
|
||||||
|
hide();
|
||||||
|
Whirlpool whirlpool = AppServices.getWhirlpoolServices().getWhirlpool(utxoEntry.getWallet());
|
||||||
|
if(whirlpool != null) {
|
||||||
|
try {
|
||||||
|
whirlpool.mixStop(utxoEntry.getHashIndex());
|
||||||
|
} catch(WhirlpoolException e) {
|
||||||
|
AppServices.showErrorDialog("Error stopping mixing UTXO", e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
getItems().add(mixStop);
|
||||||
|
} else {
|
||||||
|
MenuItem mixNow = new MenuItem("Mix Now");
|
||||||
|
if(pool != null) {
|
||||||
|
mixNow.disableProperty().bind(pool.mixingProperty().not());
|
||||||
|
}
|
||||||
|
|
||||||
|
mixNow.setGraphic(getMixGlyph());
|
||||||
|
mixNow.setOnAction(event -> {
|
||||||
|
hide();
|
||||||
|
Whirlpool whirlpool = AppServices.getWhirlpoolServices().getWhirlpool(utxoEntry.getWallet());
|
||||||
|
if(whirlpool != null) {
|
||||||
|
try {
|
||||||
|
whirlpool.mix(utxoEntry.getHashIndex());
|
||||||
|
} catch(WhirlpoolException e) {
|
||||||
|
AppServices.showErrorDialog("Error mixing UTXO", e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
getItems().add(mixNow);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
package com.sparrowwallet.sparrow.control;
|
package com.sparrowwallet.sparrow.control;
|
||||||
|
|
||||||
import com.sparrowwallet.drongo.OsType;
|
|
||||||
import com.sparrowwallet.drongo.Utils;
|
import com.sparrowwallet.drongo.Utils;
|
||||||
import com.sparrowwallet.drongo.wallet.Bip39MnemonicCode;
|
import com.sparrowwallet.drongo.wallet.Bip39MnemonicCode;
|
||||||
import com.sparrowwallet.sparrow.AppServices;
|
import com.sparrowwallet.sparrow.AppServices;
|
||||||
|
|
@ -50,7 +49,8 @@ public class MnemonicGridDialog extends Dialog<List<String>> {
|
||||||
dialogPane.getStylesheets().add(AppServices.class.getResource("general.css").toExternalForm());
|
dialogPane.getStylesheets().add(AppServices.class.getResource("general.css").toExternalForm());
|
||||||
dialogPane.getStylesheets().add(AppServices.class.getResource("grid.css").toExternalForm());
|
dialogPane.getStylesheets().add(AppServices.class.getResource("grid.css").toExternalForm());
|
||||||
dialogPane.setHeaderText("Load a Border Wallets PDF, or generate a grid from a BIP39 seed.\nThen select 11 or 23 words in a pattern on the grid.\nThe order of selection is important!");
|
dialogPane.setHeaderText("Load a Border Wallets PDF, or generate a grid from a BIP39 seed.\nThen select 11 or 23 words in a pattern on the grid.\nThe order of selection is important!");
|
||||||
dialogPane.setGraphic(new DialogImage(DialogImage.Type.BORDERWALLETS));
|
javafx.scene.image.Image image = new Image("/image/border-wallets.png");
|
||||||
|
dialogPane.setGraphic(new ImageView(image));
|
||||||
|
|
||||||
String[][] emptyWordGrid = new String[128][GRID_COLUMN_COUNT];
|
String[][] emptyWordGrid = new String[128][GRID_COLUMN_COUNT];
|
||||||
Grid grid = getGrid(emptyWordGrid);
|
Grid grid = getGrid(emptyWordGrid);
|
||||||
|
|
@ -256,7 +256,7 @@ public class MnemonicGridDialog extends Dialog<List<String>> {
|
||||||
FileChooser fileChooser = new FileChooser();
|
FileChooser fileChooser = new FileChooser();
|
||||||
fileChooser.setTitle("Open PDF");
|
fileChooser.setTitle("Open PDF");
|
||||||
fileChooser.getExtensionFilters().addAll(
|
fileChooser.getExtensionFilters().addAll(
|
||||||
new FileChooser.ExtensionFilter("All Files", OsType.getCurrent().equals(OsType.UNIX) ? "*" : "*.*"),
|
new FileChooser.ExtensionFilter("All Files", org.controlsfx.tools.Platform.getCurrent().equals(org.controlsfx.tools.Platform.UNIX) ? "*" : "*.*"),
|
||||||
new FileChooser.ExtensionFilter("PDF", "*.pdf")
|
new FileChooser.ExtensionFilter("PDF", "*.pdf")
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
package com.sparrowwallet.sparrow.control;
|
package com.sparrowwallet.sparrow.control;
|
||||||
|
|
||||||
import com.sparrowwallet.drongo.wallet.DeterministicSeed;
|
|
||||||
import com.sparrowwallet.drongo.wallet.Keystore;
|
import com.sparrowwallet.drongo.wallet.Keystore;
|
||||||
import com.sparrowwallet.drongo.wallet.WalletModel;
|
import com.sparrowwallet.drongo.wallet.WalletModel;
|
||||||
import javafx.beans.property.SimpleListProperty;
|
import javafx.beans.property.SimpleListProperty;
|
||||||
|
|
@ -16,13 +15,10 @@ import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
public class MnemonicKeystoreDisplayPane extends MnemonicKeystorePane {
|
public class MnemonicKeystoreDisplayPane extends MnemonicKeystorePane {
|
||||||
private final DeterministicSeed.Type type;
|
|
||||||
|
|
||||||
public MnemonicKeystoreDisplayPane(Keystore keystore) {
|
public MnemonicKeystoreDisplayPane(Keystore keystore) {
|
||||||
super(keystore.getSeed().getType().getName(), keystore.getSeed().needsPassphrase() && (keystore.getSeed().getPassphrase() == null || keystore.getSeed().getPassphrase().length() > 0) ? "Passphrase entered" : "No passphrase", "", WalletModel.SEED);
|
super(keystore.getSeed().getType().getName(), keystore.getSeed().needsPassphrase() && (keystore.getSeed().getPassphrase() == null || keystore.getSeed().getPassphrase().length() > 0) ? "Passphrase entered" : "No passphrase", "", "image/" + WalletModel.SEED.getType() + ".png");
|
||||||
showHideLink.setVisible(false);
|
showHideLink.setVisible(false);
|
||||||
buttonBox.getChildren().clear();
|
buttonBox.getChildren().clear();
|
||||||
this.type = keystore.getSeed().getType();
|
|
||||||
|
|
||||||
showWordList(keystore.getSeed());
|
showWordList(keystore.getSeed());
|
||||||
}
|
}
|
||||||
|
|
@ -33,7 +29,7 @@ public class MnemonicKeystoreDisplayPane extends MnemonicKeystorePane {
|
||||||
vBox.setSpacing(10);
|
vBox.setSpacing(10);
|
||||||
|
|
||||||
wordsPane = new TilePane();
|
wordsPane = new TilePane();
|
||||||
wordsPane.setPrefRows(Math.ceilDiv(numWords, 3));
|
wordsPane.setPrefRows(numWords / 3);
|
||||||
wordsPane.setHgap(10);
|
wordsPane.setHgap(10);
|
||||||
wordsPane.setVgap(10);
|
wordsPane.setVgap(10);
|
||||||
wordsPane.setOrientation(Orientation.VERTICAL);
|
wordsPane.setOrientation(Orientation.VERTICAL);
|
||||||
|
|
@ -47,7 +43,7 @@ public class MnemonicKeystoreDisplayPane extends MnemonicKeystorePane {
|
||||||
wordEntriesProperty = new SimpleListProperty<>(wordEntryList);
|
wordEntriesProperty = new SimpleListProperty<>(wordEntryList);
|
||||||
List<WordEntry> wordEntries = new ArrayList<>(numWords);
|
List<WordEntry> wordEntries = new ArrayList<>(numWords);
|
||||||
for(int i = 0; i < numWords; i++) {
|
for(int i = 0; i < numWords; i++) {
|
||||||
wordEntries.add(new WordEntry(i, wordEntryList, getWordlistProvider()));
|
wordEntries.add(new WordEntry(i, wordEntryList));
|
||||||
}
|
}
|
||||||
for(int i = 0; i < numWords - 1; i++) {
|
for(int i = 0; i < numWords - 1; i++) {
|
||||||
wordEntries.get(i).setNextEntry(wordEntries.get(i + 1));
|
wordEntries.get(i).setNextEntry(wordEntries.get(i + 1));
|
||||||
|
|
@ -61,9 +57,4 @@ public class MnemonicKeystoreDisplayPane extends MnemonicKeystorePane {
|
||||||
stackPane.getChildren().add(vBox);
|
stackPane.getChildren().add(vBox);
|
||||||
return stackPane;
|
return stackPane;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
protected WordlistProvider getWordlistProvider() {
|
|
||||||
return getWordListProvider(type);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,7 @@ public class MnemonicKeystoreEntryPane extends MnemonicKeystorePane {
|
||||||
private boolean generated;
|
private boolean generated;
|
||||||
|
|
||||||
public MnemonicKeystoreEntryPane(String name, int numWords) {
|
public MnemonicKeystoreEntryPane(String name, int numWords) {
|
||||||
super(name, "Enter seed words", "", WalletModel.SEED);
|
super(name, "Enter seed words", "", "image/" + WalletModel.SEED.getType() + ".png");
|
||||||
showHideLink.setVisible(false);
|
showHideLink.setVisible(false);
|
||||||
buttonBox.getChildren().clear();
|
buttonBox.getChildren().clear();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -33,7 +33,6 @@ import java.util.Optional;
|
||||||
public class MnemonicKeystoreImportPane extends MnemonicKeystorePane {
|
public class MnemonicKeystoreImportPane extends MnemonicKeystorePane {
|
||||||
protected final Wallet wallet;
|
protected final Wallet wallet;
|
||||||
private final KeystoreMnemonicImport importer;
|
private final KeystoreMnemonicImport importer;
|
||||||
private final KeyDerivation defaultDerivation;
|
|
||||||
|
|
||||||
private SplitMenuButton importButton;
|
private SplitMenuButton importButton;
|
||||||
|
|
||||||
|
|
@ -44,11 +43,10 @@ public class MnemonicKeystoreImportPane extends MnemonicKeystorePane {
|
||||||
private Button confirmButton;
|
private Button confirmButton;
|
||||||
private List<String> generatedMnemonicCode;
|
private List<String> generatedMnemonicCode;
|
||||||
|
|
||||||
public MnemonicKeystoreImportPane(Wallet wallet, KeystoreMnemonicImport importer, KeyDerivation defaultDerivation) {
|
public MnemonicKeystoreImportPane(Wallet wallet, KeystoreMnemonicImport importer) {
|
||||||
super(importer.getName(), "Create or enter seed", importer.getKeystoreImportDescription(), importer.getWalletModel());
|
super(importer.getName(), "Create or enter seed", importer.getKeystoreImportDescription(), "image/" + importer.getWalletModel().getType() + ".png");
|
||||||
this.wallet = wallet;
|
this.wallet = wallet;
|
||||||
this.importer = importer;
|
this.importer = importer;
|
||||||
this.defaultDerivation = defaultDerivation;
|
|
||||||
|
|
||||||
createImportButton();
|
createImportButton();
|
||||||
buttonBox.getChildren().add(importButton);
|
buttonBox.getChildren().add(importButton);
|
||||||
|
|
@ -61,7 +59,7 @@ public class MnemonicKeystoreImportPane extends MnemonicKeystorePane {
|
||||||
importButton.getStyleClass().add("default-button");
|
importButton.getStyleClass().add("default-button");
|
||||||
importButton.setOnAction(event -> {
|
importButton.setOnAction(event -> {
|
||||||
importButton.setDisable(true);
|
importButton.setDisable(true);
|
||||||
importKeystore(getDefaultDerivation(), false);
|
importKeystore(wallet.getScriptType().getDefaultDerivation(), false);
|
||||||
});
|
});
|
||||||
String[] accounts = new String[] {"Import Default Account #0", "Import Account #1", "Import Account #2", "Import Account #3", "Import Account #4", "Import Account #5", "Import Account #6", "Import Account #7", "Import Account #8", "Import Account #9"};
|
String[] accounts = new String[] {"Import Default Account #0", "Import Account #1", "Import Account #2", "Import Account #3", "Import Account #4", "Import Account #5", "Import Account #6", "Import Account #7", "Import Account #8", "Import Account #9"};
|
||||||
int scriptAccountsLength = ScriptType.P2SH.equals(wallet.getScriptType()) ? 1 : accounts.length;
|
int scriptAccountsLength = ScriptType.P2SH.equals(wallet.getScriptType()) ? 1 : accounts.length;
|
||||||
|
|
@ -79,10 +77,6 @@ public class MnemonicKeystoreImportPane extends MnemonicKeystorePane {
|
||||||
importButton.setVisible(false);
|
importButton.setVisible(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
private List<ChildNumber> getDefaultDerivation() {
|
|
||||||
return defaultDerivation == null || defaultDerivation.getDerivation().isEmpty() ? wallet.getScriptType().getDefaultDerivation() : defaultDerivation.getDerivation();
|
|
||||||
}
|
|
||||||
|
|
||||||
protected void enterMnemonic(int numWords) {
|
protected void enterMnemonic(int numWords) {
|
||||||
generatedMnemonicCode = null;
|
generatedMnemonicCode = null;
|
||||||
super.enterMnemonic(numWords);
|
super.enterMnemonic(numWords);
|
||||||
|
|
@ -249,7 +243,7 @@ public class MnemonicKeystoreImportPane extends MnemonicKeystorePane {
|
||||||
setDescription("Ready to import");
|
setDescription("Ready to import");
|
||||||
showHideLink.setText("Show Derivation...");
|
showHideLink.setText("Show Derivation...");
|
||||||
showHideLink.setVisible(false);
|
showHideLink.setVisible(false);
|
||||||
setContent(getDerivationEntry(getDefaultDerivation()));
|
setContent(getDerivationEntry(wallet.getScriptType().getDefaultDerivation()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,11 +2,8 @@ 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.WalletModel;
|
|
||||||
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.FontAwesome5;
|
||||||
import javafx.application.Platform;
|
import javafx.application.Platform;
|
||||||
import javafx.beans.property.IntegerProperty;
|
import javafx.beans.property.IntegerProperty;
|
||||||
import javafx.beans.property.SimpleIntegerProperty;
|
import javafx.beans.property.SimpleIntegerProperty;
|
||||||
|
|
@ -52,8 +49,8 @@ public class MnemonicKeystorePane extends TitledDescriptionPane {
|
||||||
protected final SimpleStringProperty passphraseProperty = new SimpleStringProperty("");
|
protected final SimpleStringProperty passphraseProperty = new SimpleStringProperty("");
|
||||||
protected IntegerProperty defaultWordSizeProperty;
|
protected IntegerProperty defaultWordSizeProperty;
|
||||||
|
|
||||||
public MnemonicKeystorePane(String title, String description, String content, WalletModel walletModel) {
|
public MnemonicKeystorePane(String title, String description, String content, String imageUrl) {
|
||||||
super(title, description, content, walletModel);
|
super(title, description, content, imageUrl);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
@ -114,9 +111,23 @@ public class MnemonicKeystorePane extends TitledDescriptionPane {
|
||||||
wordEntry.getEditor().setText(words.get(i));
|
wordEntry.getEditor().setText(words.get(i));
|
||||||
wordEntry.getEditor().setEditable(false);
|
wordEntry.getEditor().setEditable(false);
|
||||||
} else {
|
} else {
|
||||||
AppServices.runAfterDelay(500, () -> {
|
ScheduledService<Void> service = new ScheduledService<>() {
|
||||||
|
@Override
|
||||||
|
protected Task<Void> createTask() {
|
||||||
|
return new Task<>() {
|
||||||
|
@Override
|
||||||
|
protected Void call() {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
service.setDelay(Duration.millis(500));
|
||||||
|
service.setOnSucceeded(event1 -> {
|
||||||
|
service.cancel();
|
||||||
Platform.runLater(() -> wordEntry.getEditor().requestFocus());
|
Platform.runLater(() -> wordEntry.getEditor().requestFocus());
|
||||||
});
|
});
|
||||||
|
service.start();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -142,10 +153,6 @@ public class MnemonicKeystorePane extends TitledDescriptionPane {
|
||||||
|
|
||||||
protected void showWordList(DeterministicSeed seed) {
|
protected void showWordList(DeterministicSeed seed) {
|
||||||
List<String> words = seed.getMnemonicCode();
|
List<String> words = seed.getMnemonicCode();
|
||||||
showWordList(words);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected void showWordList(List<String> words) {
|
|
||||||
setContent(getMnemonicWordsEntry(words.size(), true, true));
|
setContent(getMnemonicWordsEntry(words.size(), true, true));
|
||||||
setExpanded(true);
|
setExpanded(true);
|
||||||
|
|
||||||
|
|
@ -168,7 +175,7 @@ public class MnemonicKeystorePane extends TitledDescriptionPane {
|
||||||
vBox.setSpacing(10);
|
vBox.setSpacing(10);
|
||||||
|
|
||||||
wordsPane = new TilePane();
|
wordsPane = new TilePane();
|
||||||
wordsPane.setPrefRows(Math.ceilDiv(numWords, 3));
|
wordsPane.setPrefRows(numWords/3);
|
||||||
wordsPane.setHgap(10);
|
wordsPane.setHgap(10);
|
||||||
wordsPane.setVgap(10);
|
wordsPane.setVgap(10);
|
||||||
wordsPane.setOrientation(Orientation.VERTICAL);
|
wordsPane.setOrientation(Orientation.VERTICAL);
|
||||||
|
|
@ -182,7 +189,7 @@ public class MnemonicKeystorePane extends TitledDescriptionPane {
|
||||||
wordEntriesProperty = new SimpleListProperty<>(wordEntryList);
|
wordEntriesProperty = new SimpleListProperty<>(wordEntryList);
|
||||||
List<WordEntry> wordEntries = new ArrayList<>(numWords);
|
List<WordEntry> wordEntries = new ArrayList<>(numWords);
|
||||||
for(int i = 0; i < numWords; i++) {
|
for(int i = 0; i < numWords; i++) {
|
||||||
wordEntries.add(new WordEntry(i, wordEntryList, getWordlistProvider()));
|
wordEntries.add(new WordEntry(i, wordEntryList));
|
||||||
}
|
}
|
||||||
for(int i = 0; i < numWords - 1; i++) {
|
for(int i = 0; i < numWords - 1; i++) {
|
||||||
wordEntries.get(i).setNextEntry(wordEntries.get(i + 1));
|
wordEntries.get(i).setNextEntry(wordEntries.get(i + 1));
|
||||||
|
|
@ -208,7 +215,7 @@ public class MnemonicKeystorePane extends TitledDescriptionPane {
|
||||||
buttonPane.getChildren().add(leftBox);
|
buttonPane.getChildren().add(leftBox);
|
||||||
AnchorPane.setLeftAnchor(leftBox, 0.0);
|
AnchorPane.setLeftAnchor(leftBox, 0.0);
|
||||||
|
|
||||||
validLabel = new Label("Valid checksum", GlyphUtils.getSuccessGlyph());
|
validLabel = new Label("Valid checksum", getValidGlyph());
|
||||||
validLabel.setContentDisplay(ContentDisplay.LEFT);
|
validLabel.setContentDisplay(ContentDisplay.LEFT);
|
||||||
validLabel.setGraphicTextGap(5.0);
|
validLabel.setGraphicTextGap(5.0);
|
||||||
validLabel.managedProperty().bind(validLabel.visibleProperty());
|
validLabel.managedProperty().bind(validLabel.visibleProperty());
|
||||||
|
|
@ -217,7 +224,7 @@ public class MnemonicKeystorePane extends TitledDescriptionPane {
|
||||||
AnchorPane.setTopAnchor(validLabel, 5.0);
|
AnchorPane.setTopAnchor(validLabel, 5.0);
|
||||||
AnchorPane.setLeftAnchor(validLabel, 0.0);
|
AnchorPane.setLeftAnchor(validLabel, 0.0);
|
||||||
|
|
||||||
invalidLabel = new Label("Invalid checksum", GlyphUtils.getInvalidGlyph());
|
invalidLabel = new Label("Invalid checksum", getInvalidGlyph());
|
||||||
invalidLabel.setContentDisplay(ContentDisplay.LEFT);
|
invalidLabel.setContentDisplay(ContentDisplay.LEFT);
|
||||||
invalidLabel.setGraphicTextGap(5.0);
|
invalidLabel.setGraphicTextGap(5.0);
|
||||||
invalidLabel.managedProperty().bind(invalidLabel.visibleProperty());
|
invalidLabel.managedProperty().bind(invalidLabel.visibleProperty());
|
||||||
|
|
@ -235,7 +242,7 @@ public class MnemonicKeystorePane extends TitledDescriptionPane {
|
||||||
empty = false;
|
empty = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if(!getWordlistProvider().isValid(word)) {
|
if(!WordEntry.isValid(word)) {
|
||||||
validWords = false;
|
validWords = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -271,20 +278,13 @@ public class MnemonicKeystorePane extends TitledDescriptionPane {
|
||||||
//nothing by default
|
//nothing by default
|
||||||
}
|
}
|
||||||
|
|
||||||
protected WordlistProvider getWordlistProvider() {
|
|
||||||
return getWordListProvider(DeterministicSeed.Type.BIP39);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected WordlistProvider getWordListProvider(DeterministicSeed.Type type) {
|
|
||||||
return type == DeterministicSeed.Type.SLIP39 ? new Slip39WordlistProvider() : new Bip39WordlistProvider();
|
|
||||||
}
|
|
||||||
|
|
||||||
protected static class WordEntry extends HBox {
|
protected static class WordEntry extends HBox {
|
||||||
|
private static List<String> wordList;
|
||||||
private final TextField wordField;
|
private final TextField wordField;
|
||||||
private WordEntry nextEntry;
|
private WordEntry nextEntry;
|
||||||
private TextField nextField;
|
private TextField nextField;
|
||||||
|
|
||||||
public WordEntry(int wordNumber, ObservableList<String> wordEntryList, WordlistProvider wordlistProvider) {
|
public WordEntry(int wordNumber, ObservableList<String> wordEntryList) {
|
||||||
super();
|
super();
|
||||||
setAlignment(Pos.CENTER_RIGHT);
|
setAlignment(Pos.CENTER_RIGHT);
|
||||||
|
|
||||||
|
|
@ -302,7 +302,7 @@ public class MnemonicKeystorePane extends TitledDescriptionPane {
|
||||||
for(int i = 0; i < words.length; i++) {
|
for(int i = 0; i < words.length; i++) {
|
||||||
String word = words[i];
|
String word = words[i];
|
||||||
if(entry.nextField != null) {
|
if(entry.nextField != null) {
|
||||||
if(i == words.length - 2 && wordlistProvider.isValid(word)) {
|
if(i == words.length - 2 && isValid(word)) {
|
||||||
label.requestFocus();
|
label.requestFocus();
|
||||||
} else {
|
} else {
|
||||||
entry.nextField.requestFocus();
|
entry.nextField.requestFocus();
|
||||||
|
|
@ -321,7 +321,6 @@ public class MnemonicKeystorePane extends TitledDescriptionPane {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
wordField.setMaxWidth(100);
|
wordField.setMaxWidth(100);
|
||||||
wordField.setAccessibleText("Word " + (wordNumber + 1));
|
|
||||||
TextFormatter<?> formatter = new TextFormatter<>((TextFormatter.Change change) -> {
|
TextFormatter<?> formatter = new TextFormatter<>((TextFormatter.Change change) -> {
|
||||||
String text = change.getText();
|
String text = change.getText();
|
||||||
// if text was added, fix the text to fit the requirements
|
// if text was added, fix the text to fit the requirements
|
||||||
|
|
@ -336,7 +335,8 @@ public class MnemonicKeystorePane extends TitledDescriptionPane {
|
||||||
});
|
});
|
||||||
wordField.setTextFormatter(formatter);
|
wordField.setTextFormatter(formatter);
|
||||||
|
|
||||||
AutoCompletionBinding<String> autoCompletionBinding = TextFields.bindAutoCompletion(wordField, new WordlistSuggestionProvider(wordlistProvider, wordNumber, wordEntryList));
|
wordList = Bip39MnemonicCode.INSTANCE.getWordList();
|
||||||
|
AutoCompletionBinding<String> autoCompletionBinding = TextFields.bindAutoCompletion(wordField, new WordlistSuggestionProvider(wordList, wordNumber, wordEntryList));
|
||||||
autoCompletionBinding.setDelay(50);
|
autoCompletionBinding.setDelay(50);
|
||||||
autoCompletionBinding.setOnAutoCompleted(event -> {
|
autoCompletionBinding.setOnAutoCompleted(event -> {
|
||||||
if(nextField != null) {
|
if(nextField != null) {
|
||||||
|
|
@ -357,7 +357,7 @@ public class MnemonicKeystorePane extends TitledDescriptionPane {
|
||||||
ValidationSupport validationSupport = new ValidationSupport();
|
ValidationSupport validationSupport = new ValidationSupport();
|
||||||
validationSupport.setValidationDecorator(new StyleClassValidationDecoration());
|
validationSupport.setValidationDecorator(new StyleClassValidationDecoration());
|
||||||
validationSupport.registerValidator(wordField, Validator.combine(
|
validationSupport.registerValidator(wordField, Validator.combine(
|
||||||
(Control c, String newValue) -> ValidationResult.fromErrorIf( c, "Invalid word", (newValue.length() > 0 || !lastWord) && !wordlistProvider.isValid(newValue))
|
(Control c, String newValue) -> ValidationResult.fromErrorIf( c, "Invalid word", (newValue.length() > 0 || !lastWord) && !wordList.contains(newValue))
|
||||||
));
|
));
|
||||||
|
|
||||||
wordField.textProperty().addListener((observable, oldValue, newValue) -> {
|
wordField.textProperty().addListener((observable, oldValue, newValue) -> {
|
||||||
|
|
@ -378,24 +378,28 @@ public class MnemonicKeystorePane extends TitledDescriptionPane {
|
||||||
public void setNextField(TextField field) {
|
public void setNextField(TextField field) {
|
||||||
this.nextField = field;
|
this.nextField = field;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static boolean isValid(String word) {
|
||||||
|
return wordList.contains(word);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected static class WordlistSuggestionProvider implements Callback<AutoCompletionBinding.ISuggestionRequest, Collection<String>> {
|
protected static class WordlistSuggestionProvider implements Callback<AutoCompletionBinding.ISuggestionRequest, Collection<String>> {
|
||||||
private final WordlistProvider wordlistProvider;
|
private final List<String> wordList;
|
||||||
private final int wordNumber;
|
private final int wordNumber;
|
||||||
private final ObservableList<String> wordEntryList;
|
private final ObservableList<String> wordEntryList;
|
||||||
|
|
||||||
public WordlistSuggestionProvider(WordlistProvider wordlistProvider, int wordNumber, ObservableList<String> wordEntryList) {
|
public WordlistSuggestionProvider(List<String> wordList, int wordNumber, ObservableList<String> wordEntryList) {
|
||||||
this.wordlistProvider = wordlistProvider;
|
this.wordList = wordList;
|
||||||
this.wordNumber = wordNumber;
|
this.wordNumber = wordNumber;
|
||||||
this.wordEntryList = wordEntryList;
|
this.wordEntryList = wordEntryList;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Collection<String> call(AutoCompletionBinding.ISuggestionRequest request) {
|
public Collection<String> call(AutoCompletionBinding.ISuggestionRequest request) {
|
||||||
if(wordlistProvider.supportsPossibleLastWords() && wordNumber == wordEntryList.size() - 1 && allPreviousWordsValid()) {
|
if(wordNumber == wordEntryList.size() - 1 && allPreviousWordsValid()) {
|
||||||
try {
|
try {
|
||||||
List<String> possibleLastWords = wordlistProvider.getPossibleLastWords(wordEntryList.subList(0, wordEntryList.size() - 1));
|
List<String> possibleLastWords = Bip39MnemonicCode.INSTANCE.getPossibleLastWords(wordEntryList.subList(0, wordEntryList.size() - 1));
|
||||||
if(!request.getUserText().isEmpty()) {
|
if(!request.getUserText().isEmpty()) {
|
||||||
possibleLastWords.removeIf(s -> !s.startsWith(request.getUserText()));
|
possibleLastWords.removeIf(s -> !s.startsWith(request.getUserText()));
|
||||||
}
|
}
|
||||||
|
|
@ -408,7 +412,7 @@ public class MnemonicKeystorePane extends TitledDescriptionPane {
|
||||||
|
|
||||||
List<String> suggestions = new ArrayList<>();
|
List<String> suggestions = new ArrayList<>();
|
||||||
if(!request.getUserText().isEmpty()) {
|
if(!request.getUserText().isEmpty()) {
|
||||||
for(String word : wordlistProvider.getWordlist()) {
|
for(String word : wordList) {
|
||||||
if(word.startsWith(request.getUserText())) {
|
if(word.startsWith(request.getUserText())) {
|
||||||
suggestions.add(word);
|
suggestions.add(word);
|
||||||
}
|
}
|
||||||
|
|
@ -420,7 +424,7 @@ public class MnemonicKeystorePane extends TitledDescriptionPane {
|
||||||
|
|
||||||
private boolean allPreviousWordsValid() {
|
private boolean allPreviousWordsValid() {
|
||||||
for(int i = 0; i < wordEntryList.size() - 1; i++) {
|
for(int i = 0; i < wordEntryList.size() - 1; i++) {
|
||||||
if(!wordlistProvider.isValid(wordEntryList.get(i))) {
|
if(!WordEntry.isValid(wordEntryList.get(i))) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -481,53 +485,17 @@ public class MnemonicKeystorePane extends TitledDescriptionPane {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected interface WordlistProvider {
|
public static Glyph getValidGlyph() {
|
||||||
List<String> getWordlist();
|
Glyph validGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.CHECK_CIRCLE);
|
||||||
boolean isValid(String word);
|
validGlyph.getStyleClass().add("success");
|
||||||
boolean supportsPossibleLastWords();
|
validGlyph.setFontSize(12);
|
||||||
List<String> getPossibleLastWords(List<String> previousWords) throws MnemonicException.MnemonicLengthException, MnemonicException.MnemonicWordException;
|
return validGlyph;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static class Bip39WordlistProvider implements WordlistProvider {
|
public static Glyph getInvalidGlyph() {
|
||||||
@Override
|
Glyph invalidGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.EXCLAMATION_CIRCLE);
|
||||||
public List<String> getWordlist() {
|
invalidGlyph.getStyleClass().add("failure");
|
||||||
return Bip39MnemonicCode.INSTANCE.getWordList();
|
invalidGlyph.setFontSize(12);
|
||||||
}
|
return invalidGlyph;
|
||||||
|
|
||||||
public boolean isValid(String word) {
|
|
||||||
return getWordlist().contains(word);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean supportsPossibleLastWords() {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public List<String> getPossibleLastWords(List<String> previousWords) throws MnemonicException.MnemonicLengthException, MnemonicException.MnemonicWordException {
|
|
||||||
return Bip39MnemonicCode.INSTANCE.getPossibleLastWords(previousWords);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static class Slip39WordlistProvider implements WordlistProvider {
|
|
||||||
@Override
|
|
||||||
public List<String> getWordlist() {
|
|
||||||
return Slip39MnemonicCode.INSTANCE.getWordList();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean isValid(String word) {
|
|
||||||
return getWordlist().contains(word);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean supportsPossibleLastWords() {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public List<String> getPossibleLastWords(List<String> previousWords) {
|
|
||||||
throw new UnsupportedOperationException();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,319 +0,0 @@
|
||||||
package com.sparrowwallet.sparrow.control;
|
|
||||||
|
|
||||||
import com.sparrowwallet.drongo.KeyDerivation;
|
|
||||||
import com.sparrowwallet.drongo.crypto.ChildNumber;
|
|
||||||
import com.sparrowwallet.drongo.protocol.ScriptType;
|
|
||||||
import com.sparrowwallet.drongo.wallet.DeterministicSeed;
|
|
||||||
import com.sparrowwallet.drongo.wallet.Keystore;
|
|
||||||
import com.sparrowwallet.drongo.wallet.MnemonicException;
|
|
||||||
import com.sparrowwallet.drongo.wallet.Wallet;
|
|
||||||
import com.sparrowwallet.drongo.wallet.slip39.Share;
|
|
||||||
import com.sparrowwallet.sparrow.EventManager;
|
|
||||||
import com.sparrowwallet.sparrow.event.KeystoreImportEvent;
|
|
||||||
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
|
|
||||||
import com.sparrowwallet.sparrow.glyphfont.GlyphUtils;
|
|
||||||
import com.sparrowwallet.sparrow.io.ImportException;
|
|
||||||
import com.sparrowwallet.sparrow.io.KeystoreMnemonicShareImport;
|
|
||||||
import com.sparrowwallet.sparrow.io.Slip39;
|
|
||||||
import javafx.beans.property.SimpleIntegerProperty;
|
|
||||||
import javafx.geometry.Insets;
|
|
||||||
import javafx.geometry.Pos;
|
|
||||||
import javafx.scene.Node;
|
|
||||||
import javafx.scene.control.*;
|
|
||||||
import javafx.scene.layout.HBox;
|
|
||||||
import javafx.scene.layout.Priority;
|
|
||||||
import org.controlsfx.glyphfont.Glyph;
|
|
||||||
import org.controlsfx.validation.ValidationResult;
|
|
||||||
import org.controlsfx.validation.ValidationSupport;
|
|
||||||
import org.controlsfx.validation.Validator;
|
|
||||||
import org.controlsfx.validation.decoration.StyleClassValidationDecoration;
|
|
||||||
|
|
||||||
import java.util.*;
|
|
||||||
|
|
||||||
public class MnemonicShareKeystoreImportPane extends MnemonicKeystorePane {
|
|
||||||
protected final Wallet wallet;
|
|
||||||
private final KeystoreMnemonicShareImport importer;
|
|
||||||
private final KeyDerivation defaultDerivation;
|
|
||||||
private final List<List<String>> mnemonicShares = new ArrayList<>();
|
|
||||||
|
|
||||||
private SplitMenuButton importButton;
|
|
||||||
|
|
||||||
private Button calculateButton;
|
|
||||||
private Button backButton;
|
|
||||||
private Button nextButton;
|
|
||||||
private int currentShare;
|
|
||||||
|
|
||||||
public MnemonicShareKeystoreImportPane(Wallet wallet, KeystoreMnemonicShareImport importer, KeyDerivation defaultDerivation) {
|
|
||||||
super(importer.getName(), "Enter seed share", importer.getKeystoreImportDescription(), importer.getWalletModel());
|
|
||||||
this.wallet = wallet;
|
|
||||||
this.importer = importer;
|
|
||||||
this.defaultDerivation = defaultDerivation;
|
|
||||||
|
|
||||||
createImportButton();
|
|
||||||
buttonBox.getChildren().add(importButton);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected Control createButton() {
|
|
||||||
createEnterMnemonicButton();
|
|
||||||
return enterMnemonicButton;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void createEnterMnemonicButton() {
|
|
||||||
enterMnemonicButton = new SplitMenuButton();
|
|
||||||
enterMnemonicButton.setAlignment(Pos.CENTER_RIGHT);
|
|
||||||
enterMnemonicButton.setText("Use 20 Words");
|
|
||||||
defaultWordSizeProperty = new SimpleIntegerProperty(20);
|
|
||||||
defaultWordSizeProperty.addListener((observable, oldValue, newValue) -> {
|
|
||||||
enterMnemonicButton.setText("Use " + newValue + " Words");
|
|
||||||
});
|
|
||||||
enterMnemonicButton.setOnAction(event -> {
|
|
||||||
resetShares();
|
|
||||||
enterMnemonic(defaultWordSizeProperty.get());
|
|
||||||
});
|
|
||||||
int[] numberWords = new int[] {20, 33};
|
|
||||||
for(int i = 0; i < numberWords.length; i++) {
|
|
||||||
MenuItem item = new MenuItem("Use " + numberWords[i] + " Words");
|
|
||||||
final int words = numberWords[i];
|
|
||||||
item.setOnAction(event -> {
|
|
||||||
resetShares();
|
|
||||||
defaultWordSizeProperty.set(words);
|
|
||||||
enterMnemonic(words);
|
|
||||||
});
|
|
||||||
enterMnemonicButton.getItems().add(item);
|
|
||||||
}
|
|
||||||
enterMnemonicButton.managedProperty().bind(enterMnemonicButton.visibleProperty());
|
|
||||||
}
|
|
||||||
|
|
||||||
protected List<Node> createRightButtons() {
|
|
||||||
calculateButton = new Button("Create Keystore");
|
|
||||||
calculateButton.setDefaultButton(true);
|
|
||||||
calculateButton.setOnAction(event -> {
|
|
||||||
prepareImport();
|
|
||||||
});
|
|
||||||
calculateButton.managedProperty().bind(calculateButton.visibleProperty());
|
|
||||||
calculateButton.setTooltip(new Tooltip("Create the keystore from the provided shares"));
|
|
||||||
calculateButton.setVisible(false);
|
|
||||||
|
|
||||||
backButton = new Button("Back");
|
|
||||||
backButton.setOnAction(event -> {
|
|
||||||
lastShare();
|
|
||||||
});
|
|
||||||
backButton.managedProperty().bind(backButton.visibleProperty());
|
|
||||||
backButton.setTooltip(new Tooltip("Display the last share added"));
|
|
||||||
backButton.setVisible(currentShare > 0);
|
|
||||||
|
|
||||||
nextButton = new Button("Next");
|
|
||||||
nextButton.setOnAction(event -> {
|
|
||||||
nextShare();
|
|
||||||
});
|
|
||||||
nextButton.managedProperty().bind(nextButton.visibleProperty());
|
|
||||||
nextButton.setTooltip(new Tooltip("Add the next share"));
|
|
||||||
nextButton.visibleProperty().bind(calculateButton.visibleProperty().not());
|
|
||||||
nextButton.setDefaultButton(true);
|
|
||||||
nextButton.setDisable(true);
|
|
||||||
|
|
||||||
return List.of(backButton, nextButton, calculateButton);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void enterMnemonic(int numWords) {
|
|
||||||
super.enterMnemonic(numWords);
|
|
||||||
setDescription("Enter existing share");
|
|
||||||
}
|
|
||||||
|
|
||||||
private void resetShares() {
|
|
||||||
currentShare = 0;
|
|
||||||
mnemonicShares.clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void lastShare() {
|
|
||||||
currentShare--;
|
|
||||||
showWordList(mnemonicShares.get(currentShare));
|
|
||||||
}
|
|
||||||
|
|
||||||
private void nextShare() {
|
|
||||||
if(currentShare == mnemonicShares.size()) {
|
|
||||||
mnemonicShares.add(wordEntriesProperty.get());
|
|
||||||
} else {
|
|
||||||
mnemonicShares.set(currentShare, wordEntriesProperty.get());
|
|
||||||
}
|
|
||||||
|
|
||||||
currentShare++;
|
|
||||||
|
|
||||||
if(currentShare < mnemonicShares.size()) {
|
|
||||||
showWordList(mnemonicShares.get(currentShare));
|
|
||||||
} else {
|
|
||||||
setContent(getMnemonicWordsEntry(defaultWordSizeProperty.get(), true, true));
|
|
||||||
}
|
|
||||||
setExpanded(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected void onWordChange(boolean empty, boolean validWords, boolean validChecksum) {
|
|
||||||
boolean validSet = false;
|
|
||||||
boolean complete = false;
|
|
||||||
if(!empty && validWords) {
|
|
||||||
try {
|
|
||||||
Share.fromMnemonic(String.join(" ", wordEntriesProperty.get()));
|
|
||||||
validChecksum = true;
|
|
||||||
|
|
||||||
List<List<String>> existing = new ArrayList<>(mnemonicShares);
|
|
||||||
if(currentShare >= mnemonicShares.size()) {
|
|
||||||
existing.add(wordEntriesProperty.get());
|
|
||||||
}
|
|
||||||
|
|
||||||
importer.getKeystore(wallet.getScriptType().getDefaultDerivation(), existing, passphraseProperty.get());
|
|
||||||
validSet = true;
|
|
||||||
complete = true;
|
|
||||||
} catch(MnemonicException e) {
|
|
||||||
invalidLabel.setText(e.getTitle());
|
|
||||||
invalidLabel.setTooltip(new Tooltip(e.getMessage()));
|
|
||||||
} catch(Slip39.Slip39ProgressException e) {
|
|
||||||
validSet = true;
|
|
||||||
invalidLabel.setText(e.getTitle());
|
|
||||||
invalidLabel.setTooltip(new Tooltip(e.getMessage()));
|
|
||||||
} catch(ImportException e) {
|
|
||||||
if(e.getCause() instanceof MnemonicException mnemonicException) {
|
|
||||||
invalidLabel.setText(mnemonicException.getTitle());
|
|
||||||
invalidLabel.setTooltip(new Tooltip(mnemonicException.getMessage()));
|
|
||||||
} else {
|
|
||||||
invalidLabel.setText("Import Error");
|
|
||||||
invalidLabel.setTooltip(new Tooltip(e.getMessage()));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
calculateButton.setVisible(complete);
|
|
||||||
backButton.setVisible(currentShare > 0 && !complete);
|
|
||||||
nextButton.setDisable(!validChecksum || !validSet);
|
|
||||||
validLabel.setVisible(complete);
|
|
||||||
validLabel.setText(mnemonicShares.isEmpty() ? "Valid checksum" : "Completed share set");
|
|
||||||
invalidLabel.setVisible(!complete && !empty);
|
|
||||||
invalidLabel.setGraphic(validChecksum && validSet ? getIncompleteGlyph() : GlyphUtils.getFailureGlyph());
|
|
||||||
}
|
|
||||||
|
|
||||||
private void createImportButton() {
|
|
||||||
importButton = new SplitMenuButton();
|
|
||||||
importButton.setAlignment(Pos.CENTER_RIGHT);
|
|
||||||
importButton.setText("Import Keystore");
|
|
||||||
importButton.getStyleClass().add("default-button");
|
|
||||||
importButton.setOnAction(event -> {
|
|
||||||
importButton.setDisable(true);
|
|
||||||
importKeystore(getDefaultDerivation(), false);
|
|
||||||
});
|
|
||||||
String[] accounts = new String[] {"Import Default Account #0", "Import Account #1", "Import Account #2", "Import Account #3", "Import Account #4", "Import Account #5", "Import Account #6", "Import Account #7", "Import Account #8", "Import Account #9"};
|
|
||||||
int scriptAccountsLength = ScriptType.P2SH.equals(wallet.getScriptType()) ? 1 : accounts.length;
|
|
||||||
for(int i = 0; i < scriptAccountsLength; i++) {
|
|
||||||
MenuItem item = new MenuItem(accounts[i]);
|
|
||||||
final List<ChildNumber> derivation = wallet.getScriptType().getDefaultDerivation(i);
|
|
||||||
item.setOnAction(event -> {
|
|
||||||
importButton.setDisable(true);
|
|
||||||
importKeystore(derivation, false);
|
|
||||||
});
|
|
||||||
importButton.getItems().add(item);
|
|
||||||
}
|
|
||||||
|
|
||||||
importButton.managedProperty().bind(importButton.visibleProperty());
|
|
||||||
importButton.setVisible(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
private List<ChildNumber> getDefaultDerivation() {
|
|
||||||
return defaultDerivation == null || defaultDerivation.getDerivation().isEmpty() ? wallet.getScriptType().getDefaultDerivation() : defaultDerivation.getDerivation();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void prepareImport() {
|
|
||||||
nextShare();
|
|
||||||
backButton.setVisible(false);
|
|
||||||
|
|
||||||
if(importKeystore(wallet.getScriptType().getDefaultDerivation(), true)) {
|
|
||||||
setExpanded(true);
|
|
||||||
enterMnemonicButton.setVisible(false);
|
|
||||||
importButton.setVisible(true);
|
|
||||||
importButton.setDisable(false);
|
|
||||||
setDescription("Ready to import");
|
|
||||||
showHideLink.setText("Show Derivation...");
|
|
||||||
showHideLink.setVisible(false);
|
|
||||||
setContent(getDerivationEntry(getDefaultDerivation()));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean importKeystore(List<ChildNumber> derivation, boolean dryrun) {
|
|
||||||
importButton.setDisable(true);
|
|
||||||
try {
|
|
||||||
Keystore keystore = importer.getKeystore(derivation, mnemonicShares, passphraseProperty.get());
|
|
||||||
if(!dryrun) {
|
|
||||||
if(passphraseProperty.get() != null && !passphraseProperty.get().isEmpty()) {
|
|
||||||
KeystorePassphraseDialog keystorePassphraseDialog = new KeystorePassphraseDialog(null, keystore, true);
|
|
||||||
keystorePassphraseDialog.initOwner(this.getScene().getWindow());
|
|
||||||
Optional<String> optPassphrase = keystorePassphraseDialog.showAndWait();
|
|
||||||
if(optPassphrase.isEmpty() || !optPassphrase.get().equals(passphraseProperty.get())) {
|
|
||||||
throw new ImportException("Re-entered passphrase did not match");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
EventManager.get().post(new KeystoreImportEvent(keystore));
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
} catch (ImportException e) {
|
|
||||||
String errorMessage = e.getMessage();
|
|
||||||
if(e.getCause() instanceof MnemonicException.MnemonicChecksumException) {
|
|
||||||
errorMessage = "Invalid word list - checksum incorrect";
|
|
||||||
} else if(e.getCause() != null && e.getCause().getMessage() != null && !e.getCause().getMessage().isEmpty()) {
|
|
||||||
errorMessage = e.getCause().getMessage();
|
|
||||||
}
|
|
||||||
setError("Import Error", errorMessage + ".");
|
|
||||||
importButton.setDisable(false);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private Node getDerivationEntry(List<ChildNumber> derivation) {
|
|
||||||
TextField derivationField = new TextField();
|
|
||||||
derivationField.setPromptText("Derivation path");
|
|
||||||
derivationField.setText(KeyDerivation.writePath(derivation));
|
|
||||||
HBox.setHgrow(derivationField, Priority.ALWAYS);
|
|
||||||
|
|
||||||
ValidationSupport validationSupport = new ValidationSupport();
|
|
||||||
validationSupport.setValidationDecorator(new StyleClassValidationDecoration());
|
|
||||||
validationSupport.registerValidator(derivationField, Validator.combine(
|
|
||||||
Validator.createEmptyValidator("Derivation is required"),
|
|
||||||
(Control c, String newValue) -> ValidationResult.fromErrorIf( c, "Invalid derivation", !KeyDerivation.isValid(newValue))
|
|
||||||
));
|
|
||||||
|
|
||||||
Button importDerivationButton = new Button("Import Custom Derivation Keystore");
|
|
||||||
importDerivationButton.setDisable(true);
|
|
||||||
importDerivationButton.setOnAction(event -> {
|
|
||||||
showHideLink.setVisible(true);
|
|
||||||
setExpanded(false);
|
|
||||||
List<ChildNumber> importDerivation = KeyDerivation.parsePath(derivationField.getText());
|
|
||||||
importKeystore(importDerivation, false);
|
|
||||||
});
|
|
||||||
|
|
||||||
derivationField.textProperty().addListener((observable, oldValue, newValue) -> {
|
|
||||||
importButton.setDisable(newValue.isEmpty() || !KeyDerivation.isValid(newValue) || !KeyDerivation.parsePath(newValue).equals(derivation));
|
|
||||||
importDerivationButton.setDisable(newValue.isEmpty() || !KeyDerivation.isValid(newValue) || KeyDerivation.parsePath(newValue).equals(derivation));
|
|
||||||
});
|
|
||||||
|
|
||||||
HBox contentBox = new HBox();
|
|
||||||
contentBox.setAlignment(Pos.TOP_RIGHT);
|
|
||||||
contentBox.setSpacing(20);
|
|
||||||
contentBox.getChildren().add(derivationField);
|
|
||||||
contentBox.getChildren().add(importDerivationButton);
|
|
||||||
contentBox.setPadding(new Insets(10, 30, 10, 30));
|
|
||||||
contentBox.setPrefHeight(60);
|
|
||||||
|
|
||||||
return contentBox;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static Glyph getIncompleteGlyph() {
|
|
||||||
Glyph warningGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.PLUS_CIRCLE);
|
|
||||||
warningGlyph.getStyleClass().add("warn-icon");
|
|
||||||
warningGlyph.setFontSize(12);
|
|
||||||
return warningGlyph;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected WordlistProvider getWordlistProvider() {
|
|
||||||
return getWordListProvider(DeterministicSeed.Type.SLIP39);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -41,7 +41,7 @@ public class MnemonicWalletKeystoreImportPane extends MnemonicKeystorePane {
|
||||||
private Button importButton;
|
private Button importButton;
|
||||||
|
|
||||||
public MnemonicWalletKeystoreImportPane(KeystoreMnemonicImport importer) {
|
public MnemonicWalletKeystoreImportPane(KeystoreMnemonicImport importer) {
|
||||||
super(importer.getName(), "Seed import", importer.getKeystoreImportDescription(), importer.getWalletModel());
|
super(importer.getName(), "Seed import", importer.getKeystoreImportDescription(), "image/" + importer.getWalletModel().getType() + ".png");
|
||||||
this.importer = importer;
|
this.importer = importer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -55,7 +55,7 @@ public class MnemonicWalletKeystoreImportPane extends MnemonicKeystorePane {
|
||||||
protected List<Node> createRightButtons() {
|
protected List<Node> createRightButtons() {
|
||||||
discoverButton = new Button("Discover Wallet");
|
discoverButton = new Button("Discover Wallet");
|
||||||
discoverButton.setDisable(true);
|
discoverButton.setDisable(true);
|
||||||
discoverButton.setDefaultButton(AppServices.onlineProperty().get());
|
discoverButton.setDefaultButton(true);
|
||||||
discoverButton.managedProperty().bind(discoverButton.visibleProperty());
|
discoverButton.managedProperty().bind(discoverButton.visibleProperty());
|
||||||
discoverButton.setOnAction(event -> {
|
discoverButton.setOnAction(event -> {
|
||||||
discoverWallet();
|
discoverWallet();
|
||||||
|
|
@ -66,7 +66,6 @@ public class MnemonicWalletKeystoreImportPane extends MnemonicKeystorePane {
|
||||||
|
|
||||||
importButton = new Button("Import Wallet");
|
importButton = new Button("Import Wallet");
|
||||||
importButton.setDisable(true);
|
importButton.setDisable(true);
|
||||||
importButton.setDefaultButton(!AppServices.onlineProperty().get());
|
|
||||||
importButton.managedProperty().bind(importButton.visibleProperty());
|
importButton.managedProperty().bind(importButton.visibleProperty());
|
||||||
importButton.visibleProperty().bind(discoverButton.visibleProperty().not());
|
importButton.visibleProperty().bind(discoverButton.visibleProperty().not());
|
||||||
importButton.setOnAction(event -> {
|
importButton.setOnAction(event -> {
|
||||||
|
|
@ -197,7 +196,6 @@ public class MnemonicWalletKeystoreImportPane extends MnemonicKeystorePane {
|
||||||
HBox.setHgrow(region, Priority.SOMETIMES);
|
HBox.setHgrow(region, Priority.SOMETIMES);
|
||||||
|
|
||||||
Button importMnemonicButton = new Button("Import");
|
Button importMnemonicButton = new Button("Import");
|
||||||
importMnemonicButton.setDefaultButton(true);
|
|
||||||
importMnemonicButton.setOnAction(event -> {
|
importMnemonicButton.setOnAction(event -> {
|
||||||
showHideLink.setVisible(true);
|
showHideLink.setVisible(true);
|
||||||
setExpanded(false);
|
setExpanded(false);
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,14 @@
|
||||||
package com.sparrowwallet.sparrow.control;
|
package com.sparrowwallet.sparrow.control;
|
||||||
|
|
||||||
import com.sparrowwallet.drongo.OsType;
|
|
||||||
import com.sparrowwallet.sparrow.wallet.Entry;
|
import com.sparrowwallet.sparrow.wallet.Entry;
|
||||||
import javafx.scene.control.TreeTableCell;
|
import javafx.scene.control.TreeTableCell;
|
||||||
|
import org.controlsfx.tools.Platform;
|
||||||
|
|
||||||
public class NumberCell extends TreeTableCell<Entry, Number> {
|
public class NumberCell extends TreeTableCell<Entry, Number> {
|
||||||
public NumberCell() {
|
public NumberCell() {
|
||||||
super();
|
super();
|
||||||
getStyleClass().add("number-cell");
|
getStyleClass().add("number-cell");
|
||||||
if(OsType.getCurrent() == OsType.MACOS) {
|
if(Platform.getCurrent() == Platform.OSX) {
|
||||||
getStyleClass().add("number-field");
|
getStyleClass().add("number-field");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,7 @@ import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
import java.net.Proxy;
|
import java.net.Proxy;
|
||||||
import java.net.URI;
|
import java.net.URL;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
@ -79,6 +79,10 @@ public class PayNymAvatar extends StackPane {
|
||||||
this.paymentCodeProperty.set(paymentCode);
|
this.paymentCodeProperty.set(paymentCode);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void setPaymentCode(com.samourai.wallet.bip47.rpc.PaymentCode paymentCode) {
|
||||||
|
setPaymentCode(PaymentCode.fromString(paymentCode.toString()));
|
||||||
|
}
|
||||||
|
|
||||||
public void clearPaymentCode() {
|
public void clearPaymentCode() {
|
||||||
this.paymentCodeProperty.set(null);
|
this.paymentCodeProperty.set(null);
|
||||||
}
|
}
|
||||||
|
|
@ -124,11 +128,8 @@ public class PayNymAvatar extends StackPane {
|
||||||
log.debug("Requesting PayNym avatar from " + url);
|
log.debug("Requesting PayNym avatar from " + url);
|
||||||
}
|
}
|
||||||
|
|
||||||
try(InputStream is = (proxy == null ? new URI(url).toURL().openStream() : new URI(url).toURL().openConnection(proxy).getInputStream())) {
|
try(InputStream is = (proxy == null ? new URL(url).openStream() : new URL(url).openConnection(proxy).getInputStream())) {
|
||||||
Image image = new Image(is, 150, 150, true, true);
|
Image image = new Image(is, 150, 150, true, false);
|
||||||
if(image.getException() != null) {
|
|
||||||
throw image.getException();
|
|
||||||
}
|
|
||||||
paymentCodeCache.put(cacheId, image);
|
paymentCodeCache.put(cacheId, image);
|
||||||
Platform.runLater(() -> EventManager.get().post(new PayNymImageLoadedEvent(paymentCode, image)));
|
Platform.runLater(() -> EventManager.get().post(new PayNymImageLoadedEvent(paymentCode, image)));
|
||||||
return image;
|
return image;
|
||||||
|
|
|
||||||
|
|
@ -81,7 +81,10 @@ public class PayNymCell extends ListCell<PayNym> {
|
||||||
linkButton.setDisable(true);
|
linkButton.setDisable(true);
|
||||||
payNymController.linkPayNym(payNym);
|
payNymController.linkPayNym(payNym);
|
||||||
});
|
});
|
||||||
getStyleClass().add("unlinked");
|
|
||||||
|
if(payNymController.isSelectLinkedOnly()) {
|
||||||
|
getStyleClass().add("unlinked");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,11 @@ public class PaymentCodeTextField extends CopyableTextField {
|
||||||
setPaymentCodeString();
|
setPaymentCodeString();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void setPaymentCode(com.samourai.wallet.bip47.rpc.PaymentCode paymentCode) {
|
||||||
|
this.paymentCodeStr = paymentCode.toString();
|
||||||
|
setPaymentCodeString();
|
||||||
|
}
|
||||||
|
|
||||||
private void setPaymentCodeString() {
|
private void setPaymentCodeString() {
|
||||||
String abbrevPcode = paymentCodeStr.substring(0, 12) + "..." + paymentCodeStr.substring(paymentCodeStr.length() - 5);
|
String abbrevPcode = paymentCodeStr.substring(0, 12) + "..." + paymentCodeStr.substring(paymentCodeStr.length() - 5);
|
||||||
setText(abbrevPcode);
|
setText(abbrevPcode);
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,6 @@ import com.sparrowwallet.drongo.protocol.*;
|
||||||
import com.sparrowwallet.drongo.psbt.PSBT;
|
import com.sparrowwallet.drongo.psbt.PSBT;
|
||||||
import com.sparrowwallet.drongo.psbt.PSBTInput;
|
import com.sparrowwallet.drongo.psbt.PSBTInput;
|
||||||
import com.sparrowwallet.drongo.wallet.Wallet;
|
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||||
import com.sparrowwallet.drongo.wallet.WalletModel;
|
|
||||||
import com.sparrowwallet.sparrow.AppServices;
|
import com.sparrowwallet.sparrow.AppServices;
|
||||||
import com.sparrowwallet.sparrow.EventManager;
|
import com.sparrowwallet.sparrow.EventManager;
|
||||||
import com.sparrowwallet.sparrow.UnitFormat;
|
import com.sparrowwallet.sparrow.UnitFormat;
|
||||||
|
|
@ -62,7 +61,6 @@ public class PrivateKeySweepDialog extends Dialog<Transaction> {
|
||||||
private final TextArea key;
|
private final TextArea key;
|
||||||
private final ComboBox<ScriptType> keyScriptType;
|
private final ComboBox<ScriptType> keyScriptType;
|
||||||
private final CopyableLabel keyAddress;
|
private final CopyableLabel keyAddress;
|
||||||
private final CopyableLabel keyUtxos;
|
|
||||||
private final ComboBoxTextField toAddress;
|
private final ComboBoxTextField toAddress;
|
||||||
private final ComboBox<Wallet> toWallet;
|
private final ComboBox<Wallet> toWallet;
|
||||||
private final FeeRangeSlider feeRange;
|
private final FeeRangeSlider feeRange;
|
||||||
|
|
@ -74,7 +72,14 @@ public class PrivateKeySweepDialog extends Dialog<Transaction> {
|
||||||
dialogPane.getStylesheets().add(AppServices.class.getResource("dialog.css").toExternalForm());
|
dialogPane.getStylesheets().add(AppServices.class.getResource("dialog.css").toExternalForm());
|
||||||
AppServices.setStageIcon(dialogPane.getScene().getWindow());
|
AppServices.setStageIcon(dialogPane.getScene().getWindow());
|
||||||
dialogPane.setHeaderText("Sweep Private Key");
|
dialogPane.setHeaderText("Sweep Private Key");
|
||||||
dialogPane.setGraphic(new WalletModelImage(WalletModel.SEED));
|
|
||||||
|
Image image = new Image("image/seed.png", 50, 50, false, false);
|
||||||
|
if(!image.isError()) {
|
||||||
|
ImageView imageView = new ImageView();
|
||||||
|
imageView.setSmooth(false);
|
||||||
|
imageView.setImage(image);
|
||||||
|
dialogPane.setGraphic(imageView);
|
||||||
|
}
|
||||||
|
|
||||||
Form form = new Form();
|
Form form = new Form();
|
||||||
Fieldset fieldset = new Fieldset();
|
Fieldset fieldset = new Fieldset();
|
||||||
|
|
@ -131,12 +136,6 @@ public class PrivateKeySweepDialog extends Dialog<Transaction> {
|
||||||
keyAddress.getStyleClass().add("fixed-width");
|
keyAddress.getStyleClass().add("fixed-width");
|
||||||
addressField.getInputs().add(keyAddress);
|
addressField.getInputs().add(keyAddress);
|
||||||
|
|
||||||
Field utxosField = new Field();
|
|
||||||
utxosField.setText("UTXOs:");
|
|
||||||
keyUtxos = new CopyableLabel();
|
|
||||||
utxosField.getInputs().add(keyUtxos);
|
|
||||||
|
|
||||||
|
|
||||||
Field toAddressField = new Field();
|
Field toAddressField = new Field();
|
||||||
toAddressField.setText("Sweep to:");
|
toAddressField.setText("Sweep to:");
|
||||||
toAddress = new ComboBoxTextField();
|
toAddress = new ComboBoxTextField();
|
||||||
|
|
@ -356,8 +355,6 @@ public class PrivateKeySweepDialog extends Dialog<Transaction> {
|
||||||
Optional<Date> optSince = addressScanDateDialog.showAndWait();
|
Optional<Date> optSince = addressScanDateDialog.showAndWait();
|
||||||
if(optSince.isPresent()) {
|
if(optSince.isPresent()) {
|
||||||
since = optSince.get();
|
since = optSince.get();
|
||||||
} else {
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -372,7 +369,7 @@ public class PrivateKeySweepDialog extends Dialog<Transaction> {
|
||||||
});
|
});
|
||||||
|
|
||||||
if(Config.get().getServerType() == ServerType.BITCOIN_CORE) {
|
if(Config.get().getServerType() == ServerType.BITCOIN_CORE) {
|
||||||
ServiceProgressDialog serviceProgressDialog = new ServiceProgressDialog("Address Scan", "Scanning address for transactions...", new DialogImage(DialogImage.Type.SPARROW), addressUtxosService);
|
ServiceProgressDialog serviceProgressDialog = new ServiceProgressDialog("Address Scan", "Scanning address for transactions...", "/image/sparrow.png", addressUtxosService);
|
||||||
serviceProgressDialog.initOwner(getDialogPane().getScene().getWindow());
|
serviceProgressDialog.initOwner(getDialogPane().getScene().getWindow());
|
||||||
AppServices.moveToActiveWindowScreen(serviceProgressDialog);
|
AppServices.moveToActiveWindowScreen(serviceProgressDialog);
|
||||||
}
|
}
|
||||||
|
|
@ -398,24 +395,18 @@ public class PrivateKeySweepDialog extends Dialog<Transaction> {
|
||||||
|
|
||||||
double feeRate = feeRange.getFeeRate();
|
double feeRate = feeRange.getFeeRate();
|
||||||
long fee = (long)Math.ceil(noFeeTransaction.getVirtualSize() * feeRate);
|
long fee = (long)Math.ceil(noFeeTransaction.getVirtualSize() * feeRate);
|
||||||
if(feeRate == AppServices.getMinimumRelayFeeRate() && feeRate > 0d) {
|
if(feeRate == Transaction.DEFAULT_MIN_RELAY_FEE) {
|
||||||
fee++;
|
fee++;
|
||||||
}
|
}
|
||||||
|
|
||||||
long dustThreshold = destAddress.getScriptType().getDustThreshold(sweepOutput, Transaction.DUST_RELAY_TX_FEE);
|
long dustThreshold = destAddress.getScriptType().getDustThreshold(sweepOutput, Transaction.DUST_RELAY_TX_FEE);
|
||||||
if(total - fee <= dustThreshold) {
|
if(total - fee <= dustThreshold) {
|
||||||
feeRate = AppServices.getMinimumRelayFeeRate();
|
feeRate = Transaction.DEFAULT_MIN_RELAY_FEE;
|
||||||
fee = (long)Math.ceil(noFeeTransaction.getVirtualSize() * feeRate) + (feeRate > 0d ? 1 : 0);
|
fee = (long)Math.ceil(noFeeTransaction.getVirtualSize() * feeRate) + 1;
|
||||||
|
|
||||||
if(total - fee <= dustThreshold) {
|
if(total - fee <= dustThreshold) {
|
||||||
AppServices.showErrorDialog("Insufficient funds", "The unspent outputs for this private key contain insufficient funds to spend (" + total + " sats).");
|
AppServices.showErrorDialog("Insufficient funds", "The unspent outputs for this private key contain insufficient funds to spend (" + total + " sats).");
|
||||||
return;
|
return;
|
||||||
} else {
|
|
||||||
Optional<ButtonType> optType = AppServices.showWarningDialog("Insufficient funds", "The unspent outputs for this private key contain insufficient funds (" + total + " sats) for a transaction at this fee rate." +
|
|
||||||
"\n\nContinue with a minimum fee rate transaction?", ButtonType.YES, ButtonType.NO);
|
|
||||||
if(optType.isPresent() && optType.get() == ButtonType.NO) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,6 @@ import com.sparrowwallet.hummingbird.LegacyUREncoder;
|
||||||
import com.sparrowwallet.hummingbird.registry.RegistryType;
|
import com.sparrowwallet.hummingbird.registry.RegistryType;
|
||||||
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.glyphfont.GlyphUtils;
|
|
||||||
import com.sparrowwallet.sparrow.io.Config;
|
import com.sparrowwallet.sparrow.io.Config;
|
||||||
import com.sparrowwallet.sparrow.io.ImportException;
|
import com.sparrowwallet.sparrow.io.ImportException;
|
||||||
import com.sparrowwallet.hummingbird.UR;
|
import com.sparrowwallet.hummingbird.UR;
|
||||||
|
|
@ -43,7 +42,7 @@ public class QRDisplayDialog extends Dialog<ButtonType> {
|
||||||
|
|
||||||
private static final int MIN_FRAGMENT_LENGTH = 10;
|
private static final int MIN_FRAGMENT_LENGTH = 10;
|
||||||
|
|
||||||
private static final double ANIMATION_PERIOD_MILLIS = 200d;
|
private static final int ANIMATION_PERIOD_MILLIS = 200;
|
||||||
|
|
||||||
private static final int DEFAULT_QR_SIZE = 580;
|
private static final int DEFAULT_QR_SIZE = 580;
|
||||||
private static final int REDUCED_QR_SIZE = 520;
|
private static final int REDUCED_QR_SIZE = 520;
|
||||||
|
|
@ -101,16 +100,6 @@ public class QRDisplayDialog extends Dialog<ButtonType> {
|
||||||
qrImageView = new ImageView();
|
qrImageView = new ImageView();
|
||||||
stackPane.getChildren().add(qrImageView);
|
stackPane.getChildren().add(qrImageView);
|
||||||
|
|
||||||
qrImageView.setOnScroll(scrollEvent -> {
|
|
||||||
if(animateQRService != null && animateQRService.isRunning() && scrollEvent.getDeltaY() != 0) {
|
|
||||||
Duration duration = animateQRService.getPeriod();
|
|
||||||
Duration newDuration = scrollEvent.getDeltaY() > 0 ? duration.multiply(1.1) : duration.multiply(0.9);
|
|
||||||
if(newDuration.lessThan(Duration.millis(ANIMATION_PERIOD_MILLIS*10)) && newDuration.greaterThan(Duration.millis(ANIMATION_PERIOD_MILLIS/2))) {
|
|
||||||
animateQRService.setPeriod(newDuration);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
dialogPane.setContent(Borders.wrap(stackPane).lineBorder().buildAll());
|
dialogPane.setContent(Borders.wrap(stackPane).lineBorder().buildAll());
|
||||||
|
|
||||||
nextPart();
|
nextPart();
|
||||||
|
|
@ -170,11 +159,6 @@ public class QRDisplayDialog extends Dialog<ButtonType> {
|
||||||
dialogPane.setContent(Borders.wrap(stackPane).lineBorder().buildAll());
|
dialogPane.setContent(Borders.wrap(stackPane).lineBorder().buildAll());
|
||||||
qrImageView.setImage(getQrCode(data));
|
qrImageView.setImage(getQrCode(data));
|
||||||
|
|
||||||
if(qrImageView.getImage() == null) {
|
|
||||||
Label warning = new Label("Message is too long for display as a QR code");
|
|
||||||
stackPane.getChildren().add(warning);
|
|
||||||
}
|
|
||||||
|
|
||||||
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);
|
||||||
dialogPane.getButtonTypes().addAll(cancelButtonType);
|
dialogPane.getButtonTypes().addAll(cancelButtonType);
|
||||||
|
|
||||||
|
|
@ -265,10 +249,6 @@ public class QRDisplayDialog extends Dialog<ButtonType> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean isUseBbqrEncoding() {
|
|
||||||
return useBbqrEncoding;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void setUseBbqrEncoding(boolean useBbqrEncoding) {
|
private void setUseBbqrEncoding(boolean useBbqrEncoding) {
|
||||||
if(useBbqrEncoding) {
|
if(useBbqrEncoding) {
|
||||||
this.useBbqrEncoding = true;
|
this.useBbqrEncoding = true;
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,10 @@
|
||||||
package com.sparrowwallet.sparrow.control;
|
package com.sparrowwallet.sparrow.control;
|
||||||
|
|
||||||
import com.google.common.base.Throwables;
|
import com.github.sarxos.webcam.*;
|
||||||
import com.sparrowwallet.drongo.*;
|
import com.sparrowwallet.drongo.ExtendedKey;
|
||||||
|
import com.sparrowwallet.drongo.KeyDerivation;
|
||||||
|
import com.sparrowwallet.drongo.OutputDescriptor;
|
||||||
|
import com.sparrowwallet.drongo.Utils;
|
||||||
import com.sparrowwallet.drongo.address.Address;
|
import com.sparrowwallet.drongo.address.Address;
|
||||||
import com.sparrowwallet.drongo.address.P2PKHAddress;
|
import com.sparrowwallet.drongo.address.P2PKHAddress;
|
||||||
import com.sparrowwallet.drongo.address.P2SHAddress;
|
import com.sparrowwallet.drongo.address.P2SHAddress;
|
||||||
|
|
@ -25,12 +28,10 @@ import com.sparrowwallet.hummingbird.URDecoder;
|
||||||
import com.sparrowwallet.hummingbird.registry.pathcomponent.IndexPathComponent;
|
import com.sparrowwallet.hummingbird.registry.pathcomponent.IndexPathComponent;
|
||||||
import com.sparrowwallet.hummingbird.registry.pathcomponent.PathComponent;
|
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.glyphfont.FontAwesome5;
|
||||||
import com.sparrowwallet.sparrow.event.WebcamResolutionChangedEvent;
|
|
||||||
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;
|
||||||
import com.sparrowwallet.sparrow.wallet.KeystoreController;
|
|
||||||
import javafx.application.Platform;
|
import javafx.application.Platform;
|
||||||
import javafx.beans.property.DoubleProperty;
|
import javafx.beans.property.DoubleProperty;
|
||||||
import javafx.beans.property.ObjectProperty;
|
import javafx.beans.property.ObjectProperty;
|
||||||
|
|
@ -38,16 +39,14 @@ import javafx.beans.property.SimpleDoubleProperty;
|
||||||
import javafx.beans.property.SimpleObjectProperty;
|
import javafx.beans.property.SimpleObjectProperty;
|
||||||
import javafx.beans.value.ChangeListener;
|
import javafx.beans.value.ChangeListener;
|
||||||
import javafx.beans.value.ObservableValue;
|
import javafx.beans.value.ObservableValue;
|
||||||
import javafx.collections.FXCollections;
|
|
||||||
import javafx.collections.ObservableList;
|
|
||||||
import javafx.geometry.Insets;
|
import javafx.geometry.Insets;
|
||||||
import javafx.scene.Node;
|
import javafx.scene.Node;
|
||||||
import javafx.scene.control.*;
|
import javafx.scene.control.*;
|
||||||
import javafx.scene.layout.*;
|
import javafx.scene.layout.*;
|
||||||
import javafx.util.Duration;
|
import javafx.util.Duration;
|
||||||
import javafx.util.StringConverter;
|
import javafx.util.StringConverter;
|
||||||
|
import org.controlsfx.glyphfont.Glyph;
|
||||||
import org.controlsfx.tools.Borders;
|
import org.controlsfx.tools.Borders;
|
||||||
import org.openpnp.capture.CaptureDevice;
|
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
|
@ -77,141 +76,107 @@ public class QRScanDialog extends Dialog<QRScanDialog.Result> {
|
||||||
private static final Pattern PART_PATTERN = Pattern.compile("p(\\d+)of(\\d+) (.+)");
|
private static final Pattern PART_PATTERN = Pattern.compile("p(\\d+)of(\\d+) (.+)");
|
||||||
|
|
||||||
private static final int SCAN_PERIOD_MILLIS = 100;
|
private static final int SCAN_PERIOD_MILLIS = 100;
|
||||||
private final ObjectProperty<CaptureDevice> webcamDeviceProperty = new SimpleObjectProperty<>();
|
private final ObjectProperty<WebcamResolution> webcamResolutionProperty = new SimpleObjectProperty<>(WebcamResolution.VGA);
|
||||||
private final ObjectProperty<WebcamResolution> webcamResolutionProperty = new SimpleObjectProperty<>(WebcamResolution.HD);
|
|
||||||
|
|
||||||
private final DoubleProperty percentComplete = new SimpleDoubleProperty(0.0);
|
private final DoubleProperty percentComplete = new SimpleDoubleProperty(0.0);
|
||||||
|
|
||||||
private final ObservableList<CaptureDevice> foundDevices = FXCollections.observableList(new ArrayList<>());
|
private final ObjectProperty<WebcamDevice> webcamDeviceProperty = new SimpleObjectProperty<>();
|
||||||
private final ObservableList<WebcamResolution> availableResolutions = FXCollections.observableList(new ArrayList<>());
|
|
||||||
private boolean postOpenUpdate;
|
|
||||||
|
|
||||||
public QRScanDialog() {
|
public QRScanDialog() {
|
||||||
this.urDecoder = new URDecoder();
|
this.urDecoder = new URDecoder();
|
||||||
this.legacyUrDecoder = new LegacyURDecoder();
|
this.legacyUrDecoder = new LegacyURDecoder();
|
||||||
this.bbqrDecoder = new BBQRDecoder();
|
this.bbqrDecoder = new BBQRDecoder();
|
||||||
|
|
||||||
if(Config.get().getWebcamResolution() != null) {
|
if(Config.get().isHdCapture()) {
|
||||||
webcamResolutionProperty.set(Config.get().getWebcamResolution());
|
webcamResolutionProperty.set(WebcamResolution.HD);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.webcamService = new WebcamService(webcamResolutionProperty.get(), null);
|
this.webcamService = new WebcamService(webcamResolutionProperty.get(), null, new QRScanListener(), new ScanDelayCalculator());
|
||||||
webcamService.setPeriod(Duration.millis(SCAN_PERIOD_MILLIS));
|
webcamService.setPeriod(Duration.millis(SCAN_PERIOD_MILLIS));
|
||||||
webcamService.setRestartOnFailure(false);
|
webcamService.setRestartOnFailure(false);
|
||||||
|
WebcamView webcamView = new WebcamView(webcamService);
|
||||||
|
|
||||||
final DialogPane dialogPane = new QRScanDialogPane();
|
final DialogPane dialogPane = new QRScanDialogPane();
|
||||||
setDialogPane(dialogPane);
|
setDialogPane(dialogPane);
|
||||||
AppServices.setStageIcon(dialogPane.getScene().getWindow());
|
AppServices.setStageIcon(dialogPane.getScene().getWindow());
|
||||||
|
|
||||||
WebcamView webcamView = new WebcamView(webcamService, Config.get().isMirrorCapture());
|
StackPane stackPane = new StackPane();
|
||||||
|
stackPane.getChildren().add(webcamView.getView());
|
||||||
|
Node wrappedView = Borders.wrap(stackPane).lineBorder().buildAll();
|
||||||
|
|
||||||
ProgressBar progressBar = new ProgressBar();
|
ProgressBar progressBar = new ProgressBar();
|
||||||
progressBar.setMinHeight(20);
|
progressBar.setMinHeight(20);
|
||||||
progressBar.setPadding(new Insets(0, 10, 0, 10));
|
progressBar.setPadding(new Insets(0, 10, 0, 10));
|
||||||
progressBar.setPrefWidth(Integer.MAX_VALUE);
|
progressBar.setPrefWidth(Integer.MAX_VALUE);
|
||||||
progressBar.progressProperty().bind(percentComplete);
|
progressBar.progressProperty().bind(percentComplete);
|
||||||
|
webcamService.openingProperty().addListener((observable, oldValue, newValue) -> {
|
||||||
|
if(percentComplete.get() <= 0.0) {
|
||||||
|
Platform.runLater(() -> percentComplete.set(newValue ? 0.0 : -1.0));
|
||||||
|
}
|
||||||
|
Platform.runLater(() -> {
|
||||||
|
if(Config.get().getWebcamDevice() != null && webcamDeviceProperty.get() == null) {
|
||||||
|
for(WebcamDevice device : WebcamScanDriver.getFoundDevices()) {
|
||||||
|
if(device.getName().equals(Config.get().getWebcamDevice())) {
|
||||||
|
webcamDeviceProperty.set(device);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
VBox vBox = new VBox(20);
|
VBox vBox = new VBox(20);
|
||||||
StackPane stackPane = new StackPane();
|
|
||||||
stackPane.getChildren().add(webcamView.getView());
|
|
||||||
Node wrappedView = Borders.wrap(stackPane).lineBorder().buildAll();
|
|
||||||
vBox.getChildren().addAll(wrappedView, progressBar);
|
vBox.getChildren().addAll(wrappedView, progressBar);
|
||||||
|
|
||||||
dialogPane.setContent(vBox);
|
dialogPane.setContent(vBox);
|
||||||
|
|
||||||
webcamService.openingProperty().addListener((_, _, opening) -> {
|
|
||||||
if(percentComplete.get() <= 0.0) {
|
|
||||||
Platform.runLater(() -> percentComplete.set(opening ? 0.0 : -1.0));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
webcamService.openedProperty().addListener((_, _, opened) -> {
|
|
||||||
if(opened) {
|
|
||||||
Platform.runLater(() -> {
|
|
||||||
try {
|
|
||||||
postOpenUpdate = true;
|
|
||||||
List<CaptureDevice> newDevices = new ArrayList<>(webcamService.getAvailableDevices());
|
|
||||||
newDevices.removeAll(foundDevices);
|
|
||||||
foundDevices.addAll(newDevices);
|
|
||||||
foundDevices.removeIf(device -> !webcamService.getDevices().contains(device));
|
|
||||||
|
|
||||||
if(webcamService.getDevice() != null) {
|
|
||||||
for(CaptureDevice device : foundDevices) {
|
|
||||||
if(device.equals(webcamService.getDevice())) {
|
|
||||||
webcamDeviceProperty.set(device);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
updateList(availableResolutions, webcamService.getResolutions());
|
|
||||||
webcamResolutionProperty.set(webcamService.getResolution());
|
|
||||||
} finally {
|
|
||||||
postOpenUpdate = false;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} else if(webcamResolutionProperty.get() != null) {
|
|
||||||
webcamService.setResolution(webcamResolutionProperty.get());
|
|
||||||
webcamService.setDevice(webcamDeviceProperty.get());
|
|
||||||
Platform.runLater(() -> {
|
|
||||||
if(!webcamService.isRunning()) {
|
|
||||||
webcamService.reset();
|
|
||||||
webcamService.start();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
webcamService.resultProperty().addListener(new QRResultListener());
|
webcamService.resultProperty().addListener(new QRResultListener());
|
||||||
webcamService.setOnFailed(failedEvent -> {
|
webcamService.setOnFailed(failedEvent -> {
|
||||||
Throwable exception = Throwables.getRootCause(failedEvent.getSource().getException());
|
Throwable exception = failedEvent.getSource().getException();
|
||||||
Platform.runLater(() -> setResult(new Result(exception)));
|
|
||||||
|
Throwable nested = exception;
|
||||||
|
while(nested.getCause() != null) {
|
||||||
|
nested = nested.getCause();
|
||||||
|
}
|
||||||
|
if(org.controlsfx.tools.Platform.getCurrent() == org.controlsfx.tools.Platform.WINDOWS &&
|
||||||
|
nested.getMessage().startsWith("Library 'OpenIMAJGrabber' was not loaded successfully from file")) {
|
||||||
|
exception = new WebcamDependencyException("Your system is missing a dependency required for the webcam. Follow the link below for more details.\n\n[https://sparrowwallet.com/docs/faq.html#your-system-is-missing-a-dependency-for-the-webcam]", exception);
|
||||||
|
} else if(nested.getMessage().startsWith("Cannot start native grabber") && Config.get().getWebcamDevice() != null) {
|
||||||
|
exception = new WebcamOpenException("Cannot open configured webcam " + Config.get().getWebcamDevice() + ", reverting to the default webcam");
|
||||||
|
Config.get().setWebcamDevice(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
final Throwable result = exception;
|
||||||
|
Platform.runLater(() -> setResult(new Result(result)));
|
||||||
});
|
});
|
||||||
webcamService.start();
|
webcamService.start();
|
||||||
|
webcamResolutionProperty.addListener((observable, oldValue, newResolution) -> {
|
||||||
webcamResolutionProperty.addListener((_, oldResolution, newResolution) -> {
|
|
||||||
if(newResolution != null) {
|
if(newResolution != null) {
|
||||||
if(newResolution.isStandardAspect() && oldResolution.isWidescreenAspect()) {
|
setHeight(newResolution == WebcamResolution.HD ? (getHeight() - 100) : (getHeight() + 100));
|
||||||
setWidth(getWidth());
|
|
||||||
setHeight(getHeight() + 100);
|
|
||||||
dialogPane.setMaxHeight(dialogPane.getPrefHeight() + 100);
|
|
||||||
dialogPane.setPrefHeight(dialogPane.getMaxHeight());
|
|
||||||
dialogPane.setMinHeight(dialogPane.getMaxHeight());
|
|
||||||
} else if(newResolution.isWidescreenAspect() && oldResolution.isStandardAspect()) {
|
|
||||||
setWidth(getWidth());
|
|
||||||
setHeight(getHeight() - 100);
|
|
||||||
dialogPane.setMaxHeight(dialogPane.getPrefHeight() - 100);
|
|
||||||
dialogPane.setPrefHeight(dialogPane.getMaxHeight());
|
|
||||||
dialogPane.setMinHeight(dialogPane.getMaxHeight());
|
|
||||||
}
|
|
||||||
EventManager.get().post(new WebcamResolutionChangedEvent(newResolution));
|
|
||||||
}
|
|
||||||
if(newResolution == null || !postOpenUpdate) {
|
|
||||||
webcamService.cancel();
|
|
||||||
}
|
}
|
||||||
|
webcamService.cancel();
|
||||||
});
|
});
|
||||||
webcamDeviceProperty.addListener((_, _, newValue) -> {
|
webcamDeviceProperty.addListener((observable, oldValue, newValue) -> {
|
||||||
Config.get().setWebcamDevice(newValue.getName());
|
Config.get().setWebcamDevice(newValue.getName());
|
||||||
Config.get().setWebcamDeviceId(newValue.getUniqueId());
|
|
||||||
if(!Objects.equals(webcamService.getDevice(), newValue)) {
|
if(!Objects.equals(webcamService.getDevice(), newValue)) {
|
||||||
webcamService.cancel();
|
webcamService.cancel();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
setOnCloseRequest(_ -> {
|
setOnCloseRequest(event -> {
|
||||||
if(webcamResolutionProperty.get() != null) {
|
boolean isHdCapture = (webcamResolutionProperty.get() == WebcamResolution.HD);
|
||||||
Config.get().setWebcamResolution(webcamResolutionProperty.get());
|
if(Config.get().isHdCapture() != isHdCapture) {
|
||||||
|
Config.get().setHdCapture(isHdCapture);
|
||||||
}
|
}
|
||||||
|
|
||||||
Platform.runLater(() -> {
|
Platform.runLater(() -> webcamResolutionProperty.set(null));
|
||||||
webcamResolutionProperty.set(null);
|
|
||||||
webcamService.close();
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
final ButtonType cancelButtonType = new javafx.scene.control.ButtonType("Close", ButtonBar.ButtonData.CANCEL_CLOSE);
|
final ButtonType cancelButtonType = new javafx.scene.control.ButtonType("Close", ButtonBar.ButtonData.CANCEL_CLOSE);
|
||||||
final ButtonType deviceButtonType = new javafx.scene.control.ButtonType("Default Camera", ButtonBar.ButtonData.LEFT);
|
final ButtonType hdButtonType = new javafx.scene.control.ButtonType("Use HD Capture", ButtonBar.ButtonData.LEFT);
|
||||||
final ButtonType resolutionButtonType = new javafx.scene.control.ButtonType("Resolution", ButtonBar.ButtonData.HELP_2);
|
final ButtonType camButtonType = new javafx.scene.control.ButtonType("Default Camera", ButtonBar.ButtonData.HELP_2);
|
||||||
dialogPane.getButtonTypes().addAll(deviceButtonType, resolutionButtonType, cancelButtonType);
|
dialogPane.getButtonTypes().addAll(hdButtonType, camButtonType, cancelButtonType);
|
||||||
dialogPane.setPrefWidth(646);
|
dialogPane.setPrefWidth(646);
|
||||||
dialogPane.setPrefHeight(webcamResolutionProperty.get().isWidescreenAspect() ? 490 : 590);
|
dialogPane.setPrefHeight(webcamResolutionProperty.get() == WebcamResolution.HD ? 490 : 590);
|
||||||
dialogPane.setMinHeight(dialogPane.getPrefHeight());
|
dialogPane.setMinHeight(dialogPane.getPrefHeight());
|
||||||
AppServices.moveToActiveWindowScreen(this);
|
AppServices.moveToActiveWindowScreen(this);
|
||||||
|
|
||||||
|
|
@ -656,8 +621,7 @@ public class QRScanDialog extends Dialog<QRScanDialog.Result> {
|
||||||
|
|
||||||
List<ChildNumber> path = cryptoKeypath.getComponents().stream().map(comp -> (IndexPathComponent)comp)
|
List<ChildNumber> path = cryptoKeypath.getComponents().stream().map(comp -> (IndexPathComponent)comp)
|
||||||
.map(comp -> new ChildNumber(comp.getIndex(), comp.isHardened())).collect(Collectors.toList());
|
.map(comp -> new ChildNumber(comp.getIndex(), comp.isHardened())).collect(Collectors.toList());
|
||||||
String fingerprint = cryptoKeypath.getSourceFingerprint() == null ? KeystoreController.DEFAULT_WATCH_ONLY_FINGERPRINT : Utils.bytesToHex(cryptoKeypath.getSourceFingerprint());
|
return new KeyDerivation(Utils.bytesToHex(cryptoKeypath.getSourceFingerprint()), KeyDerivation.writePath(path));
|
||||||
return new KeyDerivation(fingerprint, KeyDerivation.writePath(path));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
|
|
@ -719,32 +683,72 @@ public class QRScanDialog extends Dialog<QRScanDialog.Result> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private class QRScanListener implements WebcamListener {
|
||||||
|
@Override
|
||||||
|
public void webcamOpen(WebcamEvent webcamEvent) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void webcamClosed(WebcamEvent webcamEvent) {
|
||||||
|
if(webcamResolutionProperty.get() != null) {
|
||||||
|
webcamService.setResolution(webcamResolutionProperty.get());
|
||||||
|
webcamService.setDevice(webcamDeviceProperty.get());
|
||||||
|
Platform.runLater(() -> {
|
||||||
|
if(!webcamService.isRunning()) {
|
||||||
|
webcamService.reset();
|
||||||
|
webcamService.start();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void webcamDisposed(WebcamEvent webcamEvent) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void webcamImageObtained(WebcamEvent webcamEvent) {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private class QRScanDialogPane extends DialogPane {
|
private class QRScanDialogPane extends DialogPane {
|
||||||
@Override
|
@Override
|
||||||
protected Node createButton(ButtonType buttonType) {
|
protected Node createButton(ButtonType buttonType) {
|
||||||
Node button;
|
Node button = null;
|
||||||
if(buttonType.getButtonData() == ButtonBar.ButtonData.LEFT) {
|
if(buttonType.getButtonData() == ButtonBar.ButtonData.LEFT) {
|
||||||
ComboBox<CaptureDevice> devicesCombo = new ComboBox<>(foundDevices);
|
ToggleButton hd = new ToggleButton(buttonType.getText());
|
||||||
|
hd.setSelected(webcamResolutionProperty.get() == WebcamResolution.HD);
|
||||||
|
hd.setGraphicTextGap(5);
|
||||||
|
setHdGraphic(hd, hd.isSelected());
|
||||||
|
|
||||||
|
final ButtonBar.ButtonData buttonData = buttonType.getButtonData();
|
||||||
|
ButtonBar.setButtonData(hd, buttonData);
|
||||||
|
hd.selectedProperty().addListener((observable, oldValue, newValue) -> {
|
||||||
|
webcamResolutionProperty.set(newValue ? WebcamResolution.HD : WebcamResolution.VGA);
|
||||||
|
setHdGraphic(hd, newValue);
|
||||||
|
});
|
||||||
|
|
||||||
|
button = hd;
|
||||||
|
} else if(buttonType.getButtonData() == ButtonBar.ButtonData.HELP_2) {
|
||||||
|
ComboBox<WebcamDevice> devicesCombo = new ComboBox<>(WebcamScanDriver.getFoundDevices());
|
||||||
devicesCombo.setConverter(new StringConverter<>() {
|
devicesCombo.setConverter(new StringConverter<>() {
|
||||||
@Override
|
@Override
|
||||||
public String toString(CaptureDevice device) {
|
public String toString(WebcamDevice device) {
|
||||||
return device != null && device.getName() != null ? device.getName().replaceAll(" \\(.*\\)", "") : "Default Camera";
|
return device instanceof WebcamScanDevice ? ((WebcamScanDevice)device).getDeviceName() : "Default Camera";
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public CaptureDevice fromString(String string) {
|
public WebcamDevice fromString(String string) {
|
||||||
throw new UnsupportedOperationException();
|
throw new UnsupportedOperationException();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
devicesCombo.valueProperty().bindBidirectional(webcamDeviceProperty);
|
devicesCombo.valueProperty().bindBidirectional(webcamDeviceProperty);
|
||||||
final ButtonBar.ButtonData buttonData = buttonType.getButtonData();
|
ButtonBar.setButtonData(devicesCombo, ButtonBar.ButtonData.LEFT);
|
||||||
ButtonBar.setButtonData(devicesCombo, buttonData);
|
|
||||||
button = devicesCombo;
|
button = devicesCombo;
|
||||||
} else if(buttonType.getButtonData() == ButtonBar.ButtonData.HELP_2) {
|
|
||||||
ComboBox<WebcamResolution> resolutionsCombo = new ComboBox<>(availableResolutions);
|
|
||||||
resolutionsCombo.valueProperty().bindBidirectional(webcamResolutionProperty);
|
|
||||||
ButtonBar.setButtonData(resolutionsCombo, ButtonBar.ButtonData.LEFT);
|
|
||||||
button = resolutionsCombo;
|
|
||||||
} else {
|
} else {
|
||||||
button = super.createButton(buttonType);
|
button = super.createButton(buttonType);
|
||||||
}
|
}
|
||||||
|
|
@ -757,39 +761,19 @@ public class QRScanDialog extends Dialog<QRScanDialog.Result> {
|
||||||
button.disableProperty().bind(webcamService.openingProperty());
|
button.disableProperty().bind(webcamService.openingProperty());
|
||||||
return button;
|
return button;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
public static <T extends Comparable<T>> void updateList(List<T> targetList, Collection<T> sourceList) {
|
private void setHdGraphic(ToggleButton hd, boolean isHd) {
|
||||||
List<T> sortedSource = new ArrayList<>(sourceList);
|
if(isHd) {
|
||||||
Collections.sort(sortedSource);
|
hd.setGraphic(getGlyph(FontAwesome5.Glyph.CHECK_CIRCLE));
|
||||||
|
|
||||||
ListIterator<T> targetIter = targetList.listIterator();
|
|
||||||
int sourceIndex = 0;
|
|
||||||
|
|
||||||
while (sourceIndex < sortedSource.size() && targetIter.hasNext()) {
|
|
||||||
T sourceItem = sortedSource.get(sourceIndex);
|
|
||||||
T targetItem = targetIter.next();
|
|
||||||
int comparison = sourceItem.compareTo(targetItem);
|
|
||||||
|
|
||||||
if (comparison < 0) {
|
|
||||||
targetIter.previous(); // Back up to insert before
|
|
||||||
targetIter.add(sourceItem);
|
|
||||||
sourceIndex++;
|
|
||||||
} else if (comparison > 0) {
|
|
||||||
targetIter.remove();
|
|
||||||
} else {
|
} else {
|
||||||
sourceIndex++;
|
hd.setGraphic(getGlyph(FontAwesome5.Glyph.BAN));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
while (sourceIndex < sortedSource.size()) {
|
private Glyph getGlyph(FontAwesome5.Glyph glyphName) {
|
||||||
targetIter.add(sortedSource.get(sourceIndex));
|
Glyph glyph = new Glyph(FontAwesome5.FONT_NAME, glyphName);
|
||||||
sourceIndex++;
|
glyph.setFontSize(11);
|
||||||
}
|
return glyph;
|
||||||
|
|
||||||
while (targetIter.hasNext()) {
|
|
||||||
targetIter.next();
|
|
||||||
targetIter.remove();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1007,4 +991,10 @@ public class QRScanDialog extends Dialog<QRScanDialog.Result> {
|
||||||
super(message, cause);
|
super(message, cause);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static class ScanDelayCalculator implements WebcamUpdater.DelayCalculator {
|
||||||
|
public long calculateDelay(long snapshotDuration, double deviceFps) {
|
||||||
|
return Math.max(SCAN_PERIOD_MILLIS - snapshotDuration, 0L);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,178 +0,0 @@
|
||||||
package com.sparrowwallet.sparrow.control;
|
|
||||||
|
|
||||||
import com.sparrowwallet.sparrow.BlockSummary;
|
|
||||||
import com.sparrowwallet.sparrow.io.Config;
|
|
||||||
import com.sparrowwallet.sparrow.net.FeeRatesSource;
|
|
||||||
import io.reactivex.Observable;
|
|
||||||
import io.reactivex.disposables.CompositeDisposable;
|
|
||||||
import io.reactivex.rxjavafx.schedulers.JavaFxScheduler;
|
|
||||||
import javafx.animation.TranslateTransition;
|
|
||||||
import javafx.beans.property.ObjectProperty;
|
|
||||||
import javafx.beans.property.SimpleObjectProperty;
|
|
||||||
import javafx.scene.control.Tooltip;
|
|
||||||
import javafx.scene.layout.Pane;
|
|
||||||
import javafx.scene.shape.Line;
|
|
||||||
import javafx.scene.shape.Rectangle;
|
|
||||||
import javafx.util.Duration;
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Map;
|
|
||||||
import java.util.concurrent.TimeUnit;
|
|
||||||
|
|
||||||
import static com.sparrowwallet.sparrow.AppServices.TARGET_BLOCKS_RANGE;
|
|
||||||
import static com.sparrowwallet.sparrow.control.BlockCube.CUBE_SIZE;
|
|
||||||
|
|
||||||
public class RecentBlocksView extends Pane {
|
|
||||||
private static final double CUBE_SPACING = 100;
|
|
||||||
private static final double ANIMATION_DURATION_MILLIS = 1000;
|
|
||||||
private static final double SEPARATOR_X = 74;
|
|
||||||
|
|
||||||
private final CompositeDisposable disposables = new CompositeDisposable();
|
|
||||||
|
|
||||||
private final ObjectProperty<List<BlockCube>> cubesProperty = new SimpleObjectProperty<>(new ArrayList<>());
|
|
||||||
private final Tooltip tooltip = new Tooltip();
|
|
||||||
|
|
||||||
public RecentBlocksView() {
|
|
||||||
cubesProperty.addListener((_, _, newValue) -> {
|
|
||||||
if(newValue != null && newValue.size() == 3) {
|
|
||||||
drawView();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
Rectangle clip = new Rectangle(-20, -40, CUBE_SPACING * 3 - 20, 100);
|
|
||||||
setClip(clip);
|
|
||||||
|
|
||||||
Observable<Long> intervalObservable = Observable.interval(1, TimeUnit.MINUTES);
|
|
||||||
disposables.add(intervalObservable.observeOn(JavaFxScheduler.platform()).subscribe(_ -> {
|
|
||||||
for(BlockCube cube : getCubes()) {
|
|
||||||
cube.setElapsed(BlockCube.getElapsed(cube.getTimestamp()));
|
|
||||||
}
|
|
||||||
}));
|
|
||||||
|
|
||||||
FeeRatesSource feeRatesSource = Config.get().getFeeRatesSource();
|
|
||||||
feeRatesSource = (feeRatesSource == null ? FeeRatesSource.MEMPOOL_SPACE : feeRatesSource);
|
|
||||||
updateFeeRatesSource(feeRatesSource);
|
|
||||||
Tooltip.install(this, tooltip);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void updateFeeRatesSource(FeeRatesSource feeRatesSource) {
|
|
||||||
tooltip.setText("Fee rate estimate from " + feeRatesSource.getDescription());
|
|
||||||
if(getCubes() != null && !getCubes().isEmpty()) {
|
|
||||||
getCubes().getFirst().setFeeRatesSource(feeRatesSource);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void drawView() {
|
|
||||||
createSeparator();
|
|
||||||
|
|
||||||
for(int i = 0; i < 3; i++) {
|
|
||||||
BlockCube cube = getCubes().get(i);
|
|
||||||
cube.setTranslateX(i * CUBE_SPACING);
|
|
||||||
getChildren().add(cube);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void createSeparator() {
|
|
||||||
Line separator = new Line(SEPARATOR_X, -9, SEPARATOR_X, CUBE_SIZE);
|
|
||||||
separator.getStyleClass().add("blocks-separator");
|
|
||||||
separator.getStrokeDashArray().addAll(5.0, 5.0); // Create dotted line pattern
|
|
||||||
separator.setStrokeWidth(1.0);
|
|
||||||
getChildren().add(separator);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void update(List<BlockSummary> latestBlocks, Double currentFeeRate) {
|
|
||||||
if(getCubes().isEmpty()) {
|
|
||||||
List<BlockCube> cubes = new ArrayList<>();
|
|
||||||
cubes.add(new BlockCube(null, currentFeeRate, null, null, 0L, false));
|
|
||||||
cubes.addAll(latestBlocks.stream().map(BlockCube::fromBlockSummary).limit(2).toList());
|
|
||||||
setCubes(cubes);
|
|
||||||
} else {
|
|
||||||
int knownTip = getCubes().stream().mapToInt(BlockCube::getHeight).max().orElse(0);
|
|
||||||
int latestTip = latestBlocks.stream().mapToInt(BlockSummary::getHeight).max().orElse(0);
|
|
||||||
if(latestTip > knownTip) {
|
|
||||||
addNewBlock(latestBlocks, currentFeeRate);
|
|
||||||
} else {
|
|
||||||
for(int i = 1; i < getCubes().size() && i <= latestBlocks.size(); i++) {
|
|
||||||
BlockCube blockCube = getCubes().get(i);
|
|
||||||
BlockSummary latestBlock = latestBlocks.get(i - 1);
|
|
||||||
blockCube.setConfirmed(true);
|
|
||||||
blockCube.setHeight(latestBlock.getHeight());
|
|
||||||
blockCube.setTimestamp(latestBlock.getTimestamp().getTime());
|
|
||||||
blockCube.setWeight(latestBlock.getWeight().orElse(0));
|
|
||||||
blockCube.setMedianFee(latestBlock.getMedianFee().orElse(-1.0d));
|
|
||||||
blockCube.setTxCount(latestBlock.getTransactionCount().orElse(0));
|
|
||||||
}
|
|
||||||
updateFeeRate(currentFeeRate);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void addNewBlock(List<BlockSummary> latestBlocks, Double currentFeeRate) {
|
|
||||||
if(getCubes().isEmpty()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
for(int i = 0; i < getCubes().size() && i < latestBlocks.size(); i++) {
|
|
||||||
BlockCube blockCube = getCubes().get(i);
|
|
||||||
BlockSummary latestBlock = latestBlocks.get(i);
|
|
||||||
blockCube.setConfirmed(true);
|
|
||||||
blockCube.setHeight(latestBlock.getHeight());
|
|
||||||
blockCube.setTimestamp(latestBlock.getTimestamp().getTime());
|
|
||||||
blockCube.setWeight(latestBlock.getWeight().orElse(0));
|
|
||||||
blockCube.setMedianFee(latestBlock.getMedianFee().orElse(-1.0d));
|
|
||||||
blockCube.setTxCount(latestBlock.getTransactionCount().orElse(0));
|
|
||||||
}
|
|
||||||
|
|
||||||
add(new BlockCube(null, currentFeeRate, null, null, 0L, false));
|
|
||||||
}
|
|
||||||
|
|
||||||
public void add(BlockCube newCube) {
|
|
||||||
newCube.setTranslateX(-CUBE_SPACING);
|
|
||||||
getChildren().add(newCube);
|
|
||||||
getCubes().getFirst().setConfirmed(true);
|
|
||||||
getCubes().addFirst(newCube);
|
|
||||||
animateCubes();
|
|
||||||
if(getCubes().size() > 4) {
|
|
||||||
BlockCube lastCube = getCubes().getLast();
|
|
||||||
getChildren().remove(lastCube);
|
|
||||||
getCubes().remove(lastCube);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void updateFeeRate(Map<Integer, Double> targetBlockFeeRates) {
|
|
||||||
int defaultTarget = TARGET_BLOCKS_RANGE.get((TARGET_BLOCKS_RANGE.size() / 2) - 1);
|
|
||||||
if(targetBlockFeeRates.get(defaultTarget) != null) {
|
|
||||||
Double defaultRate = targetBlockFeeRates.get(defaultTarget);
|
|
||||||
updateFeeRate(defaultRate);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void updateFeeRate(Double currentFeeRate) {
|
|
||||||
if(!getCubes().isEmpty()) {
|
|
||||||
BlockCube firstCube = getCubes().getFirst();
|
|
||||||
firstCube.setMedianFee(currentFeeRate);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void animateCubes() {
|
|
||||||
for(int i = 0; i < getCubes().size(); i++) {
|
|
||||||
BlockCube cube = getCubes().get(i);
|
|
||||||
TranslateTransition transition = new TranslateTransition(Duration.millis(ANIMATION_DURATION_MILLIS), cube);
|
|
||||||
transition.setToX(i * CUBE_SPACING);
|
|
||||||
transition.play();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public List<BlockCube> getCubes() {
|
|
||||||
return cubesProperty.get();
|
|
||||||
}
|
|
||||||
|
|
||||||
public ObjectProperty<List<BlockCube>> cubesProperty() {
|
|
||||||
return cubesProperty;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setCubes(List<BlockCube> cubes) {
|
|
||||||
this.cubesProperty.set(cubes);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -2,7 +2,6 @@ package com.sparrowwallet.sparrow.control;
|
||||||
|
|
||||||
import com.sparrowwallet.drongo.protocol.Script;
|
import com.sparrowwallet.drongo.protocol.Script;
|
||||||
import com.sparrowwallet.drongo.protocol.ScriptChunk;
|
import com.sparrowwallet.drongo.protocol.ScriptChunk;
|
||||||
import com.sparrowwallet.drongo.protocol.ScriptOpCodes;
|
|
||||||
import javafx.geometry.Pos;
|
import javafx.geometry.Pos;
|
||||||
import org.controlsfx.control.decoration.Decorator;
|
import org.controlsfx.control.decoration.Decorator;
|
||||||
import org.controlsfx.control.decoration.GraphicDecoration;
|
import org.controlsfx.control.decoration.GraphicDecoration;
|
||||||
|
|
@ -54,15 +53,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()) {
|
||||||
if(chunk.getOpcode() == ScriptOpCodes.OP_0 && witnessScript != null) {
|
append(chunk.toString(), "script-opcode");
|
||||||
append("<empty>", "script-other");
|
|
||||||
} else {
|
|
||||||
append(chunk.toString(), "script-opcode");
|
|
||||||
}
|
|
||||||
} else if(chunk.isPubKey()) {
|
|
||||||
append("<pubkey" + pubKeyCount++ + ">", "script-pubkey");
|
|
||||||
} else if(chunk.isSignature()) {
|
} else if(chunk.isSignature()) {
|
||||||
append("<signature" + signatureCount++ + ">", "script-signature");
|
append("<signature" + signatureCount++ + ">", "script-signature");
|
||||||
|
} else if(chunk.isPubKey()) {
|
||||||
|
append("<pubkey" + pubKeyCount++ + ">", "script-pubkey");
|
||||||
} else if(chunk.isTaprootControlBlock()) {
|
} else if(chunk.isTaprootControlBlock()) {
|
||||||
append("<controlblock>", "script-controlblock");
|
append("<controlblock>", "script-controlblock");
|
||||||
} else if(chunk.isString()) {
|
} else if(chunk.isString()) {
|
||||||
|
|
|
||||||
|
|
@ -1,28 +1,18 @@
|
||||||
package com.sparrowwallet.sparrow.control;
|
package com.sparrowwallet.sparrow.control;
|
||||||
|
|
||||||
import com.csvreader.CsvWriter;
|
|
||||||
import com.sparrowwallet.drongo.BitcoinUnit;
|
|
||||||
import com.sparrowwallet.drongo.KeyPurpose;
|
import com.sparrowwallet.drongo.KeyPurpose;
|
||||||
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;
|
||||||
import com.sparrowwallet.drongo.wallet.TableType;
|
|
||||||
import com.sparrowwallet.drongo.wallet.Wallet;
|
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||||
import com.sparrowwallet.sparrow.AppServices;
|
import com.sparrowwallet.sparrow.AppServices;
|
||||||
import com.sparrowwallet.sparrow.UnitFormat;
|
|
||||||
import com.sparrowwallet.sparrow.glyphfont.GlyphUtils;
|
|
||||||
import com.sparrowwallet.sparrow.io.Config;
|
|
||||||
import com.sparrowwallet.sparrow.wallet.*;
|
import com.sparrowwallet.sparrow.wallet.*;
|
||||||
import javafx.application.Platform;
|
import javafx.application.Platform;
|
||||||
import javafx.beans.property.ReadOnlyObjectWrapper;
|
import javafx.beans.property.ReadOnlyObjectWrapper;
|
||||||
import javafx.collections.ListChangeListener;
|
import javafx.collections.ListChangeListener;
|
||||||
import javafx.event.ActionEvent;
|
|
||||||
import javafx.scene.control.*;
|
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.VBox;
|
import javafx.scene.layout.VBox;
|
||||||
import javafx.stage.FileChooser;
|
|
||||||
import javafx.stage.Stage;
|
|
||||||
import org.controlsfx.control.textfield.TextFields;
|
import org.controlsfx.control.textfield.TextFields;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
@ -30,11 +20,10 @@ import tornadofx.control.Field;
|
||||||
import tornadofx.control.Fieldset;
|
import tornadofx.control.Fieldset;
|
||||||
import tornadofx.control.Form;
|
import tornadofx.control.Form;
|
||||||
|
|
||||||
import java.io.File;
|
import java.util.ArrayList;
|
||||||
import java.io.FileOutputStream;
|
import java.util.List;
|
||||||
import java.io.IOException;
|
import java.util.Locale;
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.util.Objects;
|
||||||
import java.util.*;
|
|
||||||
|
|
||||||
public class SearchWalletDialog extends Dialog<Entry> {
|
public class SearchWalletDialog extends Dialog<Entry> {
|
||||||
private static final Logger log = LoggerFactory.getLogger(SearchWalletDialog.class);
|
private static final Logger log = LoggerFactory.getLogger(SearchWalletDialog.class);
|
||||||
|
|
@ -59,8 +48,15 @@ public class SearchWalletDialog extends Dialog<Entry> {
|
||||||
dialogPane.getStylesheets().add(AppServices.class.getResource("wallet/wallet.css").toExternalForm());
|
dialogPane.getStylesheets().add(AppServices.class.getResource("wallet/wallet.css").toExternalForm());
|
||||||
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");
|
||||||
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);
|
||||||
|
|
@ -80,12 +76,11 @@ public class SearchWalletDialog extends Dialog<Entry> {
|
||||||
form.getChildren().add(fieldset);
|
form.getChildren().add(fieldset);
|
||||||
|
|
||||||
results = new CoinTreeTable();
|
results = new CoinTreeTable();
|
||||||
results.setTableType(TableType.SEARCH_WALLET);
|
|
||||||
results.setShowRoot(false);
|
results.setShowRoot(false);
|
||||||
results.setPrefWidth(showWallet || showAccount ? 950 : 850);
|
results.setPrefWidth(showWallet || showAccount ? 950 : 850);
|
||||||
results.setUnitFormat(walletForms.iterator().next().getWallet());
|
results.setUnitFormat(walletForms.iterator().next().getWallet());
|
||||||
|
results.setColumnResizePolicy(TreeTableView.CONSTRAINED_RESIZE_POLICY);
|
||||||
results.setPlaceholder(new Label("No results"));
|
results.setPlaceholder(new Label("No results"));
|
||||||
results.setEditable(true);
|
|
||||||
|
|
||||||
if(showWallet) {
|
if(showWallet) {
|
||||||
TreeTableColumn<Entry, String> walletColumn = new TreeTableColumn<>("Wallet");
|
TreeTableColumn<Entry, String> walletColumn = new TreeTableColumn<>("Wallet");
|
||||||
|
|
@ -122,7 +117,7 @@ public class SearchWalletDialog extends Dialog<Entry> {
|
||||||
labelCol.setCellValueFactory((TreeTableColumn.CellDataFeatures<Entry, String> param) -> {
|
labelCol.setCellValueFactory((TreeTableColumn.CellDataFeatures<Entry, String> param) -> {
|
||||||
return param.getValue().getValue().labelProperty();
|
return param.getValue().getValue().labelProperty();
|
||||||
});
|
});
|
||||||
labelCol.setCellFactory(p -> new LabelCell());
|
labelCol.setCellFactory(p -> new SearchLabelCell());
|
||||||
results.getColumns().add(labelCol);
|
results.getColumns().add(labelCol);
|
||||||
|
|
||||||
TreeTableColumn<Entry, Number> amountCol = new TreeTableColumn<>("Value");
|
TreeTableColumn<Entry, Number> amountCol = new TreeTableColumn<>("Value");
|
||||||
|
|
@ -135,20 +130,12 @@ public class SearchWalletDialog extends Dialog<Entry> {
|
||||||
vBox.getChildren().addAll(form, results);
|
vBox.getChildren().addAll(form, results);
|
||||||
dialogPane.setContent(vBox);
|
dialogPane.setContent(vBox);
|
||||||
|
|
||||||
ButtonType exportButtonType = new ButtonType("Export CSV", ButtonBar.ButtonData.LEFT);
|
|
||||||
ButtonType showButtonType = new javafx.scene.control.ButtonType("Show", ButtonBar.ButtonData.APPLY);
|
ButtonType showButtonType = new javafx.scene.control.ButtonType("Show", ButtonBar.ButtonData.APPLY);
|
||||||
ButtonType cancelButtonType = new javafx.scene.control.ButtonType("Cancel", ButtonBar.ButtonData.CANCEL_CLOSE);
|
ButtonType cancelButtonType = new javafx.scene.control.ButtonType("Cancel", ButtonBar.ButtonData.CANCEL_CLOSE);
|
||||||
|
|
||||||
dialogPane.getButtonTypes().addAll(exportButtonType, cancelButtonType, showButtonType);
|
dialogPane.getButtonTypes().addAll(cancelButtonType, showButtonType);
|
||||||
|
|
||||||
Button exportButton = (Button)dialogPane.lookupButton(exportButtonType);
|
Button showButton = (Button) dialogPane.lookupButton(showButtonType);
|
||||||
exportButton.setGraphic(GlyphUtils.getDownArrowGlyph());
|
|
||||||
exportButton.addEventFilter(ActionEvent.ACTION, event -> {
|
|
||||||
event.consume();
|
|
||||||
exportResults(showWallet);
|
|
||||||
});
|
|
||||||
|
|
||||||
Button showButton = (Button)dialogPane.lookupButton(showButtonType);
|
|
||||||
showButton.setDefaultButton(true);
|
showButton.setDefaultButton(true);
|
||||||
showButton.setDisable(true);
|
showButton.setDisable(true);
|
||||||
|
|
||||||
|
|
@ -160,56 +147,52 @@ public class SearchWalletDialog extends Dialog<Entry> {
|
||||||
});
|
});
|
||||||
|
|
||||||
search.textProperty().addListener((observable, oldValue, newValue) -> {
|
search.textProperty().addListener((observable, oldValue, newValue) -> {
|
||||||
searchWallets(newValue);
|
searchWallets(newValue.toLowerCase(Locale.ROOT));
|
||||||
});
|
});
|
||||||
|
|
||||||
SearchWalletEntry rootEntry = new SearchWalletEntry(walletForms.getFirst().getWallet(), Collections.emptyList());
|
|
||||||
RecursiveTreeItem<Entry> rootItem = new RecursiveTreeItem<>(rootEntry, Entry::getChildren);
|
|
||||||
results.setRoot(rootItem);
|
|
||||||
|
|
||||||
setResizable(true);
|
setResizable(true);
|
||||||
results.setupColumnWidths();
|
|
||||||
|
|
||||||
AppServices.moveToActiveWindowScreen(this);
|
AppServices.moveToActiveWindowScreen(this);
|
||||||
|
|
||||||
Platform.runLater(search::requestFocus);
|
Platform.runLater(search::requestFocus);
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<WalletForm> getWalletForms() {
|
private void searchWallets(String searchText) {
|
||||||
return walletForms;
|
List<Entry> matchingEntries = new ArrayList<>();
|
||||||
}
|
|
||||||
|
|
||||||
private void searchWallets(String searchPhrase) {
|
if(!searchText.isEmpty()) {
|
||||||
Set<Entry> matchingEntries = new LinkedHashSet<>();
|
Long searchValue = getSearchValue(searchText);
|
||||||
|
Address searchAddress = getSearchAddress(searchText);
|
||||||
|
|
||||||
if(!searchPhrase.isEmpty()) {
|
for(WalletForm walletForm : walletForms) {
|
||||||
Set<String> searchWords = new LinkedHashSet<>(Arrays.stream(searchPhrase.split("\\s+"))
|
WalletTransactionsEntry walletTransactionsEntry = walletForm.getWalletTransactionsEntry();
|
||||||
.filter(text -> isAddress(text) || isHash(text) || isHashIndex(text)).toList());
|
for(Entry entry : walletTransactionsEntry.getChildren()) {
|
||||||
String freeText = removeOccurrences(searchPhrase, searchWords).trim();
|
if(entry instanceof TransactionEntry transactionEntry) {
|
||||||
if(!freeText.isEmpty()) {
|
if(transactionEntry.getBlockTransaction().getHash().toString().equals(searchText) ||
|
||||||
searchWords.add(freeText);
|
(transactionEntry.getLabel() != null && transactionEntry.getLabel().toLowerCase(Locale.ROOT).contains(searchText)) ||
|
||||||
}
|
(transactionEntry.getValue() != null && searchValue != null && Math.abs(transactionEntry.getValue()) == searchValue) ||
|
||||||
|
(searchAddress != null && transactionEntry.getBlockTransaction().getTransaction().getOutputs().stream().map(output -> output.getScript().getToAddress()).filter(Objects::nonNull).anyMatch(address -> address.equals(searchAddress)))) {
|
||||||
|
matchingEntries.add(entry);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
for(String searchText : searchWords) {
|
for(KeyPurpose keyPurpose : KeyPurpose.DEFAULT_PURPOSES) {
|
||||||
Long searchValue = getSearchValue(searchText);
|
NodeEntry purposeEntry = walletForm.getNodeEntry(keyPurpose);
|
||||||
Address searchAddress = getSearchAddress(searchText);
|
for(Entry entry : purposeEntry.getChildren()) {
|
||||||
searchText = searchText.toLowerCase(Locale.ROOT);
|
if(entry instanceof NodeEntry nodeEntry) {
|
||||||
|
if(nodeEntry.getAddress().toString().toLowerCase(Locale.ROOT).contains(searchText) ||
|
||||||
for(WalletForm walletForm : walletForms) {
|
(nodeEntry.getLabel() != null && nodeEntry.getLabel().toLowerCase(Locale.ROOT).contains(searchText)) ||
|
||||||
WalletTransactionsEntry walletTransactionsEntry = walletForm.getWalletTransactionsEntry();
|
(nodeEntry.getValue() != null && searchValue != null && Math.abs(nodeEntry.getValue()) == searchValue)) {
|
||||||
for(Entry entry : walletTransactionsEntry.getChildren()) {
|
|
||||||
if(entry instanceof TransactionEntry transactionEntry) {
|
|
||||||
if(transactionEntry.getBlockTransaction().getHash().toString().equals(searchText) ||
|
|
||||||
(transactionEntry.getLabel() != null && transactionEntry.getLabel().toLowerCase(Locale.ROOT).contains(searchText)) ||
|
|
||||||
(transactionEntry.getValue() != null && searchValue != null && Math.abs(transactionEntry.getValue()) == searchValue) ||
|
|
||||||
(searchAddress != null && transactionEntry.getBlockTransaction().getTransaction().getOutputs().stream().map(output -> output.getScript().getToAddress()).filter(Objects::nonNull).anyMatch(address -> address.equals(searchAddress)))) {
|
|
||||||
matchingEntries.add(entry);
|
matchingEntries.add(entry);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
for(KeyPurpose keyPurpose : KeyPurpose.DEFAULT_PURPOSES) {
|
for(WalletForm nestedWalletForm : walletForm.getNestedWalletForms()) {
|
||||||
NodeEntry purposeEntry = walletForm.getNodeEntry(keyPurpose);
|
for(KeyPurpose keyPurpose : nestedWalletForm.getWallet().getWalletKeyPurposes()) {
|
||||||
|
NodeEntry purposeEntry = nestedWalletForm.getNodeEntry(keyPurpose);
|
||||||
for(Entry entry : purposeEntry.getChildren()) {
|
for(Entry entry : purposeEntry.getChildren()) {
|
||||||
if(entry instanceof NodeEntry nodeEntry) {
|
if(entry instanceof NodeEntry nodeEntry) {
|
||||||
if(nodeEntry.getAddress().toString().toLowerCase(Locale.ROOT).contains(searchText) ||
|
if(nodeEntry.getAddress().toString().toLowerCase(Locale.ROOT).contains(searchText) ||
|
||||||
|
|
@ -220,38 +203,22 @@ public class SearchWalletDialog extends Dialog<Entry> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
for(WalletForm nestedWalletForm : walletForm.getNestedWalletForms()) {
|
WalletUtxosEntry walletUtxosEntry = walletForm.getWalletUtxosEntry();
|
||||||
for(KeyPurpose keyPurpose : nestedWalletForm.getWallet().getWalletKeyPurposes()) {
|
for(Entry entry : walletUtxosEntry.getChildren()) {
|
||||||
NodeEntry purposeEntry = nestedWalletForm.getNodeEntry(keyPurpose);
|
if(entry instanceof HashIndexEntry hashIndexEntry) {
|
||||||
for(Entry entry : purposeEntry.getChildren()) {
|
if(hashIndexEntry.getBlockTransaction().getHash().toString().toLowerCase(Locale.ROOT).equals(searchText) ||
|
||||||
if(entry instanceof NodeEntry nodeEntry) {
|
(hashIndexEntry.getLabel() != null && hashIndexEntry.getLabel().toLowerCase(Locale.ROOT).contains(searchText)) ||
|
||||||
if(nodeEntry.getAddress().toString().toLowerCase(Locale.ROOT).contains(searchText) ||
|
(hashIndexEntry.getValue() != null && searchValue != null && Math.abs(hashIndexEntry.getValue()) == searchValue)) {
|
||||||
(nodeEntry.getLabel() != null && nodeEntry.getLabel().toLowerCase(Locale.ROOT).contains(searchText)) ||
|
matchingEntries.add(entry);
|
||||||
(nodeEntry.getValue() != null && searchValue != null && Math.abs(nodeEntry.getValue()) == searchValue)) {
|
|
||||||
matchingEntries.add(entry);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
WalletUtxosEntry walletUtxosEntry = walletForm.getWalletUtxosEntry();
|
|
||||||
for(Entry entry : walletUtxosEntry.getChildren()) {
|
|
||||||
if(entry instanceof HashIndexEntry hashIndexEntry) {
|
|
||||||
if(hashIndexEntry.getBlockTransaction().getHash().toString().toLowerCase(Locale.ROOT).equals(searchText) ||
|
|
||||||
hashIndexEntry.getHashIndex().toString().toLowerCase(Locale.ROOT).equals(searchText) ||
|
|
||||||
(hashIndexEntry.getLabel() != null && hashIndexEntry.getLabel().toLowerCase(Locale.ROOT).contains(searchText)) ||
|
|
||||||
(hashIndexEntry.getValue() != null && searchValue != null && Math.abs(hashIndexEntry.getValue()) == searchValue)) {
|
|
||||||
matchingEntries.add(entry);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
SearchWalletEntry rootEntry = new SearchWalletEntry(walletForms.iterator().next().getWallet(), new ArrayList<>(matchingEntries));
|
SearchWalletEntry rootEntry = new SearchWalletEntry(walletForms.iterator().next().getWallet(), matchingEntries);
|
||||||
RecursiveTreeItem<Entry> rootItem = new RecursiveTreeItem<>(rootEntry, Entry::getChildren);
|
RecursiveTreeItem<Entry> rootItem = new RecursiveTreeItem<>(rootEntry, Entry::getChildren);
|
||||||
results.setRoot(rootItem);
|
results.setRoot(rootItem);
|
||||||
}
|
}
|
||||||
|
|
@ -272,95 +239,6 @@ public class SearchWalletDialog extends Dialog<Entry> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean isAddress(String text) {
|
|
||||||
try {
|
|
||||||
Address.fromString(text);
|
|
||||||
return true;
|
|
||||||
} catch(InvalidAddressException e) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean isHash(String text) {
|
|
||||||
return text.length() == 64 && Utils.isHex(text);
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean isHashIndex(String text) {
|
|
||||||
String[] parts = text.split(":");
|
|
||||||
if(parts.length == 2 && isHash(parts[0])) {
|
|
||||||
try {
|
|
||||||
Integer.parseInt(parts[1]);
|
|
||||||
return true;
|
|
||||||
} catch(NumberFormatException e) {
|
|
||||||
//ignore
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
private String removeOccurrences(String inputString, Collection<String> stringsToRemove) {
|
|
||||||
for(String str : stringsToRemove) {
|
|
||||||
inputString = inputString.replaceAll("(?i)" + str, "");
|
|
||||||
}
|
|
||||||
|
|
||||||
return inputString;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void exportResults(boolean showWallet) {
|
|
||||||
Stage window = new Stage();
|
|
||||||
|
|
||||||
FileChooser fileChooser = new FileChooser();
|
|
||||||
fileChooser.setTitle("Export search results to CSV");
|
|
||||||
fileChooser.setInitialFileName(getDialogPane().getHeaderText() + ".csv");
|
|
||||||
|
|
||||||
AppServices.moveToActiveWindowScreen(window, 800, 450);
|
|
||||||
File file = fileChooser.showSaveDialog(window);
|
|
||||||
if(file != null) {
|
|
||||||
try(FileOutputStream outputStream = new FileOutputStream(file)) {
|
|
||||||
CsvWriter writer = new CsvWriter(outputStream, ',', StandardCharsets.UTF_8);
|
|
||||||
List<String> headers = new ArrayList<>(List.of("Wallet", "Account", "Type", "Date", "Txid / Address / Output", "Label", "Value"));
|
|
||||||
if(!showWallet) {
|
|
||||||
headers.remove(0);
|
|
||||||
}
|
|
||||||
writer.writeRecord(headers.toArray(new String[0]));
|
|
||||||
for(TreeItem<Entry> item : results.getRoot().getChildren()) {
|
|
||||||
Entry entry = item.getValue();
|
|
||||||
if(showWallet) {
|
|
||||||
writer.write(entry.getWallet().getMasterName());
|
|
||||||
}
|
|
||||||
writer.write(entry.getWallet().getDisplayName());
|
|
||||||
writer.write(entry.getEntryType());
|
|
||||||
if(entry instanceof TransactionEntry transactionEntry) {
|
|
||||||
writer.write(transactionEntry.getBlockTransaction().getDate() == null ? "Unconfirmed" : EntryCell.DATE_FORMAT.format(transactionEntry.getBlockTransaction().getDate()));
|
|
||||||
writer.write(transactionEntry.getBlockTransaction().getHash().toString());
|
|
||||||
} else if(entry instanceof NodeEntry nodeEntry) {
|
|
||||||
writer.write("");
|
|
||||||
writer.write(nodeEntry.getAddress().toString());
|
|
||||||
} else if(entry instanceof HashIndexEntry hashIndexEntry) {
|
|
||||||
writer.write(hashIndexEntry.getBlockTransaction().getDate() == null ? "Unconfirmed" : EntryCell.DATE_FORMAT.format(hashIndexEntry.getBlockTransaction().getDate()));
|
|
||||||
writer.write(hashIndexEntry.getHashIndex().toString());
|
|
||||||
} else {
|
|
||||||
writer.write("");
|
|
||||||
writer.write("");
|
|
||||||
}
|
|
||||||
writer.write(entry.getLabel());
|
|
||||||
writer.write(getCoinValue(entry.getValue() == null ? 0 : entry.getValue()));
|
|
||||||
writer.endRecord();
|
|
||||||
}
|
|
||||||
writer.close();
|
|
||||||
} catch(IOException e) {
|
|
||||||
log.error("Error exporting search results as CSV", e);
|
|
||||||
AppServices.showErrorDialog("Error exporting search results as CSV", e.getMessage());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private String getCoinValue(Long value) {
|
|
||||||
UnitFormat format = Config.get().getUnitFormat() == null ? UnitFormat.DOT : Config.get().getUnitFormat();
|
|
||||||
return BitcoinUnit.BTC.equals(results.getBitcoinUnit()) ? format.tableFormatBtcValue(value) : String.format(Locale.ENGLISH, "%d", value);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static class SearchWalletEntry extends Entry {
|
private static class SearchWalletEntry extends Entry {
|
||||||
public SearchWalletEntry(Wallet wallet, List<Entry> entries) {
|
public SearchWalletEntry(Wallet wallet, List<Entry> entries) {
|
||||||
super(wallet, wallet.getName(), entries);
|
super(wallet, wallet.getName(), entries);
|
||||||
|
|
@ -386,19 +264,15 @@ public class SearchWalletDialog extends Dialog<Entry> {
|
||||||
@Override
|
@Override
|
||||||
protected void updateItem(Entry entry, boolean empty) {
|
protected void updateItem(Entry entry, boolean empty) {
|
||||||
super.updateItem(entry, empty);
|
super.updateItem(entry, empty);
|
||||||
|
setContextMenu(null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
ContextMenu copyMenu;
|
private static class SearchLabelCell extends LabelCell {
|
||||||
if(entry instanceof TransactionEntry transactionEntry) {
|
@Override
|
||||||
copyMenu = new TransactionContextMenu(getText(), transactionEntry.getBlockTransaction());
|
public void updateItem(String label, boolean empty) {
|
||||||
} else if(entry instanceof NodeEntry nodeEntry) {
|
super.updateItem(label, empty);
|
||||||
copyMenu = new AddressContextMenu(nodeEntry.getAddress(), nodeEntry.getOutputDescriptor(), null, false, null);
|
setContextMenu(null);
|
||||||
} else if(entry instanceof UtxoEntry utxoEntry) {
|
|
||||||
copyMenu = new HashIndexEntryContextMenu(null, utxoEntry);
|
|
||||||
} else {
|
|
||||||
copyMenu = new ContextMenu();
|
|
||||||
}
|
|
||||||
copyMenu.getItems().removeIf(menuItem -> !menuItem.getText().startsWith("Copy"));
|
|
||||||
setContextMenu(copyMenu);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,24 +1,19 @@
|
||||||
package com.sparrowwallet.sparrow.control;
|
package com.sparrowwallet.sparrow.control;
|
||||||
|
|
||||||
import com.sparrowwallet.drongo.wallet.DeterministicSeed;
|
|
||||||
import com.sparrowwallet.drongo.wallet.Keystore;
|
import com.sparrowwallet.drongo.wallet.Keystore;
|
||||||
import com.sparrowwallet.drongo.wallet.SeedQR;
|
|
||||||
import com.sparrowwallet.sparrow.AppServices;
|
import com.sparrowwallet.sparrow.AppServices;
|
||||||
import javafx.application.Platform;
|
import javafx.application.Platform;
|
||||||
import javafx.event.ActionEvent;
|
|
||||||
import javafx.scene.control.*;
|
import javafx.scene.control.*;
|
||||||
import javafx.scene.layout.AnchorPane;
|
import javafx.scene.layout.AnchorPane;
|
||||||
import javafx.scene.layout.StackPane;
|
import javafx.scene.layout.StackPane;
|
||||||
|
|
||||||
import java.util.Optional;
|
|
||||||
|
|
||||||
public class SeedDisplayDialog extends Dialog<Void> {
|
public class SeedDisplayDialog extends Dialog<Void> {
|
||||||
public SeedDisplayDialog(Keystore decryptedKeystore) {
|
public SeedDisplayDialog(Keystore decryptedKeystore) {
|
||||||
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());
|
||||||
AppServices.setStageIcon(dialogPane.getScene().getWindow());
|
AppServices.setStageIcon(dialogPane.getScene().getWindow());
|
||||||
|
|
||||||
int lines = Math.ceilDiv(decryptedKeystore.getSeed().getMnemonicCode().size(), 3);
|
int lines = decryptedKeystore.getSeed().getMnemonicCode().size() / 3;
|
||||||
int height = lines * 40;
|
int height = lines * 40;
|
||||||
|
|
||||||
StackPane stackPane = new StackPane();
|
StackPane stackPane = new StackPane();
|
||||||
|
|
@ -44,19 +39,8 @@ public class SeedDisplayDialog extends Dialog<Void> {
|
||||||
|
|
||||||
stackPane.getChildren().addAll(anchorPane);
|
stackPane.getChildren().addAll(anchorPane);
|
||||||
|
|
||||||
if(decryptedKeystore.getSeed().getType() == DeterministicSeed.Type.BIP39) {
|
|
||||||
final ButtonType seedQRButtonType = new javafx.scene.control.ButtonType("Show SeedQR", ButtonBar.ButtonData.LEFT);
|
|
||||||
dialogPane.getButtonTypes().add(seedQRButtonType);
|
|
||||||
|
|
||||||
Button seedQRButton = (Button)dialogPane.lookupButton(seedQRButtonType);
|
|
||||||
seedQRButton.addEventFilter(ActionEvent.ACTION, event -> {
|
|
||||||
event.consume();
|
|
||||||
showSeedQR(decryptedKeystore);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
final ButtonType cancelButtonType = new javafx.scene.control.ButtonType("Cancel", ButtonBar.ButtonData.CANCEL_CLOSE);
|
final ButtonType cancelButtonType = new javafx.scene.control.ButtonType("Cancel", ButtonBar.ButtonData.CANCEL_CLOSE);
|
||||||
dialogPane.getButtonTypes().add(cancelButtonType);
|
dialogPane.getButtonTypes().addAll(cancelButtonType);
|
||||||
|
|
||||||
dialogPane.setPrefWidth(500);
|
dialogPane.setPrefWidth(500);
|
||||||
dialogPane.setPrefHeight(150 + height);
|
dialogPane.setPrefHeight(150 + height);
|
||||||
|
|
@ -64,15 +48,4 @@ public class SeedDisplayDialog extends Dialog<Void> {
|
||||||
|
|
||||||
Platform.runLater(() -> keystoreAccordion.setExpandedPane(keystorePane));
|
Platform.runLater(() -> keystoreAccordion.setExpandedPane(keystorePane));
|
||||||
}
|
}
|
||||||
|
|
||||||
private void showSeedQR(Keystore decryptedKeystore) {
|
|
||||||
Optional<ButtonType> optButtonType = AppServices.showWarningDialog("Sensitive QR", "The following QR contains these seed words. " +
|
|
||||||
"Be careful before displaying or digitally recording it.\n\nAre you sure you want to continue?", ButtonType.YES, ButtonType.NO);
|
|
||||||
if(optButtonType.isPresent() && optButtonType.get() == ButtonType.YES) {
|
|
||||||
String seedQR = SeedQR.getSeedQR(decryptedKeystore.getSeed());
|
|
||||||
QRDisplayDialog qrDisplayDialog = new QRDisplayDialog(seedQR);
|
|
||||||
qrDisplayDialog.initOwner(getDialogPane().getScene().getWindow());
|
|
||||||
qrDisplayDialog.showAndWait();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,34 +0,0 @@
|
||||||
package com.sparrowwallet.sparrow.control;
|
|
||||||
|
|
||||||
import javafx.scene.control.ContextMenu;
|
|
||||||
import javafx.scene.control.MenuItem;
|
|
||||||
import javafx.scene.input.Clipboard;
|
|
||||||
import javafx.scene.input.ClipboardContent;
|
|
||||||
import org.fxmisc.richtext.CodeArea;
|
|
||||||
|
|
||||||
public class SelectableCodeArea extends CodeArea {
|
|
||||||
public SelectableCodeArea() {
|
|
||||||
super();
|
|
||||||
|
|
||||||
ContextMenu contextMenu = new ContextMenu();
|
|
||||||
MenuItem copy = new MenuItem("Copy");
|
|
||||||
copy.setDisable(true);
|
|
||||||
copy.setOnAction(event -> {
|
|
||||||
ClipboardContent content = new ClipboardContent();
|
|
||||||
content.putString(getSelectedText());
|
|
||||||
Clipboard.getSystemClipboard().setContent(content);
|
|
||||||
});
|
|
||||||
MenuItem copyAll = new MenuItem("Copy All");
|
|
||||||
copyAll.setOnAction(event -> {
|
|
||||||
ClipboardContent content = new ClipboardContent();
|
|
||||||
content.putString(getText());
|
|
||||||
Clipboard.getSystemClipboard().setContent(content);
|
|
||||||
});
|
|
||||||
contextMenu.getItems().addAll(copy, copyAll);
|
|
||||||
setContextMenu(contextMenu);
|
|
||||||
|
|
||||||
selectedTextProperty().addListener((observable, oldValue, newValue) -> {
|
|
||||||
copy.setDisable(newValue.isEmpty());
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -2,52 +2,39 @@ package com.sparrowwallet.sparrow.control;
|
||||||
|
|
||||||
import com.csvreader.CsvReader;
|
import com.csvreader.CsvReader;
|
||||||
import com.sparrowwallet.drongo.BitcoinUnit;
|
import com.sparrowwallet.drongo.BitcoinUnit;
|
||||||
import com.sparrowwallet.drongo.OsType;
|
|
||||||
import com.sparrowwallet.drongo.address.Address;
|
import com.sparrowwallet.drongo.address.Address;
|
||||||
import com.sparrowwallet.drongo.address.InvalidAddressException;
|
import com.sparrowwallet.drongo.address.InvalidAddressException;
|
||||||
import com.sparrowwallet.drongo.dns.DnsPayment;
|
|
||||||
import com.sparrowwallet.drongo.dns.DnsPaymentCache;
|
|
||||||
import com.sparrowwallet.drongo.dns.DnsPaymentResolver;
|
|
||||||
import com.sparrowwallet.drongo.dns.DnsPaymentValidationException;
|
|
||||||
import com.sparrowwallet.drongo.protocol.Transaction;
|
import com.sparrowwallet.drongo.protocol.Transaction;
|
||||||
import com.sparrowwallet.drongo.silentpayments.SilentPayment;
|
|
||||||
import com.sparrowwallet.drongo.silentpayments.SilentPaymentAddress;
|
|
||||||
import com.sparrowwallet.drongo.uri.BitcoinURIParseException;
|
|
||||||
import com.sparrowwallet.drongo.wallet.Payment;
|
import com.sparrowwallet.drongo.wallet.Payment;
|
||||||
import com.sparrowwallet.sparrow.AppServices;
|
import com.sparrowwallet.sparrow.AppServices;
|
||||||
import com.sparrowwallet.sparrow.EventManager;
|
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
|
||||||
import com.sparrowwallet.sparrow.event.RequestConnectEvent;
|
|
||||||
import com.sparrowwallet.sparrow.glyphfont.GlyphUtils;
|
|
||||||
import com.sparrowwallet.sparrow.io.Config;
|
|
||||||
import javafx.application.Platform;
|
|
||||||
import javafx.collections.FXCollections;
|
import javafx.collections.FXCollections;
|
||||||
import javafx.collections.ObservableList;
|
import javafx.collections.ObservableList;
|
||||||
import javafx.concurrent.Service;
|
|
||||||
import javafx.concurrent.Task;
|
|
||||||
import javafx.event.ActionEvent;
|
|
||||||
import javafx.scene.Node;
|
import javafx.scene.Node;
|
||||||
import javafx.scene.control.*;
|
import javafx.scene.control.*;
|
||||||
|
import javafx.scene.image.Image;
|
||||||
|
import javafx.scene.image.ImageView;
|
||||||
import javafx.scene.input.Clipboard;
|
import javafx.scene.input.Clipboard;
|
||||||
import javafx.scene.layout.StackPane;
|
import javafx.scene.layout.StackPane;
|
||||||
import javafx.stage.FileChooser;
|
import javafx.stage.FileChooser;
|
||||||
import javafx.util.StringConverter;
|
import javafx.util.StringConverter;
|
||||||
import org.controlsfx.control.spreadsheet.*;
|
import org.controlsfx.control.spreadsheet.*;
|
||||||
|
import org.controlsfx.glyphfont.Glyph;
|
||||||
|
import org.controlsfx.tools.Platform;
|
||||||
|
|
||||||
import java.io.*;
|
import java.io.*;
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Optional;
|
|
||||||
import java.util.concurrent.ExecutionException;
|
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
import java.util.stream.IntStream;
|
import java.util.stream.IntStream;
|
||||||
|
|
||||||
public class SendToManyDialog extends Dialog<List<Payment>> {
|
public class SendToManyDialog extends Dialog<List<Payment>> {
|
||||||
private final BitcoinUnit bitcoinUnit;
|
private final BitcoinUnit bitcoinUnit;
|
||||||
private final SpreadsheetView spreadsheetView;
|
private final SpreadsheetView spreadsheetView;
|
||||||
public static final SendToAddressCellType SEND_TO_ADDRESS = new SendToAddressCellType();
|
public static final AddressCellType ADDRESS = new AddressCellType();
|
||||||
|
|
||||||
public SendToManyDialog(BitcoinUnit bitcoinUnit, List<Payment> payments) {
|
public SendToManyDialog(BitcoinUnit bitcoinUnit) {
|
||||||
this.bitcoinUnit = bitcoinUnit;
|
this.bitcoinUnit = bitcoinUnit;
|
||||||
|
|
||||||
final DialogPane dialogPane = new SendToManyDialogPane();
|
final DialogPane dialogPane = new SendToManyDialogPane();
|
||||||
|
|
@ -55,10 +42,10 @@ public class SendToManyDialog extends Dialog<List<Payment>> {
|
||||||
setTitle("Send to Many");
|
setTitle("Send to Many");
|
||||||
dialogPane.getStylesheets().add(AppServices.class.getResource("general.css").toExternalForm());
|
dialogPane.getStylesheets().add(AppServices.class.getResource("general.css").toExternalForm());
|
||||||
dialogPane.setHeaderText("Send to many recipients by specifying addresses and amounts.\nOnly the first row's label is necessary.");
|
dialogPane.setHeaderText("Send to many recipients by specifying addresses and amounts.\nOnly the first row's label is necessary.");
|
||||||
dialogPane.setGraphic(new DialogImage(DialogImage.Type.SPARROW));
|
Image image = new Image("/image/sparrow-small.png");
|
||||||
|
dialogPane.setGraphic(new ImageView(image));
|
||||||
|
|
||||||
List<Payment> initialPayments = IntStream.range(0, 100)
|
List<Payment> initialPayments = IntStream.range(0, 100).mapToObj(i -> new Payment(null, null, -1, false)).collect(Collectors.toList());
|
||||||
.mapToObj(i -> i < payments.size() ? payments.get(i) : new Payment(null, null, -1, false)).collect(Collectors.toList());
|
|
||||||
Grid grid = getGrid(initialPayments);
|
Grid grid = getGrid(initialPayments);
|
||||||
|
|
||||||
spreadsheetView = new SpreadsheetView(grid) {
|
spreadsheetView = new SpreadsheetView(grid) {
|
||||||
|
|
@ -83,16 +70,14 @@ public class SendToManyDialog extends Dialog<List<Payment>> {
|
||||||
dialogPane.setContent(stackPane);
|
dialogPane.setContent(stackPane);
|
||||||
|
|
||||||
dialogPane.getButtonTypes().addAll(ButtonType.OK, ButtonType.CANCEL);
|
dialogPane.getButtonTypes().addAll(ButtonType.OK, ButtonType.CANCEL);
|
||||||
Button okButton = (Button) dialogPane.lookupButton(ButtonType.OK);
|
|
||||||
okButton.addEventFilter(ActionEvent.ACTION, event -> {
|
|
||||||
getPayments();
|
|
||||||
event.consume();
|
|
||||||
});
|
|
||||||
|
|
||||||
final ButtonType loadCsvButtonType = new javafx.scene.control.ButtonType("Load CSV", ButtonBar.ButtonData.LEFT);
|
final ButtonType loadCsvButtonType = new javafx.scene.control.ButtonType("Load CSV", ButtonBar.ButtonData.LEFT);
|
||||||
dialogPane.getButtonTypes().add(loadCsvButtonType);
|
dialogPane.getButtonTypes().add(loadCsvButtonType);
|
||||||
|
|
||||||
setResultConverter((_) -> null);
|
setResultConverter((dialogButton) -> {
|
||||||
|
ButtonBar.ButtonData data = dialogButton == null ? null : dialogButton.getButtonData();
|
||||||
|
return data == ButtonBar.ButtonData.OK_DONE ? getPayments() : null;
|
||||||
|
});
|
||||||
|
|
||||||
dialogPane.setPrefWidth(850);
|
dialogPane.setPrefWidth(850);
|
||||||
dialogPane.setPrefHeight(500);
|
dialogPane.setPrefHeight(500);
|
||||||
|
|
@ -102,36 +87,30 @@ public class SendToManyDialog extends Dialog<List<Payment>> {
|
||||||
}
|
}
|
||||||
|
|
||||||
private Grid getGrid(List<Payment> payments) {
|
private Grid getGrid(List<Payment> payments) {
|
||||||
return createGrid(payments.stream().map(payment -> new SendToPayment(payment, SendToAddress.fromPayment(payment))).collect(Collectors.toList()));
|
int rowCount = payments.size();
|
||||||
}
|
|
||||||
|
|
||||||
private Grid createGrid(List<SendToPayment> sendToPayments) {
|
|
||||||
int rowCount = sendToPayments.size();
|
|
||||||
int columnCount = 3;
|
int columnCount = 3;
|
||||||
GridBase grid = new GridBase(rowCount, columnCount);
|
GridBase grid = new GridBase(rowCount, columnCount);
|
||||||
ObservableList<ObservableList<SpreadsheetCell>> rows = FXCollections.observableArrayList();
|
ObservableList<ObservableList<SpreadsheetCell>> rows = FXCollections.observableArrayList();
|
||||||
for(int row = 0; row < grid.getRowCount(); ++row) {
|
for(int row = 0; row < grid.getRowCount(); ++row) {
|
||||||
SendToPayment sendToPayment = sendToPayments.get(row);
|
|
||||||
final ObservableList<SpreadsheetCell> list = FXCollections.observableArrayList();
|
final ObservableList<SpreadsheetCell> list = FXCollections.observableArrayList();
|
||||||
|
|
||||||
SendToAddress sendToAddress = sendToPayment.sendToAddress();
|
SpreadsheetCell addressCell = ADDRESS.createCell(row, 0, 1, 1, payments.get(row).getAddress());
|
||||||
SpreadsheetCell addressCell = SEND_TO_ADDRESS.createCell(row, 0, 1, 1, sendToAddress);
|
|
||||||
addressCell.getStyleClass().add("fixed-width");
|
addressCell.getStyleClass().add("fixed-width");
|
||||||
list.add(addressCell);
|
list.add(addressCell);
|
||||||
|
|
||||||
double amount = (double)sendToPayment.payment().getAmount();
|
double amount = (double)payments.get(row).getAmount();
|
||||||
if(bitcoinUnit == BitcoinUnit.BTC) {
|
if(bitcoinUnit == BitcoinUnit.BTC) {
|
||||||
amount = amount / Transaction.SATOSHIS_PER_BITCOIN;
|
amount = amount / Transaction.SATOSHIS_PER_BITCOIN;
|
||||||
}
|
}
|
||||||
SpreadsheetCell amountCell = SpreadsheetCellType.DOUBLE.createCell(row, 1, 1, 1, amount < 0 ? null : amount);
|
SpreadsheetCell amountCell = SpreadsheetCellType.DOUBLE.createCell(row, 1, 1, 1, amount < 0 ? null : amount);
|
||||||
amountCell.setFormat(bitcoinUnit == BitcoinUnit.BTC ? "0.00000000" : "###,###");
|
amountCell.setFormat(bitcoinUnit == BitcoinUnit.BTC ? "0.00000000" : "###,###");
|
||||||
amountCell.getStyleClass().add("number-value");
|
amountCell.getStyleClass().add("number-value");
|
||||||
if(OsType.getCurrent() == OsType.MACOS) {
|
if(Platform.getCurrent() == Platform.OSX) {
|
||||||
amountCell.getStyleClass().add("number-field");
|
amountCell.getStyleClass().add("number-field");
|
||||||
}
|
}
|
||||||
list.add(amountCell);
|
list.add(amountCell);
|
||||||
|
|
||||||
list.add(SpreadsheetCellType.STRING.createCell(row, 2, 1, 1, sendToPayment.payment().getLabel()));
|
list.add(SpreadsheetCellType.STRING.createCell(row, 2, 1, 1, payments.get(row).getLabel()));
|
||||||
rows.add(list);
|
rows.add(list);
|
||||||
}
|
}
|
||||||
grid.setRows(rows);
|
grid.setRows(rows);
|
||||||
|
|
@ -140,49 +119,32 @@ public class SendToManyDialog extends Dialog<List<Payment>> {
|
||||||
return grid;
|
return grid;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void getPayments() {
|
private List<Payment> getPayments() {
|
||||||
if(needsResolution() && Config.get().hasServer() && !AppServices.isConnected() && !AppServices.isConnecting()) {
|
List<Payment> payments = new ArrayList<>();
|
||||||
if(Config.get().getConnectToResolve() == null || Config.get().getConnectToResolve() == Boolean.FALSE) {
|
Grid grid = spreadsheetView.getGrid();
|
||||||
Platform.runLater(() -> {
|
String firstLabel = null;
|
||||||
ConfirmationAlert confirmationAlert = new ConfirmationAlert("Connect to resolve?", "You are currently offline. Connect to resolve the addresses?", ButtonType.NO, ButtonType.YES);
|
for(int row = 0; row < grid.getRowCount(); row++) {
|
||||||
Optional<ButtonType> optType = confirmationAlert.showAndWait();
|
|
||||||
if(confirmationAlert.isDontAskAgain() && optType.isPresent()) {
|
|
||||||
Config.get().setConnectToResolve(optType.get() == ButtonType.YES);
|
|
||||||
}
|
|
||||||
if(optType.isPresent() && optType.get() == ButtonType.YES) {
|
|
||||||
EventManager.get().post(new RequestConnectEvent());
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
Platform.runLater(() -> EventManager.get().post(new RequestConnectEvent()));
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
CreatePaymentsService createPaymentsService = new CreatePaymentsService();
|
|
||||||
createPaymentsService.setOnSucceeded(_ -> {
|
|
||||||
List<Payment> payments = createPaymentsService.getValue();
|
|
||||||
if(payments != null) {
|
|
||||||
setResult(payments);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
createPaymentsService.setOnFailed(event -> {
|
|
||||||
Throwable ex = event.getSource().getException();
|
|
||||||
AppServices.showErrorDialog("Error creating payments", ex.getMessage());
|
|
||||||
});
|
|
||||||
createPaymentsService.start();
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean needsResolution() {
|
|
||||||
for(int row = 0; row < spreadsheetView.getGrid().getRowCount(); row++) {
|
|
||||||
ObservableList<SpreadsheetCell> rowCells = spreadsheetView.getItems().get(row);
|
ObservableList<SpreadsheetCell> rowCells = spreadsheetView.getItems().get(row);
|
||||||
SendToAddress sendToAddress = (SendToAddress)rowCells.getFirst().getItem();
|
Address address = (Address)rowCells.get(0).getItem();
|
||||||
if(sendToAddress.hrn != null && DnsPaymentCache.getDnsPayment(sendToAddress.hrn) == null) {
|
Double value = (Double)rowCells.get(1).getItem();
|
||||||
return true;
|
String label = (String)rowCells.get(2).getItem();
|
||||||
|
if(firstLabel == null) {
|
||||||
|
firstLabel = label;
|
||||||
|
}
|
||||||
|
if(label == null || label.isEmpty()) {
|
||||||
|
label = firstLabel;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(address != null && value != null) {
|
||||||
|
if(bitcoinUnit == BitcoinUnit.BTC) {
|
||||||
|
value = value * Transaction.SATOSHIS_PER_BITCOIN;
|
||||||
|
}
|
||||||
|
|
||||||
|
payments.add(new Payment(address, label, value.longValue(), false));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
return payments;
|
||||||
}
|
}
|
||||||
|
|
||||||
private class SendToManyDialogPane extends DialogPane {
|
private class SendToManyDialogPane extends DialogPane {
|
||||||
|
|
@ -192,14 +154,14 @@ public class SendToManyDialog extends Dialog<List<Payment>> {
|
||||||
if(buttonType.getButtonData() == ButtonBar.ButtonData.LEFT) {
|
if(buttonType.getButtonData() == ButtonBar.ButtonData.LEFT) {
|
||||||
Button loadButton = new Button(buttonType.getText());
|
Button loadButton = new Button(buttonType.getText());
|
||||||
loadButton.setGraphicTextGap(5);
|
loadButton.setGraphicTextGap(5);
|
||||||
loadButton.setGraphic(GlyphUtils.getUpArrowGlyph());
|
loadButton.setGraphic(getGlyph(FontAwesome5.Glyph.ARROW_UP));
|
||||||
final ButtonBar.ButtonData buttonData = buttonType.getButtonData();
|
final ButtonBar.ButtonData buttonData = buttonType.getButtonData();
|
||||||
ButtonBar.setButtonData(loadButton, buttonData);
|
ButtonBar.setButtonData(loadButton, buttonData);
|
||||||
loadButton.setOnAction(event -> {
|
loadButton.setOnAction(event -> {
|
||||||
FileChooser fileChooser = new FileChooser();
|
FileChooser fileChooser = new FileChooser();
|
||||||
fileChooser.setTitle("Open CSV");
|
fileChooser.setTitle("Open CSV");
|
||||||
fileChooser.getExtensionFilters().addAll(
|
fileChooser.getExtensionFilters().addAll(
|
||||||
new FileChooser.ExtensionFilter("All Files", OsType.getCurrent().equals(OsType.UNIX) ? "*" : "*.*"),
|
new FileChooser.ExtensionFilter("All Files", org.controlsfx.tools.Platform.getCurrent().equals(org.controlsfx.tools.Platform.UNIX) ? "*" : "*.*"),
|
||||||
new FileChooser.ExtensionFilter("CSV", "*.csv")
|
new FileChooser.ExtensionFilter("CSV", "*.csv")
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -207,7 +169,7 @@ public class SendToManyDialog extends Dialog<List<Payment>> {
|
||||||
File file = fileChooser.showOpenDialog(this.getScene().getWindow());
|
File file = fileChooser.showOpenDialog(this.getScene().getWindow());
|
||||||
if(file != null) {
|
if(file != null) {
|
||||||
try {
|
try {
|
||||||
List<SendToPayment> csvPayments = new ArrayList<>();
|
List<Payment> csvPayments = new ArrayList<>();
|
||||||
try(Reader reader = new FileReader(file, StandardCharsets.UTF_8)) {
|
try(Reader reader = new FileReader(file, StandardCharsets.UTF_8)) {
|
||||||
CsvReader csvReader = new CsvReader(reader);
|
CsvReader csvReader = new CsvReader(reader);
|
||||||
while(csvReader.readRecord()) {
|
while(csvReader.readRecord()) {
|
||||||
|
|
@ -223,22 +185,9 @@ public class SendToManyDialog extends Dialog<List<Payment>> {
|
||||||
} else {
|
} else {
|
||||||
amount = Long.parseLong(csvReader.get(1).replace(",", ""));
|
amount = Long.parseLong(csvReader.get(1).replace(",", ""));
|
||||||
}
|
}
|
||||||
|
Address address = Address.fromString(csvReader.get(0));
|
||||||
String label = csvReader.get(2);
|
String label = csvReader.get(2);
|
||||||
Optional<String> optDnsPaymentHrn = DnsPayment.getHrn(csvReader.get(0));
|
csvPayments.add(new Payment(address, label, amount, false));
|
||||||
if(optDnsPaymentHrn.isPresent()) {
|
|
||||||
Payment payment = new Payment(null, label, amount, false);
|
|
||||||
csvPayments.add(new SendToPayment(payment, new SendToAddress(optDnsPaymentHrn.get())));
|
|
||||||
} else {
|
|
||||||
try {
|
|
||||||
SilentPaymentAddress silentPaymentAddress = SilentPaymentAddress.from(csvReader.get(0));
|
|
||||||
Payment payment = new SilentPayment(silentPaymentAddress, label, amount, false);
|
|
||||||
csvPayments.add(new SendToPayment(payment, SendToAddress.fromPayment(payment)));
|
|
||||||
} catch(Exception e) {
|
|
||||||
Address address = Address.fromString(csvReader.get(0));
|
|
||||||
Payment payment = new Payment(address, label, amount, false);
|
|
||||||
csvPayments.add(new SendToPayment(payment, SendToAddress.fromPayment(payment)));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch(NumberFormatException e) {
|
} catch(NumberFormatException e) {
|
||||||
//ignore and continue - probably a header line
|
//ignore and continue - probably a header line
|
||||||
} catch(InvalidAddressException e) {
|
} catch(InvalidAddressException e) {
|
||||||
|
|
@ -251,7 +200,7 @@ public class SendToManyDialog extends Dialog<List<Payment>> {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
spreadsheetView.setGrid(createGrid(csvPayments));
|
spreadsheetView.setGrid(getGrid(csvPayments));
|
||||||
}
|
}
|
||||||
} catch(IOException e) {
|
} catch(IOException e) {
|
||||||
AppServices.showErrorDialog("Cannot load CSV", e.getMessage());
|
AppServices.showErrorDialog("Cannot load CSV", e.getMessage());
|
||||||
|
|
@ -266,18 +215,24 @@ public class SendToManyDialog extends Dialog<List<Payment>> {
|
||||||
|
|
||||||
return button;
|
return button;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private Glyph getGlyph(FontAwesome5.Glyph glyphName) {
|
||||||
|
Glyph glyph = new Glyph(FontAwesome5.FONT_NAME, glyphName);
|
||||||
|
glyph.setFontSize(11);
|
||||||
|
return glyph;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static class SendToAddressCellType extends SpreadsheetCellType<SendToAddress> {
|
public static class AddressCellType extends SpreadsheetCellType<Address> {
|
||||||
public SendToAddressCellType() {
|
public AddressCellType() {
|
||||||
this(new StringConverterWithFormat<>(new SendToAddressStringConverter()) {
|
this(new StringConverterWithFormat<>(new AddressStringConverter()) {
|
||||||
@Override
|
@Override
|
||||||
public String toString(SendToAddress item) {
|
public String toString(Address item) {
|
||||||
return toStringFormat(item, ""); //$NON-NLS-1$
|
return toStringFormat(item, ""); //$NON-NLS-1$
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public SendToAddress fromString(String str) {
|
public Address fromString(String str) {
|
||||||
if(str == null || str.isEmpty()) { //$NON-NLS-1$
|
if(str == null || str.isEmpty()) { //$NON-NLS-1$
|
||||||
return null;
|
return null;
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -286,7 +241,7 @@ public class SendToManyDialog extends Dialog<List<Payment>> {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String toStringFormat(SendToAddress item, String format) {
|
public String toStringFormat(Address item, String format) {
|
||||||
try {
|
try {
|
||||||
if(item == null) {
|
if(item == null) {
|
||||||
return ""; //$NON-NLS-1$
|
return ""; //$NON-NLS-1$
|
||||||
|
|
@ -300,7 +255,7 @@ public class SendToManyDialog extends Dialog<List<Payment>> {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public SendToAddressCellType(StringConverter<SendToAddress> converter) {
|
public AddressCellType(StringConverter<Address> converter) {
|
||||||
super(converter);
|
super(converter);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -310,7 +265,7 @@ public class SendToManyDialog extends Dialog<List<Payment>> {
|
||||||
}
|
}
|
||||||
|
|
||||||
public SpreadsheetCell createCell(final int row, final int column, final int rowSpan, final int columnSpan,
|
public SpreadsheetCell createCell(final int row, final int column, final int rowSpan, final int columnSpan,
|
||||||
final SendToAddress value) {
|
final Address value) {
|
||||||
SpreadsheetCell cell = new SpreadsheetCellBase(row, column, rowSpan, columnSpan, this);
|
SpreadsheetCell cell = new SpreadsheetCellBase(row, column, rowSpan, columnSpan, this);
|
||||||
cell.setItem(value);
|
cell.setItem(value);
|
||||||
return cell;
|
return cell;
|
||||||
|
|
@ -323,7 +278,7 @@ public class SendToManyDialog extends Dialog<List<Payment>> {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean match(Object value, Object... options) {
|
public boolean match(Object value, Object... options) {
|
||||||
if(value instanceof SendToAddress)
|
if(value instanceof Address)
|
||||||
return true;
|
return true;
|
||||||
else {
|
else {
|
||||||
try {
|
try {
|
||||||
|
|
@ -336,9 +291,9 @@ public class SendToManyDialog extends Dialog<List<Payment>> {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public SendToAddress convertValue(Object value) {
|
public Address convertValue(Object value) {
|
||||||
if(value instanceof SendToAddress)
|
if(value instanceof Address)
|
||||||
return (SendToAddress)value;
|
return (Address)value;
|
||||||
else {
|
else {
|
||||||
try {
|
try {
|
||||||
return converter.fromString(value == null ? null : value.toString());
|
return converter.fromString(value == null ? null : value.toString());
|
||||||
|
|
@ -349,155 +304,13 @@ public class SendToManyDialog extends Dialog<List<Payment>> {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String toString(SendToAddress item) {
|
public String toString(Address item) {
|
||||||
return converter.toString(item);
|
return converter.toString(item);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String toString(SendToAddress item, String format) {
|
public String toString(Address item, String format) {
|
||||||
return ((StringConverterWithFormat<SendToAddress>)converter).toStringFormat(item, format);
|
return ((StringConverterWithFormat<Address>)converter).toStringFormat(item, format);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
public static class SendToAddress {
|
|
||||||
private final String hrn;
|
|
||||||
private final Address address;
|
|
||||||
private final SilentPaymentAddress silentPaymentAddress;
|
|
||||||
|
|
||||||
public SendToAddress(String hrn) {
|
|
||||||
this.hrn = hrn;
|
|
||||||
this.address = null;
|
|
||||||
this.silentPaymentAddress = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
public SendToAddress(Address address) {
|
|
||||||
this.hrn = null;
|
|
||||||
this.address = address;
|
|
||||||
this.silentPaymentAddress = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
public SendToAddress(SilentPaymentAddress silentPaymentAddress) {
|
|
||||||
this.hrn = null;
|
|
||||||
this.address = null;
|
|
||||||
this.silentPaymentAddress = silentPaymentAddress;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String toString() {
|
|
||||||
return hrn == null ? silentPaymentAddress == null ? (address == null ? null : address.toString()) : silentPaymentAddress.toString() : hrn;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static SendToAddress fromPayment(Payment payment) {
|
|
||||||
DnsPayment dnsPayment = DnsPaymentCache.getDnsPayment(payment);
|
|
||||||
if(dnsPayment != null) {
|
|
||||||
return new SendToAddress(dnsPayment.hrn());
|
|
||||||
}
|
|
||||||
return payment instanceof SilentPayment ? new SendToAddress(((SilentPayment)payment).getSilentPaymentAddress()) : new SendToAddress(payment.getAddress());
|
|
||||||
}
|
|
||||||
|
|
||||||
public Payment toPayment(String label, long value, boolean sendMax) throws DnsPaymentValidationException, IOException, ExecutionException, InterruptedException, BitcoinURIParseException {
|
|
||||||
if(hrn != null) {
|
|
||||||
DnsPayment dnsPayment = DnsPaymentCache.getDnsPayment(hrn);
|
|
||||||
if(dnsPayment == null) {
|
|
||||||
DnsPaymentResolver resolver = new DnsPaymentResolver(hrn);
|
|
||||||
Optional<DnsPayment> optDnsPayment = resolver.resolve();
|
|
||||||
if(optDnsPayment.isPresent()) {
|
|
||||||
dnsPayment = optDnsPayment.get();
|
|
||||||
if(dnsPayment.hasAddress()) {
|
|
||||||
DnsPaymentCache.putDnsPayment(dnsPayment.bitcoinURI().getAddress(), dnsPayment);
|
|
||||||
} else if(dnsPayment.hasSilentPaymentAddress()) {
|
|
||||||
DnsPaymentCache.putDnsPayment(dnsPayment.bitcoinURI().getSilentPaymentAddress(), dnsPayment);
|
|
||||||
}
|
|
||||||
return getPayment(optDnsPayment.get(), label, value, sendMax);
|
|
||||||
} else {
|
|
||||||
throw new IllegalArgumentException("Payment to " + hrn + " could not be resolved.");
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return getPayment(dnsPayment, label, value, sendMax);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if(silentPaymentAddress != null) {
|
|
||||||
return new SilentPayment(silentPaymentAddress, label, value, sendMax);
|
|
||||||
} else {
|
|
||||||
return new Payment(address, label, value, sendMax);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static Payment getPayment(DnsPayment dnsPayment, String label, long value, boolean sendMax) {
|
|
||||||
if(dnsPayment.hasAddress()) {
|
|
||||||
return new Payment(dnsPayment.bitcoinURI().getAddress(), label, value, sendMax);
|
|
||||||
} else if(dnsPayment.hasSilentPaymentAddress()) {
|
|
||||||
return new SilentPayment(dnsPayment.bitcoinURI().getSilentPaymentAddress(), label, value, sendMax);
|
|
||||||
} else {
|
|
||||||
throw new IllegalArgumentException("Payment to " + dnsPayment + " has no associated address.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static class SendToAddressStringConverter extends StringConverter<SendToAddress> {
|
|
||||||
private final AddressStringConverter addressStringConverter = new AddressStringConverter();
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public SendToAddress fromString(String value) {
|
|
||||||
Optional<String> optDnsPaymentHrn = DnsPayment.getHrn(value);
|
|
||||||
if(optDnsPaymentHrn.isPresent()) {
|
|
||||||
return new SendToAddress(optDnsPaymentHrn.get());
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
SilentPaymentAddress silentPaymentAddress = SilentPaymentAddress.from(value);
|
|
||||||
return new SendToAddress(silentPaymentAddress);
|
|
||||||
} catch(Exception e) {
|
|
||||||
Address address = addressStringConverter.fromString(value);
|
|
||||||
return address == null ? null : new SendToAddress(address);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String toString(SendToAddress value) {
|
|
||||||
return value.toString();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private class CreatePaymentsService extends Service<List<Payment>> {
|
|
||||||
@Override
|
|
||||||
protected Task<List<Payment>> createTask() {
|
|
||||||
return new Task<>() {
|
|
||||||
@Override
|
|
||||||
protected List<Payment> call() throws Exception {
|
|
||||||
return getPayments();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
private List<Payment> getPayments() throws DnsPaymentValidationException, IOException, ExecutionException, InterruptedException, BitcoinURIParseException {
|
|
||||||
List<Payment> payments = new ArrayList<>();
|
|
||||||
Grid grid = spreadsheetView.getGrid();
|
|
||||||
String firstLabel = null;
|
|
||||||
for(int row = 0; row < grid.getRowCount(); row++) {
|
|
||||||
ObservableList<SpreadsheetCell> rowCells = spreadsheetView.getItems().get(row);
|
|
||||||
SendToAddress sendToAddress = (SendToAddress)rowCells.get(0).getItem();
|
|
||||||
Double value = (Double)rowCells.get(1).getItem();
|
|
||||||
String label = (String)rowCells.get(2).getItem();
|
|
||||||
if(firstLabel == null) {
|
|
||||||
firstLabel = label;
|
|
||||||
}
|
|
||||||
if(label == null || label.isEmpty()) {
|
|
||||||
label = firstLabel;
|
|
||||||
}
|
|
||||||
|
|
||||||
if(sendToAddress != null && value != null) {
|
|
||||||
if(bitcoinUnit == BitcoinUnit.BTC) {
|
|
||||||
value = value * Transaction.SATOSHIS_PER_BITCOIN;
|
|
||||||
}
|
|
||||||
|
|
||||||
payments.add(sendToAddress.toPayment(label, value.longValue(), false));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return payments;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private record SendToPayment(Payment payment, SendToAddress sendToAddress) {}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,12 +3,13 @@ package com.sparrowwallet.sparrow.control;
|
||||||
import com.sparrowwallet.sparrow.AppServices;
|
import com.sparrowwallet.sparrow.AppServices;
|
||||||
import javafx.beans.property.*;
|
import javafx.beans.property.*;
|
||||||
import javafx.concurrent.Worker;
|
import javafx.concurrent.Worker;
|
||||||
import javafx.scene.Node;
|
|
||||||
import javafx.scene.control.DialogPane;
|
import javafx.scene.control.DialogPane;
|
||||||
|
import javafx.scene.image.Image;
|
||||||
|
import javafx.scene.image.ImageView;
|
||||||
import org.controlsfx.dialog.ProgressDialog;
|
import org.controlsfx.dialog.ProgressDialog;
|
||||||
|
|
||||||
public class ServiceProgressDialog extends ProgressDialog {
|
public class ServiceProgressDialog extends ProgressDialog {
|
||||||
public ServiceProgressDialog(String title, String header, Node graphic, Worker<?> worker) {
|
public ServiceProgressDialog(String title, String header, String imagePath, Worker<?> worker) {
|
||||||
super(worker);
|
super(worker);
|
||||||
|
|
||||||
final DialogPane dialogPane = getDialogPane();
|
final DialogPane dialogPane = getDialogPane();
|
||||||
|
|
@ -19,7 +20,8 @@ public class ServiceProgressDialog extends ProgressDialog {
|
||||||
setHeaderText(header);
|
setHeaderText(header);
|
||||||
|
|
||||||
dialogPane.getStyleClass().remove("progress-dialog");
|
dialogPane.getStyleClass().remove("progress-dialog");
|
||||||
dialogPane.setGraphic(graphic);
|
Image image = new Image(imagePath);
|
||||||
|
dialogPane.setGraphic(new ImageView(image));
|
||||||
}
|
}
|
||||||
|
|
||||||
public static class ProxyWorker implements Worker<Boolean> {
|
public static class ProxyWorker implements Worker<Boolean> {
|
||||||
|
|
|
||||||
|
|
@ -44,7 +44,8 @@ public class TextAreaDialog extends Dialog<String> {
|
||||||
final DialogPane dialogPane = new TextAreaDialogPane();
|
final DialogPane dialogPane = new TextAreaDialogPane();
|
||||||
setDialogPane(dialogPane);
|
setDialogPane(dialogPane);
|
||||||
|
|
||||||
dialogPane.setGraphic(new DialogImage(DialogImage.Type.SPARROW));
|
Image image = new Image("/image/sparrow-small.png");
|
||||||
|
dialogPane.setGraphic(new ImageView(image));
|
||||||
|
|
||||||
HBox hbox = new HBox();
|
HBox hbox = new HBox();
|
||||||
this.textArea = new TextArea(defaultValue);
|
this.textArea = new TextArea(defaultValue);
|
||||||
|
|
|
||||||
|
|
@ -29,7 +29,8 @@ public class TextfieldDialog extends Dialog<String> {
|
||||||
final DialogPane dialogPane = getDialogPane();
|
final DialogPane dialogPane = getDialogPane();
|
||||||
setDialogPane(dialogPane);
|
setDialogPane(dialogPane);
|
||||||
|
|
||||||
dialogPane.setGraphic(new DialogImage(DialogImage.Type.SPARROW));
|
Image image = new Image("/image/sparrow-small.png");
|
||||||
|
dialogPane.setGraphic(new ImageView(image));
|
||||||
|
|
||||||
HBox hbox = new HBox();
|
HBox hbox = new HBox();
|
||||||
this.textField = new TextField(defaultValue);
|
this.textField = new TextField(defaultValue);
|
||||||
|
|
|
||||||
|
|
@ -2,19 +2,20 @@ 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;
|
||||||
|
|
||||||
import java.util.regex.Matcher;
|
import java.util.Arrays;
|
||||||
import java.util.regex.Pattern;
|
import java.util.OptionalDouble;
|
||||||
|
|
||||||
public class TitledDescriptionPane extends TitledPane {
|
public class TitledDescriptionPane extends TitledPane {
|
||||||
private Label mainLabel;
|
private Label mainLabel;
|
||||||
|
|
@ -22,18 +23,17 @@ public class TitledDescriptionPane extends TitledPane {
|
||||||
protected Hyperlink showHideLink;
|
protected Hyperlink showHideLink;
|
||||||
protected HBox buttonBox;
|
protected HBox buttonBox;
|
||||||
|
|
||||||
public TitledDescriptionPane(String title, String description, String content, WalletModel walletModel) {
|
public TitledDescriptionPane(String title, String description, String content, String imageUrl) {
|
||||||
getStylesheets().add(AppServices.class.getResource("general.css").toExternalForm());
|
getStylesheets().add(AppServices.class.getResource("general.css").toExternalForm());
|
||||||
getStyleClass().add("titled-description-pane");
|
getStyleClass().add("titled-description-pane");
|
||||||
setAccessibleText(title);
|
|
||||||
|
|
||||||
setPadding(Insets.EMPTY);
|
setPadding(Insets.EMPTY);
|
||||||
setGraphic(getTitle(title, description, walletModel));
|
setGraphic(getTitle(title, description, imageUrl));
|
||||||
setContent(getContentBox(content));
|
setContent(getContentBox(content));
|
||||||
removeArrow();
|
removeArrow();
|
||||||
}
|
}
|
||||||
|
|
||||||
protected Node getTitle(String title, String description, WalletModel walletModel) {
|
protected Node getTitle(String title, String description, String imageUrl) {
|
||||||
HBox listItem = new HBox();
|
HBox listItem = new HBox();
|
||||||
listItem.setPadding(new Insets(10, 20, 10, 10));
|
listItem.setPadding(new Insets(10, 20, 10, 10));
|
||||||
listItem.setSpacing(10);
|
listItem.setSpacing(10);
|
||||||
|
|
@ -43,8 +43,12 @@ public class TitledDescriptionPane extends TitledPane {
|
||||||
imageBox.setMinHeight(50);
|
imageBox.setMinHeight(50);
|
||||||
listItem.getChildren().add(imageBox);
|
listItem.getChildren().add(imageBox);
|
||||||
|
|
||||||
WalletModelImage walletModelImage = new WalletModelImage(walletModel);
|
Image image = new Image(imageUrl, 50, 50, true, true);
|
||||||
imageBox.getChildren().add(walletModelImage);
|
if (!image.isError()) {
|
||||||
|
ImageView imageView = new ImageView();
|
||||||
|
imageView.setImage(image);
|
||||||
|
imageBox.getChildren().add(imageView);
|
||||||
|
}
|
||||||
|
|
||||||
VBox labelsBox = new VBox();
|
VBox labelsBox = new VBox();
|
||||||
labelsBox.setSpacing(5);
|
labelsBox.setSpacing(5);
|
||||||
|
|
@ -123,45 +127,25 @@ public class TitledDescriptionPane extends TitledPane {
|
||||||
}
|
}
|
||||||
|
|
||||||
protected Node getContentBox(String message) {
|
protected Node getContentBox(String message) {
|
||||||
// Create the VBox to hold text and Hyperlink components
|
Label details = new Label(message);
|
||||||
VBox contentBox = new VBox();
|
details.setWrapText(true);
|
||||||
|
|
||||||
|
HBox contentBox = new HBox();
|
||||||
contentBox.setAlignment(Pos.TOP_LEFT);
|
contentBox.setAlignment(Pos.TOP_LEFT);
|
||||||
|
contentBox.getChildren().add(details);
|
||||||
contentBox.setPadding(new Insets(10, 30, 10, 30));
|
contentBox.setPadding(new Insets(10, 30, 10, 30));
|
||||||
contentBox.setPrefWidth(400); // Set preferred width for wrapping
|
|
||||||
contentBox.setMinHeight(60);
|
|
||||||
|
|
||||||
// Define the regex pattern to match URLs
|
double width = TextUtils.computeTextWidth(details.getFont(), message, 0.0D);
|
||||||
String urlPattern = "(\\[https?://\\S+])";
|
double numLines = Math.max(1, Math.ceil(width / 400d));
|
||||||
Pattern pattern = Pattern.compile(urlPattern);
|
|
||||||
Matcher matcher = pattern.matcher(message);
|
|
||||||
|
|
||||||
// StringBuilder to track the non-URL text
|
//Handle long words like txids
|
||||||
int lastMatchEnd = 0;
|
OptionalDouble maxWordLength = Arrays.stream(message.split(" ")).mapToDouble(word -> TextUtils.computeTextWidth(details.getFont(), message, 0.0D)).max();
|
||||||
|
if(maxWordLength.isPresent() && maxWordLength.getAsDouble() > 300.0) {
|
||||||
// Iterate through the matches and build the components
|
numLines += 1.0;
|
||||||
while (matcher.find()) {
|
|
||||||
// Add the text before the URL as a normal Label
|
|
||||||
if (matcher.start() > lastMatchEnd) {
|
|
||||||
String nonUrlText = message.substring(lastMatchEnd, matcher.start());
|
|
||||||
Label textLabel = createWrappedLabel(nonUrlText);
|
|
||||||
contentBox.getChildren().add(textLabel);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Extract the URL and create a Hyperlink for it
|
|
||||||
String url = matcher.group(1).replaceAll("\\[", "").replaceAll("\\]", "");
|
|
||||||
Hyperlink hyperlink = createHyperlink(url);
|
|
||||||
contentBox.getChildren().add(hyperlink);
|
|
||||||
|
|
||||||
// Update last match end
|
|
||||||
lastMatchEnd = matcher.end();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add remaining text after the last URL (if any)
|
double height = Math.max(60, numLines * 20);
|
||||||
if (lastMatchEnd < message.length()) {
|
contentBox.setPrefHeight(height);
|
||||||
String remainingText = message.substring(lastMatchEnd);
|
|
||||||
Label remainingLabel = createWrappedLabel(remainingText);
|
|
||||||
contentBox.getChildren().add(remainingLabel);
|
|
||||||
}
|
|
||||||
|
|
||||||
return contentBox;
|
return contentBox;
|
||||||
}
|
}
|
||||||
|
|
@ -194,21 +178,4 @@ public class TitledDescriptionPane extends TitledPane {
|
||||||
|
|
||||||
return account;
|
return account;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Helper method to create a wrapped Label with a specified maxWidth
|
|
||||||
private Label createWrappedLabel(String text) {
|
|
||||||
Label label = new Label(text);
|
|
||||||
label.setWrapText(true);
|
|
||||||
label.setMaxWidth(400);
|
|
||||||
return label;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Helper method to create a Hyperlink
|
|
||||||
private Hyperlink createHyperlink(String url) {
|
|
||||||
Hyperlink hyperlink = new Hyperlink(url);
|
|
||||||
hyperlink.setMaxWidth(400); // Set maximum width for wrapping
|
|
||||||
hyperlink.setWrapText(true); // Ensure text wrapping in the hyperlink
|
|
||||||
hyperlink.setOnAction(_ -> AppServices.get().getApplication().getHostServices().showDocument(url));
|
|
||||||
return hyperlink;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
package com.sparrowwallet.sparrow.control;
|
package com.sparrowwallet.sparrow.control;
|
||||||
|
|
||||||
import com.google.common.net.HostAndPort;
|
import com.google.common.net.HostAndPort;
|
||||||
import com.sparrowwallet.drongo.OsType;
|
|
||||||
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.Config;
|
import com.sparrowwallet.sparrow.io.Config;
|
||||||
|
|
@ -15,6 +14,7 @@ import javafx.scene.control.Label;
|
||||||
import javafx.scene.control.Tooltip;
|
import javafx.scene.control.Tooltip;
|
||||||
import javafx.util.Duration;
|
import javafx.util.Duration;
|
||||||
import org.controlsfx.glyphfont.Glyph;
|
import org.controlsfx.glyphfont.Glyph;
|
||||||
|
import org.controlsfx.tools.Platform;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
|
@ -28,7 +28,7 @@ public class TorStatusLabel extends Label {
|
||||||
|
|
||||||
public TorStatusLabel() {
|
public TorStatusLabel() {
|
||||||
getStyleClass().add("tor-status");
|
getStyleClass().add("tor-status");
|
||||||
setPadding(OsType.getCurrent() == OsType.WINDOWS ? new Insets(0, 0, 1, 3) : new Insets(1, 0, 0, 3));
|
setPadding(Platform.getCurrent() == Platform.WINDOWS ? new Insets(0, 0, 1, 3) : new Insets(1, 0, 0, 3));
|
||||||
setGraphic(getIcon());
|
setGraphic(getIcon());
|
||||||
update();
|
update();
|
||||||
}
|
}
|
||||||
|
|
@ -62,7 +62,7 @@ public class TorStatusLabel extends Label {
|
||||||
|
|
||||||
private Node getIcon() {
|
private Node getIcon() {
|
||||||
Glyph adjust = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.ADJUST);
|
Glyph adjust = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.ADJUST);
|
||||||
adjust.setFontSize(OsType.getCurrent() == OsType.WINDOWS ? 14 : 15);
|
adjust.setFontSize(Platform.getCurrent() == Platform.WINDOWS ? 14 : 15);
|
||||||
adjust.setRotate(180);
|
adjust.setRotate(180);
|
||||||
|
|
||||||
Glyph bullseye = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.BULLSEYE);
|
Glyph bullseye = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.BULLSEYE);
|
||||||
|
|
|
||||||
|
|
@ -1,24 +1,22 @@
|
||||||
package com.sparrowwallet.sparrow.control;
|
package com.sparrowwallet.sparrow.control;
|
||||||
|
|
||||||
import com.sparrowwallet.drongo.KeyPurpose;
|
import com.sparrowwallet.drongo.KeyPurpose;
|
||||||
import com.sparrowwallet.drongo.OsType;
|
|
||||||
import com.sparrowwallet.drongo.address.Address;
|
import com.sparrowwallet.drongo.address.Address;
|
||||||
import com.sparrowwallet.drongo.bip47.PaymentCode;
|
|
||||||
import com.sparrowwallet.drongo.dns.DnsPayment;
|
|
||||||
import com.sparrowwallet.drongo.dns.DnsPaymentCache;
|
|
||||||
import com.sparrowwallet.drongo.protocol.Sha256Hash;
|
import com.sparrowwallet.drongo.protocol.Sha256Hash;
|
||||||
import com.sparrowwallet.drongo.protocol.TransactionOutput;
|
import com.sparrowwallet.drongo.protocol.TransactionOutput;
|
||||||
import com.sparrowwallet.drongo.silentpayments.SilentPayment;
|
|
||||||
import com.sparrowwallet.drongo.silentpayments.SilentPaymentAddress;
|
|
||||||
import com.sparrowwallet.drongo.uri.BitcoinURI;
|
import com.sparrowwallet.drongo.uri.BitcoinURI;
|
||||||
import com.sparrowwallet.drongo.wallet.*;
|
import com.sparrowwallet.drongo.wallet.*;
|
||||||
import com.sparrowwallet.sparrow.*;
|
import com.sparrowwallet.sparrow.UnitFormat;
|
||||||
|
import com.sparrowwallet.sparrow.AppServices;
|
||||||
|
import com.sparrowwallet.sparrow.EventManager;
|
||||||
|
import com.sparrowwallet.sparrow.Theme;
|
||||||
import com.sparrowwallet.sparrow.event.ExcludeUtxoEvent;
|
import com.sparrowwallet.sparrow.event.ExcludeUtxoEvent;
|
||||||
import com.sparrowwallet.sparrow.event.ReplaceChangeAddressEvent;
|
import com.sparrowwallet.sparrow.event.ReplaceChangeAddressEvent;
|
||||||
|
import com.sparrowwallet.sparrow.event.SorobanInitiatedEvent;
|
||||||
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.soroban.SorobanServices;
|
||||||
import com.sparrowwallet.sparrow.wallet.OptimizationStrategy;
|
import com.sparrowwallet.sparrow.wallet.OptimizationStrategy;
|
||||||
import javafx.beans.property.BooleanProperty;
|
import javafx.beans.property.BooleanProperty;
|
||||||
import javafx.beans.property.ObjectProperty;
|
import javafx.beans.property.ObjectProperty;
|
||||||
|
|
@ -26,7 +24,6 @@ import javafx.beans.property.SimpleBooleanProperty;
|
||||||
import javafx.beans.property.SimpleObjectProperty;
|
import javafx.beans.property.SimpleObjectProperty;
|
||||||
import javafx.embed.swing.SwingFXUtils;
|
import javafx.embed.swing.SwingFXUtils;
|
||||||
import javafx.event.EventHandler;
|
import javafx.event.EventHandler;
|
||||||
import javafx.geometry.HPos;
|
|
||||||
import javafx.geometry.Insets;
|
import javafx.geometry.Insets;
|
||||||
import javafx.geometry.Pos;
|
import javafx.geometry.Pos;
|
||||||
import javafx.scene.Group;
|
import javafx.scene.Group;
|
||||||
|
|
@ -43,9 +40,13 @@ import javafx.scene.paint.Color;
|
||||||
import javafx.scene.shape.Circle;
|
import javafx.scene.shape.Circle;
|
||||||
import javafx.scene.shape.CubicCurve;
|
import javafx.scene.shape.CubicCurve;
|
||||||
import javafx.scene.shape.Line;
|
import javafx.scene.shape.Line;
|
||||||
import javafx.stage.*;
|
import javafx.stage.FileChooser;
|
||||||
|
import javafx.stage.Modality;
|
||||||
|
import javafx.stage.Stage;
|
||||||
|
import javafx.stage.StageStyle;
|
||||||
import javafx.util.Duration;
|
import javafx.util.Duration;
|
||||||
import org.controlsfx.glyphfont.Glyph;
|
import org.controlsfx.glyphfont.Glyph;
|
||||||
|
import org.controlsfx.tools.Platform;
|
||||||
|
|
||||||
import javax.imageio.ImageIO;
|
import javax.imageio.ImageIO;
|
||||||
import java.awt.image.*;
|
import java.awt.image.*;
|
||||||
|
|
@ -90,7 +91,7 @@ public class TransactionDiagram extends GridPane {
|
||||||
stage.setResizable(false);
|
stage.setResizable(false);
|
||||||
|
|
||||||
StackPane scenePane = new StackPane();
|
StackPane scenePane = new StackPane();
|
||||||
if(OsType.getCurrent() == OsType.WINDOWS || OsType.getCurrent() == OsType.UNIX) {
|
if(Platform.getCurrent() == Platform.WINDOWS || Platform.getCurrent() == Platform.UNIX) {
|
||||||
scenePane.setBorder(new Border(new BorderStroke(Color.DARKGRAY, BorderStrokeStyle.SOLID, CornerRadii.EMPTY, BorderWidths.DEFAULT)));
|
scenePane.setBorder(new Border(new BorderStroke(Color.DARKGRAY, BorderStrokeStyle.SOLID, CornerRadii.EMPTY, BorderWidths.DEFAULT)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -108,7 +109,6 @@ public class TransactionDiagram extends GridPane {
|
||||||
expandedDiagram.setId("transactionDiagram");
|
expandedDiagram.setId("transactionDiagram");
|
||||||
expandedDiagram.setExpanded(true);
|
expandedDiagram.setExpanded(true);
|
||||||
expandedDiagram.setFinal(isFinal());
|
expandedDiagram.setFinal(isFinal());
|
||||||
expandedDiagram.setMaxWidth(AppServices.getActiveWindow().getWidth() - 200);
|
|
||||||
updateDerivedDiagram(expandedDiagram);
|
updateDerivedDiagram(expandedDiagram);
|
||||||
|
|
||||||
HBox buttonBox = new HBox();
|
HBox buttonBox = new HBox();
|
||||||
|
|
@ -126,7 +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, expandedDiagram.getMaxWidth(), 460);
|
AppServices.moveToActiveWindowScreen(stage, 600, 460);
|
||||||
});
|
});
|
||||||
stage.setOnHidden(e -> {
|
stage.setOnHidden(e -> {
|
||||||
expandedDiagram = null;
|
expandedDiagram = null;
|
||||||
|
|
@ -143,39 +143,6 @@ public class TransactionDiagram extends GridPane {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
public TransactionDiagram() {
|
|
||||||
ColumnConstraints col1 = new ColumnConstraints();
|
|
||||||
col1.setPrefWidth(22);
|
|
||||||
col1.setHgrow(Priority.NEVER);
|
|
||||||
|
|
||||||
ColumnConstraints col2 = new ColumnConstraints();
|
|
||||||
col2.setHgrow(Priority.ALWAYS);
|
|
||||||
col2.setPercentWidth(25);
|
|
||||||
col2.setFillWidth(true);
|
|
||||||
|
|
||||||
ColumnConstraints col3 = new ColumnConstraints();
|
|
||||||
col3.setPrefWidth(140);
|
|
||||||
col3.setHgrow(Priority.NEVER);
|
|
||||||
|
|
||||||
ColumnConstraints col4 = new ColumnConstraints();
|
|
||||||
Label label = new Label();
|
|
||||||
col4.setMinWidth(TextUtils.computeTextWidth(label.getFont(), "Transaction", 0) + 20);
|
|
||||||
col4.setHgrow(Priority.NEVER);
|
|
||||||
col4.setHalignment(HPos.CENTER);
|
|
||||||
|
|
||||||
ColumnConstraints col5 = new ColumnConstraints();
|
|
||||||
col5.setPrefWidth(140);
|
|
||||||
col5.setHgrow(Priority.NEVER);
|
|
||||||
|
|
||||||
ColumnConstraints col6 = new ColumnConstraints();
|
|
||||||
col6.setHgrow(Priority.ALWAYS);
|
|
||||||
col6.setPercentWidth(25);
|
|
||||||
col6.setFillWidth(true);
|
|
||||||
|
|
||||||
getColumnConstraints().addAll(col1, col2, col3, col4, col5, col6);
|
|
||||||
setPadding(new Insets(0, 0, 0, 40));
|
|
||||||
}
|
|
||||||
|
|
||||||
public void update(WalletTransaction walletTx) {
|
public void update(WalletTransaction walletTx) {
|
||||||
setMinHeight(getDiagramHeight());
|
setMinHeight(getDiagramHeight());
|
||||||
setMaxHeight(getDiagramHeight());
|
setMaxHeight(getDiagramHeight());
|
||||||
|
|
@ -204,7 +171,7 @@ public class TransactionDiagram extends GridPane {
|
||||||
|
|
||||||
VBox messagePane = new VBox();
|
VBox messagePane = new VBox();
|
||||||
messagePane.setPrefHeight(getDiagramHeight());
|
messagePane.setPrefHeight(getDiagramHeight());
|
||||||
messagePane.setPadding(new Insets(0, 10, 0, 10));
|
messagePane.setPadding(new Insets(0, 10, 0, 280));
|
||||||
messagePane.setAlignment(Pos.CENTER);
|
messagePane.setAlignment(Pos.CENTER);
|
||||||
messagePane.getChildren().add(createSpacer());
|
messagePane.getChildren().add(createSpacer());
|
||||||
|
|
||||||
|
|
@ -264,14 +231,6 @@ public class TransactionDiagram extends GridPane {
|
||||||
GridPane.setConstraints(outputsPane, 5, 0);
|
GridPane.setConstraints(outputsPane, 5, 0);
|
||||||
|
|
||||||
getChildren().clear();
|
getChildren().clear();
|
||||||
|
|
||||||
List<Payment> userPayments = getUserPayments();
|
|
||||||
if(!isFinal() && userPayments.size() > 1) {
|
|
||||||
Pane totalsPane = getTotalsPane(userPayments);
|
|
||||||
GridPane.setConstraints(totalsPane, 2, 0, 3, 1);
|
|
||||||
getChildren().add(totalsPane);
|
|
||||||
}
|
|
||||||
|
|
||||||
getChildren().addAll(inputsTypePane, inputsPane, inputsLinesPane, txPane, outputsLinesPane, outputsPane);
|
getChildren().addAll(inputsTypePane, inputsPane, inputsLinesPane, txPane, outputsLinesPane, outputsPane);
|
||||||
|
|
||||||
if(contextMenu == null) {
|
if(contextMenu == null) {
|
||||||
|
|
@ -287,9 +246,19 @@ public class TransactionDiagram extends GridPane {
|
||||||
}
|
}
|
||||||
|
|
||||||
private List<Map<BlockTransactionHashIndex, WalletNode>> getDisplayedUtxoSets() {
|
private List<Map<BlockTransactionHashIndex, WalletNode>> getDisplayedUtxoSets() {
|
||||||
|
boolean addUserSet = getOptimizationStrategy() == OptimizationStrategy.PRIVACY && SorobanServices.canWalletMix(walletTx.getWallet())
|
||||||
|
&& walletTx.getPayments().size() == 1
|
||||||
|
&& (walletTx.getPayments().get(0).getAddress().getScriptType() == walletTx.getWallet().getFreshNode(KeyPurpose.RECEIVE).getAddress().getScriptType());
|
||||||
|
|
||||||
List<Map<BlockTransactionHashIndex, WalletNode>> displayedUtxoSets = new ArrayList<>();
|
List<Map<BlockTransactionHashIndex, WalletNode>> displayedUtxoSets = new ArrayList<>();
|
||||||
for(Map<BlockTransactionHashIndex, WalletNode> selectedUtxoSet : walletTx.getSelectedUtxoSets()) {
|
for(Map<BlockTransactionHashIndex, WalletNode> selectedUtxoSet : walletTx.getSelectedUtxoSets()) {
|
||||||
displayedUtxoSets.add(getDisplayedUtxos(selectedUtxoSet, walletTx.getSelectedUtxoSets().size()));
|
displayedUtxoSets.add(getDisplayedUtxos(selectedUtxoSet, addUserSet ? 2 : walletTx.getSelectedUtxoSets().size()));
|
||||||
|
}
|
||||||
|
|
||||||
|
if(addUserSet && displayedUtxoSets.size() == 1) {
|
||||||
|
Map<BlockTransactionHashIndex, WalletNode> addUserUtxoSet = new HashMap<>();
|
||||||
|
addUserUtxoSet.put(new AddUserBlockTransactionHashIndex(!walletTx.isTwoPersonCoinjoin()), null);
|
||||||
|
displayedUtxoSets.add(addUserUtxoSet);
|
||||||
}
|
}
|
||||||
|
|
||||||
List<Map<BlockTransactionHashIndex, WalletNode>> paddedUtxoSets = new ArrayList<>();
|
List<Map<BlockTransactionHashIndex, WalletNode>> paddedUtxoSets = new ArrayList<>();
|
||||||
|
|
@ -370,9 +339,11 @@ public class TransactionDiagram extends GridPane {
|
||||||
double setHeight = (height / numSets) - 5;
|
double setHeight = (height / numSets) - 5;
|
||||||
for(int set = 0; set < numSets; set++) {
|
for(int set = 0; set < numSets; set++) {
|
||||||
boolean externalUserSet = displayedUtxoSets.get(set).values().stream().anyMatch(Objects::nonNull);
|
boolean externalUserSet = displayedUtxoSets.get(set).values().stream().anyMatch(Objects::nonNull);
|
||||||
if(externalUserSet) {
|
boolean addUserSet = displayedUtxoSets.get(set).keySet().stream().anyMatch(ref -> ref instanceof AddUserBlockTransactionHashIndex);
|
||||||
Glyph bracketGlyph = walletTx.isCoinControlUsed() ? getLockGlyph() : getCoinsGlyph();
|
if(externalUserSet || addUserSet) {
|
||||||
String tooltipText = walletTx.getWallet().getFullDisplayName();
|
boolean replace = !isFinal() && set > 0 && SorobanServices.canWalletMix(walletTx.getWallet());
|
||||||
|
Glyph bracketGlyph = !replace && walletTx.isCoinControlUsed() ? getLockGlyph() : (addUserSet ? getUserAddGlyph() : getCoinsGlyph(replace));
|
||||||
|
String tooltipText = addUserSet ? "Click to add a mix partner" : (walletTx.getWallet().getFullDisplayName() + (replace ? "\nClick to replace with a mix partner" : ""));
|
||||||
StackPane stackPane = getBracket(width, setHeight, bracketGlyph, tooltipText);
|
StackPane stackPane = getBracket(width, setHeight, bracketGlyph, tooltipText);
|
||||||
allBrackets.getChildren().add(stackPane);
|
allBrackets.getChildren().add(stackPane);
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -447,6 +418,8 @@ public class TransactionDiagram extends GridPane {
|
||||||
|
|
||||||
private Pane getInputsLabels(List<Map<BlockTransactionHashIndex, WalletNode>> displayedUtxoSets) {
|
private Pane getInputsLabels(List<Map<BlockTransactionHashIndex, WalletNode>> displayedUtxoSets) {
|
||||||
VBox inputsBox = new VBox();
|
VBox inputsBox = new VBox();
|
||||||
|
inputsBox.setMaxWidth(isExpanded() ? 300 : 150);
|
||||||
|
inputsBox.setPrefWidth(isExpanded() ? 230 : 150);
|
||||||
inputsBox.setPadding(new Insets(0, 10, 0, 10));
|
inputsBox.setPadding(new Insets(0, 10, 0, 10));
|
||||||
inputsBox.minHeightProperty().bind(minHeightProperty());
|
inputsBox.minHeightProperty().bind(minHeightProperty());
|
||||||
inputsBox.setAlignment(Pos.BASELINE_RIGHT);
|
inputsBox.setAlignment(Pos.BASELINE_RIGHT);
|
||||||
|
|
@ -473,14 +446,8 @@ public class TransactionDiagram extends GridPane {
|
||||||
if(walletNode != null) {
|
if(walletNode != null) {
|
||||||
inputValue = input.getValue();
|
inputValue = input.getValue();
|
||||||
Wallet nodeWallet = walletNode.getWallet();
|
Wallet nodeWallet = walletNode.getWallet();
|
||||||
StringJoiner joiner = new StringJoiner("\n");
|
tooltip.setText("Spending " + getSatsValue(inputValue) + " sats from " + (isFinal() ? nodeWallet.getFullDisplayName() : (nodeWallet.isNested() ? nodeWallet.getDisplayName() : "")) + " " + walletNode + "\n" +
|
||||||
joiner.add("Spending " + getSatsValue(inputValue) + " sats from " + (isFinal() ? nodeWallet.getFullDisplayName() : (nodeWallet.isNested() ? nodeWallet.getDisplayName() : "")) + " " + walletNode);
|
input.getHashAsString() + ":" + input.getIndex() + "\n" + walletNode.getAddress());
|
||||||
joiner.add(input.getHashAsString() + ":" + input.getIndex());
|
|
||||||
joiner.add(walletNode.getAddress().toString());
|
|
||||||
if(input.getLabel() != null) {
|
|
||||||
joiner.add(input.getLabel());
|
|
||||||
}
|
|
||||||
tooltip.setText(joiner.toString());
|
|
||||||
tooltip.getStyleClass().add("input-label");
|
tooltip.getStyleClass().add("input-label");
|
||||||
|
|
||||||
if(input.getLabel() == null || input.getLabel().isEmpty()) {
|
if(input.getLabel() == null || input.getLabel().isEmpty()) {
|
||||||
|
|
@ -507,6 +474,14 @@ public class TransactionDiagram extends GridPane {
|
||||||
tooltip.setText(joiner.toString());
|
tooltip.setText(joiner.toString());
|
||||||
} else if(input instanceof InvisibleBlockTransactionHashIndex) {
|
} else if(input instanceof InvisibleBlockTransactionHashIndex) {
|
||||||
tooltip.setText("");
|
tooltip.setText("");
|
||||||
|
} else if(input instanceof AddUserBlockTransactionHashIndex) {
|
||||||
|
tooltip.setText("");
|
||||||
|
label.setGraphic(walletTx.isTwoPersonCoinjoin() ? getQuestionGlyph() : getFeeWarningGlyph());
|
||||||
|
label.setOnMouseClicked(event -> {
|
||||||
|
EventManager.get().post(new SorobanInitiatedEvent(walletTx.getWallet()));
|
||||||
|
closeExpanded();
|
||||||
|
event.consume();
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
if(walletTx.getInputTransactions() != null && walletTx.getInputTransactions().get(input.getHash()) != null) {
|
if(walletTx.getInputTransactions() != null && walletTx.getInputTransactions().get(input.getHash()) != null) {
|
||||||
BlockTransaction blockTransaction = walletTx.getInputTransactions().get(input.getHash());
|
BlockTransaction blockTransaction = walletTx.getInputTransactions().get(input.getHash());
|
||||||
|
|
@ -531,11 +506,6 @@ public class TransactionDiagram extends GridPane {
|
||||||
}
|
}
|
||||||
tooltip.setShowDelay(new Duration(TOOLTIP_SHOW_DELAY));
|
tooltip.setShowDelay(new Duration(TOOLTIP_SHOW_DELAY));
|
||||||
tooltip.setShowDuration(Duration.INDEFINITE);
|
tooltip.setShowDuration(Duration.INDEFINITE);
|
||||||
tooltip.setWrapText(true);
|
|
||||||
Window activeWindow = AppServices.getActiveWindow();
|
|
||||||
if(activeWindow != null) {
|
|
||||||
tooltip.setMaxWidth(activeWindow.getWidth());
|
|
||||||
}
|
|
||||||
if(!tooltip.getText().isEmpty()) {
|
if(!tooltip.getText().isEmpty()) {
|
||||||
label.setTooltip(tooltip);
|
label.setTooltip(tooltip);
|
||||||
}
|
}
|
||||||
|
|
@ -600,7 +570,7 @@ public class TransactionDiagram extends GridPane {
|
||||||
CubicCurve curve = new CubicCurve();
|
CubicCurve curve = new CubicCurve();
|
||||||
curve.getStyleClass().add("input-line");
|
curve.getStyleClass().add("input-line");
|
||||||
|
|
||||||
if(inputs.get(numUtxos-i) instanceof PayjoinBlockTransactionHashIndex) {
|
if(inputs.get(numUtxos-i) instanceof PayjoinBlockTransactionHashIndex || inputs.get(numUtxos-i) instanceof AddUserBlockTransactionHashIndex) {
|
||||||
curve.getStyleClass().add("input-dashed-line");
|
curve.getStyleClass().add("input-dashed-line");
|
||||||
} else if(inputs.get(numUtxos-i) instanceof InvisibleBlockTransactionHashIndex) {
|
} else if(inputs.get(numUtxos-i) instanceof InvisibleBlockTransactionHashIndex) {
|
||||||
continue;
|
continue;
|
||||||
|
|
@ -659,10 +629,6 @@ public class TransactionDiagram extends GridPane {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private List<Payment> getUserPayments() {
|
|
||||||
return walletTx.getPayments().stream().filter(payment -> payment.getType() == Payment.Type.DEFAULT || payment.getType() == Payment.Type.ANCHOR).toList();
|
|
||||||
}
|
|
||||||
|
|
||||||
private Pane getOutputsLines(List<Payment> displayedPayments) {
|
private Pane getOutputsLines(List<Payment> displayedPayments) {
|
||||||
VBox pane = new VBox();
|
VBox pane = new VBox();
|
||||||
Group group = new Group();
|
Group group = new Group();
|
||||||
|
|
@ -678,8 +644,7 @@ public class TransactionDiagram extends GridPane {
|
||||||
|
|
||||||
double width = 140.0;
|
double width = 140.0;
|
||||||
long sum = walletTx.getTotal();
|
long sum = walletTx.getTotal();
|
||||||
List<Long> values = walletTx.getOutputs().stream().filter(output -> !(output instanceof WalletTransaction.NonAddressOutput))
|
List<Long> values = walletTx.getTransaction().getOutputs().stream().filter(txo -> txo.getScript().getToAddress() != null).map(TransactionOutput::getValue).collect(Collectors.toList());
|
||||||
.map(output -> output.getTransactionOutput().getValue()).collect(Collectors.toList());
|
|
||||||
values.add(walletTx.getFee());
|
values.add(walletTx.getFee());
|
||||||
int numOutputs = displayedPayments.size() + walletTx.getChangeMap().size() + 1;
|
int numOutputs = displayedPayments.size() + walletTx.getChangeMap().size() + 1;
|
||||||
for(int i = 1; i <= numOutputs; i++) {
|
for(int i = 1; i <= numOutputs; i++) {
|
||||||
|
|
@ -715,6 +680,8 @@ public class TransactionDiagram extends GridPane {
|
||||||
|
|
||||||
private Pane getOutputsLabels(List<Payment> displayedPayments) {
|
private Pane getOutputsLabels(List<Payment> displayedPayments) {
|
||||||
VBox outputsBox = new VBox();
|
VBox outputsBox = new VBox();
|
||||||
|
outputsBox.setMaxWidth(isExpanded() ? 350 : 150);
|
||||||
|
outputsBox.setPrefWidth(isExpanded() ? 230 : 150);
|
||||||
outputsBox.setPadding(new Insets(0, 20, 0, 10));
|
outputsBox.setPadding(new Insets(0, 20, 0, 10));
|
||||||
outputsBox.setAlignment(Pos.BASELINE_LEFT);
|
outputsBox.setAlignment(Pos.BASELINE_LEFT);
|
||||||
outputsBox.getChildren().add(createSpacer());
|
outputsBox.getChildren().add(createSpacer());
|
||||||
|
|
@ -722,26 +689,20 @@ public class TransactionDiagram extends GridPane {
|
||||||
List<OutputNode> outputNodes = new ArrayList<>();
|
List<OutputNode> outputNodes = new ArrayList<>();
|
||||||
for(Payment payment : displayedPayments) {
|
for(Payment payment : displayedPayments) {
|
||||||
Glyph outputGlyph = GlyphUtils.getOutputGlyph(walletTx, payment);
|
Glyph outputGlyph = GlyphUtils.getOutputGlyph(walletTx, payment);
|
||||||
boolean labelledPayment = outputGlyph.getStyleClass().stream().anyMatch(style -> List.of("premix-icon", "badbank-icon", "whirlpoolfee-icon", "anchor-icon").contains(style)) || payment instanceof AdditionalPayment || payment.getLabel() != null;
|
boolean labelledPayment = outputGlyph.getStyleClass().stream().anyMatch(style -> List.of("premix-icon", "badbank-icon", "whirlpoolfee-icon").contains(style)) || payment instanceof AdditionalPayment;
|
||||||
Label recipientLabel = new Label(payment.getLabel() == null || payment.getType() == Payment.Type.FAKE_MIX || payment.getType() == Payment.Type.MIX ? payment.toString().substring(0, 8) + "..." : payment.getLabel(), outputGlyph);
|
Label recipientLabel = new Label(payment.getLabel() == null || payment.getType() == Payment.Type.FAKE_MIX || payment.getType() == Payment.Type.MIX ? payment.getAddress().toString().substring(0, 8) + "..." : payment.getLabel(), outputGlyph);
|
||||||
recipientLabel.getStyleClass().add("output-label");
|
recipientLabel.getStyleClass().add("output-label");
|
||||||
recipientLabel.getStyleClass().add(labelledPayment ? "payment-label" : "recipient-label");
|
recipientLabel.getStyleClass().add(labelledPayment ? "payment-label" : "recipient-label");
|
||||||
Wallet toWallet = walletTx.getToWallet(AppServices.get().getOpenWallets().keySet(), payment);
|
Wallet toWallet = walletTx.getToWallet(AppServices.get().getOpenWallets().keySet(), payment);
|
||||||
WalletNode toNode = payment instanceof WalletNodePayment walletNodePayment ? walletNodePayment.getWalletNode() : null;
|
WalletNode toNode = walletTx.getWallet() != null && !walletTx.getWallet().isBip47() ? walletTx.getAddressNodeMap().get(payment.getAddress()) : null;
|
||||||
Wallet toBip47Wallet = getBip47SendWallet(payment);
|
Wallet toBip47Wallet = getBip47SendWallet(payment);
|
||||||
DnsPayment dnsPayment = DnsPaymentCache.getDnsPayment(payment);
|
|
||||||
Tooltip recipientTooltip = new Tooltip((toWallet == null ? (toNode != null ? "Consolidate " : "Pay ") : "Receive ")
|
Tooltip recipientTooltip = new Tooltip((toWallet == null ? (toNode != null ? "Consolidate " : "Pay ") : "Receive ")
|
||||||
+ getSatsValue(payment.getAmount()) + " sats to "
|
+ getSatsValue(payment.getAmount()) + " sats to "
|
||||||
+ (payment instanceof AdditionalPayment ? (isExpanded() ? "\n" : "(click to expand)\n") + payment : (toWallet == null ? (dnsPayment == null ? (payment.getLabel() == null ? (toNode != null ? toNode : (toBip47Wallet == null ? "external address" : toBip47Wallet.getDisplayName())) : payment.getLabel()) : dnsPayment.toString()) : toWallet.getFullDisplayName()) + "\n" + payment.getDisplayAddress())
|
+ (payment instanceof AdditionalPayment ? (isExpanded() ? "\n" : "(click to expand)\n") + payment : (toWallet == null ? (payment.getLabel() == null ? (toNode != null ? toNode : (toBip47Wallet == null ? "external address" : toBip47Wallet.getDisplayName())) : payment.getLabel()) : toWallet.getFullDisplayName()) + "\n" + payment.getAddress().toString())
|
||||||
+ (walletTx.isDuplicateAddress(payment) ? " (Duplicate)" : ""));
|
+ (walletTx.isDuplicateAddress(payment) ? " (Duplicate)" : ""));
|
||||||
recipientTooltip.getStyleClass().add("recipient-label");
|
recipientTooltip.getStyleClass().add("recipient-label");
|
||||||
recipientTooltip.setShowDelay(new Duration(TOOLTIP_SHOW_DELAY));
|
recipientTooltip.setShowDelay(new Duration(TOOLTIP_SHOW_DELAY));
|
||||||
recipientTooltip.setShowDuration(Duration.INDEFINITE);
|
recipientTooltip.setShowDuration(Duration.INDEFINITE);
|
||||||
recipientTooltip.setWrapText(true);
|
|
||||||
Window activeWindow = AppServices.getActiveWindow();
|
|
||||||
if(activeWindow != null) {
|
|
||||||
recipientTooltip.setMaxWidth(activeWindow.getWidth());
|
|
||||||
}
|
|
||||||
recipientLabel.setTooltip(recipientTooltip);
|
recipientLabel.setTooltip(recipientTooltip);
|
||||||
HBox paymentBox = new HBox();
|
HBox paymentBox = new HBox();
|
||||||
paymentBox.setAlignment(Pos.CENTER_LEFT);
|
paymentBox.setAlignment(Pos.CENTER_LEFT);
|
||||||
|
|
@ -757,13 +718,7 @@ public class TransactionDiagram extends GridPane {
|
||||||
paymentBox.getChildren().addAll(region, amountLabel);
|
paymentBox.getChildren().addAll(region, amountLabel);
|
||||||
}
|
}
|
||||||
|
|
||||||
if(payment instanceof SilentPayment silentPayment) {
|
outputNodes.add(new OutputNode(paymentBox, payment.getAddress(), payment.getAmount()));
|
||||||
outputNodes.add(new OutputNode(paymentBox, silentPayment.isAddressComputed() ? silentPayment.getAddress() : null, payment.getAmount(), null, silentPayment.getSilentPaymentAddress()));
|
|
||||||
} else {
|
|
||||||
Wallet bip47Wallet = toWallet != null && toWallet.isBip47() ? toWallet : (toBip47Wallet != null && toBip47Wallet.isBip47() ? toBip47Wallet : null);
|
|
||||||
PaymentCode paymentCode = bip47Wallet == null ? null : bip47Wallet.getKeystores().getFirst().getExternalPaymentCode();
|
|
||||||
outputNodes.add(new OutputNode(paymentBox, payment.getAddress(), payment.getAmount(), paymentCode, null));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Set<Integer> seenIndexes = new HashSet<>();
|
Set<Integer> seenIndexes = new HashSet<>();
|
||||||
|
|
@ -827,7 +782,7 @@ public class TransactionDiagram extends GridPane {
|
||||||
outputsBox.getChildren().add(outputNode.outputLabel);
|
outputsBox.getChildren().add(outputNode.outputLabel);
|
||||||
outputsBox.getChildren().add(createSpacer());
|
outputsBox.getChildren().add(createSpacer());
|
||||||
|
|
||||||
ContextMenu contextMenu = new LabelContextMenu(outputNode.address, outputNode.amount, outputNode.paymentCode, outputNode.silentPaymentAddress);
|
ContextMenu contextMenu = new LabelContextMenu(outputNode.address, outputNode.amount);
|
||||||
if(!outputNode.outputLabel.getChildren().isEmpty() && outputNode.outputLabel.getChildren().get(0) instanceof Label outputLabelControl) {
|
if(!outputNode.outputLabel.getChildren().isEmpty() && outputNode.outputLabel.getChildren().get(0) instanceof Label outputLabelControl) {
|
||||||
outputLabelControl.setContextMenu(contextMenu);
|
outputLabelControl.setContextMenu(contextMenu);
|
||||||
}
|
}
|
||||||
|
|
@ -836,7 +791,7 @@ public class TransactionDiagram extends GridPane {
|
||||||
boolean highFee = (walletTx.getFeePercentage() > 0.1);
|
boolean highFee = (walletTx.getFeePercentage() > 0.1);
|
||||||
Label feeLabel = highFee ? new Label("High Fee", getFeeWarningGlyph()) : new Label("Fee", getFeeGlyph());
|
Label feeLabel = highFee ? new Label("High Fee", getFeeWarningGlyph()) : new Label("Fee", getFeeGlyph());
|
||||||
feeLabel.getStyleClass().addAll("output-label", "fee-label");
|
feeLabel.getStyleClass().addAll("output-label", "fee-label");
|
||||||
String percentage = walletTx.getFeePercentage() < 0.0001d ? "<0.01" : String.format("%.2f", walletTx.getFeePercentage() * 100.0);
|
String percentage = String.format("%.2f", walletTx.getFeePercentage() * 100.0);
|
||||||
Tooltip feeTooltip = new Tooltip(walletTx.getFee() < 0 ? "Unknown fee" : "Fee of " + getSatsValue(walletTx.getFee()) + " sats (" + percentage + "%)");
|
Tooltip feeTooltip = new Tooltip(walletTx.getFee() < 0 ? "Unknown fee" : "Fee of " + getSatsValue(walletTx.getFee()) + " sats (" + percentage + "%)");
|
||||||
feeTooltip.getStyleClass().add("fee-tooltip");
|
feeTooltip.getStyleClass().add("fee-tooltip");
|
||||||
feeTooltip.setShowDelay(new Duration(TOOLTIP_SHOW_DELAY));
|
feeTooltip.setShowDelay(new Duration(TOOLTIP_SHOW_DELAY));
|
||||||
|
|
@ -890,33 +845,6 @@ public class TransactionDiagram extends GridPane {
|
||||||
return txPane;
|
return txPane;
|
||||||
}
|
}
|
||||||
|
|
||||||
private Pane getTotalsPane(List<Payment> userPayments) {
|
|
||||||
VBox totalsBox = new VBox();
|
|
||||||
totalsBox.setPadding(new Insets(0, 0, 15, 0));
|
|
||||||
totalsBox.setAlignment(Pos.CENTER);
|
|
||||||
|
|
||||||
long amount = userPayments.stream().mapToLong(Payment::getAmount).sum();
|
|
||||||
|
|
||||||
HBox coinLabelBox = new HBox();
|
|
||||||
coinLabelBox.setAlignment(Pos.CENTER);
|
|
||||||
CoinLabel totalCoinLabel = new CoinLabel();
|
|
||||||
totalCoinLabel.setValue(amount);
|
|
||||||
coinLabelBox.getChildren().addAll(totalCoinLabel, new Label(" in "), new Label(Long.toString(userPayments.size())), new Label(" payments"));
|
|
||||||
totalsBox.getChildren().addAll(createSpacer(), coinLabelBox);
|
|
||||||
|
|
||||||
CurrencyRate currencyRate = AppServices.getFiatCurrencyExchangeRate();
|
|
||||||
if(currencyRate != null && currencyRate.isAvailable() && Config.get().getExchangeSource() != ExchangeSource.NONE) {
|
|
||||||
HBox fiatLabelBox = new HBox();
|
|
||||||
fiatLabelBox.setAlignment(Pos.CENTER);
|
|
||||||
FiatLabel fiatLabel = new FiatLabel();
|
|
||||||
fiatLabel.set(currencyRate, amount);
|
|
||||||
fiatLabelBox.getChildren().add(fiatLabel);
|
|
||||||
totalsBox.getChildren().add(fiatLabelBox);
|
|
||||||
}
|
|
||||||
|
|
||||||
return totalsBox;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void saveAsImage() {
|
private void saveAsImage() {
|
||||||
Stage window = new Stage();
|
Stage window = new Stage();
|
||||||
FileChooser fileChooser = new FileChooser();
|
FileChooser fileChooser = new FileChooser();
|
||||||
|
|
@ -1002,11 +930,8 @@ public class TransactionDiagram extends GridPane {
|
||||||
}
|
}
|
||||||
|
|
||||||
private int getOutputIndex(Address address, long amount, Collection<Integer> seenIndexes) {
|
private int getOutputIndex(Address address, long amount, Collection<Integer> seenIndexes) {
|
||||||
List<TransactionOutput> addressOutputs = walletTx.getOutputs().stream().filter(output -> !(output instanceof WalletTransaction.NonAddressOutput))
|
List<TransactionOutput> addressOutputs = walletTx.getTransaction().getOutputs().stream().filter(txOutput -> txOutput.getScript().getToAddress() != null).collect(Collectors.toList());
|
||||||
.map(WalletTransaction.Output::getTransactionOutput).collect(Collectors.toList());
|
TransactionOutput output = addressOutputs.stream().filter(txOutput -> address.equals(txOutput.getScript().getToAddress()) && txOutput.getValue() == amount && !seenIndexes.contains(txOutput.getIndex())).findFirst().orElseThrow();
|
||||||
TransactionOutput output = addressOutputs.stream()
|
|
||||||
.filter(txOutput -> address.equals(txOutput.getScript().getToAddress()) && txOutput.getValue() == amount && !seenIndexes.contains(txOutput.getIndex()))
|
|
||||||
.findFirst().orElseThrow();
|
|
||||||
return addressOutputs.indexOf(output);
|
return addressOutputs.indexOf(output);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1027,10 +952,46 @@ public class TransactionDiagram extends GridPane {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private Glyph getCoinsGlyph() {
|
private Glyph getUserAddGlyph() {
|
||||||
|
Glyph userAddGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.USER_PLUS);
|
||||||
|
userAddGlyph.getStyleClass().add("useradd-icon");
|
||||||
|
userAddGlyph.setFontSize(12);
|
||||||
|
userAddGlyph.setOnMouseEntered(event -> {
|
||||||
|
userAddGlyph.setFontSize(18);
|
||||||
|
});
|
||||||
|
userAddGlyph.setOnMouseExited(event -> {
|
||||||
|
userAddGlyph.setFontSize(12);
|
||||||
|
});
|
||||||
|
userAddGlyph.setOnMouseClicked(event -> {
|
||||||
|
EventManager.get().post(new SorobanInitiatedEvent(walletTx.getWallet()));
|
||||||
|
closeExpanded();
|
||||||
|
event.consume();
|
||||||
|
});
|
||||||
|
return userAddGlyph;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Glyph getCoinsGlyph(boolean allowReplacement) {
|
||||||
Glyph coinsGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.COINS);
|
Glyph coinsGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.COINS);
|
||||||
coinsGlyph.setFontSize(12);
|
coinsGlyph.setFontSize(12);
|
||||||
coinsGlyph.getStyleClass().add("coins-icon");
|
if(allowReplacement) {
|
||||||
|
coinsGlyph.getStyleClass().add("coins-replace-icon");
|
||||||
|
coinsGlyph.setOnMouseEntered(event -> {
|
||||||
|
coinsGlyph.setIcon(FontAwesome5.Glyph.USER_PLUS);
|
||||||
|
coinsGlyph.setFontSize(18);
|
||||||
|
});
|
||||||
|
coinsGlyph.setOnMouseExited(event -> {
|
||||||
|
coinsGlyph.setIcon(FontAwesome5.Glyph.COINS);
|
||||||
|
coinsGlyph.setFontSize(12);
|
||||||
|
});
|
||||||
|
coinsGlyph.setOnMouseClicked(event -> {
|
||||||
|
EventManager.get().post(new SorobanInitiatedEvent(walletTx.getWallet()));
|
||||||
|
closeExpanded();
|
||||||
|
event.consume();
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
coinsGlyph.getStyleClass().add("coins-icon");
|
||||||
|
}
|
||||||
|
|
||||||
return coinsGlyph;
|
return coinsGlyph;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1133,6 +1094,20 @@ public class TransactionDiagram extends GridPane {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static class AddUserBlockTransactionHashIndex extends BlockTransactionHashIndex {
|
||||||
|
private final boolean required;
|
||||||
|
|
||||||
|
public AddUserBlockTransactionHashIndex(boolean required) {
|
||||||
|
super(Sha256Hash.ZERO_HASH, 0, new Date(), 0L, 0, 0);
|
||||||
|
this.required = required;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getLabel() {
|
||||||
|
return "Add Mix Partner" + (required ? "" : "?");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public static class AdditionalPayment extends Payment {
|
public static class AdditionalPayment extends Payment {
|
||||||
private final List<Payment> additionalPayments;
|
private final List<Payment> additionalPayments;
|
||||||
|
|
||||||
|
|
@ -1156,7 +1131,7 @@ public class TransactionDiagram extends GridPane {
|
||||||
}
|
}
|
||||||
|
|
||||||
public String toString() {
|
public String toString() {
|
||||||
return additionalPayments.stream().map(Payment::toString).collect(Collectors.joining("\n"));
|
return additionalPayments.stream().map(payment -> payment.getAddress().toString()).collect(Collectors.joining("\n"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1164,28 +1139,16 @@ public class TransactionDiagram extends GridPane {
|
||||||
public Pane outputLabel;
|
public Pane outputLabel;
|
||||||
public Address address;
|
public Address address;
|
||||||
public long amount;
|
public long amount;
|
||||||
public PaymentCode paymentCode;
|
|
||||||
public SilentPaymentAddress silentPaymentAddress;
|
|
||||||
|
|
||||||
public OutputNode(Pane outputLabel, Address address, long amount) {
|
public OutputNode(Pane outputLabel, Address address, long amount) {
|
||||||
this(outputLabel, address, amount, null, null);
|
|
||||||
}
|
|
||||||
|
|
||||||
public OutputNode(Pane outputLabel, Address address, long amount, PaymentCode paymentCode, SilentPaymentAddress silentPaymentAddress) {
|
|
||||||
this.outputLabel = outputLabel;
|
this.outputLabel = outputLabel;
|
||||||
this.address = address;
|
this.address = address;
|
||||||
this.amount = amount;
|
this.amount = amount;
|
||||||
this.paymentCode = paymentCode;
|
|
||||||
this.silentPaymentAddress = silentPaymentAddress;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private class LabelContextMenu extends ContextMenu {
|
private class LabelContextMenu extends ContextMenu {
|
||||||
public LabelContextMenu(Address address, long value) {
|
public LabelContextMenu(Address address, long value) {
|
||||||
this(address, value, null, null);
|
|
||||||
}
|
|
||||||
|
|
||||||
public LabelContextMenu(Address address, long value, PaymentCode paymentCode, SilentPaymentAddress silentPaymentAddress) {
|
|
||||||
if(address != null) {
|
if(address != null) {
|
||||||
MenuItem copyAddress = new MenuItem("Copy Address");
|
MenuItem copyAddress = new MenuItem("Copy Address");
|
||||||
copyAddress.setOnAction(event -> {
|
copyAddress.setOnAction(event -> {
|
||||||
|
|
@ -1222,28 +1185,6 @@ public class TransactionDiagram extends GridPane {
|
||||||
Clipboard.getSystemClipboard().setContent(content);
|
Clipboard.getSystemClipboard().setContent(content);
|
||||||
});
|
});
|
||||||
getItems().addAll(copySatsValue, copyBtcValue);
|
getItems().addAll(copySatsValue, copyBtcValue);
|
||||||
|
|
||||||
if(paymentCode != null) {
|
|
||||||
MenuItem copyPaymentCode = new MenuItem("Copy Payment Code");
|
|
||||||
copyPaymentCode.setOnAction(AE -> {
|
|
||||||
hide();
|
|
||||||
ClipboardContent content = new ClipboardContent();
|
|
||||||
content.putString(paymentCode.toString());
|
|
||||||
Clipboard.getSystemClipboard().setContent(content);
|
|
||||||
});
|
|
||||||
getItems().add(copyPaymentCode);
|
|
||||||
}
|
|
||||||
|
|
||||||
if(silentPaymentAddress != null) {
|
|
||||||
MenuItem copySilentPaymentAddress = new MenuItem("Copy Silent Payment Address");
|
|
||||||
copySilentPaymentAddress.setOnAction(AE -> {
|
|
||||||
hide();
|
|
||||||
ClipboardContent content = new ClipboardContent();
|
|
||||||
content.putString(silentPaymentAddress.toString());
|
|
||||||
Clipboard.getSystemClipboard().setContent(content);
|
|
||||||
});
|
|
||||||
getItems().add(copySilentPaymentAddress);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -90,20 +90,20 @@ public class TransactionDiagramLabel extends HBox {
|
||||||
outputLabels.add(mixOutputLabel);
|
outputLabels.add(mixOutputLabel);
|
||||||
}
|
}
|
||||||
} else if(walletTx.getPayments().size() >= 5 && walletTx.getPayments().stream().mapToLong(Payment::getAmount).distinct().count() <= 1 && walletTx.getWallet() != null
|
} else if(walletTx.getPayments().size() >= 5 && walletTx.getPayments().stream().mapToLong(Payment::getAmount).distinct().count() <= 1 && walletTx.getWallet() != null
|
||||||
&& walletTx.getWallet().getStandardAccountType() == StandardAccount.WHIRLPOOL_POSTMIX && !walletTx.getWalletNodePayments().isEmpty()) {
|
&& walletTx.getWallet().getStandardAccountType() == StandardAccount.WHIRLPOOL_POSTMIX && walletTx.getPayments().stream().anyMatch(walletTx::isConsolidationSend)) {
|
||||||
OutputLabel remixOutputLabel = getRemixOutputLabel(transactionDiagram, walletTx.getPayments());
|
OutputLabel remixOutputLabel = getRemixOutputLabel(transactionDiagram, walletTx.getPayments());
|
||||||
if(remixOutputLabel != null) {
|
if(remixOutputLabel != null) {
|
||||||
outputLabels.add(remixOutputLabel);
|
outputLabels.add(remixOutputLabel);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
List<Payment> payments = walletTx.getExternalPayments().stream().filter(payment -> payment.getType() == Payment.Type.DEFAULT).collect(Collectors.toList());
|
List<Payment> payments = walletTx.getPayments().stream().filter(payment -> payment.getType() == Payment.Type.DEFAULT && !walletTx.isConsolidationSend(payment)).collect(Collectors.toList());
|
||||||
List<OutputLabel> paymentLabels = payments.stream().map(payment -> getOutputLabel(transactionDiagram, payment)).collect(Collectors.toList());
|
List<OutputLabel> paymentLabels = payments.stream().map(payment -> getOutputLabel(transactionDiagram, payment)).collect(Collectors.toList());
|
||||||
if(walletTx.getSelectedUtxos().values().stream().allMatch(Objects::isNull)) {
|
if(walletTx.getSelectedUtxos().values().stream().allMatch(Objects::isNull)) {
|
||||||
paymentLabels.sort(Comparator.comparingInt(paymentLabel -> (paymentLabel.text.startsWith("Receive") ? 0 : 1)));
|
paymentLabels.sort(Comparator.comparingInt(paymentLabel -> (paymentLabel.text.startsWith("Receive") ? 0 : 1)));
|
||||||
}
|
}
|
||||||
outputLabels.addAll(paymentLabels);
|
outputLabels.addAll(paymentLabels);
|
||||||
|
|
||||||
List<Payment> consolidations = walletTx.getWalletNodePayments().stream().filter(payment -> payment.getType() == Payment.Type.DEFAULT).collect(Collectors.toList());
|
List<Payment> consolidations = walletTx.getPayments().stream().filter(payment -> payment.getType() == Payment.Type.DEFAULT && walletTx.isConsolidationSend(payment)).collect(Collectors.toList());
|
||||||
outputLabels.addAll(consolidations.stream().map(consolidation -> getOutputLabel(transactionDiagram, consolidation)).collect(Collectors.toList()));
|
outputLabels.addAll(consolidations.stream().map(consolidation -> getOutputLabel(transactionDiagram, consolidation)).collect(Collectors.toList()));
|
||||||
|
|
||||||
List<Payment> mixes = walletTx.getPayments().stream().filter(payment -> payment.getType() == Payment.Type.MIX || payment.getType() == Payment.Type.FAKE_MIX).collect(Collectors.toList());
|
List<Payment> mixes = walletTx.getPayments().stream().filter(payment -> payment.getType() == Payment.Type.MIX || payment.getType() == Payment.Type.FAKE_MIX).collect(Collectors.toList());
|
||||||
|
|
@ -203,10 +203,10 @@ public class TransactionDiagramLabel extends HBox {
|
||||||
private OutputLabel getOutputLabel(TransactionDiagram transactionDiagram, Payment payment) {
|
private OutputLabel getOutputLabel(TransactionDiagram transactionDiagram, Payment payment) {
|
||||||
WalletTransaction walletTx = transactionDiagram.getWalletTransaction();
|
WalletTransaction walletTx = transactionDiagram.getWalletTransaction();
|
||||||
Wallet toWallet = walletTx.getToWallet(AppServices.get().getOpenWallets().keySet(), payment);
|
Wallet toWallet = walletTx.getToWallet(AppServices.get().getOpenWallets().keySet(), payment);
|
||||||
WalletNode toNode = payment instanceof WalletNodePayment walletNodePayment ? walletNodePayment.getWalletNode() : null;
|
WalletNode toNode = walletTx.getWallet() != null && !walletTx.getWallet().isBip47() ? walletTx.getAddressNodeMap().get(payment.getAddress()) : null;
|
||||||
|
|
||||||
Glyph glyph = GlyphUtils.getOutputGlyph(transactionDiagram.getWalletTransaction(), payment);
|
Glyph glyph = GlyphUtils.getOutputGlyph(transactionDiagram.getWalletTransaction(), payment);
|
||||||
String text = (toWallet == null ? (toNode != null ? "Consolidate " : "Pay ") : "Receive ") + transactionDiagram.getSatsValue(payment.getAmount()) + " sats to " + payment;
|
String text = (toWallet == null ? (toNode != null ? "Consolidate " : "Pay ") : "Receive ") + transactionDiagram.getSatsValue(payment.getAmount()) + " sats to " + payment.getAddress().toString();
|
||||||
|
|
||||||
return getOutputLabel(glyph, text);
|
return getOutputLabel(glyph, text);
|
||||||
}
|
}
|
||||||
|
|
@ -227,8 +227,7 @@ public class TransactionDiagramLabel extends HBox {
|
||||||
}
|
}
|
||||||
|
|
||||||
Glyph glyph = GlyphUtils.getFeeGlyph();
|
Glyph glyph = GlyphUtils.getFeeGlyph();
|
||||||
String percentage = walletTx.getFeePercentage() < 0.0001d ? "<0.01" : String.format("%.2f", walletTx.getFeePercentage() * 100.0);
|
String text = "Fee of " + transactionDiagram.getSatsValue(walletTx.getFee()) + " sats (" + String.format("%.2f", walletTx.getFeePercentage() * 100.0) + "%)";
|
||||||
String text = "Fee of " + transactionDiagram.getSatsValue(walletTx.getFee()) + " sats (" + percentage + "%)";
|
|
||||||
|
|
||||||
return getOutputLabel(glyph, text);
|
return getOutputLabel(glyph, text);
|
||||||
}
|
}
|
||||||
|
|
@ -240,7 +239,7 @@ public class TransactionDiagramLabel extends HBox {
|
||||||
icon.setGraphic(glyph);
|
icon.setGraphic(glyph);
|
||||||
|
|
||||||
CopyableLabel label = new CopyableLabel();
|
CopyableLabel label = new CopyableLabel();
|
||||||
label.setFont(Font.font("Fragment Mono Italic", 13));
|
label.setFont(Font.font("Roboto Mono Italic", 13));
|
||||||
label.setText(text);
|
label.setText(text);
|
||||||
|
|
||||||
HBox output = new HBox(5);
|
HBox output = new HBox(5);
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,11 @@
|
||||||
package com.sparrowwallet.sparrow.control;
|
package com.sparrowwallet.sparrow.control;
|
||||||
|
|
||||||
import com.sparrowwallet.drongo.wallet.TableType;
|
|
||||||
import com.sparrowwallet.sparrow.wallet.Entry;
|
import com.sparrowwallet.sparrow.wallet.Entry;
|
||||||
import com.sparrowwallet.sparrow.wallet.TransactionEntry;
|
import com.sparrowwallet.sparrow.wallet.TransactionEntry;
|
||||||
import com.sparrowwallet.sparrow.wallet.WalletTransactionsEntry;
|
import com.sparrowwallet.sparrow.wallet.WalletTransactionsEntry;
|
||||||
import javafx.beans.property.ReadOnlyObjectWrapper;
|
import javafx.beans.property.ReadOnlyObjectWrapper;
|
||||||
import javafx.scene.control.TreeTableColumn;
|
import javafx.scene.control.TreeTableColumn;
|
||||||
|
import javafx.scene.control.TreeTableView;
|
||||||
|
|
||||||
public class TransactionsTreeTable extends CoinTreeTable {
|
public class TransactionsTreeTable extends CoinTreeTable {
|
||||||
public void initialize(WalletTransactionsEntry rootEntry) {
|
public void initialize(WalletTransactionsEntry rootEntry) {
|
||||||
|
|
@ -49,8 +49,8 @@ public class TransactionsTreeTable extends CoinTreeTable {
|
||||||
|
|
||||||
setPlaceholder(getDefaultPlaceholder(rootEntry.getWallet()));
|
setPlaceholder(getDefaultPlaceholder(rootEntry.getWallet()));
|
||||||
setEditable(true);
|
setEditable(true);
|
||||||
setupColumnWidths();
|
setColumnResizePolicy(TreeTableView.CONSTRAINED_RESIZE_POLICY);
|
||||||
setupColumnSort(0, TreeTableColumn.SortType.DESCENDING);
|
setSortColumn(0, TreeTableColumn.SortType.DESCENDING);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void updateAll(WalletTransactionsEntry rootEntry) {
|
public void updateAll(WalletTransactionsEntry rootEntry) {
|
||||||
|
|
@ -60,13 +60,13 @@ public class TransactionsTreeTable extends CoinTreeTable {
|
||||||
setRoot(rootItem);
|
setRoot(rootItem);
|
||||||
rootItem.setExpanded(true);
|
rootItem.setExpanded(true);
|
||||||
|
|
||||||
resetSortColumn();
|
setSortColumn(0, TreeTableColumn.SortType.DESCENDING);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void updateHistory() {
|
public void updateHistory() {
|
||||||
//Transaction entries should have already been updated using WalletTransactionsEntry.updateHistory, so only a resort required
|
//Transaction entries should have already been updated using WalletTransactionsEntry.updateHistory, so only a resort required
|
||||||
sort();
|
sort();
|
||||||
resetSortColumn();
|
setSortColumn(0, TreeTableColumn.SortType.DESCENDING);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void updateLabel(Entry entry) {
|
public void updateLabel(Entry entry) {
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
package com.sparrowwallet.sparrow.control;
|
package com.sparrowwallet.sparrow.control;
|
||||||
|
|
||||||
import com.sparrowwallet.drongo.OsType;
|
|
||||||
import javafx.application.Platform;
|
import javafx.application.Platform;
|
||||||
import javafx.stage.Stage;
|
import javafx.stage.Stage;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
|
|
@ -32,7 +31,7 @@ public class TrayManager {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
List<Image> imgList = new ArrayList<>();
|
List<Image> imgList = new ArrayList<>();
|
||||||
if(OsType.getCurrent() == OsType.WINDOWS) {
|
if(org.controlsfx.tools.Platform.getCurrent() == org.controlsfx.tools.Platform.WINDOWS) {
|
||||||
imgList.add(ImageIO.read(getClass().getResource("/image/sparrow-black-small.png")));
|
imgList.add(ImageIO.read(getClass().getResource("/image/sparrow-black-small.png")));
|
||||||
imgList.add(ImageIO.read(getClass().getResource("/image/sparrow-black-small@2x.png")));
|
imgList.add(ImageIO.read(getClass().getResource("/image/sparrow-black-small@2x.png")));
|
||||||
imgList.add(ImageIO.read(getClass().getResource("/image/sparrow-black-small@3x.png")));
|
imgList.add(ImageIO.read(getClass().getResource("/image/sparrow-black-small@3x.png")));
|
||||||
|
|
|
||||||
|
|
@ -27,9 +27,8 @@ public class UsbStatusButton extends MenuButton {
|
||||||
public void setDevices(List<Device> devices) {
|
public void setDevices(List<Device> devices) {
|
||||||
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_5 || device.getModel() == WalletModel.KEEPKEY || device.getModel() == WalletModel.BITBOX_02 ||
|
device.getModel() == WalletModel.TREZOR_SAFE_3 || 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 -> {
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,10 @@
|
||||||
package com.sparrowwallet.sparrow.control;
|
package com.sparrowwallet.sparrow.control;
|
||||||
|
|
||||||
import com.sparrowwallet.drongo.wallet.TableType;
|
|
||||||
import com.sparrowwallet.sparrow.wallet.*;
|
import com.sparrowwallet.sparrow.wallet.*;
|
||||||
import javafx.beans.property.ReadOnlyObjectWrapper;
|
import javafx.beans.property.ReadOnlyObjectWrapper;
|
||||||
import javafx.scene.control.SelectionMode;
|
import javafx.scene.control.SelectionMode;
|
||||||
import javafx.scene.control.TreeTableColumn;
|
import javafx.scene.control.TreeTableColumn;
|
||||||
|
import javafx.scene.control.TreeTableView;
|
||||||
|
|
||||||
import java.util.Comparator;
|
import java.util.Comparator;
|
||||||
|
|
||||||
|
|
@ -82,8 +82,8 @@ public class UtxosTreeTable extends CoinTreeTable {
|
||||||
|
|
||||||
setPlaceholder(getDefaultPlaceholder(rootEntry.getWallet()));
|
setPlaceholder(getDefaultPlaceholder(rootEntry.getWallet()));
|
||||||
setEditable(true);
|
setEditable(true);
|
||||||
setupColumnWidths();
|
setColumnResizePolicy(TreeTableView.CONSTRAINED_RESIZE_POLICY);
|
||||||
setupColumnSort(getColumns().size() - 1, TreeTableColumn.SortType.DESCENDING);
|
setSortColumn(getColumns().size() - 1, TreeTableColumn.SortType.DESCENDING);
|
||||||
|
|
||||||
getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE);
|
getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE);
|
||||||
}
|
}
|
||||||
|
|
@ -95,14 +95,14 @@ public class UtxosTreeTable extends CoinTreeTable {
|
||||||
setRoot(rootItem);
|
setRoot(rootItem);
|
||||||
rootItem.setExpanded(true);
|
rootItem.setExpanded(true);
|
||||||
|
|
||||||
resetSortColumn();
|
setSortColumn(getColumns().size() - 1, TreeTableColumn.SortType.DESCENDING);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void updateHistory() {
|
public void updateHistory() {
|
||||||
//Utxo entries should have already been updated, so only a resort required
|
//Utxo entries should have already been updated, so only a resort required
|
||||||
if(!getRoot().getChildren().isEmpty()) {
|
if(!getRoot().getChildren().isEmpty()) {
|
||||||
sort();
|
sort();
|
||||||
resetSortColumn();
|
setSortColumn(getColumns().size() - 1, TreeTableColumn.SortType.DESCENDING);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -18,8 +18,8 @@ import java.util.List;
|
||||||
public class WalletExportDialog extends Dialog<Wallet> {
|
public class WalletExportDialog extends Dialog<Wallet> {
|
||||||
private Wallet wallet;
|
private Wallet wallet;
|
||||||
|
|
||||||
public WalletExportDialog(WalletForm selectedWalletForm, List<WalletForm> allWalletForms) {
|
public WalletExportDialog(WalletForm walletForm) {
|
||||||
this.wallet = selectedWalletForm.getWallet();
|
this.wallet = walletForm.getWallet();
|
||||||
|
|
||||||
EventManager.get().register(this);
|
EventManager.get().register(this);
|
||||||
setOnCloseRequest(event -> {
|
setOnCloseRequest(event -> {
|
||||||
|
|
@ -45,10 +45,10 @@ public class WalletExportDialog extends Dialog<Wallet> {
|
||||||
|
|
||||||
List<WalletExport> exporters;
|
List<WalletExport> exporters;
|
||||||
if(wallet.getPolicyType() == PolicyType.SINGLE) {
|
if(wallet.getPolicyType() == PolicyType.SINGLE) {
|
||||||
exporters = List.of(new Electrum(), new ElectrumPersonalServer(), new Descriptor(), new SpecterDesktop(), new Sparrow(), new WalletLabels(allWalletForms), new WalletTransactions(selectedWalletForm));
|
exporters = List.of(new Electrum(), new ElectrumPersonalServer(), new Descriptor(), new SpecterDesktop(), new Sparrow(), new WalletLabels(), new WalletTransactions(walletForm));
|
||||||
} else if(wallet.getPolicyType() == PolicyType.MULTI) {
|
} else if(wallet.getPolicyType() == PolicyType.MULTI) {
|
||||||
exporters = List.of(new Bip129(), new CaravanMultisig(), new ColdcardMultisig(), new CoboVaultMultisig(), new Electrum(), new ElectrumPersonalServer(), new KeystoneMultisig(),
|
exporters = List.of(new Bip129(), new CaravanMultisig(), new ColdcardMultisig(), new CoboVaultMultisig(), new Electrum(), new ElectrumPersonalServer(), new KeystoneMultisig(),
|
||||||
new Descriptor(), new JadeMultisig(), new PassportMultisig(), new SpecterDesktop(), new BlueWalletMultisig(), new SpecterDIY(), new Sparrow(), new WalletLabels(allWalletForms), new WalletTransactions(selectedWalletForm));
|
new Descriptor(), new JadeMultisig(), new PassportMultisig(), new SpecterDesktop(), new BlueWalletMultisig(), new SpecterDIY(), new Sparrow(), new WalletLabels(), new WalletTransactions(walletForm));
|
||||||
} else {
|
} else {
|
||||||
throw new UnsupportedOperationException("Cannot export wallet with policy type " + wallet.getPolicyType());
|
throw new UnsupportedOperationException("Cannot export wallet with policy type " + wallet.getPolicyType());
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -19,10 +19,9 @@ import javafx.scene.shape.Circle;
|
||||||
import org.controlsfx.glyphfont.Glyph;
|
import org.controlsfx.glyphfont.Glyph;
|
||||||
import org.girod.javafx.svgimage.SVGImage;
|
import org.girod.javafx.svgimage.SVGImage;
|
||||||
import org.girod.javafx.svgimage.SVGLoader;
|
import org.girod.javafx.svgimage.SVGLoader;
|
||||||
import org.slf4j.Logger;
|
|
||||||
import org.slf4j.LoggerFactory;
|
|
||||||
|
|
||||||
import java.io.*;
|
import java.io.*;
|
||||||
|
import java.net.MalformedURLException;
|
||||||
import java.net.URL;
|
import java.net.URL;
|
||||||
import java.net.URLConnection;
|
import java.net.URLConnection;
|
||||||
import java.net.URLStreamHandler;
|
import java.net.URLStreamHandler;
|
||||||
|
|
@ -74,15 +73,33 @@ public class WalletIcon extends StackPane {
|
||||||
|
|
||||||
SVGImage svgImage;
|
SVGImage svgImage;
|
||||||
if(Config.get().getTheme() == Theme.DARK) {
|
if(Config.get().getTheme() == Theme.DARK) {
|
||||||
svgImage = loadSVGImage("/image/walletmodel/" + walletModel.getType() + "-icon-invert.svg");
|
svgImage = loadSVGImage("/image/" + walletModel.getType() + "-icon-invert.svg");
|
||||||
} else {
|
} else {
|
||||||
svgImage = loadSVGImage("/image/walletmodel/" + walletModel.getType() + "-icon.svg");
|
svgImage = loadSVGImage("/image/" + walletModel.getType() + "-icon.svg");
|
||||||
}
|
}
|
||||||
|
|
||||||
if(svgImage != null) {
|
if(svgImage != null) {
|
||||||
getChildren().add(svgImage);
|
getChildren().add(svgImage);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Image image = null;
|
||||||
|
if(Config.get().getTheme() == Theme.DARK) {
|
||||||
|
image = loadImage("image/" + walletModel.getType() + "-icon-invert.png");
|
||||||
|
}
|
||||||
|
|
||||||
|
if(image == null) {
|
||||||
|
image = loadImage("image/" + walletModel.getType() + "-icon.png");
|
||||||
|
}
|
||||||
|
|
||||||
|
if(image == null) {
|
||||||
|
image = loadImage("image/" + walletModel.getType() + ".png");
|
||||||
|
}
|
||||||
|
|
||||||
|
if(image != null && !image.isError()) {
|
||||||
|
ImageView imageView = new ImageView(image);
|
||||||
|
getChildren().add(imageView);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -109,8 +126,18 @@ 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 + "?" + QUERY, WIDTH, HEIGHT, true, false);
|
||||||
getChildren().clear();
|
getChildren().clear();
|
||||||
Circle circle = new Circle(getPrefWidth() / 2,getPrefHeight() / 2,getPrefWidth() / 2);
|
Circle circle = new Circle(getPrefWidth() / 2,getPrefHeight() / 2,getPrefWidth() / 2);
|
||||||
circle.setFill(new ImagePattern(image));
|
circle.setFill(new ImagePattern(image));
|
||||||
|
|
@ -149,8 +176,6 @@ public class WalletIcon extends StackPane {
|
||||||
}
|
}
|
||||||
|
|
||||||
public static class WalletIconStreamHandler extends URLStreamHandler {
|
public static class WalletIconStreamHandler extends URLStreamHandler {
|
||||||
private static final Logger log = LoggerFactory.getLogger(WalletIconStreamHandler.class);
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected URLConnection openConnection(URL url) throws IOException {
|
protected URLConnection openConnection(URL url) throws IOException {
|
||||||
return new URLConnection(url) {
|
return new URLConnection(url) {
|
||||||
|
|
@ -161,19 +186,17 @@ public class WalletIcon extends StackPane {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public InputStream getInputStream() throws IOException {
|
public InputStream getInputStream() throws IOException {
|
||||||
String walletId = url.getPath().replaceAll("%20", " ").replaceAll("%23", "#");
|
String walletId = url.getPath();
|
||||||
String query = url.getQuery();
|
String query = url.getQuery();
|
||||||
|
|
||||||
Wallet wallet = AppServices.get().getWallet(walletId);
|
Wallet wallet = AppServices.get().getWallet(walletId);
|
||||||
if(wallet == null) {
|
if(wallet == null) {
|
||||||
log.warn("Cannot find wallet for wallet id " + walletId);
|
throw new IOException("Cannot find wallet for wallet id " + walletId);
|
||||||
return getFallbackIconStream();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if(query.startsWith(QUERY)) {
|
if(query.startsWith(QUERY)) {
|
||||||
if(wallet.getWalletConfig() == null || wallet.getWalletConfig().getIconData() == null) {
|
if(wallet.getWalletConfig() == null || wallet.getWalletConfig().getIconData() == null) {
|
||||||
log.warn("No icon data for " + walletId);
|
throw new IOException("No icon data for " + walletId);
|
||||||
return getFallbackIconStream();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ByteArrayInputStream bais = new ByteArrayInputStream(wallet.getWalletConfig().getIconData());
|
ByteArrayInputStream bais = new ByteArrayInputStream(wallet.getWalletConfig().getIconData());
|
||||||
|
|
@ -184,12 +207,7 @@ public class WalletIcon extends StackPane {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
log.warn("Cannot load url " + url);
|
throw new MalformedURLException("Cannot load url " + url);
|
||||||
return getFallbackIconStream();
|
|
||||||
}
|
|
||||||
|
|
||||||
private static InputStream getFallbackIconStream() {
|
|
||||||
return WalletIconStreamHandler.class.getResourceAsStream("/image/sparrow.png");
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue