Compare commits

..

1 Commits

Author SHA1 Message Date
Craig Raw
b3d09bd972 initial changes to replace monocle with johanvos-headless javafx branch 2024-04-11 09:45:50 +02:00
770 changed files with 14544 additions and 21777 deletions

View File

@ -10,64 +10,47 @@ jobs:
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [windows-2022, ubuntu-22.04, ubuntu-22.04-arm, macos-15-intel, macos-14]
os: [windows-2022, ubuntu-20.04, macos-12]
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v4
with:
submodules: recursive
- name: Clear Java tool-cache for reproducibility
shell: bash
run: rm -rf "$RUNNER_TOOL_CACHE"/Java_*
- name: Set up JDK 25.0.2
uses: actions/setup-java@v5
submodules: true
- name: Set up JDK 18.0.1
uses: actions/setup-java@v4
with:
distribution: 'temurin'
java-version: '25.0.2'
java-version: '18.0.1'
- name: Show Build Versions
run: ./gradlew -v
- name: Build with Gradle
run: ./gradlew jpackage
- name: Codesign, package and notarize macOS distribution
if: ${{ runner.os == 'macOS' }}
uses: sparrowwallet/github-actions/codesign-macos@v1
with:
app-name: Sparrow
certificate: ${{ secrets.MACOS_CERTIFICATE }}
certificate-password: ${{ secrets.MACOS_CERTIFICATE_PASSWORD }}
apple-id: ${{ secrets.MACOS_NOTARIZATION_APPLE_ID }}
team-id: ${{ secrets.MACOS_NOTARIZATION_TEAM_ID }}
notarization-password: ${{ secrets.MACOS_NOTARIZATION_PASSWORD }}
- name: Package Windows zip distribution
if: ${{ runner.os == 'Windows' }}
- name: Package zip distribution
if: ${{ runner.os == 'Windows' || runner.os == 'macOS' }}
run: ./gradlew packageZipDistribution
- name: Package Linux tar distribution
- name: Package tar distribution
if: ${{ runner.os == 'Linux' }}
run: ./gradlew packageTarDistribution
- name: Repackage Linux deb distribution
if: ${{ runner.os == 'Linux' }}
run: ./repackage.sh
- name: Upload Artifact
uses: actions/upload-artifact@v6
- name: Upload Artifacts
uses: actions/upload-artifact@v4
with:
name: Sparrow Build - ${{ runner.os }} ${{ runner.arch }}
name: Sparrow Build - ${{ runner.os }}
path: |
build/jpackage/*
!build/jpackage/Sparrow/
!build/jpackage/Sparrow.app/
- name: Headless build with Gradle
if: ${{ runner.os == 'Linux' }}
run: ./gradlew -Djava.awt.headless=true clean jpackage
- name: Package Linux headless tar distribution
- name: Package headless tar distribution
if: ${{ runner.os == 'Linux' }}
run: ./gradlew -Djava.awt.headless=true packageTarDistribution
- name: Repackage Linux headless deb distribution
- name: Rename Headless Artifacts
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
if: ${{ runner.os == 'Linux' }}
uses: actions/upload-artifact@v6
uses: actions/upload-artifact@v4
with:
name: Sparrow Build - ${{ runner.os }} ${{ runner.arch }} Headless
name: Sparrow Build - ${{ runner.os }} Headless
path: |
build/jpackage/*
!build/jpackage/Sparrow/

3
.gitmodules vendored
View File

@ -1,6 +1,3 @@
[submodule "drongo"]
path = drongo
url = ../../sparrowwallet/drongo.git
[submodule "lark"]
path = lark
url = ../../sparrowwallet/lark.git

View File

@ -1,3 +0,0 @@
# Enable auto-env through the sdkman_auto_env config
# Add key=value pairs of SDKs to use below
java=25.0.2-tem

View File

@ -16,14 +16,14 @@ or for those without SSH credentials:
`git clone --recursive https://github.com/sparrowwallet/sparrow.git`
In order to build, Sparrow requires Java 25 or higher to be installed.
The release binaries are built with [Eclipse Temurin 25.0.2+10](https://github.com/adoptium/temurin25-binaries/releases/tag/jdk-25.0.2%2B10).
If you are using [SDKMAN](https://sdkman.io/), you can use `sdk env install` to ensure you have the correct version.
In order to build, Sparrow requires Java 18 or higher to be installed.
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:
`sudo apt install -y rpm fakeroot binutils`
The Sparrow binaries can be built from source using
`./gradlew jpackage`
@ -44,7 +44,7 @@ If you prefer to run Sparrow directly from source, it can be launched from withi
`./sparrow`
Java 25 or higher must be installed.
Java 18 or higher must be installed.
## Configuration
@ -64,12 +64,10 @@ Usage: sparrow [options]
Possible Values: [ERROR, WARN, INFO, DEBUG, TRACE]
--network, -n
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, testnet4, regtest or signet) can also be set using an environment variable `SPARROW_NETWORK`. For example:
As a fallback, the network (mainnet, testnet, regtest or signet) can also be set using an environment variable `SPARROW_NETWORK`. For example:
`export SPARROW_NETWORK=testnet`
@ -85,7 +83,7 @@ When not explicitly configured using the command line argument above, Sparrow st
| Linux | ~/.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

View File

@ -1,38 +1,59 @@
import java.awt.GraphicsEnvironment
plugins {
id 'application'
id 'org.openjfx.javafxplugin' version '0.1.0'
id 'org.beryx.jlink' version '3.2.1'
id 'org.gradlex.extra-java-module-info' version '1.13.1'
id 'io.matthewnelson.kmp.tor.resource-filterjar' version '408.21.0'
id 'extra-java-module-info'
id 'org-openjfx-javafxplugin'
id 'org.beryx.jlink' version '3.0.1'
}
def sparrowVersion = '1.8.5'
def os = org.gradle.internal.os.OperatingSystem.current()
def osName = os.getFamilyName()
if(os.macOsX) {
osName = "osx"
}
def targetName = ""
def osArch = "x64"
def releaseArch = "x86_64"
if(System.getProperty("os.arch") == "aarch64") {
osArch = "aarch64"
releaseArch = "aarch64"
targetName = "-" + osArch
}
def headless = "true".equals(System.getProperty("java.awt.headless"))
group = 'com.sparrowwallet'
version = '2.5.3'
def vTor = '4.7.13-4'
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 {
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 {
useFileSystemPermissions()
tasks.withType(AbstractArchiveTask) {
preserveFileTimestamps = false
reproducibleFileOrder = true
}
javafx {
version = "26"
sdk = "/home/scy/git/jfx-sandbox/build/sdk"
modules = [ 'javafx.controls', 'javafx.fxml', 'javafx.swing', 'javafx.graphics' ]
}
@ -43,26 +64,25 @@ java {
dependencies {
//Any changes to the dependencies must be reflected in the module definitions below!
implementation(project(':drongo'))
implementation(project(':lark'))
implementation('com.google.guava:guava:33.5.0-jre')
implementation('com.google.code.gson:gson:2.13.2')
implementation('com.google.guava:guava:33.0.0-jre')
implementation('com.google.code.gson:gson:2.9.1')
implementation('com.h2database:h2:2.1.214')
implementation('com.zaxxer:HikariCP:7.0.2') {
implementation('com.zaxxer:HikariCP:4.0.3') {
exclude group: 'org.slf4j'
}
implementation('org.jdbi:jdbi3-core:3.51.0') {
implementation('org.jdbi:jdbi3-core:3.20.0') {
exclude group: 'org.slf4j'
}
implementation('org.jdbi:jdbi3-sqlobject:3.51.0') {
implementation('org.jdbi:jdbi3-sqlobject:3.20.0') {
exclude group: 'org.slf4j'
}
implementation('org.flywaydb:flyway-core:9.22.3')
implementation('org.fxmisc.richtext:richtextfx:0.11.7')
implementation('org.flywaydb:flyway-core:7.10.7-SNAPSHOT')
implementation('org.fxmisc.richtext:richtextfx:0.10.4')
implementation('no.tornado:tornadofx-controls:1.0.4')
implementation('com.google.zxing:javase:3.5.4') {
implementation('com.google.zxing:javase:3.4.0') {
exclude group: 'com.beust', module: 'jcommander'
}
implementation('org.jcommander:jcommander:3.0')
implementation('com.beust:jcommander:1.81')
implementation('com.github.arteam:simple-json-rpc-core:1.3')
implementation('com.github.arteam:simple-json-rpc-client:1.3') {
exclude group: 'com.github.arteam', module: 'simple-json-rpc-core'
@ -70,19 +90,25 @@ dependencies {
implementation('com.github.arteam:simple-json-rpc-server:1.3') {
exclude group: 'org.slf4j'
}
implementation('com.fasterxml.jackson.core:jackson-databind:2.21.1')
implementation('com.sparrowwallet:hummingbird:1.7.4')
implementation('com.sparrowwallet:hummingbird:1.7.3')
implementation('co.nstant.in:cbor:0.9')
implementation('org.openpnp:openpnp-capture-java:0.0.30-1')
implementation("io.matthewnelson.kmp-tor:runtime:2.5.0")
implementation("io.matthewnelson.kmp-tor:resource-exec-tor-gpl:408.21.0")
implementation('org.jetbrains.kotlinx:kotlinx-coroutines-javafx:1.10.2') {
exclude group: 'org.jetbrains.kotlin', module: 'kotlin-stdlib-common'
implementation("com.nativelibs4java:bridj${targetName}:0.7-20140918-3") {
exclude group: 'com.google.android.tools', module: 'dx'
}
implementation('de.jangassen:nsmenufx:3.1.0') {
exclude group: 'net.java.dev.jna', module: 'jna'
implementation("com.github.sarxos:webcam-capture${targetName}:0.3.13-SNAPSHOT") {
exclude group: 'com.nativelibs4java', module: 'bridj'
}
implementation('org.controlsfx:controlsfx:11.2.3' ) {
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' ) {
exclude group: 'org.openjfx', module: 'javafx-base'
exclude group: 'org.openjfx', module: 'javafx-graphics'
exclude group: 'org.openjfx', module: 'javafx-controls'
@ -93,25 +119,26 @@ dependencies {
}
implementation('dev.bwt:bwt-jni:0.1.8')
implementation('net.sourceforge.javacsv:javacsv:2.0')
implementation ('org.slf4j:slf4j-api:2.0.17')
implementation('org.slf4j:jul-to-slf4j:2.0.17') {
implementation ('org.slf4j:slf4j-api:2.0.12')
implementation('org.slf4j:jul-to-slf4j:2.0.12') {
exclude group: 'org.slf4j'
}
implementation('com.sparrowwallet.bokmakierie:bokmakierie:1.0')
implementation('com.sparrowwallet:tern:1.0.6')
implementation('io.reactivex.rxjava2:rxjava:2.2.21')
implementation('io.samourai.code.whirlpool:whirlpool-client:1.0.5')
implementation('io.samourai.code.wallet:java-http-client:2.0.2')
implementation('io.reactivex.rxjava2:rxjava:2.2.15')
implementation('io.reactivex.rxjava2:rxjavafx:2.2.2')
implementation('org.apache.commons:commons-lang3:3.20.0')
implementation('org.apache.commons:commons-compress:1.28.0')
implementation('com.github.librepdf:openpdf:1.3.43')
implementation('com.googlecode.lanterna:lanterna:3.1.5')
implementation('net.coobird:thumbnailator:0.4.21')
implementation('com.github.hervegirod:fxsvgimage:1.1')
implementation('org.apache.commons:commons-lang3:3.7')
implementation('org.apache.commons:commons-compress:1.25.0')
implementation('net.sourceforge.streamsupport:streamsupport:1.7.0')
implementation('com.github.librepdf:openpdf:1.3.30')
implementation('com.googlecode.lanterna:lanterna:3.1.1')
implementation('net.coobird:thumbnailator:0.4.18')
implementation('com.github.hervegirod:fxsvgimage:1.0b2')
implementation('com.sparrowwallet:toucan:0.9.0')
implementation('com.jcraft:jzlib:1.1.3')
implementation('io.github.doblon8:jzbar:0.4.0')
testImplementation('org.junit.jupiter:junit-jupiter-api:5.14.1')
testRuntimeOnly('org.junit.jupiter:junit-jupiter-engine:5.14.1')
testImplementation('org.junit.jupiter:junit-jupiter-api:5.10.0')
testRuntimeOnly('org.junit.jupiter:junit-jupiter-engine:5.10.0')
testRuntimeOnly('org.junit.platform:junit-platform-launcher')
}
@ -124,9 +151,17 @@ compileJava {
}
}
processResources {
doLast {
delete fileTree("$buildDir/resources/main/native").matching {
exclude "${osName}/${osArch}/**"
}
}
}
test {
useJUnitPlatform()
jvmArgs = ["--add-opens=java.base/java.io=ALL-UNNAMED", "--enable-native-access=ALL-UNNAMED"]
jvmArgs = ["--add-opens=java.base/java.io=ALL-UNNAMED", "--add-opens=java.base/java.io=com.google.gson"]
}
application {
@ -134,12 +169,6 @@ application {
mainClass = 'com.sparrowwallet.sparrow.SparrowWallet'
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/javafx.scene=org.controlsfx.controls",
"--add-opens=javafx.controls/com.sun.javafx.scene.control.behavior=org.controlsfx.controls",
@ -149,20 +178,27 @@ application {
"--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=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.javafx.application=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.io=com.google.gson",
"--add-opens=java.smartcardio/sun.security.smartcardio=com.sparrowwallet.sparrow",
"--add-reads=kotlin.stdlib=kotlinx.coroutines.core",
"--add-reads=org.flywaydb.core=java.desktop"]
"--add-opens=com.samourai.whirlpool.client/com.samourai.whirlpool.client.whirlpool=com.sparrowwallet.sparrow",
"--add-opens=com.samourai.soroban.client/com.samourai.soroban.client.rpc=com.sparrowwallet.sparrow",
"--add-reads=kotlin.stdlib=kotlinx.coroutines.core"]
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) {
applicationDefaultJvmArgs += ["-Dglass.platform=Headless"]
applicationDefaultJvmArgs += ["-Dglass.platform=Headless", "-Dprism.order=sw"]
}
}
@ -176,49 +212,15 @@ jlink {
requires 'jdk.crypto.cryptoki'
requires 'java.management'
requires 'io.leangen.geantyref'
requires 'static jdk.jfr'
uses 'org.flywaydb.core.extensibility.FlywayExtension'
uses 'org.flywaydb.core.internal.database.DatabaseType'
uses 'org.eclipse.jetty.http.HttpFieldPreEncoder'
}
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/*,' +
'glob:/javafx.graphics/*.dylib,' +
'glob:/javafx.graphics/*.so,' +
'glob:/javafx.graphics/*.dll,' +
'glob:/com.sparrowwallet.drongo/native/**,' +
'glob:/com.sparrowwallet.sparrow/native/**,' +
'glob:/com.sparrowwallet.merged.module/com/sun/jna/**/*.so,' +
'glob:/com.sparrowwallet.merged.module/com/sun/jna/**/*.dylib,' +
'glob:/com.sparrowwallet.merged.module/com/sun/jna/**/*.jnilib,' +
'glob:/com.sparrowwallet.merged.module/com/sun/jna/**/*.dll,' +
'glob:/com.sparrowwallet.merged.module/com/sun/jna/**/*.a,' +
'glob:/com.sparrowwallet.merged.module/darwin-*/**,' +
'glob:/com.sparrowwallet.merged.module/linux-*/**,' +
'glob:/com.sparrowwallet.merged.module/win32-*/**,' +
'glob:/org.usb4java/org/usb4java/darwin-*/**,' +
'glob:/org.usb4java/org/usb4java/linux-*/**,' +
'glob:/org.usb4java/org/usb4java/win32-*/**,' +
'glob:/org.hid4java/darwin-*/**,' +
'glob:/org.hid4java/linux-*/**,' +
'glob:/org.hid4java/win32-*/**,' +
'glob:/openpnp.capture.java/darwin-*/**,' +
'glob:/openpnp.capture.java/linux-*/**,' +
'glob:/openpnp.capture.java/win32-*/**,' +
'glob:/io.github.doblon8.jzbar/native/**']
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 {
name = 'sparrow'
jvmArgs = ["--enable-native-access=com.sparrowwallet.drongo",
"--enable-native-access=com.sun.jna",
"--enable-native-access=javafx.graphics",
"--enable-native-access=com.sparrowwallet.merged.module",
"--enable-native-access=com.fazecast.jSerialComm",
"--enable-native-access=org.usb4java",
"--enable-native-access=io.github.doblon8.jzbar",
"--enable-native-access=com.sparrowwallet.sparrow",
"--add-opens=javafx.graphics/com.sun.javafx.css=org.controlsfx.controls",
jvmArgs = ["--add-opens=javafx.graphics/com.sun.javafx.css=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.inputmap=org.controlsfx.controls",
@ -227,12 +229,19 @@ jlink {
"--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=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/javafx.scene.input=com.sparrowwallet.sparrow",
"--add-opens=javafx.graphics/com.sun.javafx.application=com.sparrowwallet.sparrow",
"--add-opens=java.base/java.net=com.sparrowwallet.sparrow",
"--add-opens=java.base/java.io=com.google.gson",
"--add-opens=java.smartcardio/sun.security.smartcardio=com.sparrowwallet.sparrow",
"--add-opens=com.samourai.whirlpool.client/com.samourai.whirlpool.client.whirlpool=com.sparrowwallet.sparrow",
"--add-opens=com.samourai.soroban.client/com.samourai.soroban.client.rpc=com.sparrowwallet.sparrow",
"--add-reads=com.sparrowwallet.merged.module=java.desktop",
"--add-reads=com.sparrowwallet.merged.module=java.sql",
"--add-reads=com.sparrowwallet.merged.module=com.sparrowwallet.sparrow",
@ -244,10 +253,7 @@ jlink {
"--add-reads=com.sparrowwallet.merged.module=co.nstant.in.cbor",
"--add-reads=com.sparrowwallet.merged.module=org.bouncycastle.pg",
"--add-reads=com.sparrowwallet.merged.module=org.bouncycastle.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"]
"--add-reads=kotlin.stdlib=kotlinx.coroutines.core"]
if(os.windows) {
jvmArgs += ["-Djavax.accessibility.assistive_technologies", "-Djavax.accessibility.screen_magnifier_present=false"]
@ -256,125 +262,55 @@ jlink {
jvmArgs += ["-Dprism.lcdtext=false", "--add-opens=javafx.graphics/com.sun.glass.ui.mac=com.sparrowwallet.merged.module"]
}
if(headless) {
jvmArgs += ["-Dglass.platform=Headless"]
jvmArgs += ["-Dglass.platform=Headless", "-Dprism.order=sw"]
}
}
addExtraDependencies("javafx")
jpackage {
imageName = "Sparrow"
installerName = "Sparrow"
appVersion = "${version}"
appVersion = "${sparrowVersion}"
skipInstaller = os.macOsX || properties.skipInstallers
imageOptions = []
installerOptions = ['--file-associations', 'src/main/deploy/psbt.properties', '--file-associations', 'src/main/deploy/txn.properties', '--file-associations', 'src/main/deploy/asc.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) {
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']
installerType = "msi"
installerType = "exe"
}
if(os.linux) {
if(headless) {
installerName = "sparrowserver"
installerOptions = ['--license-file', 'LICENSE']
} else {
installerName = "sparrowwallet"
installerOptions += ['--linux-shortcut', '--linux-menu-group', 'Sparrow']
installerOptions += ['--resource-dir', 'src/main/deploy/package/linux/', '--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/']
}
if(os.macOsX) {
installerOptions += ['--mac-sign', '--mac-signing-key-user-name', 'Craig Raw (UPLVMSK9D7)']
imageOptions += ['--icon', 'src/main/deploy/package/macos/sparrow.icns', '--resource-dir', 'src/main/deploy/package/macos/']
imageOptions += ['--icon', 'src/main/deploy/package/osx/sparrow.icns', '--resource-dir', 'src/main/deploy/package/osx/']
installerType = "dmg"
}
}
if(os.linux) {
jpackageImage {
dependsOn('prepareModulesDir', 'copyUdevRules')
}
}
}
if(os.linux) {
tasks.jlink.finalizedBy('addUserWritePermission', 'copyUdevRules', 'extractNativeLibraries')
tasks.jpackageImage.finalizedBy('prepareResourceDir')
if(!headless) {
tasks.jpackage.dependsOn('copyMimeInfo')
}
} else {
tasks.jlink.finalizedBy('addUserWritePermission', 'extractNativeLibraries')
}
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')
}
}
}
}
tasks.register('copyMimeInfo', Copy) {
mustRunAfter tasks.jpackageImage
from('src/main/deploy/package/linux')
into(layout.buildDirectory.dir('jpackage/Sparrow/lib'))
include('sparrowwallet-Sparrow-MimeInfo.xml')
}
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) {
task removeGroupWritePermission(type: Exec) {
commandLine 'chmod', '-R', 'g-w', "$buildDir/jpackage/Sparrow"
}
tasks.register('packageZipDistribution', Zip) {
archiveFileName = "Sparrow-${version}.zip"
task packageZipDistribution(type: Zip) {
archiveFileName = "Sparrow-${sparrowVersion}.zip"
destinationDirectory = file("$buildDir/jpackage")
preserveFileTimestamps = os.macOsX
from("$buildDir/jpackage/") {
include "Sparrow/**"
include "Sparrow.app/**"
}
}
tasks.register('packageTarDistribution', Tar) {
task packageTarDistribution(type: Tar) {
dependsOn removeGroupWritePermission
archiveFileName = "sparrow${headless ? 'server': 'wallet'}-${version}-${releaseArch}.tar.gz"
archiveFileName = "sparrow-${sparrowVersion}-${releaseArch}.tar.gz"
destinationDirectory = file("$buildDir/jpackage")
compression = Compression.GZIP
from("$buildDir/jpackage/") {
@ -382,80 +318,61 @@ tasks.register('packageTarDistribution', Tar) {
}
}
def jnaPlatform
if(os.macOsX) {
jnaPlatform = "darwin-${osArch == 'aarch64' ? 'aarch64' : 'x86-64'}"
} else if(os.windows) {
jnaPlatform = "win32-x86-64"
} else {
jnaPlatform = "linux-${osArch == 'aarch64' ? 'aarch64' : 'x86-64'}"
}
def serialOs = os.macOsX ? "OSX" : (os.windows ? "Windows" : "Linux")
def serialArch = osArch == "aarch64" ? "aarch64" : "x86_64"
// Map of JAR name prefix to the include glob for platform-specific natives inside the JAR.
def nativeLibJars = [
'jna-' : "com/sun/jna/${jnaPlatform}/*",
'argon2-jvm-2' : "${jnaPlatform}/*",
'hid4java-' : "${jnaPlatform}/*",
'openpnp-capture-java': "${jnaPlatform}/*",
'jSerialComm-' : "${serialOs}/${serialArch}/*",
'usb4java-' : "org/usb4java/${jnaPlatform}/*",
'jzbar-' : "native/${osName}/${osArch}/*",
]
tasks.register('extractNativeLibraries') {
dependsOn 'jlink'
doLast {
def imageLib = file("$buildDir/image/lib")
// Project-owned natives
copy {
from "${project(':drongo').projectDir}/src/main/resources/native/${osName}/${osArch}", "src/main/resources/native/${osName}/${osArch}"
into imageLib
eachFile { it.permissions { unix('rw-r--r--') } }
}
// JavaFX natives
def javafxClassifier = ""
if(os.macOsX) {
javafxClassifier = osArch == "aarch64" ? "mac-aarch64" : "mac"
} else if(os.windows) {
javafxClassifier = "win"
} else {
javafxClassifier = osArch == "aarch64" ? "linux-aarch64" : "linux"
}
def javafxJar = configurations.runtimeClasspath.find { it.name == "javafx-graphics-${javafx.version}-${javafxClassifier}.jar" }
if(javafxJar) {
copy {
from(zipTree(javafxJar)) { include "*.dylib", "*.so", "*.dll" }
into imageLib
eachFile { it.permissions { unix('rw-r--r--') } }
}
}
// Third-party natives
nativeLibJars.each { prefix, includePattern ->
def jar = configurations.runtimeClasspath.find { it.name.startsWith(prefix) }
if(jar) {
copy {
from(zipTree(jar)) { include includePattern }
into imageLib
eachFile { it.path = it.name; it.permissions { unix('rw-r--r--') } }
includeEmptyDirs = false
}
}
}
}
}
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')
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.domain')
requires('com.fasterxml.jackson.core')
@ -463,7 +380,7 @@ extraJavaModuleInfo {
requires('com.fasterxml.jackson.databind')
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.builder')
exports('com.github.arteam.simplejsonrpc.client.exception')
@ -471,26 +388,61 @@ extraJavaModuleInfo {
requires('com.fasterxml.jackson.databind')
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')
requires('simple.json.rpc.core')
requires('com.google.common')
requires('org.slf4j')
requires('com.fasterxml.jackson.databind')
}
module('org.openpnp:openpnp-capture-java', 'openpnp.capture.java') {
exports('org.openpnp.capture')
exports('org.openpnp.capture.library')
requires('java.desktop')
requires('com.sun.jna')
module("bridj${targetName}-0.7-20140918-3.jar", 'com.nativelibs4java.bridj', '0.7-20140918-3') {
exports('org.bridj')
exports('org.bridj.cpp')
requires('java.logging')
}
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')
}
module('com.google.guava:listenablefuture|empty-to-avoid-conflict-with-guava', 'com.google.guava.listenablefuture')
module('com.google.code.findbugs:jsr305', 'com.google.code.findbugs.jsr305')
module('jeromq-0.5.0.jar', 'jeromq', '0.5.0') {
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('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.event')
exports('org.fxmisc.richtext.model')
@ -499,23 +451,23 @@ extraJavaModuleInfo {
requires('javafx.graphics')
requires('org.fxmisc.flowless')
requires('org.reactfx.reactfx')
requires('org.fxmisc.undo')
requires('org.fxmisc.undo.undofx')
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.controls')
requires('javafx.graphics')
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')
requires('javafx.base')
requires('javafx.controls')
requires('javafx.graphics')
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.value')
exports('org.reactfx.collection')
@ -524,47 +476,187 @@ extraJavaModuleInfo {
requires('javafx.graphics')
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')
requires('io.reactivex.rxjava2')
requires('javafx.graphics')
}
module('org.flywaydb:flyway-core', 'org.flywaydb.core') {
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') {
module('wellbehavedfx-0.3.3.jar', 'org.fxmisc.wellbehaved', '0.3.3') {
requires('javafx.base')
requires('javafx.graphics')
}
module('com.github.jai-imageio:jai-imageio-core', 'com.github.jai.imageio.jai.imageio.core') {
requires('java.desktop')
module('jai-imageio-core-1.4.0.jar', 'com.github.jai.imageio.jai.imageio.core', '1.4.0')
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.model')
exports('co.nstant.in.cbor.builder')
}
module('org.jcommander:jcommander', 'org.jcommander') {
module('commons-codec-1.10.jar', 'commons.codec', '1.10') {
exports('org.apache.commons.codec')
}
module('logback-core-1.2.13.jar', 'ch.qos.logback.core', '1.2.13') {
exports('ch.qos.logback.core')
}
module('jackson-datatype-jsr310-2.13.2.jar', 'jackson-datatype-jsr310', '2.13.2') {
exports('com.fasterxml.jackson.datatype.jsr310')
}
module('json-20240205.jar', 'org.json', '20240205') {
exports('org.json')
}
module('scrypt-1.4.0.jar', 'scrypt', '1.4.0') {
exports('com.lambdaworks.codec')
exports('com.lambdaworks.crypto')
}
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('streamsupport-1.7.0.jar', 'net.sourceforge.streamsupport', '1.7.0') {
requires('jdk.unsupported')
exports('java8.util')
exports('java8.util.function')
exports('java8.util.stream')
}
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')
requires('java.desktop')
}
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')
}
module('com.sparrowwallet:hid4java', 'org.hid4java') {
requires('com.sun.jna')
exports('org.hid4java')
exports('org.hid4java.jna')
}
module('com.sparrowwallet:usb4java', 'org.usb4java') {
exports('org.usb4java')
}
module('com.jcraft:jzlib', 'com.jcraft.jzlib') {
module('jzlib-1.1.3.jar', 'com.jcraft.jzlib', '1.1.3') {
exports('com.jcraft.jzlib')
}
}
kmpTorResourceFilterJar {
keepTorCompilation("current","current")
}

30
buildSrc/build.gradle Normal file
View File

@ -0,0 +1,30 @@
plugins {
id 'java-gradle-plugin' // so we can assign and ID to our plugin
}
dependencies {
implementation 'com.google.gradle:osdetector-gradle-plugin:1.7.3'
implementation 'org.javamodularity:moduleplugin:1.8.14'
implementation 'org.ow2.asm:asm:9.6'
}
repositories {
mavenCentral()
maven {
url "https://plugins.gradle.org/m2/"
}
}
gradlePlugin {
plugins {
// here we register our plugin with an ID
register("extra-java-module-info") {
id = "extra-java-module-info"
implementationClass = "org.gradle.sample.transform.javamodules.ExtraModuleInfoPlugin"
}
register("org-openjfx-javafxplugin") {
id = "org-openjfx-javafxplugin"
implementationClass = "org.openjfx.gradle.JavaFXPlugin"
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,114 @@
/*
* 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;
}
}

View File

@ -0,0 +1,164 @@
/*
* 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()));
}
}
}

View File

@ -0,0 +1,91 @@
/*
* 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 )
);
}
}

View File

@ -0,0 +1,49 @@
/*
* 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.javamodularity.moduleplugin.ModuleSystemPlugin;
import org.openjfx.gradle.tasks.ExecTask;
public class JavaFXPlugin implements Plugin<Project> {
@Override
public void apply(Project project) {
project.getPlugins().apply(OsDetectorPlugin.class);
project.getPlugins().apply(ModuleSystemPlugin.class);
project.getExtensions().create("javafx", JavaFXOptions.class, project);
project.getTasks().create("configJavafxRun", ExecTask.class, project);
}
}

View File

@ -0,0 +1,124 @@
/*
* 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.logging.Logger;
import org.gradle.api.logging.Logging;
import org.gradle.api.plugins.ApplicationPlugin;
import org.gradle.api.tasks.JavaExec;
import org.gradle.api.tasks.TaskAction;
import org.javamodularity.moduleplugin.extensions.RunModuleOptions;
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.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.TreeSet;
public class ExecTask extends DefaultTask {
private static final Logger LOGGER = Logging.getLogger(ExecTask.class);
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()) {
RunModuleOptions moduleOptions = execTask.getExtensions().findByType(RunModuleOptions.class);
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()));
if (moduleOptions != null) {
LOGGER.info("Modular JavaFX application found");
// Remove empty JavaFX jars from classpath
execTask.setClasspath(classpathWithoutJavaFXJars.plus(javaFXPlatformJars));
definedJavaFXModuleNames.forEach(javaFXModule -> moduleOptions.getAddModules().add(javaFXModule));
} else {
LOGGER.info("Non-modular JavaFX application found");
// Remove all JavaFX jars from classpath
execTask.setClasspath(classpathWithoutJavaFXJars);
var javaFXModuleJvmArgs = List.of("--module-path", javaFXPlatformJars.getAsPath());
var jvmArgs = new ArrayList<String>();
jvmArgs.add("--add-modules");
jvmArgs.add(String.join(",", definedJavaFXModuleNames));
List<String> execJvmArgs = execTask.getJvmArgs();
if (execJvmArgs != null) {
jvmArgs.addAll(execJvmArgs);
}
jvmArgs.addAll(javaFXModuleJvmArgs);
execTask.setJvmArgs(jvmArgs);
}
}
} else {
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()));
}
}

View File

@ -12,36 +12,62 @@ Work on resolving both of these issues is ongoing.
### 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.
For v1.6.6 to v1.9.1, this was Eclipse Temurin 18.0.1+10. For v2.0.0 to v2.3.1, this was Eclipse Temurin 22.0.2+9. For v2.4.0 and later, Eclipse Temurin 25.0.2+10 is used.
Note: Do not install Java using a system package manager (e.g. apt, dnf, rpm).
Linux packages replace the JDK's bundled `cacerts` file with a symlink to the system CA certificates, which differ from those in the release tarballs and will produce a non-reproducible build.
For v1.6.6 and later, this is Eclipse Temurin 18.0.1+10.
#### Java from Adoptium github repo
It is available for all supported platforms from [Eclipse Temurin 25.0.2+10](https://github.com/adoptium/temurin25-binaries/releases/tag/jdk-25.0.2%2B10).
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:
- [Linux x64](https://github.com/adoptium/temurin25-binaries/releases/download/jdk-25.0.2%2B10/OpenJDK25U-jdk_x64_linux_hotspot_25.0.2_10.tar.gz)
- [Linux aarch64](https://github.com/adoptium/temurin25-binaries/releases/download/jdk-25.0.2%2B10/OpenJDK25U-jdk_aarch64_linux_hotspot_25.0.2_10.tar.gz)
- [MacOS x64](https://github.com/adoptium/temurin25-binaries/releases/download/jdk-25.0.2%2B10/OpenJDK25U-jdk_x64_mac_hotspot_25.0.2_10.tar.gz)
- [MacOS aarch64](https://github.com/adoptium/temurin25-binaries/releases/download/jdk-25.0.2%2B10/OpenJDK25U-jdk_aarch64_mac_hotspot_25.0.2_10.tar.gz)
- [Windows x64](https://github.com/adoptium/temurin25-binaries/releases/download/jdk-25.0.2%2B10/OpenJDK25U-jdk_x64_windows_hotspot_25.0.2_10.zip)
- [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)
- [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 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)
- [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)
On Linux, extract the tarball and set `JAVA_HOME` to use it for the build:
```shell
tar -xzf OpenJDK25U-jdk_x64_linux_hotspot_25.0.2_10.tar.gz
export JAVA_HOME=$PWD/jdk-25.0.2+10
export PATH=$JAVA_HOME/bin:$PATH
#### Java from Adoptium deb repo
It is also possible to install via a package manager on *nix systems. For example, on Debian/Ubuntu systems:
- Install dependencies:
```sh
sudo apt-get install -y wget curl apt-transport-https gnupg
```
#### Java from SDKMAN
Download Adoptium public PGP key:
```sh
curl --tlsv1.2 --proto =https --location -o adoptium.asc https://packages.adoptium.net/artifactory/api/gpg/key/public
```
An alternative option for all platforms is to use the [sdkman.io](https://sdkman.io/) package manager ([Git Bash for Windows](https://git-scm.com/download/win) is a good choice on that platform).
Check if key fingerprint matches: `3B04D753C9050D9A5D343F39843C48A565F8F04B`:
```
gpg --import --import-options show-only adoptium.asc
```
If key doesn't match, do not proceed.
Add Adoptium PGP key to a the keyring shared folder:
```sh
sudo cp adoptium.asc /usr/share/keyrings/
```
Add Adoptium debian repository:
```sh
echo "deb [signed-by=/usr/share/keyrings/adoptium.asc] https://packages.adoptium.net/artifactory/deb $(awk -F= '/^VERSION_CODENAME/{print$2}' /etc/os-release) main" | sudo tee /etc/apt/sources.list.d/adoptium.list
```
Update cache, install the desired temurin version and configure java to be linked to this same version:
```
sudo apt update -y
sudo apt-get install -y temurin-18-jdk=18.0.1+10
sudo update-alternatives --config java
```
#### Java from SDK
A alternative option for all platforms is to use the [sdkman.io](https://sdkman.io/) package manager ([Git Bash for Windows](https://git-scm.com/download/win) is a good choice on that platform).
See the installation [instructions here](https://sdkman.io/install).
Once installed, run
```shell
sdk install java 25.0.2-tem
sdk install java 18.0.1-tem
```
### Other requirements
@ -56,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:
```shell
GIT_TAG="2.5.2"
GIT_TAG="1.8.4"
```
The project can then be initially cloned as follows:
@ -74,7 +100,7 @@ git checkout "${GIT_TAG}"
```
Note - there is an additional step if you updated rather than initially cloned your repo at `GIT_TAG`.
This is due to the Git submodules which need to be checked out to the commit state they had at the time of the release.
This is due to the [drongo submodule](https://github.com/sparrowwallet/drongo/tree/master) which needs to be checked out to the commit state it had at the time of the release.
Only then your build will be comparable to the provided one in the release section of Github.
To checkout the submodule to the correct commit for `GIT_TAG`, additionally run:

2
drongo

@ -1 +1 @@
Subproject commit 077d2142cc3aad84f6f58868cf8f17fc61027fdc
Subproject commit 9872c6b6ecfa6f5d14cd2edcb5605b76e9cb7396

Binary file not shown.

View File

@ -1,6 +1,6 @@
distributionBase=GRADLE_USER_HOME
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
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME

15
gradlew vendored
View File

@ -1,7 +1,7 @@
#!/bin/sh
#
# Copyright © 2015 the original authors.
# Copyright © 2015-2021 the original authors.
#
# Licensed under the Apache License, Version 2.0 (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
# limitations under the License.
#
# SPDX-License-Identifier: Apache-2.0
#
##############################################################################
#
@ -57,7 +55,7 @@
# Darwin, MinGW, and NonStop.
#
# (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.
#
# You can find Gradle at https://github.com/gradle/gradle/.
@ -86,7 +84,7 @@ done
# shellcheck disable=SC2034
APP_BASE_NAME=${0##*/}
# 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.
MAX_FD=maximum
@ -114,6 +112,7 @@ case "$( uname )" in #(
NONSTOP* ) nonstop=true ;;
esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
# 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
if "$cygwin" || "$msys" ; then
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
JAVACMD=$( cygpath --unix "$JAVACMD" )
@ -203,14 +203,15 @@ fi
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# 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.
# * 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.
set -- \
"-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.

25
gradlew.bat vendored
View File

@ -13,8 +13,6 @@
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem
@rem SPDX-License-Identifier: Apache-2.0
@rem
@if "%DEBUG%"=="" @echo off
@rem ##########################################################################
@ -45,11 +43,11 @@ set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if %ERRORLEVEL% equ 0 goto execute
echo. 1>&2
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. 1>&2
echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
@ -59,21 +57,22 @@ set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto execute
echo. 1>&2
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. 1>&2
echo.
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:execute
@rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@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
@rem End local scope for the variables with windows NT shell

1
lark

@ -1 +0,0 @@
Subproject commit e9c6f35fe66aee105ef3c532fcefeb7130dab169

View File

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

View File

@ -1,4 +1,3 @@
rootProject.name = 'sparrow'
include 'drongo'
include 'lark'

View File

@ -0,0 +1,2 @@
mime-type=x-scheme-handler/auth47
description=Auth47 Authentication URI

View File

@ -0,0 +1,2 @@
mime-type=x-scheme-handler/bitcoin
description=Bitcoin Scheme URI

View File

@ -0,0 +1,2 @@
mime-type=x-scheme-handler/lightning
description=LNURL URI

View File

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

View File

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

View File

@ -1,11 +1,10 @@
[Desktop Entry]
Name=Sparrow
Comment=Sparrow
Exec=/opt/sparrowwallet/bin/Sparrow %U
Icon=/opt/sparrowwallet/lib/Sparrow.png
Exec=/opt/sparrow/bin/Sparrow %U
Icon=/opt/sparrow/lib/Sparrow.png
Terminal=false
Type=Application
Categories=Finance;Network;
MimeType=application/psbt;application/bitcoin-transaction;application/pgp-signature;x-scheme-handler/bitcoin;x-scheme-handler/auth47;x-scheme-handler/lightning
StartupWMClass=Sparrow
SingleMainWindow=true

View File

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

View File

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

View File

@ -1,24 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<mime-info xmlns="http://www.freedesktop.org/standards/shared-mime-info">
<mime-type type="application/psbt">
<comment>Partially Signed Bitcoin Transaction</comment>
<glob pattern="*.psbt"/>
</mime-type>
<mime-type type="application/bitcoin-transaction">
<comment>Bitcoin Transaction</comment>
<glob pattern="*.txn"/>
</mime-type>
<mime-type type="application/pgp-signature">
<comment>ASCII Armored File</comment>
<glob pattern="*.asc"/>
</mime-type>
<mime-type type="x-scheme-handler/bitcoin">
<comment>Bitcoin Scheme URI</comment>
</mime-type>
<mime-type type="x-scheme-handler/auth47">
<comment>Auth47 Authentication URI</comment>
</mime-type>
<mime-type type="x-scheme-handler/lightning">
<comment>LNURL URI</comment>
</mime-type>
</mime-info>

View File

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

View File

@ -21,7 +21,7 @@
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>2.5.3</string>
<string>1.8.5</string>
<key>CFBundleSignature</key>
<string>????</string>
<!-- See https://developer.apple.com/app-store/categories/ for list of AppStore categories -->
@ -33,12 +33,8 @@
<string>Copyright (C) 2021</string>
<key>NSHighResolutionCapable</key>
<string>true</string>
<key>NSCameraUseContinuityCameraDeviceType</key>
<true/>
<key>NSCameraUsageDescription</key>
<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>
<array>
<dict>

View File

Before

Width:  |  Height:  |  Size: 99 KiB

After

Width:  |  Height:  |  Size: 99 KiB

File diff suppressed because it is too large Load Diff

View File

@ -3,7 +3,6 @@ package com.sparrowwallet.sparrow;
import com.google.common.eventbus.Subscribe;
import com.google.common.net.HostAndPort;
import com.sparrowwallet.drongo.Network;
import com.sparrowwallet.drongo.OsType;
import com.sparrowwallet.drongo.SecureString;
import com.sparrowwallet.drongo.address.Address;
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.policy.PolicyType;
import com.sparrowwallet.drongo.wallet.*;
import com.sparrowwallet.sparrow.control.DialogImage;
import com.sparrowwallet.sparrow.control.WalletPasswordDialog;
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
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.io.*;
import com.sparrowwallet.sparrow.net.*;
import io.reactivex.rxjavafx.schedulers.JavaFxScheduler;
import io.reactivex.subjects.PublishSubject;
import com.sparrowwallet.sparrow.soroban.SorobanServices;
import com.sparrowwallet.sparrow.whirlpool.WhirlpoolServices;
import javafx.application.Application;
import javafx.application.Platform;
import javafx.beans.property.BooleanProperty;
@ -45,6 +43,7 @@ import javafx.scene.Scene;
import javafx.scene.control.*;
import javafx.scene.control.Dialog;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.input.KeyCode;
import javafx.scene.text.Font;
import javafx.stage.Screen;
@ -68,12 +67,8 @@ import java.time.ZonedDateTime;
import java.time.temporal.ChronoUnit;
import java.util.*;
import java.util.List;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ThreadLocalRandom;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
import static com.sparrowwallet.sparrow.AppController.CONNECTION_FAILED_PREFIX;
import static com.sparrowwallet.sparrow.control.DownloadVerifierDialog.*;
public class AppServices {
@ -93,13 +88,18 @@ public class AppServices {
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);
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 TESTNET_FALLBACK_FEE_RATE = 1000d / 1000;
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;
@ -109,8 +109,6 @@ public class AppServices {
private TrayManager trayManager;
private final PublishSubject<NewBlockEvent> newBlockSubject = PublishSubject.create();
private static Image windowIcon;
private static final BooleanProperty onlineProperty = new SimpleBooleanProperty(false);
@ -133,18 +131,12 @@ public class AppServices {
private static BlockHeader latestBlockHeader;
private static final Map<Integer, BlockSummary> blockSummaries = new ConcurrentHashMap<>();
private static Map<Integer, Double> targetBlockFeeRates;
private static Double nextBlockMedianFeeRate;
private static final TreeMap<Date, Set<MempoolRateSize>> mempoolHistogram = new TreeMap<>();
private static Double minimumRelayFeeRate;
private static Double serverMinimumRelayFeeRate;
private static CurrencyRate fiatCurrencyExchangeRate;
private static List<Device> devices;
@ -195,13 +187,9 @@ public class AppServices {
private AppServices(Application application, InteractionServices interactionServices) {
this.application = application;
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(whirlpoolServices);
EventManager.get().register(sorobanServices);
}
public void start() {
@ -214,7 +202,6 @@ public class AppServices {
preventSleepService = createPreventSleepService();
onlineProperty.addListener(onlineServicesListener);
minimumRelayFeeRate = getConfiguredMinimumRelayFeeRate(config);
if(config.getMode() == Mode.ONLINE) {
if(config.requiresInternalTor()) {
@ -281,7 +268,7 @@ public class AppServices {
}
if(Tor.getDefault() != null) {
Tor.getDefault().close();
Tor.getDefault().getTorManager().destroy(true, success -> {});
}
}
@ -311,6 +298,12 @@ public class AppServices {
if(event != null) {
EventManager.get().post(event);
}
FeeRatesSource feeRatesSource = Config.get().getFeeRatesSource();
feeRatesSource = (feeRatesSource == null ? FeeRatesSource.MEMPOOL_SPACE : feeRatesSource);
if(event instanceof ConnectionEvent && Network.get().equals(Network.MAINNET) && feeRatesSource.isExternal()) {
EventManager.get().post(new FeeRatesSourceChangedEvent(feeRatesSource));
}
});
connectionService.setOnFailed(failEvent -> {
//Close connection here to create a new transport next time we try
@ -330,9 +323,6 @@ public class AppServices {
"\n\nChange the configured server certificate if you would like to proceed.");
} else {
crtFile = Storage.getCertificateFile(tlsServerException.getServer().getHost());
if(crtFile == null) {
crtFile = Storage.getCaCertificateFile(tlsServerException.getServer().getHost());
}
if(crtFile != null) {
Optional<ButtonType> optButton = AppServices.showErrorDialog("SSL Handshake Failed", "The certificate provided by the server at " + tlsServerException.getServer().getHost() + " appears to have changed." +
"\n\nThis may be simply due to a certificate renewal, or it may indicate a man-in-the-middle attack." +
@ -369,18 +359,15 @@ public class AppServices {
onlineProperty.setValue(false);
onlineProperty.addListener(onlineServicesListener);
log.debug("Connection failed", failEvent.getSource().getException());
if(Config.get().getServerType() == ServerType.PUBLIC_ELECTRUM_SERVER) {
boolean changed = changePublicServer();
connectionService.setPeriod(changed ? Duration.seconds(PUBLIC_SERVER_RETRY_PERIOD_SECS) : Duration.seconds(PRIVATE_SERVER_RETRY_PERIOD_SECS));
EventManager.get().post(new ConnectionFailedEvent(failEvent.getSource().getException()));
if(!changed) {
Platform.runLater(() -> EventManager.get().post(new StatusEvent(CONNECTION_FAILED_PREFIX + "No public servers available that can serve the open wallets, retrying later...")));
}
Config.get().changePublicServer();
connectionService.setPeriod(Duration.seconds(PUBLIC_SERVER_RETRY_PERIOD_SECS));
} else {
connectionService.setPeriod(Duration.seconds(PRIVATE_SERVER_RETRY_PERIOD_SECS));
EventManager.get().post(new ConnectionFailedEvent(failEvent.getSource().getException()));
}
log.debug("Connection failed", failEvent.getSource().getException());
EventManager.get().post(new ConnectionFailedEvent(failEvent.getSource().getException()));
});
return connectionService;
@ -400,7 +387,7 @@ public class AppServices {
exchangeSource == null ? DEFAULT_EXCHANGE_SOURCE : exchangeSource,
currency == null ? DEFAULT_FIAT_CURRENCY : currency);
//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.setRestartOnFailure(true);
@ -500,26 +487,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() {
return Tor.getDefault() != null;
}
@ -535,7 +502,7 @@ public class AppServices {
public static Proxy getProxy(String proxyCircuitId) {
Config config = Config.get();
Proxy proxy = null;
if(config.isUseProxy() && config.getProxyServer() != null) {
if(config.isUseProxy()) {
HostAndPort proxyHostAndPort = HostAndPort.fromString(config.getProxyServer());
InetSocketAddress proxyAddress = new InetSocketAddress(proxyHostAndPort.getHost(), proxyHostAndPort.getPortOrDefault(ProxyTcpOverTlsTransport.DEFAULT_PROXY_PORT));
proxy = new Proxy(Proxy.Type.SOCKS, proxyAddress);
@ -567,6 +534,14 @@ public class AppServices {
return INSTANCE;
}
public static WhirlpoolServices getWhirlpoolServices() {
return get().whirlpoolServices;
}
public static SorobanServices getSorobanServices() {
return get().sorobanServices;
}
public static InteractionServices getInteractionServices() {
return get().interactionServices;
}
@ -614,34 +589,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() {
if(windowIcon == null) {
windowIcon = new Image(SparrowWallet.class.getResourceAsStream("/image/sparrow-icon.png"));
@ -660,7 +607,7 @@ public class AppServices {
}
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() {
@ -745,10 +692,6 @@ public class AppServices {
return latestBlockHeader;
}
public static Map<Integer, BlockSummary> getBlockSummaries() {
return blockSummaries;
}
public static Double getDefaultFeeRate() {
int defaultTarget = TARGET_BLOCKS_RANGE.get((TARGET_BLOCKS_RANGE.size() / 2) - 1);
return getTargetBlockFeeRates() == null ? getFallbackFeeRate() : getTargetBlockFeeRates().get(defaultTarget);
@ -760,30 +703,6 @@ public class AppServices {
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() {
return Network.get() == Network.MAINNET ? FALLBACK_FEE_RATE : TESTNET_FALLBACK_FEE_RATE;
}
@ -818,18 +737,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() {
return minimumRelayFeeRate == null ? Transaction.DEFAULT_MIN_RELAY_FEE : minimumRelayFeeRate;
}
public static Double getServerMinimumRelayFeeRate() {
return serverMinimumRelayFeeRate;
}
public static CurrencyRate getFiatCurrencyExchangeRate() {
return fiatCurrencyExchangeRate;
}
@ -843,8 +754,8 @@ public class AppServices {
}
public static void addPayjoinURI(BitcoinURI bitcoinURI) {
if(bitcoinURI.getPayjoinUrl() == null || bitcoinURI.getAddress() == null) {
throw new IllegalArgumentException("Not a valid payjoin URI");
if(bitcoinURI.getPayjoinUrl() == null) {
throw new IllegalArgumentException("Not a payjoin URI");
}
payjoinURIs.put(bitcoinURI.getAddress(), bitcoinURI);
}
@ -856,10 +767,6 @@ public class AppServices {
public static void clearTransactionHistoryCache(Wallet wallet) {
ElectrumServer.clearRetrievedScriptHashes(wallet);
if(wallet.getPolicyType() == PolicyType.SINGLE_SP && wallet.isValid()) {
ElectrumServer.releaseSilentPaymentSubscription(wallet.getSilentPaymentScanAddress());
}
for(Wallet childWallet : wallet.getChildWallets()) {
if(childWallet.isNested()) {
AppServices.clearTransactionHistoryCache(childWallet);
@ -871,22 +778,6 @@ public class AppServices {
return Storage.isWalletFile(file);
}
public boolean changePublicServer() {
List<PolicyType> policyTypes = getOpenWallets().keySet().stream().map(Wallet::getPolicyType).filter(Objects::nonNull).collect(Collectors.toList());
return changePublicServer(policyTypes.isEmpty() ? List.of(PolicyType.SINGLE_HD) : policyTypes);
}
private boolean changePublicServer(List<PolicyType> policyTypes) {
Config config = Config.get();
List<Server> otherServers = PublicElectrumServer.getServers().stream().filter(pes -> pes.supportsAllPolicyTypes(policyTypes))
.map(PublicElectrumServer::getServer).filter(server -> !server.equals(config.getPublicElectrumServer())).collect(Collectors.toList());
if(!otherServers.isEmpty()) {
config.setPublicElectrumServer(otherServers.get(ThreadLocalRandom.current().nextInt(otherServers.size())));
return true;
}
return false;
}
public static Optional<ButtonType> showWarningDialog(String title, String content, ButtonType... buttons) {
return showAlertDialog(title, content, Alert.AlertType.WARNING, buttons);
}
@ -915,13 +806,8 @@ public class AppServices {
Stage stage = (Stage)window;
stage.getIcons().add(getWindowIcon());
if(stage.getScene() != null) {
if(Config.get().getTheme() == Theme.DARK) {
stage.getScene().getStylesheets().add(AppServices.class.getResource("darktheme.css").toExternalForm());
}
if(Config.get().isChunkAddresses()) {
stage.getScene().getRoot().getStyleClass().add("chunk-addresses");
}
if(stage.getScene() != null && Config.get().getTheme() == Theme.DARK) {
stage.getScene().getStylesheets().add(AppServices.class.getResource("darktheme.css").toExternalForm());
}
}
@ -1037,7 +923,6 @@ public class AppServices {
}
if(openWindow instanceof Stage) {
((Stage)openWindow).setIconified(false);
((Stage)openWindow).setAlwaysOnTop(true);
((Stage)openWindow).setAlwaysOnTop(false);
}
@ -1102,7 +987,7 @@ public class AppServices {
try {
Auth47 auth47 = new Auth47(uri);
List<ScriptType> scriptTypes = PaymentCode.SEGWIT_SCRIPT_TYPES;
Wallet wallet = selectWallet(List.of(PolicyType.SINGLE_HD), scriptTypes, false, true, "login to " + auth47.getCallback().getHost(), true);
Wallet wallet = selectWallet(List.of(PolicyType.SINGLE), scriptTypes, false, true, "login to " + auth47.getCallback().getHost(), true);
if(wallet != null) {
try {
@ -1122,8 +1007,8 @@ public class AppServices {
private static void openLnurlAuthUri(URI uri) {
try {
LnurlAuth lnurlAuth = new LnurlAuth(uri);
List<ScriptType> scriptTypes = ScriptType.getAddressableScriptTypes(PolicyType.SINGLE_HD);
Wallet wallet = selectWallet(List.of(PolicyType.SINGLE_HD), scriptTypes, true, true, lnurlAuth.getLoginMessage(), true);
List<ScriptType> scriptTypes = ScriptType.getAddressableScriptTypes(PolicyType.SINGLE);
Wallet wallet = selectWallet(List.of(PolicyType.SINGLE), scriptTypes, true, true, lnurlAuth.getLoginMessage(), true);
if(wallet != null) {
if(wallet.isEncrypted()) {
@ -1196,7 +1081,8 @@ public class AppServices {
walletChoiceDialog.initOwner(getActiveWindow());
walletChoiceDialog.setTitle("Choose Wallet");
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());
moveToActiveWindowScreen(walletChoiceDialog);
Optional<Wallet> optWallet = walletChoiceDialog.showAndWait();
@ -1208,98 +1094,17 @@ public class AppServices {
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.getPolicyType() == PolicyType.SINGLE_HD
&& 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.getPolicyType() == PolicyType.SINGLE_HD
&& 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() {
return Font.font("Fragment Mono Regular", 13);
}
public static boolean isOnWayland() {
if(OsType.getCurrent() != OsType.UNIX) {
return false;
}
String waylandDisplay = System.getenv("WAYLAND_DISPLAY");
return waylandDisplay != null && !waylandDisplay.isEmpty();
return Font.font("Roboto Mono", 13);
}
@Subscribe
public void newConnection(ConnectionEvent event) {
currentBlockHeight = event.getBlockHeight();
System.setProperty(Network.BLOCK_HEIGHT_PROPERTY, Integer.toString(currentBlockHeight));
if(getConfiguredMinimumRelayFeeRate(Config.get()) == null) {
minimumRelayFeeRate = event.getMinimumRelayFeeRate() == null ? Transaction.DEFAULT_MIN_RELAY_FEE : event.getMinimumRelayFeeRate();
}
serverMinimumRelayFeeRate = event.getMinimumRelayFeeRate();
minimumRelayFeeRate = Math.max(event.getMinimumRelayFeeRate(), Transaction.DEFAULT_MIN_RELAY_FEE);
latestBlockHeader = event.getBlockHeader();
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
@ -1314,22 +1119,11 @@ public class AppServices {
latestBlockHeader = event.getBlockHeader();
String status = "Updating to new block height " + event.getHeight();
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
public void feesUpdated(FeeRatesUpdatedEvent event) {
targetBlockFeeRates = event.getTargetBlockFeeRates();
nextBlockMedianFeeRate = event.getNextBlockMedianFeeRate();
}
@Subscribe
@ -1342,8 +1136,10 @@ public class AppServices {
@Subscribe
public void feeRateSourceChanged(FeeRatesSourceChangedEvent event) {
//Perform once-off fee rates retrieval to immediately change displayed rates
fetchFeeRates();
fetchBlockSummaries(Collections.emptyList());
if(feeRatesService != null && !feeRatesService.isRunning() && Config.get().getMode() != Mode.OFFLINE) {
feeRatesService = createFeeRatesService();
feeRatesService.start();
}
}
@Subscribe
@ -1484,28 +1280,10 @@ public class AppServices {
@Subscribe
public void walletHistoryFailed(WalletHistoryFailedEvent event) {
if(Config.get().getServerType() == ServerType.PUBLIC_ELECTRUM_SERVER && isConnected()) {
String currentName = Config.get().getServerDisplayName();
onlineProperty.set(false);
boolean changed = changePublicServer();
if(changed) {
log.warn("Failed to fetch wallet history from " + currentName + ", reconnecting to another server...");
} else {
log.warn("Failed to fetch wallet history from " + currentName + ", retrying later");
connectionService.setDelay(Duration.seconds(PRIVATE_SERVER_RETRY_PERIOD_SECS));
EventManager.get().post(new StatusEvent("Wallet load failed: No other public servers available that can serve the open wallets, retrying later..."));
}
log.warn("Failed to fetch wallet history from " + Config.get().getServerDisplayName() + ", reconnecting to another server...");
Config.get().changePublicServer();
onlineProperty.set(true);
}
}
@Subscribe
public void silentPaymentsUnsubscribe(SilentPaymentsUnsubscribeEvent event) {
if(isConnected()) {
ElectrumServer.SilentPaymentsUnsubscribeService unsubscribeService = new ElectrumServer.SilentPaymentsUnsubscribeService(event.getScanAddress());
unsubscribeService.setOnFailed(workerStateEvent -> {
log.warn("Failed to unsubscribe silent payments for " + event.getScanAddress().getAddress(), workerStateEvent.getSource().getException());
});
unsubscribeService.start();
}
}
}

View File

@ -1,6 +1,5 @@
package com.sparrowwallet.sparrow;
import com.sparrowwallet.drongo.KeyDerivation;
import com.sparrowwallet.drongo.policy.PolicyType;
import com.sparrowwallet.drongo.protocol.ScriptChunk;
import com.sparrowwallet.drongo.wallet.Keystore;
@ -55,7 +54,7 @@ public abstract class BaseController {
descriptorArea.setMouseOverTextDelay(Duration.ofMillis(150));
descriptorArea.addEventHandler(MouseOverTextEvent.MOUSE_OVER_TEXT_BEGIN, e -> {
TwoDimensional.Position position = descriptorArea.getParagraph(0).getStyleSpans().offsetToPosition(e.getCharacterIndex(), Backward);
int index = descriptorArea.getWallet().getPolicyType() == PolicyType.SINGLE_HD || descriptorArea.getWallet().getPolicyType() == PolicyType.SINGLE_SP ? position.getMajor() - 1 : ((position.getMajor() - 1) / 2);
int index = descriptorArea.getWallet().getPolicyType() == PolicyType.SINGLE ? position.getMajor() - 1 : ((position.getMajor() - 1) / 2);
if(position.getMajor() > 0 && index >= 0 && index < descriptorArea.getWallet().getKeystores().size()) {
Keystore hoverKeystore = descriptorArea.getWallet().getKeystores().get(index);
Point2D pos = e.getScreenPosition();
@ -73,13 +72,10 @@ public abstract class BaseController {
StringBuilder builder = new StringBuilder();
builder.append("[");
builder.append(keystore.getKeyDerivation().getMasterFingerprint());
builder.append(KeyDerivation.writePath(KeyDerivation.parsePath(keystore.getKeyDerivation().getDerivationPath())).substring(1));
builder.append("/");
builder.append(keystore.getKeyDerivation().getDerivationPath().replaceFirst("^m?/", ""));
builder.append("]");
if(keystore.getExtendedPublicKey() != null) {
builder.append(keystore.getExtendedPublicKey().toString());
} else if(keystore.getSilentPaymentScanAddress() != null) {
builder.append(keystore.getSilentPaymentScanAddress().toKeyString());
}
builder.append(keystore.getExtendedPublicKey().toString());
return builder.toString();
}

View File

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

View File

@ -1,6 +1,5 @@
package com.sparrowwallet.sparrow;
import com.sparrowwallet.drongo.OsType;
import com.sparrowwallet.drongo.wallet.Keystore;
import com.sparrowwallet.sparrow.control.KeystorePassphraseDialog;
import com.sparrowwallet.sparrow.control.TextUtils;
@ -49,7 +48,7 @@ public class DefaultInteractionServices implements InteractionServices {
}
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();
alert.getDialogPane().setPrefHeight(200 + numLines * 20);
}

View File

@ -8,13 +8,13 @@ public enum Interface {
public static Interface get() {
if(currentInterface == null) {
boolean headless = java.awt.GraphicsEnvironment.isHeadless();
boolean headlessPlatform = "Headless".equalsIgnoreCase(System.getProperty("glass.platform"));
boolean glassHeadless = "Headless".equalsIgnoreCase(System.getProperty("glass.platform"));
if(headless || headlessPlatform) {
if(headless || glassHeadless) {
currentInterface = TERMINAL;
if(headless && !headlessPlatform) {
throw new UnsupportedOperationException("Headless environment detected but headless glass platform not found");
if(headless && !glassHeadless) {
throw new UnsupportedOperationException("Headless environment detected but Headless platform not found");
}
} else {
currentInterface = DESKTOP;

View File

@ -1,7 +1,6 @@
package com.sparrowwallet.sparrow;
import com.sparrowwallet.drongo.Network;
import com.sparrowwallet.drongo.OsType;
import com.sparrowwallet.drongo.wallet.Wallet;
import com.sparrowwallet.sparrow.control.WalletIcon;
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.net.PublicElectrumServer;
import com.sparrowwallet.sparrow.net.ServerType;
import com.sparrowwallet.sparrow.settings.SettingsGroup;
import com.sparrowwallet.sparrow.settings.SettingsDialog;
import com.sparrowwallet.sparrow.preferences.PreferenceGroup;
import com.sparrowwallet.sparrow.preferences.PreferencesDialog;
import javafx.application.Application;
import javafx.scene.text.Font;
import javafx.stage.Stage;
import org.controlsfx.glyphfont.GlyphFontRegistry;
import org.controlsfx.tools.Platform;
import org.slf4j.LoggerFactory;
import java.io.File;
@ -42,7 +42,10 @@ public class SparrowDesktop extends Application {
public void start(Stage stage) throws Exception {
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);
AppServices.initialize(this);
@ -57,8 +60,8 @@ public class SparrowDesktop extends Application {
Config.get().setMode(mode);
if(mode.equals(Mode.ONLINE)) {
SettingsDialog settingsDialog = new SettingsDialog(SettingsGroup.SERVER, true);
Optional<Boolean> optNewWallet = settingsDialog.showAndWait();
PreferencesDialog preferencesDialog = new PreferencesDialog(PreferenceGroup.SERVER, true);
Optional<Boolean> optNewWallet = preferencesDialog.showAndWait();
createNewWallet = optNewWallet.isPresent() && optNewWallet.get();
} else if(Network.get() == Network.MAINNET) {
Config.get().setServerType(ServerType.PUBLIC_ELECTRUM_SERVER);
@ -72,6 +75,10 @@ public class SparrowDesktop extends Application {
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_NETWORKS_PROPERTY, Boolean.toString(!Config.get().isValidateDerivationPaths()));
@ -82,42 +89,28 @@ public class SparrowDesktop extends Application {
AppController appController = AppServices.newAppWindow(stage);
final boolean showNewWallet = createNewWallet;
//Delay opening new dialogs on Wayland
AppServices.runAfterDelay(AppServices.isOnWayland() ? 1000 : 0, () -> {
if(showNewWallet) {
appController.newWallet(null);
}
if(createNewWallet) {
appController.newWallet(null);
}
List<File> recentWalletFiles = Config.get().getRecentWalletFiles();
if(recentWalletFiles != null) {
//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> sortedWalletFiles = new ArrayList<>(recentWalletFiles);
sortedWalletFiles.removeAll(encryptedWalletFiles);
sortedWalletFiles.addAll(encryptedWalletFiles);
List<File> recentWalletFiles = Config.get().getRecentWalletFiles();
if(recentWalletFiles != null) {
//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> sortedWalletFiles = new ArrayList<>(recentWalletFiles);
sortedWalletFiles.removeAll(encryptedWalletFiles);
sortedWalletFiles.addAll(encryptedWalletFiles);
for(File walletFile : sortedWalletFiles) {
if(walletFile.exists()) {
appController.openWalletFile(walletFile, false);
}
for(File walletFile : sortedWalletFiles) {
if(walletFile.exists()) {
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

View File

@ -18,24 +18,14 @@ import java.util.*;
public class SparrowWallet {
public static final String APP_ID = "sparrow";
public static final String APP_NAME = "Sparrow";
public static final String APP_VERSION = "2.5.3";
public static final String APP_VERSION = "1.8.5";
public static final String APP_VERSION_SUFFIX = "";
public static final String APP_HOME_PROPERTY = "sparrow.home";
public static final String NETWORK_ENV_PROPERTY = "SPARROW_NETWORK";
public static final String JPACKAGE_APP_PATH = "jpackage.app-path";
private static Instance instance;
public static void main(String[] argv) {
if(System.getProperty(JPACKAGE_APP_PATH) != null) {
String libDir = System.getProperty("java.home") + File.separator + "lib";
System.setProperty("jna.boot.library.path", libDir);
System.setProperty("jna.library.path", libDir);
System.setProperty("jSerialComm.library.path", libDir);
System.setProperty("org.usb4java.LibraryName", "usb4java");
System.setProperty("java.library.path", libDir);
}
Args args = new Args();
JCommander jCommander = JCommander.newBuilder().addObject(args).programName(APP_NAME.toLowerCase(Locale.ROOT)).acceptUnknownOptions(true).build();
jCommander.parse(argv);
@ -76,11 +66,6 @@ public class SparrowWallet {
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());
if(signetFlag.exists()) {
Network.set(Network.SIGNET);

View File

@ -21,7 +21,7 @@ public class WelcomeDialog extends Dialog<Mode> {
welcomeController.initializeView();
dialogPane.setPrefWidth(600);
dialogPane.setPrefHeight(540);
dialogPane.setPrefHeight(520);
dialogPane.setMinHeight(dialogPane.getPrefHeight());
AppServices.moveToActiveWindowScreen(this);

View File

@ -1,18 +1,21 @@
package com.sparrowwallet.sparrow.control;
import com.sparrowwallet.drongo.policy.PolicyType;
import com.sparrowwallet.drongo.wallet.KeystoreSource;
import com.sparrowwallet.drongo.wallet.StandardAccount;
import com.sparrowwallet.drongo.wallet.Wallet;
import com.sparrowwallet.sparrow.AppServices;
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.scene.control.*;
import javafx.scene.layout.VBox;
import javafx.util.StringConverter;
import org.controlsfx.glyphfont.Glyph;
import java.util.*;
import java.util.ArrayList;
import java.util.List;
import static com.sparrowwallet.drongo.wallet.StandardAccount.*;
@ -43,14 +46,12 @@ public class AddAccountDialog extends Dialog<List<StandardAccount>> {
standardAccountCombo = new ComboBox<>();
standardAccountCombo.setMaxWidth(Double.MAX_VALUE);
Set<Integer> existingIndexes = new LinkedHashSet<>();
List<Integer> existingIndexes = new ArrayList<>();
Wallet masterWallet = wallet.isMasterWallet() ? wallet : wallet.getMasterWallet();
existingIndexes.add(masterWallet.getAccountIndex());
for(Wallet childWallet : masterWallet.getChildWallets()) {
if(!childWallet.isNested()) {
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()));
}
}
@ -61,15 +62,16 @@ public class AddAccountDialog extends Dialog<List<StandardAccount>> {
}
}
if(AppServices.isWhirlpoolCompatible(masterWallet) && !masterWallet.isWhirlpoolMasterWallet()) {
if(WhirlpoolServices.canWalletMix(masterWallet) && !masterWallet.isWhirlpoolMasterWallet()) {
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);
}
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)
|| (masterWallet.getPolicyType() == PolicyType.SINGLE_HD && masterWallet.getKeystores().stream().allMatch(ks -> ks.getSource() == KeystoreSource.HW_USB)))) {
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)))) {
dialogPane.getButtonTypes().add(discoverButtonType);
Button discoverButton = (Button)dialogPane.lookupButton(discoverButtonType);
discoverButton.disableProperty().bind(AppServices.onlineProperty().not());

View File

@ -45,7 +45,6 @@ public class AddressCell extends TreeTableCell<Entry, UtxoEntry.AddressStatus> {
tooltip.setShowDelay(Duration.millis(250));
tooltip.setText(getTooltipText(utxoEntry, addressStatus.isDuplicate(), addressStatus.isDustAttack()));
setTooltip(tooltip);
getStyleClass().add("address-cell");
if(addressStatus.isDustAttack()) {
setGraphic(getDustAttackHyperlink(utxoEntry));

View File

@ -22,7 +22,6 @@ public class AddressLabel extends IdLabel {
public AddressLabel(String text) {
super(text);
setSkin(new AddressTextFieldSkin(this));
addressProperty().addListener((observable, oldValue, newValue) -> {
setAddressAsText(newValue);
contextMenu.copyHex.setText("Copy " + newValue.getOutputScriptDataType());

View File

@ -1,148 +0,0 @@
package com.sparrowwallet.sparrow.control;
import com.sparrowwallet.drongo.Network;
import javafx.beans.value.ChangeListener;
import javafx.geometry.Insets;
import javafx.scene.Node;
import javafx.scene.control.ContentDisplay;
import javafx.scene.control.Label;
import javafx.scene.control.skin.LabelSkin;
import javafx.scene.text.Font;
import javafx.scene.text.Text;
import javafx.scene.text.TextFlow;
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class AddressLabelSkin extends LabelSkin {
public static final int CHUNK_SIZE = 4;
public static final Pattern CHUNK_PATTERN = Pattern.compile("(?<=\\G.{" + CHUNK_SIZE + "})");
private final TextFlow displayFlow;
private final ChangeListener<String> textListener;
private final ChangeListener<Font> fontListener;
public AddressLabelSkin(Label control) {
super(control);
displayFlow = new TextFlow();
displayFlow.setManaged(false);
displayFlow.setMouseTransparent(true);
getChildren().addFirst(displayFlow);
textListener = (_, _, newText) -> updateDisplay(newText);
fontListener = (_, _, _) -> updateDisplay(control.getText());
control.textProperty().addListener(textListener);
control.fontProperty().addListener(fontListener);
updateDisplay(control.getText());
control.setStyle("-fx-text-fill: transparent;");
}
@Override
public void dispose() {
getSkinnable().textProperty().removeListener(textListener);
getSkinnable().fontProperty().removeListener(fontListener);
super.dispose();
}
private void updateDisplay(String text) {
displayFlow.getChildren().clear();
if(text == null || text.isEmpty()) {
return;
}
List<AddressSpan> addresses = findAddresses(text);
int pos = 0;
for(AddressSpan span : addresses) {
if(span.start > pos) {
Text normalText = createText(text.substring(pos, span.start), false);
displayFlow.getChildren().add(normalText);
}
addChunkedAddress(text.substring(span.start, span.end));
pos = span.end;
}
if(pos < text.length()) {
Text normalText = createText(text.substring(pos), false);
displayFlow.getChildren().add(normalText);
}
}
private void addChunkedAddress(String address) {
String[] chunks = CHUNK_PATTERN.split(address);
for(int i = 0; i < chunks.length; i++) {
Text chunk = createText(chunks[i], i % 2 != 0);
displayFlow.getChildren().add(chunk);
}
}
private Text createText(String content, boolean alternate) {
Text text = new Text(content);
text.setFont(getSkinnable().getFont());
text.getStyleClass().add("address-chunk");
if(alternate) {
text.getStyleClass().add("alternate");
}
return text;
}
private List<AddressSpan> findAddresses(String text) {
List<AddressSpan> spans = new ArrayList<>();
Pattern wordBoundary = Pattern.compile("\\S+");
Matcher matcher = wordBoundary.matcher(text);
while(matcher.find()) {
String candidate = matcher.group();
if(isValidAddress(candidate)) {
spans.add(new AddressSpan(matcher.start(), matcher.end()));
}
}
return spans;
}
private boolean isValidAddress(String candidate) {
Network network = Network.get();
return network.hasP2PKHAddressPrefix(candidate) || network.hasP2SHAddressPrefix(candidate) ||
candidate.startsWith(network.getBech32AddressHRP()) || candidate.startsWith(network.getSilentPaymentsAddressHrp());
}
@Override
protected void updateChildren() {
super.updateChildren();
if(displayFlow != null && !getChildren().contains(displayFlow)) {
getChildren().addFirst(displayFlow);
}
}
@Override
protected void layoutChildren(double x, double y, double w, double h) {
super.layoutChildren(x, y, w, h);
// Position TextFlow to align with the label's text area
Label label = getSkinnable();
Insets padding = label.getPadding();
Node graphic = label.getGraphic();
double graphicOffset = 0;
if(graphic != null && label.getContentDisplay() == ContentDisplay.LEFT) {
graphicOffset = graphic.getLayoutBounds().getWidth() + label.getGraphicTextGap();
}
displayFlow.resizeRelocate(
x + padding.getLeft() + graphicOffset,
y + padding.getTop(),
w - padding.getLeft() - padding.getRight() - graphicOffset,
h - padding.getTop() - padding.getBottom()
);
}
private record AddressSpan(int start, int end) {}
}

View File

@ -1,274 +0,0 @@
package com.sparrowwallet.sparrow.control;
import com.sparrowwallet.drongo.Network;
import com.sparrowwallet.drongo.protocol.Base58;
import com.sparrowwallet.drongo.protocol.Bech32;
import impl.org.controlsfx.skin.CustomTextFieldSkin;
import javafx.beans.property.ObjectProperty;
import javafx.beans.value.ChangeListener;
import javafx.beans.property.SimpleObjectProperty;
import javafx.geometry.Insets;
import javafx.scene.Node;
import javafx.scene.control.TextField;
import javafx.scene.layout.Region;
import javafx.scene.shape.Path;
import javafx.scene.shape.Rectangle;
import javafx.scene.text.Font;
import javafx.scene.text.Text;
import javafx.scene.text.TextFlow;
import org.controlsfx.control.textfield.CustomTextField;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class AddressTextFieldSkin extends CustomTextFieldSkin {
private static final boolean[] BASE58_OK = buildOkTable(new String(Base58.ALPHABET));
private static final boolean[] BECH32_DATA_OK = buildOkTable(Bech32.CHARSET);
private final TextFlow displayFlow;
private final Rectangle clip;
private final ChangeListener<String> textListener;
private final ChangeListener<Font> fontListener;
public AddressTextFieldSkin(TextField control) {
super(control);
displayFlow = new TextFlow();
displayFlow.setManaged(false);
displayFlow.setMouseTransparent(true);
clip = new Rectangle();
displayFlow.setClip(clip);
getChildren().addFirst(displayFlow);
textListener = (_, _, newText) -> updateDisplay(newText);
fontListener = (_, _, _) -> updateDisplay(control.getText());
control.textProperty().addListener(textListener);
control.fontProperty().addListener(fontListener);
updateDisplay(control.getText());
control.setStyle("-fx-text-fill: transparent;");
// Unbind caret color since it's normally bound to textFill
unbindCaretColor(getChildren());
}
@Override
public void dispose() {
getSkinnable().textProperty().removeListener(textListener);
getSkinnable().fontProperty().removeListener(fontListener);
super.dispose();
}
private void unbindCaretColor(javafx.collections.ObservableList<Node> children) {
for(Node node : children) {
if(node instanceof Path path && path.getStroke() != null) {
path.fillProperty().unbind();
path.strokeProperty().unbind();
path.getStyleClass().add("address-field-caret");
} else if(node instanceof javafx.scene.Parent parent) {
unbindCaretColor(parent.getChildrenUnmodifiable());
}
}
}
@Override
public ObjectProperty<Node> leftProperty() {
if(getSkinnable() instanceof CustomTextField customTextField) {
return customTextField.leftProperty();
}
return new SimpleObjectProperty<>();
}
@Override
public ObjectProperty<Node> rightProperty() {
if(getSkinnable() instanceof CustomTextField customTextField) {
return customTextField.rightProperty();
}
return new SimpleObjectProperty<>();
}
private void updateDisplay(String text) {
displayFlow.getChildren().clear();
if(text == null || text.isEmpty()) {
return;
}
List<AddressSpan> addresses = findAddresses(text);
int pos = 0;
for(AddressSpan span : addresses) {
if(span.start > pos) {
Text normalText = createText(text.substring(pos, span.start), false);
displayFlow.getChildren().add(normalText);
}
addChunkedAddress(text.substring(span.start, span.end));
pos = span.end;
}
if(pos < text.length()) {
Text normalText = createText(text.substring(pos), false);
displayFlow.getChildren().add(normalText);
}
}
private void addChunkedAddress(String address) {
String[] chunks = AddressLabelSkin.CHUNK_PATTERN.split(address);
for(int i = 0; i < chunks.length; i++) {
Text chunk = createText(chunks[i], i % 2 != 0);
displayFlow.getChildren().add(chunk);
}
}
private Text createText(String content, boolean alternate) {
Text text = new Text(content);
text.setFont(getSkinnable().getFont());
text.getStyleClass().add("address-chunk");
if(alternate) {
text.getStyleClass().add("alternate");
}
return text;
}
private List<AddressSpan> findAddresses(String text) {
List<AddressSpan> spans = new ArrayList<>();
Pattern wordBoundary = Pattern.compile("\\S+");
Matcher matcher = wordBoundary.matcher(text);
while(matcher.find()) {
String candidate = matcher.group();
if(isValidAddress(candidate)) {
spans.add(new AddressSpan(matcher.start(), matcher.end()));
}
}
return spans;
}
private boolean isValidAddress(String candidate) {
if(candidate == null || candidate.isEmpty()) {
return false;
}
Network network = Network.get();
// Base58 (legacy) partial: must start with a legacy prefix and contain only base58 chars.
if(network.hasP2PKHAddressPrefix(candidate) || network.hasP2SHAddressPrefix(candidate)) {
return containsOnlyAscii(candidate, BASE58_OK);
}
String lower = candidate.toLowerCase(Locale.ROOT);
// Bech32 (segwit v0/v1) partial: starts with HRP, then optional '1', then bech32 data charset.
if(lower.startsWith(network.getBech32AddressHRP())) {
return isBech32LikePartial(lower);
}
// Silent payments partial (bech32-like): starts with its HRP, then optional '1', then bech32 data charset.
if(lower.startsWith(network.getSilentPaymentsAddressHrp())) {
return isBech32LikePartial(lower);
}
return false;
}
private static boolean isBech32LikePartial(String lower) {
int sep = lower.indexOf(Bech32.BECH32_SEPARATOR);
if(sep < 0) {
return containsOnlyHrpChars(lower);
}
String hrp = lower.substring(0, sep);
String dataPart = lower.substring(sep + 1);
if(hrp.isEmpty()) {
return false;
}
return containsOnlyHrpChars(hrp) && containsOnlyAscii(dataPart, BECH32_DATA_OK);
}
private static boolean containsOnlyHrpChars(String s) {
for(int i = 0; i < s.length(); i++) {
char c = s.charAt(i);
boolean ok = (c >= 'a' && c <= 'z') || (c >= '0' && c <= '9');
if(!ok) {
return false;
}
}
return true;
}
private static boolean[] buildOkTable(String allowed) {
boolean[] ok = new boolean[128];
for(int i = 0; i < allowed.length(); i++) {
char c = allowed.charAt(i);
if(c < ok.length) {
ok[c] = true;
} else {
throw new IllegalArgumentException("Non-ASCII allowed char: " + c);
}
}
return ok;
}
private static boolean containsOnlyAscii(String s, boolean[] ok) {
for(int i = 0; i < s.length(); i++) {
char c = s.charAt(i);
if(c >= ok.length || !ok[c]) {
return false;
}
}
return true;
}
@Override
protected void layoutChildren(double x, double y, double w, double h) {
super.layoutChildren(x, y, w, h);
Insets padding = getSkinnable().getPadding();
double leftWidth = 0;
double rightWidth = 0;
if(getSkinnable() instanceof CustomTextField customTextField) {
Node left = customTextField.getLeft();
Node right = customTextField.getRight();
if(left != null) {
leftWidth = left.getLayoutBounds().getWidth();
if(left instanceof Region leftRegion) {
leftWidth += leftRegion.getPadding().getLeft() + leftRegion.getPadding().getRight() + 1;
}
}
if(right != null) {
rightWidth = right.getLayoutBounds().getWidth();
if(right instanceof Region rightRegion) {
rightWidth += rightRegion.getPadding().getLeft() + rightRegion.getPadding().getRight();
}
}
}
double availableWidth = w - padding.getLeft() - padding.getRight() - leftWidth - rightWidth;
clip.setWidth(availableWidth);
clip.setHeight(h);
double topOffset = getSkinnable().getBaselineOffset() - displayFlow.getBaselineOffset();
displayFlow.resizeRelocate(
padding.getLeft() + leftWidth,
topOffset,
displayFlow.prefWidth(-1),
h - padding.getTop() - padding.getBottom()
);
}
private record AddressSpan(int start, int end) {}
}

View File

@ -1,111 +0,0 @@
package com.sparrowwallet.sparrow.control;
import com.sparrowwallet.drongo.address.Address;
import com.sparrowwallet.drongo.address.InvalidAddressException;
import javafx.beans.value.ChangeListener;
import javafx.scene.Node;
import javafx.scene.control.*;
import javafx.scene.text.Text;
import javafx.scene.text.TextFlow;
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class AddressTooltipSkin implements Skin<Tooltip> {
private final Tooltip tooltip;
private final TextFlow textFlow;
private final ChangeListener<String> textListener;
public AddressTooltipSkin(Tooltip tooltip) {
this.tooltip = tooltip;
textFlow = new TextFlow();
textFlow.getStyleClass().addAll(tooltip.getStyleClass());
textListener = (_, _, newText) -> updateDisplay(newText);
tooltip.textProperty().addListener(textListener);
updateDisplay(tooltip.getText());
}
@Override
public Tooltip getSkinnable() {
return tooltip;
}
@Override
public Node getNode() {
return textFlow;
}
@Override
public void dispose() {
tooltip.textProperty().removeListener(textListener);
}
private void updateDisplay(String text) {
textFlow.getChildren().clear();
if(text == null || text.isEmpty()) {
return;
}
List<AddressSpan> addresses = findAddresses(text);
int pos = 0;
for(AddressSpan span : addresses) {
if(span.start > pos) {
textFlow.getChildren().add(createText(text.substring(pos, span.start), false));
}
addChunkedAddress(text.substring(span.start, span.end));
pos = span.end;
}
if(pos < text.length()) {
textFlow.getChildren().add(createText(text.substring(pos), false));
}
}
private void addChunkedAddress(String address) {
String[] chunks = AddressLabelSkin.CHUNK_PATTERN.split(address);
for(int i = 0; i < chunks.length; i++) {
textFlow.getChildren().add(createText(chunks[i], i % 2 != 0));
}
}
private Text createText(String content, boolean alternate) {
Text text = new Text(content);
text.getStyleClass().add("address-chunk");
if(alternate) {
text.getStyleClass().add("alternate");
}
return text;
}
private List<AddressSpan> findAddresses(String text) {
List<AddressSpan> spans = new ArrayList<>();
Pattern wordBoundary = Pattern.compile("\\S+");
Matcher matcher = wordBoundary.matcher(text);
while(matcher.find()) {
String candidate = matcher.group();
if(isValidAddress(candidate)) {
spans.add(new AddressSpan(matcher.start(), matcher.end()));
}
}
return spans;
}
private boolean isValidAddress(String candidate) {
try {
Address.fromString(candidate);
return true;
} catch(InvalidAddressException e) {
return false;
}
}
private record AddressSpan(int start, int end) {}
}

View File

@ -30,11 +30,7 @@ public class AddressTreeTable extends CoinTreeTable {
addressCol.setCellValueFactory((TreeTableColumn.CellDataFeatures<Entry, Entry> param) -> {
return new ReadOnlyObjectWrapper<>(param.getValue().getValue());
});
addressCol.setCellFactory(p -> {
EntryCell entryCell = new EntryCell();
entryCell.setSkin(new AddressTreeTableCellSkin<>(entryCell));
return entryCell;
});
addressCol.setCellFactory(p -> new EntryCell());
addressCol.setSortable(false);
getColumns().add(addressCol);
@ -80,9 +76,8 @@ public class AddressTreeTable extends CoinTreeTable {
contextMenu.getItems().add(showCountItem);
getColumns().forEach(col -> col.setContextMenu(contextMenu));
setPlaceholder(getDefaultPlaceholder(rootEntry.getWallet()));
setEditable(true);
setupColumnWidths();
setColumnResizePolicy(TreeTableView.CONSTRAINED_RESIZE_POLICY);
addressCol.setSortType(TreeTableColumn.SortType.ASCENDING);
getSortOrder().add(addressCol);

View File

@ -1,128 +0,0 @@
package com.sparrowwallet.sparrow.control;
import javafx.beans.value.ChangeListener;
import javafx.geometry.Insets;
import javafx.scene.control.TreeTableCell;
import javafx.scene.control.skin.TreeTableCellSkin;
import javafx.scene.layout.Region;
import javafx.scene.text.Text;
import javafx.scene.text.TextFlow;
public class AddressTreeTableCellSkin<S, T> extends TreeTableCellSkin<S, T> {
private final TextFlow displayFlow;
private final ChangeListener<String> textListener;
private final Text ellipsisText;
private String currentDisplayedText;
public AddressTreeTableCellSkin(TreeTableCell<S, T> cell) {
super(cell);
displayFlow = new TextFlow();
displayFlow.setManaged(false);
displayFlow.setMouseTransparent(true);
displayFlow.setMinWidth(Region.USE_PREF_SIZE);
getChildren().add(displayFlow);
ellipsisText = new Text("...");
ellipsisText.fontProperty().bind(cell.fontProperty());
ellipsisText.getStyleClass().add("address-chunk");
textListener = (_, _, newText) -> updateDisplay(newText);
cell.textProperty().addListener(textListener);
updateDisplay(cell.getText());
cell.setStyle("-fx-text-fill: transparent;");
}
private void updateDisplay(String text) {
currentDisplayedText = text;
buildDisplay(text, false);
}
private void buildDisplay(String text, boolean truncated) {
displayFlow.getChildren().clear();
if(text == null || text.isEmpty()) {
return;
}
if(getSkinnable().getStyleClass().contains("address-cell")) {
String[] chunks = AddressLabelSkin.CHUNK_PATTERN.split(text);
for(int i = 0; i < chunks.length; i++) {
displayFlow.getChildren().add(createText(chunks[i], i % 2 != 0));
}
} else {
Text normalText = createText(text, false);
displayFlow.getChildren().add(normalText);
}
if(truncated) {
displayFlow.getChildren().add(ellipsisText);
}
}
private Text createText(String content, boolean alternate) {
Text text = new Text(content);
text.fontProperty().bind(getSkinnable().fontProperty());
text.getStyleClass().add("address-chunk");
if(alternate) {
text.getStyleClass().add("alternate");
}
return text;
}
@Override
protected void layoutChildren(double x, double y, double w, double h) {
super.layoutChildren(x, y, w, h);
TreeTableCell<S, T> cell = getSkinnable();
Insets padding = cell.getPadding();
double leftOffset = 0;
double topOffset = y + padding.getTop();
Text labeledText = (Text)getChildren().stream().filter(n -> n instanceof Text).findFirst().orElse(null);
if(labeledText != null) {
leftOffset = labeledText.getLayoutX();
topOffset = labeledText.getLayoutY() - labeledText.getBaselineOffset();
String fullText = cell.getText();
String displayedText = labeledText.getText();
if(fullText != null && displayedText != null && !fullText.equals(displayedText)) {
String ellipsis = cell.getEllipsisString();
if(displayedText.endsWith(ellipsis)) {
String truncatedText = displayedText.substring(0, displayedText.length() - ellipsis.length());
if(!truncatedText.equals(currentDisplayedText)) {
currentDisplayedText = truncatedText;
buildDisplay(truncatedText, true);
}
}
} else if(fullText != null && !fullText.equals(currentDisplayedText)) {
currentDisplayedText = fullText;
buildDisplay(fullText, false);
}
}
displayFlow.resizeRelocate(
leftOffset,
topOffset,
w - padding.getLeft() - padding.getRight(),
h - padding.getTop() - padding.getBottom()
);
}
@Override
protected void updateChildren() {
super.updateChildren();
if(displayFlow != null && !getChildren().contains(displayFlow)) {
getChildren().add(displayFlow);
}
}
@Override
public void dispose() {
getSkinnable().textProperty().removeListener(textListener);
super.dispose();
}
}

View File

@ -128,15 +128,4 @@ public class BalanceChart extends LineChart<Number, Number> {
NumberAxis yaxis = (NumberAxis)getYAxis();
yaxis.setTickLabelFormatter(new CoinAxisFormatter(yaxis, format, unit));
}
public void refreshAxisLabels() {
NumberAxis yaxis = (NumberAxis)getYAxis();
// Force the axis to redraw by invalidating the upper and lower bounds
yaxis.setAutoRanging(false);
double lower = yaxis.getLowerBound();
double upper = yaxis.getUpperBound();
yaxis.setLowerBound(lower);
yaxis.setUpperBound(upper);
yaxis.setAutoRanging(true);
}
}

View File

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

View File

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

View File

@ -3,8 +3,6 @@ package com.sparrowwallet.sparrow.control;
import com.google.common.base.Throwables;
import com.sparrowwallet.drongo.KeyDerivation;
import com.sparrowwallet.drongo.crypto.ChildNumber;
import com.sparrowwallet.drongo.policy.PolicyType;
import com.sparrowwallet.drongo.protocol.ScriptType;
import com.sparrowwallet.drongo.protocol.Sha256Hash;
import com.sparrowwallet.drongo.wallet.*;
import com.sparrowwallet.sparrow.AppServices;
@ -45,24 +43,14 @@ public class CardImportPane extends TitledDescriptionPane {
private static final Logger log = LoggerFactory.getLogger(CardImportPane.class);
private final KeystoreCardImport importer;
private final PolicyType policyType;
private List<ChildNumber> derivation;
protected Button importButton;
private final SimpleStringProperty pin = new SimpleStringProperty("");
public CardImportPane(Wallet wallet, KeystoreCardImport importer, KeyDerivation defaultDerivation, KeyDerivation requiredDerivation) {
super(importer.getName(), "Place card on reader", importer.getKeystoreImportDescription(getAccount(wallet, requiredDerivation)), importer.getWalletModel());
public CardImportPane(Wallet wallet, KeystoreCardImport importer, KeyDerivation requiredDerivation) {
super(importer.getName(), "Place card on reader", importer.getKeystoreImportDescription(getAccount(wallet, requiredDerivation)), "image/" + importer.getWalletModel().getType() + ".png");
this.importer = importer;
this.policyType = wallet.getPolicyType();
this.derivation = requiredDerivation == null ? getDefaultDerivation(wallet, defaultDerivation) : requiredDerivation.getDerivation();
}
private static List<ChildNumber> getDefaultDerivation(Wallet wallet, KeyDerivation defaultDerivation) {
if(defaultDerivation != null && !defaultDerivation.getDerivation().isEmpty()) {
return defaultDerivation.getDerivation();
}
return wallet.getScriptType() == null ? ScriptType.P2WPKH.getDefaultDerivation() : wallet.getScriptType().getDefaultDerivation();
this.derivation = requiredDerivation == null ? wallet.getScriptType().getDefaultDerivation() : requiredDerivation.getDerivation();
}
@Override
@ -114,7 +102,7 @@ public class CardImportPane extends TitledDescriptionPane {
return;
}
CardImportService cardImportService = new CardImportService(importer, policyType, pin.get(), derivation, messageProperty);
CardImportService cardImportService = new CardImportService(importer, pin.get(), derivation, messageProperty);
cardImportService.setOnSucceeded(event -> {
EventManager.get().post(new KeystoreImportEvent(cardImportService.getValue()));
});
@ -355,14 +343,12 @@ public class CardImportPane extends TitledDescriptionPane {
public static class CardImportService extends Service<Keystore> {
private final KeystoreCardImport cardImport;
private final PolicyType policyType;
private final String pin;
private final List<ChildNumber> derivation;
private final StringProperty messageProperty;
public CardImportService(KeystoreCardImport cardImport, PolicyType policyType, String pin, List<ChildNumber> derivation, StringProperty messageProperty) {
public CardImportService(KeystoreCardImport cardImport, String pin, List<ChildNumber> derivation, StringProperty messageProperty) {
this.cardImport = cardImport;
this.policyType = policyType;
this.pin = pin;
this.derivation = derivation;
this.messageProperty = messageProperty;
@ -373,7 +359,7 @@ public class CardImportPane extends TitledDescriptionPane {
return new Task<>() {
@Override
protected Keystore call() throws Exception {
return cardImport.getKeystore(policyType, pin, derivation, messageProperty);
return cardImport.getKeystore(pin, derivation, messageProperty);
}
};
}

View File

@ -1,251 +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.Keystore;
import com.sparrowwallet.drongo.wallet.Wallet;
import com.sparrowwallet.sparrow.EventManager;
import com.sparrowwallet.sparrow.event.KeystoreImportEvent;
import com.sparrowwallet.sparrow.glyphfont.GlyphUtils;
import com.sparrowwallet.sparrow.io.ImportException;
import com.sparrowwallet.sparrow.io.KeystoreCodexImport;
import javafx.application.Platform;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.value.ChangeListener;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Node;
import javafx.scene.control.Button;
import javafx.scene.control.ContentDisplay;
import javafx.scene.control.Control;
import javafx.scene.control.Label;
import javafx.scene.control.MenuItem;
import javafx.scene.control.SplitMenuButton;
import javafx.scene.control.TextField;
import javafx.scene.control.Tooltip;
import javafx.scene.layout.AnchorPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority;
import javafx.scene.layout.VBox;
import org.controlsfx.validation.ValidationResult;
import org.controlsfx.validation.ValidationSupport;
import org.controlsfx.validation.Validator;
import org.controlsfx.validation.decoration.StyleClassValidationDecoration;
import java.util.List;
public class CodexKeystoreImportPane extends TitledDescriptionPane {
protected final Wallet wallet;
private final KeystoreCodexImport importer;
private final KeyDerivation defaultDerivation;
private SplitMenuButton importButton;
private Button enterCodexButton;
private Button calculateButton;
protected Label validLabel;
protected Label invalidLabel;
protected final SimpleStringProperty secretShareProperty = new SimpleStringProperty("");
public CodexKeystoreImportPane(Wallet wallet, KeystoreCodexImport importer, KeyDerivation defaultDerivation) {
super(importer.getName(), "Enter secret share", importer.getKeystoreImportDescription(), importer.getWalletModel());
this.wallet = wallet;
this.importer = importer;
this.defaultDerivation = defaultDerivation;
createImportButton();
buttonBox.getChildren().add(importButton);
}
@Override
protected Control createButton() {
enterCodexButton = new Button("Enter Secret Share");
enterCodexButton.managedProperty().bind(enterCodexButton.visibleProperty());
enterCodexButton.setOnAction(event -> {
enterCodex();
});
return enterCodexButton;
}
private void enterCodex() {
setDescription("Enter secret share");
showHideLink.setVisible(false);
setContent(getSecretShareEntry());
setExpanded(true);
}
private void importKeystore(List<ChildNumber> derivation) {
importButton.setDisable(true);
try {
Keystore keystore = importer.getKeystore(wallet.getPolicyType(), derivation, secretShareProperty.get());
EventManager.get().post(new KeystoreImportEvent(keystore));
} catch(ImportException e) {
String errorMessage = e.getMessage();
if(e.getCause() != null && e.getCause().getMessage() != null && !e.getCause().getMessage().isEmpty()) {
errorMessage = e.getCause().getMessage();
}
setError("Import Error", errorMessage);
importButton.setDisable(false);
}
}
private void createImportButton() {
importButton = new SplitMenuButton();
importButton.setAlignment(Pos.CENTER_RIGHT);
importButton.setText("Import Keystore");
setDefaultButton(importButton);
importButton.setOnAction(event -> {
importButton.setDisable(true);
importKeystore(getDefaultDerivation());
});
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);
});
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 onInputChange(boolean empty, boolean validChecksum) {
if(!empty) {
try {
importer.getKeystore(wallet.getPolicyType(), ScriptType.P2WPKH.getDefaultDerivation(), secretShareProperty.get());
validChecksum = true;
} catch(ImportException e) {
invalidLabel.setText("Invalid checksum");
invalidLabel.setTooltip(null);
}
}
calculateButton.setDisable(!validChecksum);
validLabel.setVisible(validChecksum);
invalidLabel.setVisible(!validChecksum && !empty);
}
private Node getSecretShareEntry() {
VBox vBox = new VBox(20);
vBox.setPadding(new Insets(10, 30, 10, 30));
HBox shareEntry = new HBox(10);
shareEntry.setAlignment(Pos.CENTER_LEFT);
Label shareLabel = new Label("Secret:");
TextField shareField = new TextField();
HBox.setHgrow(shareField, Priority.ALWAYS);
shareField.setPromptText("ms...");
shareField.textProperty().addListener((observable, oldValue, newValue) -> {
secretShareProperty.set(newValue);
});
shareEntry.getChildren().addAll(shareLabel, shareField);
vBox.getChildren().add(shareEntry);
AnchorPane buttonPane = new AnchorPane();
validLabel = new Label("Valid checksum", GlyphUtils.getSuccessGlyph());
validLabel.setContentDisplay(ContentDisplay.LEFT);
validLabel.setGraphicTextGap(5.0);
validLabel.managedProperty().bind(validLabel.visibleProperty());
validLabel.setVisible(false);
buttonPane.getChildren().add(validLabel);
AnchorPane.setTopAnchor(validLabel, 5.0);
AnchorPane.setLeftAnchor(validLabel, 0.0);
invalidLabel = new Label("Invalid checksum", GlyphUtils.getInvalidGlyph());
invalidLabel.setContentDisplay(ContentDisplay.LEFT);
invalidLabel.setGraphicTextGap(5.0);
invalidLabel.managedProperty().bind(invalidLabel.visibleProperty());
invalidLabel.setVisible(false);
buttonPane.getChildren().add(invalidLabel);
AnchorPane.setTopAnchor(invalidLabel, 5.0);
AnchorPane.setLeftAnchor(invalidLabel, 0.0);
secretShareProperty.addListener((ChangeListener<String>) (c, oldval, newval) -> {
boolean empty = secretShareProperty.isEmpty().get();
boolean validChecksum = false;
onInputChange(empty, validChecksum);
});
HBox rightBox = new HBox();
rightBox.setSpacing(10);
calculateButton = new Button("Create Keystore");
calculateButton.setDisable(true);
calculateButton.setDefaultButton(true);
calculateButton.managedProperty().bind(calculateButton.visibleProperty());
calculateButton.setTooltip(new Tooltip("Create the keystore from the provided secret share"));
calculateButton.setOnAction(event -> {
setExpanded(true);
enterCodexButton.setVisible(false);
importButton.setVisible(true);
importButton.setDisable(false);
setDescription("Ready to import");
showHideLink.setText("Show Derivation...");
showHideLink.setVisible(false);
setContent(getDerivationEntry(getDefaultDerivation()));
});
rightBox.getChildren().add(calculateButton);
buttonPane.getChildren().add(rightBox);
AnchorPane.setRightAnchor(rightBox, 0.0);
vBox.getChildren().add(buttonPane);
Platform.runLater(shareField::requestFocus);
return vBox;
}
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);
});
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;
}
}

View File

@ -2,7 +2,6 @@ package com.sparrowwallet.sparrow.control;
import com.sparrowwallet.drongo.BitcoinUnit;
import com.sparrowwallet.sparrow.UnitFormat;
import com.sparrowwallet.sparrow.io.Config;
import javafx.scene.chart.NumberAxis;
import javafx.util.StringConverter;
@ -19,10 +18,6 @@ final class CoinAxisFormatter extends StringConverter<Number> {
@Override
public String toString(Number object) {
if(Config.get().isHideAmounts()) {
return "";
}
Double value = bitcoinUnit.getValue(object.longValue());
return new CoinTextFormatter(unitFormat).getCoinFormat().format(value);
}

View File

@ -1,7 +1,6 @@
package com.sparrowwallet.sparrow.control;
import com.sparrowwallet.drongo.BitcoinUnit;
import com.sparrowwallet.drongo.OsType;
import com.sparrowwallet.drongo.protocol.Transaction;
import com.sparrowwallet.drongo.wallet.BlockTransactionHash;
import com.sparrowwallet.sparrow.UnitFormat;
@ -17,6 +16,7 @@ import javafx.scene.input.Clipboard;
import javafx.scene.input.ClipboardContent;
import javafx.scene.layout.Region;
import javafx.util.Duration;
import org.controlsfx.tools.Platform;
import java.text.DecimalFormat;
@ -32,7 +32,7 @@ class CoinCell extends TreeTableCell<Entry, Number> implements ConfirmationsList
tooltip.setShowDelay(Duration.millis(500));
contextMenu = new CoinContextMenu();
getStyleClass().add("coin-cell");
if(OsType.getCurrent() == OsType.MACOS) {
if(Platform.getCurrent() == Platform.OSX) {
getStyleClass().add("number-field");
}
}
@ -58,22 +58,16 @@ class CoinCell extends TreeTableCell<Entry, Number> implements ConfirmationsList
DecimalFormat decimalFormat = (amount.longValue() == 0L ? format.getBtcFormat() : format.getTableBtcFormat());
final String btcValue = decimalFormat.format(amount.doubleValue() / Transaction.SATOSHIS_PER_BITCOIN);
if(Config.get().isHideAmounts()) {
setText(CoinLabel.HIDDEN_AMOUNT_TEXT);
setTooltip(null);
setContextMenu(null);
if(unit.equals(BitcoinUnit.BTC)) {
tooltip.setValue(satsValue + " " + BitcoinUnit.SATOSHIS.getLabel());
setText(btcValue);
} else {
if(unit.equals(BitcoinUnit.BTC)) {
tooltip.setValue(satsValue + " " + BitcoinUnit.SATOSHIS.getLabel());
setText(btcValue);
} else {
tooltip.setValue(btcValue + " " + BitcoinUnit.BTC.getLabel());
setText(satsValue);
}
setTooltip(tooltip);
contextMenu.updateAmount(amount);
setContextMenu(contextMenu);
tooltip.setValue(btcValue + " " + BitcoinUnit.BTC.getLabel());
setText(satsValue);
}
setTooltip(tooltip);
contextMenu.updateAmount(amount);
setContextMenu(contextMenu);
if(entry instanceof TransactionEntry transactionEntry) {
tooltip.showConfirmations(transactionEntry.confirmationsProperty(), transactionEntry.isCoinbase());
@ -92,16 +86,14 @@ class CoinCell extends TreeTableCell<Entry, Number> implements ConfirmationsList
}
} else if(entry instanceof UtxoEntry) {
setGraphic(null);
} else if(entry instanceof HashIndexEntry hashIndexEntry) {
tooltip.hideConfirmations();
} else if(entry instanceof HashIndexEntry) {
Region node = new Region();
node.setPrefWidth(10);
setGraphic(node);
setContentDisplay(ContentDisplay.RIGHT);
if(hashIndexEntry.getType() == HashIndexEntry.Type.INPUT && !Config.get().isHideAmounts()) {
setText("-" + getText());
if(((HashIndexEntry) entry).getType() == HashIndexEntry.Type.INPUT) {
satsValue = "-" + satsValue;
}
} else {
setGraphic(null);
@ -156,14 +148,6 @@ class CoinCell extends TreeTableCell<Entry, Number> implements ConfirmationsList
setTooltipText();
}
public void hideConfirmations() {
showConfirmations = false;
isCoinbase = false;
confirmationsProperty.unbind();
setTooltipText();
}
private void setTooltipText() {
setText(value + (showConfirmations ? " (" + getConfirmationsDescription() + ")" : ""));
}

View File

@ -13,8 +13,6 @@ import javafx.scene.input.Clipboard;
import javafx.scene.input.ClipboardContent;
public class CoinLabel extends Label {
public static final String HIDDEN_AMOUNT_TEXT = "\u2022\u2022\u2022\u2022\u2022";
private final LongProperty valueProperty = new SimpleLongProperty(-1);
private final Tooltip tooltip;
private final CoinContextMenu contextMenu;
@ -51,13 +49,6 @@ public class CoinLabel extends Label {
}
private void setValueAsText(Long value, BitcoinUnit bitcoinUnit) {
if(Config.get().isHideAmounts()) {
setText(HIDDEN_AMOUNT_TEXT);
setTooltip(null);
setContextMenu(null);
return;
}
setTooltip(tooltip);
setContextMenu(contextMenu);

View File

@ -5,9 +5,9 @@ import javafx.scene.control.TextFormatter;
import javafx.scene.control.TextInputControl;
import java.text.DecimalFormat;
import java.text.DecimalFormatSymbols;
import java.text.ParseException;
import java.util.function.UnaryOperator;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class CoinTextFormatter extends TextFormatter<String> {
@ -51,14 +51,8 @@ public class CoinTextFormatter extends TextFormatter<String> {
commasRemoved = newText.length() - noFractionCommaText.length();
}
Matcher matcher = coinValidation.matcher(noFractionCommaText);
if(!matcher.matches()) {
matcher.reset();
if(matcher.find()) {
noFractionCommaText = matcher.group();
} else {
return null;
}
if(!coinValidation.matcher(noFractionCommaText).matches()) {
return null;
}
if(unitFormat.getGroupingSeparator().equals(change.getText())) {

View File

@ -1,58 +1,36 @@
package com.sparrowwallet.sparrow.control;
import com.sparrowwallet.drongo.BitcoinUnit;
import com.sparrowwallet.drongo.policy.PolicyType;
import com.sparrowwallet.drongo.wallet.SortDirection;
import com.sparrowwallet.drongo.wallet.TableType;
import com.sparrowwallet.drongo.wallet.Wallet;
import com.sparrowwallet.drongo.wallet.WalletTable;
import com.sparrowwallet.sparrow.CurrencyRate;
import com.sparrowwallet.sparrow.UnitFormat;
import com.sparrowwallet.sparrow.AppServices;
import com.sparrowwallet.sparrow.EventManager;
import com.sparrowwallet.sparrow.event.WalletTableChangedEvent;
import com.sparrowwallet.sparrow.event.WalletAddressesChangedEvent;
import com.sparrowwallet.sparrow.event.WalletDataChangedEvent;
import com.sparrowwallet.sparrow.event.WalletHistoryStatusEvent;
import com.sparrowwallet.sparrow.io.Config;
import com.sparrowwallet.sparrow.io.Storage;
import com.sparrowwallet.sparrow.net.ElectrumServer;
import com.sparrowwallet.sparrow.net.ServerType;
import com.sparrowwallet.sparrow.net.cormorant.Cormorant;
import com.sparrowwallet.sparrow.net.cormorant.bitcoind.BitcoindClient;
import com.sparrowwallet.sparrow.wallet.Entry;
import io.reactivex.Observable;
import io.reactivex.subjects.PublishSubject;
import javafx.application.Platform;
import javafx.collections.ListChangeListener;
import javafx.geometry.Pos;
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 java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Optional;
import java.util.concurrent.TimeUnit;
public class CoinTreeTable extends TreeTableView<Entry> {
private TableType tableType;
private BitcoinUnit bitcoinUnit;
private UnitFormat unitFormat;
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() {
return bitcoinUnit;
@ -110,7 +88,7 @@ public class CoinTreeTable extends TreeTableView<Entry> {
setPlaceholder(new Label("Error loading transactions: " + event.getErrorMessage()));
} else if(event.isLoading()) {
if(event.getStatusMessage() != null) {
setPlaceholder(new Label(event.getStatusMessage() + (event.getStatusMessage().contains("...") ? "" : "...")));
setPlaceholder(new Label(event.getStatusMessage() + "..."));
} else {
setPlaceholder(new Label("Loading transactions..."));
}
@ -126,7 +104,7 @@ public class CoinTreeTable extends TreeTableView<Entry> {
StackPane stackPane = new StackPane();
stackPane.getChildren().add(AppServices.isConnecting() ? new Label("Loading transactions...") : new Label("No transactions"));
if((Config.get().getServerType() == ServerType.BITCOIN_CORE || wallet.getPolicyType() == PolicyType.SINGLE_SP) && !AppServices.isConnecting() && !isFullyScanned(wallet)) {
if(Config.get().getServerType() == ServerType.BITCOIN_CORE && !AppServices.isConnecting()) {
Hyperlink hyperlink = new Hyperlink();
hyperlink.setTranslateY(30);
hyperlink.setOnAction(event -> {
@ -137,7 +115,6 @@ public class CoinTreeTable extends TreeTableView<Entry> {
Storage storage = AppServices.get().getOpenWallets().get(wallet);
Wallet pastWallet = wallet.copy();
wallet.setBirthDate(optDate.get());
wallet.setBirthHeight(null);
//Trigger background save of birthdate
EventManager.get().post(new WalletDataChangedEvent(wallet));
//Trigger full wallet rescan
@ -153,146 +130,17 @@ public class CoinTreeTable extends TreeTableView<Entry> {
}
stackPane.getChildren().add(hyperlink);
} else if(!AppServices.isConnecting() && Config.get().getServerType() == ServerType.BITCOIN_CORE && isFullyScanned(wallet)) {
Date prunedDate = getPrunedDate();
if(prunedDate != null) {
DateFormat dateFormat = new SimpleDateFormat(DateStringConverter.FORMAT_PATTERN);
Label prunedLabel = new Label("Scanned to pruned start date of " + dateFormat.format(prunedDate));
prunedLabel.setTranslateY(30);
stackPane.getChildren().add(prunedLabel);
}
}
stackPane.setAlignment(Pos.CENTER);
return stackPane;
}
private boolean isFullyScanned(Wallet wallet) {
if(wallet.getPolicyType() == PolicyType.SINGLE_SP) {
return wallet.isValid() && ElectrumServer.isSilentPaymentsFullyCovered(wallet.getSilentPaymentScanAddress());
}
if(Config.get().getServerType() == ServerType.BITCOIN_CORE) {
Date prunedDate = getPrunedDate();
return prunedDate != null && wallet.getBirthDate() != null && !wallet.getBirthDate().after(prunedDate);
}
return false;
}
private static Date getPrunedDate() {
Cormorant cormorant = ElectrumServer.getCormorant();
if(cormorant == null) {
return null;
}
BitcoindClient bitcoindClient = cormorant.getBitcoindClient();
if(bitcoindClient == null || !bitcoindClient.isPruned()) {
return null;
}
return bitcoindClient.getCachedPrunedDate();
}
protected void setupColumnSort(int defaultColumnIndex, TreeTableColumn.SortType defaultSortType) {
WalletTable.Sort columnSort = getSavedColumnSort();
if(columnSort == null) {
columnSort = new WalletTable.Sort(defaultColumnIndex, getSortDirection(defaultSortType));
}
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);
public void setSortColumn(int columnIndex, TreeTableColumn.SortType sortType) {
if(columnIndex >= 0 && columnIndex < getColumns().size() && getSortOrder().isEmpty() && !getRoot().getChildren().isEmpty()) {
TreeTableColumn<Entry, ?> column = getColumns().get(columnIndex);
column.setSortType(sortType == null ? TreeTableColumn.SortType.DESCENDING : sortType);
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;
}
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);
}
setColumnResizePolicy(TreeTableView.CONSTRAINED_RESIZE_POLICY_FLEX_LAST_COLUMN);
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;
}
}

View File

@ -6,16 +6,10 @@ import javafx.beans.property.SimpleObjectProperty;
import javafx.scene.Cursor;
import javafx.scene.Node;
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.StackPane;
import org.controlsfx.control.textfield.CustomTextField;
import java.util.List;
public class ComboBoxTextField extends CustomTextField {
private final ObjectProperty<ComboBox<?>> comboProperty = new SimpleObjectProperty<>();
@ -74,53 +68,4 @@ public class ComboBoxTextField extends CustomTextField {
public void setComboProperty(ComboBox<?> comboProperty) {
this.comboProperty.set(comboProperty);
}
public ContextMenu getCustomContextMenu(List<MenuItem> customItems) {
return new CustomContextMenu(customItems);
}
public class CustomContextMenu extends ContextMenu {
public CustomContextMenu(List<MenuItem> customItems) {
super();
setFont(null);
MenuItem undo = new MenuItem("Undo");
undo.setOnAction(_ -> undo());
MenuItem redo = new MenuItem("Redo");
redo.setOnAction(_ -> redo());
MenuItem cut = new MenuItem("Cut");
cut.setOnAction(_ -> cut());
MenuItem copy = new MenuItem("Copy");
copy.setOnAction(_ -> copy());
MenuItem paste = new MenuItem("Paste");
paste.setOnAction(_ -> paste());
MenuItem delete = new MenuItem("Delete");
delete.setOnAction(_ -> deleteText(getSelection()));
MenuItem selectAll = new MenuItem("Select All");
selectAll.setOnAction(_ -> selectAll());
getItems().addAll(undo, redo, new SeparatorMenuItem(), cut, copy, paste, delete, new SeparatorMenuItem(), selectAll);
getItems().addAll(customItems);
setOnShowing(_ -> {
boolean hasSelection = getSelection().getLength() > 0;
boolean hasText = getText() != null && !getText().isEmpty();
boolean clipboardHasContent = Clipboard.getSystemClipboard().hasString();
undo.setDisable(!isUndoable());
redo.setDisable(!isRedoable());
cut.setDisable(!isEditable() || !hasSelection);
copy.setDisable(!hasSelection);
paste.setDisable(!isEditable() || !clipboardHasContent);
delete.setDisable(!hasSelection);
selectAll.setDisable(!hasText);
});
}
}
}

View File

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

View File

@ -10,7 +10,8 @@ import javafx.scene.control.MenuItem;
import javafx.scene.control.Tooltip;
import javafx.scene.input.Clipboard;
import javafx.scene.input.ClipboardContent;
import javafx.scene.input.MouseButton;
import javafx.event.EventHandler;
import javafx.scene.input.MouseEvent;
public class CopyableCoinLabel extends CopyableLabel {
private final LongProperty valueProperty = new SimpleLongProperty(-1);
@ -28,10 +29,6 @@ public class CopyableCoinLabel extends CopyableLabel {
valueProperty().addListener((observable, oldValue, newValue) -> setValueAsText((Long)newValue, Config.get().getUnitFormat(), Config.get().getBitcoinUnit()));
setOnMouseClicked(event -> {
if(!event.getButton().equals(MouseButton.PRIMARY)) {
return;
}
if(bitcoinUnit == null) {
bitcoinUnit = Config.get().getBitcoinUnit();
}
@ -70,13 +67,6 @@ public class CopyableCoinLabel extends CopyableLabel {
}
private void setValueAsText(Long value, UnitFormat unitFormat, BitcoinUnit bitcoinUnit) {
if(Config.get().isHideAmounts()) {
setText(CoinLabel.HIDDEN_AMOUNT_TEXT);
setTooltip(null);
setContextMenu(null);
return;
}
setTooltip(tooltip);
setContextMenu(contextMenu);

View File

@ -10,7 +10,6 @@ import javafx.beans.value.ChangeListener;
import javafx.event.EventHandler;
import javafx.scene.Cursor;
import javafx.scene.Node;
import javafx.scene.control.ContextMenu;
import javafx.scene.control.Tooltip;
import javafx.scene.input.Clipboard;
import javafx.scene.input.ClipboardContent;
@ -53,7 +52,6 @@ public class CopyableTextField extends CustomTextField {
selectedTextProperty().removeListener(selectionListener);
}
});
setContextMenu(new ContextMenu());
}
private void setupCopyButtonField(ObjectProperty<Node> rightProperty) {

View File

@ -11,7 +11,7 @@ import java.util.Date;
public class DateAxisFormatter extends StringConverter<Number> {
private static final DateFormat HOUR_FORMAT = new SimpleDateFormat("HH:mm");
private static final DateFormat DAY_FORMAT = new SimpleDateFormat("d MMM");
private static final DateFormat MONTH_FORMAT = new SimpleDateFormat("MMM yyyy");
private static final DateFormat MONTH_FORMAT = new SimpleDateFormat("MMM yy");
private final DateFormat dateFormat;
private int oddCounter;

View File

@ -14,7 +14,8 @@ import org.fxmisc.richtext.CodeArea;
import java.util.List;
import static com.sparrowwallet.drongo.policy.PolicyType.*;
import static com.sparrowwallet.drongo.policy.PolicyType.MULTI;
import static com.sparrowwallet.drongo.policy.PolicyType.SINGLE;
import static com.sparrowwallet.drongo.protocol.ScriptType.MULTISIG;
public class DescriptorArea extends CodeArea {
@ -32,13 +33,13 @@ public class DescriptorArea extends CodeArea {
List<Keystore> keystores = wallet.getKeystores();
int threshold = wallet.getDefaultPolicy().getNumSignaturesRequired();
if(SINGLE_HD.equals(policyType)) {
if(SINGLE.equals(policyType)) {
append(scriptType.getDescriptor(), "descriptor-text");
replace(getLength(), getLength(), keystores.get(0).getScriptName(), List.of(keystores.get(0).isValid() ? "descriptor-text" : "descriptor-error", keystores.get(0).getScriptName()));
append(scriptType.getCloseDescriptor(), "descriptor-text");
}
if(MULTI_HD.equals(policyType)) {
if(MULTI.equals(policyType)) {
append(scriptType.getDescriptor(), "descriptor-text");
append(MULTISIG.getDescriptor(), "descriptor-text");
append(Integer.toString(threshold), "descriptor-text");
@ -51,12 +52,6 @@ public class DescriptorArea extends CodeArea {
append(MULTISIG.getCloseDescriptor(), "descriptor-text");
append(scriptType.getCloseDescriptor(), "descriptor-text");
}
if(SINGLE_SP.equals(policyType)) {
append("sp(", "descriptor-text");
replace(getLength(), getLength(), keystores.get(0).getScriptName(), List.of(keystores.get(0).isValid() ? "descriptor-text" : "descriptor-error", keystores.get(0).getScriptName()));
append(")", "descriptor-text");
}
}
public Wallet getWallet() {

View File

@ -3,14 +3,13 @@ package com.sparrowwallet.sparrow.control;
import com.sparrowwallet.hummingbird.UR;
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
import com.sparrowwallet.sparrow.io.PdfUtils;
import com.sparrowwallet.sparrow.io.bbqr.BBQR;
import javafx.event.ActionEvent;
import javafx.scene.control.*;
import javafx.scene.control.Button;
public class DescriptorQRDisplayDialog extends QRDisplayDialog {
public DescriptorQRDisplayDialog(String walletName, String outputDescriptor, UR ur, BBQR bbqr, QREncoding encoding) {
super(ur, bbqr, false, false, encoding);
public DescriptorQRDisplayDialog(String walletName, String outputDescriptor, UR ur) {
super(ur);
DialogPane dialogPane = getDialogPane();
final ButtonType pdfButtonType = new javafx.scene.control.ButtonType("Save PDF...", ButtonBar.ButtonData.HELP_2);
@ -20,13 +19,8 @@ public class DescriptorQRDisplayDialog extends QRDisplayDialog {
pdfButton.setGraphicTextGap(5);
pdfButton.setGraphic(getGlyph(FontAwesome5.Glyph.FILE_PDF));
pdfButton.addEventFilter(ActionEvent.ACTION, event -> {
PdfUtils.saveOutputDescriptor(walletName, outputDescriptor, ur, getEncoding() == QREncoding.BBQR ? bbqr : null);
PdfUtils.saveOutputDescriptor(walletName, outputDescriptor, ur);
event.consume();
});
ButtonBar buttonBar = (ButtonBar)dialogPane.lookup(".button-bar");
if(buttonBar != null) {
buttonBar.setButtonOrder("E+L+B+C+O");
}
}
}

View File

@ -12,15 +12,16 @@ import com.sparrowwallet.drongo.policy.PolicyType;
import com.sparrowwallet.drongo.protocol.ScriptType;
import com.sparrowwallet.drongo.protocol.Sha256Hash;
import com.sparrowwallet.drongo.psbt.PSBT;
import com.sparrowwallet.drongo.silentpayments.SilentPaymentScanAddress;
import com.sparrowwallet.drongo.wallet.*;
import com.sparrowwallet.sparrow.AppServices;
import com.sparrowwallet.sparrow.EventManager;
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.io.CardAuthorizationException;
import com.sparrowwallet.sparrow.net.ElectrumServer;
import com.sparrowwallet.sparrow.net.ServerType;
import javafx.application.Platform;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
@ -53,8 +54,7 @@ public class DevicePane extends TitledDescriptionPane {
private final Wallet wallet;
private final PSBT psbt;
private final OutputDescriptor outputDescriptor;
private final KeyDerivation defaultDerivation;
private final KeyDerivation requiredDerivation;
private final KeyDerivation keyDerivation;
private final String message;
private final List<StandardAccount> availableAccounts;
private final Device device;
@ -77,14 +77,13 @@ public class DevicePane extends TitledDescriptionPane {
private boolean defaultDevice;
public DevicePane(Wallet wallet, Device device, boolean defaultDevice, KeyDerivation defaultDerivation, KeyDerivation requiredDerivation) {
super(device.getModel().toDisplayString(), "", "", device.getModel());
public DevicePane(Wallet wallet, Device device, boolean defaultDevice, KeyDerivation requiredDerivation) {
super(device.getModel().toDisplayString(), "", "", "image/" + device.getType() + ".png");
this.deviceOperation = DeviceOperation.IMPORT;
this.wallet = wallet;
this.psbt = null;
this.outputDescriptor = null;
this.defaultDerivation = defaultDerivation;
this.requiredDerivation = requiredDerivation;
this.keyDerivation = requiredDerivation;
this.message = null;
this.availableAccounts = null;
this.device = device;
@ -106,13 +105,12 @@ public class DevicePane extends TitledDescriptionPane {
}
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.wallet = wallet;
this.psbt = psbt;
this.outputDescriptor = null;
this.defaultDerivation = null;
this.requiredDerivation = null;
this.keyDerivation = null;
this.message = null;
this.availableAccounts = null;
this.device = device;
@ -134,13 +132,12 @@ public class DevicePane extends TitledDescriptionPane {
}
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.wallet = wallet;
this.psbt = null;
this.outputDescriptor = outputDescriptor;
this.defaultDerivation = null;
this.requiredDerivation = null;
this.keyDerivation = null;
this.message = null;
this.availableAccounts = null;
this.device = device;
@ -157,14 +154,13 @@ public class DevicePane extends TitledDescriptionPane {
buttonBox.getChildren().addAll(setPassphraseButton, displayAddressButton);
}
public DevicePane(Wallet wallet, String message, KeyDerivation requiredDerivation, Device device, boolean defaultDevice) {
super(device.getModel().toDisplayString(), "", "", device.getModel());
public DevicePane(Wallet wallet, String message, KeyDerivation keyDerivation, Device device, boolean defaultDevice) {
super(device.getModel().toDisplayString(), "", "", "image/" + device.getType() + ".png");
this.deviceOperation = DeviceOperation.SIGN_MESSAGE;
this.wallet = wallet;
this.psbt = null;
this.outputDescriptor = null;
this.defaultDerivation = requiredDerivation;
this.requiredDerivation = requiredDerivation;
this.keyDerivation = keyDerivation;
this.message = message;
this.availableAccounts = null;
this.device = device;
@ -186,13 +182,12 @@ public class DevicePane extends TitledDescriptionPane {
}
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.wallet = wallet;
this.psbt = null;
this.outputDescriptor = null;
this.defaultDerivation = null;
this.requiredDerivation = null;
this.keyDerivation = null;
this.message = null;
this.device = device;
this.defaultDevice = defaultDevice;
@ -210,13 +205,12 @@ public class DevicePane extends TitledDescriptionPane {
}
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.wallet = null;
this.psbt = null;
this.outputDescriptor = null;
this.defaultDerivation = null;
this.requiredDerivation = null;
this.keyDerivation = null;
this.message = null;
this.device = device;
this.defaultDevice = defaultDevice;
@ -295,50 +289,27 @@ public class DevicePane extends TitledDescriptionPane {
}
private void createImportButton() {
importButton = requiredDerivation == null ? new SplitMenuButton() : new Button();
importButton = keyDerivation == null ? new SplitMenuButton() : new Button();
importButton.setAlignment(Pos.CENTER_RIGHT);
importButton.setText("Import Keystore");
importButton.setOnAction(event -> {
importButton.setDisable(true);
importKeystore(requiredDerivation == null ? getDefaultDerivation() : requiredDerivation.getDerivation());
List<ChildNumber> defaultDerivation = wallet.getScriptType() == null ? ScriptType.P2WPKH.getDefaultDerivation() : wallet.getScriptType().getDefaultDerivation();
importKeystore(keyDerivation == null ? defaultDerivation : keyDerivation.getDerivation());
});
if(importButton instanceof SplitMenuButton importMenuButton) {
if(wallet.getScriptType() == null) {
if(wallet.getPolicyType() == null) {
List<PolicyAndScriptType> types = new ArrayList<>();
for(PolicyType policyType : List.of(PolicyType.SINGLE_HD, PolicyType.SINGLE_SP)) {
for(ScriptType scriptType : ScriptType.getAddressableScriptTypes(policyType)) {
types.add(new PolicyAndScriptType(policyType, scriptType));
}
}
for(PolicyAndScriptType type : types) {
MenuItem item = new MenuItem(type.getDescription());
final List<ChildNumber> derivation = type.scriptType().getDefaultDerivation();
item.setOnAction(event -> {
importMenuButton.setDisable(true);
wallet.setPolicyType(type.policyType());
importKeystore(derivation);
});
importMenuButton.getItems().add(item);
}
} else {
List<ScriptType> scriptTypes = ScriptType.getScriptTypesForPolicyType(wallet.getPolicyType());
for(ScriptType scriptType : scriptTypes) {
MenuItem item = new MenuItem(scriptType.getDescription());
final List<ChildNumber> derivation = scriptType.getDefaultDerivation();
item.setOnAction(event -> {
importMenuButton.setDisable(true);
importKeystore(derivation);
});
importMenuButton.getItems().add(item);
}
ScriptType[] scriptTypes = new ScriptType[] {ScriptType.P2WPKH, ScriptType.P2SH_P2WPKH, ScriptType.P2PKH, ScriptType.P2TR};
for(ScriptType scriptType : scriptTypes) {
MenuItem item = new MenuItem(scriptType.getDescription());
final List<ChildNumber> derivation = scriptType.getDefaultDerivation();
item.setOnAction(event -> {
importMenuButton.setDisable(true);
importKeystore(derivation);
});
importMenuButton.getItems().add(item);
}
importMenuButton.getItems().add(new SeparatorMenuItem());
MenuItem discoverItem = new MenuItem("Discover Wallet...");
discoverItem.setDisable(!AppServices.isConnected());
discoverItem.setOnAction(_ -> discoverWallet());
importMenuButton.getItems().add(discoverItem);
} else {
String[] accounts = new String[] {"Default Account #0", "Account #1", "Account #2", "Account #3", "Account #4", "Account #5", "Account #6", "Account #7", "Account #8", "Account #9"};
int scriptAccountsLength = ScriptType.P2SH.equals(wallet.getScriptType()) ? 1 : accounts.length;
@ -395,7 +366,7 @@ public class DevicePane extends TitledDescriptionPane {
signMessageButton.managedProperty().bind(signMessageButton.visibleProperty());
signMessageButton.setVisible(false);
if(device.getFingerprint() != null && !device.getFingerprint().equals(requiredDerivation.getMasterFingerprint())) {
if(device.getFingerprint() != null && !device.getFingerprint().equals(keyDerivation.getMasterFingerprint())) {
signMessageButton.setDisable(true);
}
}
@ -404,6 +375,7 @@ public class DevicePane extends TitledDescriptionPane {
discoverKeystoresButton = new Button("Discover");
discoverKeystoresButton.setAlignment(Pos.CENTER_RIGHT);
discoverKeystoresButton.setOnAction(event -> {
discoverKeystoresButton.setDisable(true);
discoverKeystores();
});
discoverKeystoresButton.managedProperty().bind(discoverKeystoresButton.visibleProperty());
@ -464,14 +436,6 @@ public class DevicePane extends TitledDescriptionPane {
getAddressButton.setVisible(false);
}
private List<ChildNumber> getDefaultDerivation() {
if(defaultDerivation != null && !defaultDerivation.getDerivation().isEmpty()) {
return defaultDerivation.getDerivation();
}
return wallet == null || wallet.getScriptType() == null ? ScriptType.P2WPKH.getDefaultDerivation() : wallet.getScriptType().getDefaultDerivation();
}
private void unlock(Device device) {
if(device.getModel().requiresPinPrompt()) {
promptPin();
@ -492,26 +456,20 @@ public class DevicePane extends TitledDescriptionPane {
});
vBox.getChildren().addAll(pinField, enterPinButton);
GridPane gridPane = new GridPane();
gridPane.setHgap(10);
gridPane.setVgap(10);
gridPane.setMaxWidth(150);
gridPane.setMaxHeight(device.getModel().hasZeroInPin() ? 160 : 120);
TilePane tilePane = new TilePane();
tilePane.setPrefColumns(3);
tilePane.setHgap(10);
tilePane.setVgap(10);
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++) {
Button pinButton = new Button();
Glyph circle = new Glyph(FontAwesome5.FONT_NAME, "CIRCLE");
pinButton.setGraphic(circle);
pinButton.setUserData(digits[i]);
GridPane.setRowIndex(pinButton, i / 3);
GridPane.setColumnIndex(pinButton, i % 3);
if((i / 3) == 3) {
GridPane.setHgrow(pinButton, Priority.ALWAYS);
GridPane.setColumnSpan(pinButton, 3);
pinButton.setMaxWidth(Double.MAX_VALUE);
}
gridPane.getChildren().add(pinButton);
tilePane.getChildren().add(pinButton);
pinButton.setOnAction(event -> {
pinField.setText(pinField.getText() + pinButton.getUserData());
});
@ -519,7 +477,7 @@ public class DevicePane extends TitledDescriptionPane {
HBox contentBox = new HBox();
contentBox.setSpacing(50);
contentBox.getChildren().add(gridPane);
contentBox.getChildren().add(tilePane);
contentBox.getChildren().add(vBox);
contentBox.setPadding(new Insets(10, 0, 10, 0));
contentBox.setAlignment(Pos.TOP_CENTER);
@ -539,7 +497,7 @@ public class DevicePane extends TitledDescriptionPane {
SplitMenuButton sendPassphraseButton = new SplitMenuButton();
sendPassphraseButton.setText("Send Passphrase");
setDefaultButton(sendPassphraseButton);
sendPassphraseButton.getStyleClass().add("default-button");
sendPassphraseButton.setOnAction(event -> {
setExpanded(false);
setDescription("Confirm passphrase on device...");
@ -731,7 +689,7 @@ public class DevicePane extends TitledDescriptionPane {
return;
}
Service<Keystore> importService = cardApi.getImportService(wallet.getPolicyType(), derivation, messageProperty);
Service<Keystore> importService = cardApi.getImportService(derivation, messageProperty);
handleCardOperation(importService, importButton, "Import", true, event -> {
importKeystore(derivation, importService.getValue());
});
@ -750,21 +708,13 @@ public class DevicePane extends TitledDescriptionPane {
}
}
importKey(derivation);
importXpub(derivation);
});
enumerateService.setOnFailed(workerStateEvent -> {
setError("Error", enumerateService.getException().getMessage());
importButton.setDisable(false);
});
enumerateService.start();
} else {
importKey(derivation);
}
}
private void importKey(List<ChildNumber> derivation) {
if(wallet != null && wallet.getPolicyType() == PolicyType.SINGLE_SP) {
importSpscan(derivation);
} else {
importXpub(derivation);
}
@ -775,7 +725,7 @@ public class DevicePane extends TitledDescriptionPane {
Hwi.GetXpubService getXpubService = new Hwi.GetXpubService(device, passphrase.get(), derivationPath);
getXpubService.setOnSucceeded(workerStateEvent -> {
ExtendedKey xpub = getXpubService.getValue();
String xpub = getXpubService.getValue();
try {
Keystore keystore = new Keystore();
@ -783,7 +733,7 @@ public class DevicePane extends TitledDescriptionPane {
keystore.setSource(KeystoreSource.HW_USB);
keystore.setWalletModel(device.getModel());
keystore.setKeyDerivation(new KeyDerivation(device.getFingerprint(), derivationPath));
keystore.setExtendedPublicKey(xpub);
keystore.setExtendedPublicKey(ExtendedKey.fromDescriptor(xpub));
importKeystore(derivation, keystore);
} catch(Exception e) {
@ -799,44 +749,14 @@ public class DevicePane extends TitledDescriptionPane {
getXpubService.start();
}
private void importSpscan(List<ChildNumber> derivation) {
String derivationPath = KeyDerivation.writePath(derivation);
Hwi.GetSpscanService getSpscanService = new Hwi.GetSpscanService(device, passphrase.get(), derivationPath);
getSpscanService.setOnSucceeded(workerStateEvent -> {
SilentPaymentScanAddress spscan = getSpscanService.getValue();
try {
Keystore keystore = new Keystore();
keystore.setLabel(device.getModel().toDisplayString());
keystore.setSource(KeystoreSource.HW_USB);
keystore.setWalletModel(device.getModel());
keystore.setKeyDerivation(new KeyDerivation(device.getFingerprint(), derivationPath));
keystore.setSilentPaymentScanAddress(spscan);
importKeystore(derivation, keystore);
} catch(Exception e) {
setError("Could not retrieve spscan", e.getMessage());
}
});
getSpscanService.setOnFailed(workerStateEvent -> {
setError("Could not retrieve spscan", getSpscanService.getException().getMessage());
importButton.setDisable(false);
});
setDescription("Importing...");
showHideLink.setVisible(false);
getSpscanService.start();
}
private void importKeystore(List<ChildNumber> derivation, Keystore keystore) {
if(wallet.getScriptType() == null) {
ScriptType scriptType = Arrays.stream(ScriptType.ADDRESSABLE_TYPES).filter(type -> type.getDefaultDerivation().getFirst().equals(derivation.getFirst())).findFirst().orElse(ScriptType.P2PKH);
PolicyType policyType = wallet.getPolicyType() != null ? wallet.getPolicyType() : PolicyType.SINGLE_HD;
ScriptType scriptType = Arrays.stream(ScriptType.ADDRESSABLE_TYPES).filter(type -> type.getDefaultDerivation().get(0).equals(derivation.get(0))).findFirst().orElse(ScriptType.P2PKH);
wallet.setName(device.getModel().toDisplayString());
wallet.setPolicyType(policyType);
wallet.setPolicyType(PolicyType.SINGLE);
wallet.setScriptType(scriptType);
wallet.getKeystores().add(keystore);
wallet.setDefaultPolicy(Policy.getPolicy(policyType, scriptType, wallet.getKeystores(), null));
wallet.setDefaultPolicy(Policy.getPolicy(PolicyType.SINGLE, scriptType, wallet.getKeystores(), null));
EventManager.get().post(new WalletImportEvent(wallet));
} else {
@ -858,12 +778,10 @@ public class DevicePane extends TitledDescriptionPane {
signButton.setDisable(false);
}
} else {
Hwi.SignPSBTService signPSBTService = new Hwi.SignPSBTService(device, passphrase.get(), psbt,
OutputDescriptor.getOutputDescriptor(wallet), wallet.getFullName(), getDeviceRegistration());
Hwi.SignPSBTService signPSBTService = new Hwi.SignPSBTService(device, passphrase.get(), psbt);
signPSBTService.setOnSucceeded(workerStateEvent -> {
PSBT signedPsbt = signPSBTService.getValue();
EventManager.get().post(new PSBTSignedEvent(psbt, signedPsbt));
updateDeviceRegistrations(signPSBTService.getNewDeviceRegistrations());
});
signPSBTService.setOnFailed(workerStateEvent -> {
setError("Signing Error", signPSBTService.getException().getMessage());
@ -902,12 +820,10 @@ public class DevicePane extends TitledDescriptionPane {
}
private void displayAddress() {
Hwi.DisplayAddressService displayAddressService = new Hwi.DisplayAddressService(device, passphrase.get(), wallet.getScriptType(), outputDescriptor,
OutputDescriptor.getOutputDescriptor(wallet), wallet.getFullName(), getDeviceRegistration());
Hwi.DisplayAddressService displayAddressService = new Hwi.DisplayAddressService(device, passphrase.get(), wallet.getScriptType(), outputDescriptor);
displayAddressService.setOnSucceeded(successEvent -> {
String address = displayAddressService.getValue();
EventManager.get().post(new AddressDisplayedEvent(address));
updateDeviceRegistrations(displayAddressService.getNewDeviceRegistrations());
});
displayAddressService.setOnFailed(failedEvent -> {
setError("Could not display address", displayAddressService.getException().getMessage());
@ -917,31 +833,11 @@ public class DevicePane extends TitledDescriptionPane {
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() {
if(device.isCard()) {
try {
CardApi cardApi = CardApi.getCardApi(device.getModel(), pin.get());
Service<String> signMessageService = cardApi.getSignMessageService(message, wallet.getScriptType(), requiredDerivation.getDerivation(), messageProperty);
Service<String> signMessageService = cardApi.getSignMessageService(message, wallet.getScriptType(), keyDerivation.getDerivation(), messageProperty);
handleCardOperation(signMessageService, signMessageButton, "Signing", true, event -> {
String signature = signMessageService.getValue();
EventManager.get().post(new MessageSignedEvent(wallet, signature));
@ -952,7 +848,7 @@ public class DevicePane extends TitledDescriptionPane {
signButton.setDisable(false);
}
} else {
Hwi.SignMessageService signMessageService = new Hwi.SignMessageService(device, passphrase.get(), message, requiredDerivation.getDerivationPath());
Hwi.SignMessageService signMessageService = new Hwi.SignMessageService(device, passphrase.get(), message, keyDerivation.getDerivationPath());
signMessageService.setOnSucceeded(successEvent -> {
String signature = signMessageService.getValue();
EventManager.get().post(new MessageSignedEvent(wallet, signature));
@ -966,141 +862,37 @@ public class DevicePane extends TitledDescriptionPane {
}
}
private void discoverWallet() {
importButton.setDisable(true);
importButton.setMaxHeight(importButton.getHeight());
ProgressIndicator progressIndicator = new ProgressIndicator(0);
progressIndicator.getStyleClass().add("button-progress");
importButton.setGraphic(progressIndicator);
List<Wallet> wallets = new ArrayList<>();
RangeInputDialog rangeInputDialog = new RangeInputDialog(StandardAccount.ACCOUNT_0.getAccountNumber(), StandardAccount.ACCOUNT_30.getAccountNumber(), StandardAccount.ACCOUNT_10.getAccountNumber());
rangeInputDialog.setTitle("Choose number of accounts");
rangeInputDialog.setHeaderText("Enter the number of additional accounts to scan for existing funds.\n\nThis may take a few minutes depending on how many accounts are selected.");
Optional<Integer> optRange = rangeInputDialog.showAndWait();
if(optRange.isEmpty()) {
return;
}
List<StandardAccount> discoveryAccounts = new ArrayList<>(Arrays.asList(StandardAccount.values()).subList(0, optRange.get() + 1));
Map<Hwi.WalletType, String> derivationPaths = new LinkedHashMap<>();
List<ScriptType> scriptTypes = new ArrayList<>(ScriptType.getAddressableScriptTypes(PolicyType.SINGLE_HD));
if(device.getModel() == WalletModel.BITBOX_02) {
scriptTypes.remove(ScriptType.P2PKH);
}
for(ScriptType scriptType : scriptTypes) {
for(StandardAccount discoveryAccount : discoveryAccounts) {
derivationPaths.put(new Hwi.WalletType(scriptType, discoveryAccount), KeyDerivation.writePath(scriptType.getDefaultDerivation(discoveryAccount.getAccountNumber())));
}
}
Hwi.GetXpubsService getXpubsService = new Hwi.GetXpubsService(device, passphrase.get(), derivationPaths);
getXpubsService.setOnSucceeded(_ -> {
Map<Hwi.WalletType, ExtendedKey> accountXpubs = getXpubsService.getValue();
for(Map.Entry<Hwi.WalletType, ExtendedKey> entry : accountXpubs.entrySet()) {
try {
Wallet wallet = new Wallet(device.getModel().toDisplayString());
wallet.setPolicyType(PolicyType.SINGLE_HD);
wallet.setScriptType(entry.getKey().scriptType());
Keystore keystore = new Keystore();
keystore.setLabel(device.getModel().toDisplayString());
keystore.setSource(KeystoreSource.HW_USB);
keystore.setWalletModel(device.getModel());
keystore.setKeyDerivation(new KeyDerivation(device.getFingerprint(), derivationPaths.get(entry.getKey())));
keystore.setExtendedPublicKey(entry.getValue());
wallet.getKeystores().add(keystore);
wallet.setDefaultPolicy(Policy.getPolicy(PolicyType.SINGLE_HD, entry.getKey().scriptType(), wallet.getKeystores(), 1));
if(entry.getKey().standardAccount().equals(StandardAccount.ACCOUNT_0)) {
wallets.add(wallet);
} else {
Wallet masterWallet = wallets.getLast();
wallet.setName(entry.getKey().standardAccount().getName());
wallet.setMasterWallet(masterWallet);
masterWallet.getChildWallets().add(wallet);
}
} catch(Exception e) {
setError("Could not retrieve xpub", e.getMessage());
}
}
ElectrumServer.WalletDiscoveryService walletDiscoveryService = new ElectrumServer.WalletDiscoveryService(wallets);
walletDiscoveryService.setOnSucceeded(_ -> {
importButton.setGraphic(null);
Optional<List<Wallet>> optWallets = walletDiscoveryService.getValue();
if(optWallets.isPresent()) {
List<Wallet> discoveredWallets = optWallets.get();
if(discoveredWallets.size() > 1) {
for(Wallet wallet : discoveredWallets) {
wallet.setName(wallet.getName() + " " + wallet.getScriptType().getDescription());
}
}
EventManager.get().post(new WalletImportEvent(discoveredWallets));
} else {
AppServices.showErrorDialog("No existing wallet found",
Config.get().getServerType() == ServerType.BITCOIN_CORE ? "The configured server type is Bitcoin Core, which does not support wallet discovery.\n\n" +
"You can however import the " + device.getModel().toDisplayString() + " and scan the blockchain by supplying a start date." :
"Could not find an HD wallet with existing transactions using the " + device.getModel().toDisplayString() + ".");
setDefaultStatus();
importButton.setDisable(false);
}
});
walletDiscoveryService.setOnFailed(failedEvent -> {
log.error("Failed to discover wallets", failedEvent.getSource().getException());
setError("Failed to discover wallets", failedEvent.getSource().getException().getMessage());
importButton.setGraphic(null);
importButton.setDisable(false);
});
walletDiscoveryService.start();
});
getXpubsService.setOnFailed(_ -> {
setError("Could not retrieve xpub", getXpubsService.getException().getMessage());
importButton.setGraphic(null);
importButton.setDisable(false);
});
progressIndicator.progressProperty().bind(getXpubsService.progressProperty());
getXpubsService.progressProperty().addListener((_, _, newValue) -> setDescription("Discovering... (" + Math.round(newValue.doubleValue() * 100) + "%)"));
showHideLink.setVisible(false);
getXpubsService.start();
}
private void discoverKeystores() {
if(wallet.getKeystores().size() != 1) {
setError("Could not discover keystores", "Only single signature wallets are supported for keystore discovery");
return;
}
discoverKeystoresButton.setDisable(true);
discoverKeystoresButton.setMaxHeight(discoverKeystoresButton.getHeight());
ProgressIndicator progressIndicator = new ProgressIndicator(0);
progressIndicator.getStyleClass().add("button-progress");
discoverKeystoresButton.setGraphic(progressIndicator);
String masterFingerprint = wallet.getKeystores().get(0).getKeyDerivation().getMasterFingerprint();
Wallet copyWallet = wallet.copy();
Map<Hwi.WalletType, String> accountDerivationPaths = new LinkedHashMap<>();
Map<StandardAccount, String> accountDerivationPaths = new LinkedHashMap<>();
for(StandardAccount availableAccount : availableAccounts) {
Wallet availableWallet = copyWallet.addChildWallet(availableAccount);
Keystore availableKeystore = availableWallet.getKeystores().get(0);
String derivationPath = availableKeystore.getKeyDerivation().getDerivationPath();
accountDerivationPaths.put(new Hwi.WalletType(wallet.getScriptType(), availableAccount), derivationPath);
accountDerivationPaths.put(availableAccount, derivationPath);
}
Map<StandardAccount, Keystore> importedKeystores = new LinkedHashMap<>();
Hwi.GetXpubsService getXpubsService = new Hwi.GetXpubsService(device, passphrase.get(), accountDerivationPaths);
getXpubsService.setOnSucceeded(workerStateEvent -> {
Map<Hwi.WalletType, ExtendedKey> accountXpubs = getXpubsService.getValue();
Map<StandardAccount, String> accountXpubs = getXpubsService.getValue();
for(Map.Entry<Hwi.WalletType, ExtendedKey> entry : accountXpubs.entrySet()) {
for(Map.Entry<StandardAccount, String> entry : accountXpubs.entrySet()) {
try {
Keystore keystore = new Keystore();
keystore.setLabel(device.getModel().toDisplayString());
keystore.setSource(KeystoreSource.HW_USB);
keystore.setWalletModel(device.getModel());
keystore.setKeyDerivation(new KeyDerivation(masterFingerprint, accountDerivationPaths.get(entry.getKey())));
keystore.setExtendedPublicKey(entry.getValue());
importedKeystores.put(entry.getKey().standardAccount(), keystore);
keystore.setExtendedPublicKey(ExtendedKey.fromDescriptor(entry.getValue()));
importedKeystores.put(entry.getKey(), keystore);
} catch(Exception e) {
setError("Could not retrieve xpub", e.getMessage());
}
@ -1114,18 +906,15 @@ public class DevicePane extends TitledDescriptionPane {
accountDiscoveryService.setOnFailed(event -> {
log.error("Failed to discover accounts", event.getSource().getException());
setError("Failed to discover accounts", event.getSource().getException().getMessage());
discoverKeystoresButton.setGraphic(null);
discoverKeystoresButton.setDisable(false);
});
accountDiscoveryService.start();
});
getXpubsService.setOnFailed(workerStateEvent -> {
setError("Could not retrieve xpub", getXpubsService.getException().getMessage());
discoverKeystoresButton.setGraphic(null);
discoverKeystoresButton.setDisable(false);
});
progressIndicator.progressProperty().bind(getXpubsService.progressProperty());
getXpubsService.progressProperty().addListener((_, _, newValue) -> setDescription("Discovering... (" + Math.round(newValue.doubleValue() * 100) + "%)"));
setDescription("Discovering...");
showHideLink.setVisible(false);
getXpubsService.start();
}
@ -1182,12 +971,13 @@ public class DevicePane extends TitledDescriptionPane {
private void showOperationButton() {
if(deviceOperation.equals(DeviceOperation.IMPORT)) {
if(defaultDevice) {
setDefaultButton(importButton);
importButton.getStyleClass().add("default-button");
}
importButton.setVisible(true);
showHideLink.setText("Show derivation...");
showHideLink.setVisible(!device.isCard());
setContent(getDerivationEntry(requiredDerivation == null ? getDefaultDerivation() : requiredDerivation.getDerivation()));
List<ChildNumber> defaultDerivation = wallet.getScriptType() == null ? ScriptType.P2WPKH.getDefaultDerivation() : wallet.getScriptType().getDefaultDerivation();
setContent(getDerivationEntry(keyDerivation == null ? defaultDerivation : keyDerivation.getDerivation()));
} else if(deviceOperation.equals(DeviceOperation.SIGN)) {
signButton.setDefaultButton(defaultDevice);
signButton.setVisible(true);
@ -1206,7 +996,7 @@ public class DevicePane extends TitledDescriptionPane {
showHideLink.setVisible(false);
} else if(deviceOperation.equals(DeviceOperation.GET_PRIVATE_KEY)) {
if(defaultDevice) {
setDefaultButton(getPrivateKeyButton);
getPrivateKeyButton.getStyleClass().add("default-button");
}
getPrivateKeyButton.setVisible(true);
showHideLink.setVisible(false);
@ -1221,7 +1011,7 @@ public class DevicePane extends TitledDescriptionPane {
TextField derivationField = new TextField();
derivationField.setPromptText("Derivation path");
derivationField.setText(KeyDerivation.writePath(derivation));
derivationField.setDisable(device.isCard() || requiredDerivation != null);
derivationField.setDisable(device.isCard() || keyDerivation != null);
HBox.setHgrow(derivationField, Priority.ALWAYS);
ValidationSupport validationSupport = new ValidationSupport();
@ -1237,7 +1027,7 @@ public class DevicePane extends TitledDescriptionPane {
showHideLink.setVisible(true);
setExpanded(false);
List<ChildNumber> importDerivation = KeyDerivation.parsePath(derivationField.getText());
importKey(importDerivation);
importXpub(importDerivation);
});
derivationField.textProperty().addListener((observable, oldValue, newValue) -> {
@ -1413,10 +1203,4 @@ public class DevicePane extends TitledDescriptionPane {
public enum DeviceOperation {
IMPORT, SIGN, DISPLAY_ADDRESS, SIGN_MESSAGE, DISCOVER_KEYSTORES, GET_PRIVATE_KEY, GET_ADDRESS;
}
protected record PolicyAndScriptType(PolicyType policyType, ScriptType scriptType) {
public String getDescription() {
return scriptType.getDescription() + (policyType == PolicyType.SINGLE_SP ? " SP" : " HD");
}
}
}

View File

@ -1,86 +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() {
getStyleClass().add("dialog-image");
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;
}
}

View File

@ -1,6 +1,5 @@
package com.sparrowwallet.sparrow.control;
import com.sparrowwallet.drongo.OsType;
import com.sparrowwallet.drongo.Utils;
import com.sparrowwallet.drongo.pgp.PGPKeySource;
import com.sparrowwallet.drongo.pgp.PGPUtils;
@ -25,6 +24,7 @@ import javafx.scene.input.TransferMode;
import javafx.scene.layout.*;
import javafx.stage.FileChooser;
import javafx.stage.Stage;
import org.controlsfx.tools.Platform;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
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> PUBLIC_KEY_EXTENSIONS = List.of("asc");
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> 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 String SPARROW_RELEASE_PREFIX = "sparrow-";
private static final String[] SPARROW_RELEASE_ALT_PREFIXES = { "sparrowwallet-", "sparrowwallet_", "sparrowserver-", "sparrowserver_" };
private static final String SPARROW_MANIFEST_SUFFIX = "-manifest.txt";
private static final String SPARROW_SIGNATURE_SUFFIX = SPARROW_MANIFEST_SUFFIX + ".asc";
private static final String SPARROW_SIGNATURE_SUFFIX = "-manifest.txt.asc";
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;
@ -72,7 +70,6 @@ public class DownloadVerifierDialog extends Dialog<ButtonBar.ButtonData> {
private final ObjectProperty<File> manifest = new SimpleObjectProperty<>();
private final ObjectProperty<File> publicKey = 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();
@ -84,7 +81,7 @@ public class DownloadVerifierDialog extends Dialog<ButtonBar.ButtonData> {
private static File lastFileParent;
public DownloadVerifierDialog(File initialFile) {
public DownloadVerifierDialog(File initialSignatureFile) {
final DialogPane dialogPane = getDialogPane();
dialogPane.getStylesheets().add(AppServices.class.getResource("general.css").toExternalForm());
dialogPane.getStylesheets().add(AppServices.class.getResource("dialog.css").toExternalForm());
@ -226,17 +223,11 @@ public class DownloadVerifierDialog extends Dialog<ButtonBar.ButtonData> {
});
release.addListener((observable, oldValue, releaseFile) -> {
if(releaseFile != null) {
initial.set(null);
}
verify();
});
if(initialFile != null) {
javafx.application.Platform.runLater(() -> {
initial.set(initialFile);
signature.set(initialFile);
});
if(initialSignatureFile != null) {
javafx.application.Platform.runLater(() -> signature.set(initialSignatureFile));
}
}
@ -301,7 +292,7 @@ public class DownloadVerifierDialog extends Dialog<ButtonBar.ButtonData> {
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.setGraphic(GlyphUtils.getSuccessGlyph());
@ -464,8 +455,7 @@ public class DownloadVerifierDialog extends Dialog<ButtonBar.ButtonData> {
}
}
String providedName = providedFile.getName().toLowerCase(Locale.ROOT);
if(providedName.startsWith(SPARROW_RELEASE_PREFIX) || Arrays.stream(SPARROW_RELEASE_ALT_PREFIXES).anyMatch(providedName::startsWith)) {
if(providedFile.getName().toLowerCase(Locale.ROOT).startsWith(SPARROW_RELEASE_PREFIX)) {
Matcher matcher = SPARROW_RELEASE_VERSION.matcher(providedFile.getName());
if(matcher.find()) {
String version = matcher.group();
@ -492,22 +482,6 @@ public class DownloadVerifierDialog extends Dialog<ButtonBar.ButtonData> {
}
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<List<String>> extensionLists = List.of(releaseExtensions, DISK_IMAGE_EXTENSIONS, ARCHIVE_EXTENSIONS, List.of(""));
@ -526,9 +500,9 @@ public class DownloadVerifierDialog extends Dialog<ButtonBar.ButtonData> {
}
private List<String> getReleaseFileExtensions() {
OsType osType = OsType.getCurrent();
switch(osType) {
case MACOS -> {
Platform platform = Platform.getCurrent();
switch(platform) {
case OSX -> {
return MACOS_RELEASE_EXTENSIONS;
}
case WINDOWS -> {
@ -541,10 +515,10 @@ public class DownloadVerifierDialog extends Dialog<ButtonBar.ButtonData> {
}
private String getReleaseFileExample(String version) {
OsType osType = OsType.getCurrent();
Platform platform = Platform.getCurrent();
String arch = System.getProperty("os.arch");
switch(osType) {
case MACOS -> {
switch(platform) {
case OSX -> {
return "Sparrow-" + version + "-" + arch;
}
case WINDOWS -> {
@ -591,8 +565,7 @@ public class DownloadVerifierDialog extends Dialog<ButtonBar.ButtonData> {
}
}
if((name.startsWith(SPARROW_RELEASE_PREFIX) || Arrays.stream(SPARROW_RELEASE_ALT_PREFIXES).anyMatch(name::startsWith))
&& 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);
return matcher.find();
}
@ -601,18 +574,10 @@ public class DownloadVerifierDialog extends Dialog<ButtonBar.ButtonData> {
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) {
signature.set(signatureFile);
}
public void setInitialFile(File initialFile) {
initial.set(initialFile);
}
private static class Header extends GridPane {
public Header() {
setMaxWidth(Double.MAX_VALUE);
@ -633,8 +598,15 @@ public class DownloadVerifierDialog extends Dialog<ButtonBar.ButtonData> {
vBox.getChildren().addAll(headerLabel, descriptionLabel);
add(vBox, 0, 0);
StackPane graphicContainer = new DialogImage(DialogImage.Type.SPARROW);
StackPane graphicContainer = new StackPane();
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);
ColumnConstraints textColumn = new ColumnConstraints();

View File

@ -1,13 +1,9 @@
package com.sparrowwallet.sparrow.control;
import com.sparrowwallet.drongo.KeyPurpose;
import com.sparrowwallet.drongo.OsType;
import com.sparrowwallet.drongo.Utils;
import com.sparrowwallet.drongo.address.Address;
import com.sparrowwallet.drongo.policy.PolicyType;
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.sparrow.AppServices;
import com.sparrowwallet.sparrow.EventManager;
@ -58,7 +54,7 @@ public class EntryCell extends TreeTableCell<Entry, Entry> implements Confirmati
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)
if(this == lastCell && !getTableRow().isVisible() && isTableSizeRecalculation()) {
if(this == lastCell && !getTableRow().isVisible()) {
return;
}
lastCell = this;
@ -69,7 +65,8 @@ public class EntryCell extends TreeTableCell<Entry, Entry> implements Confirmati
setText(null);
setGraphic(null);
} else {
if(entry instanceof TransactionEntry transactionEntry) {
if(entry instanceof TransactionEntry) {
TransactionEntry transactionEntry = (TransactionEntry)entry;
if(transactionEntry.getBlockTransaction().getHeight() == -1) {
setText("Unconfirmed Parent");
setContextMenu(new UnconfirmedTransactionContextMenu(transactionEntry));
@ -103,7 +100,7 @@ public class EntryCell extends TreeTableCell<Entry, Entry> implements Confirmati
actionBox.getChildren().add(viewTransactionButton);
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())) {
Button increaseFeeButton = new Button("");
increaseFeeButton.setGraphic(getIncreaseFeeRBFGlyph());
@ -123,20 +120,21 @@ public class EntryCell extends TreeTableCell<Entry, Entry> implements Confirmati
}
setGraphic(actionBox);
} else if(entry instanceof NodeEntry nodeEntry) {
} else if(entry instanceof NodeEntry) {
NodeEntry nodeEntry = (NodeEntry)entry;
Address address = nodeEntry.getAddress();
getStyleClass().add("address-cell");
setText(address.toString());
setContextMenu(new AddressContextMenu(address, nodeEntry.getOutputDescriptor(), nodeEntry, true, getTreeTableView()));
Tooltip tooltip = new Tooltip();
tooltip.setShowDelay(Duration.millis(250));
tooltip.setText(nodeEntry.getNode().toString());
setTooltip(tooltip);
getStyleClass().add("address-cell");
HBox actionBox = new HBox();
actionBox.getStyleClass().add("cell-actions");
if(!nodeEntry.getNode().getWallet().isBip47() && nodeEntry.getNode().getWallet().getPolicyType() != PolicyType.SINGLE_SP) {
if(!nodeEntry.getNode().getWallet().isBip47()) {
Button receiveButton = new Button("");
receiveButton.setGraphic(getReceiveGlyph());
receiveButton.setOnAction(event -> {
@ -164,7 +162,8 @@ public class EntryCell extends TreeTableCell<Entry, Entry> implements Confirmati
setContextMenu(null);
setGraphic(new HBox());
}
} else if(entry instanceof HashIndexEntry hashIndexEntry) {
} else if(entry instanceof HashIndexEntry) {
HashIndexEntry hashIndexEntry = (HashIndexEntry)entry;
setText(hashIndexEntry.getDescription());
setContextMenu(getTreeTableView().getStyleClass().contains("bip47") ? null : new HashIndexEntryContextMenu(getTreeTableView(), hashIndexEntry));
Tooltip tooltip = new Tooltip();
@ -212,14 +211,13 @@ public class EntryCell extends TreeTableCell<Entry, Entry> implements Confirmati
private static void increaseFee(TransactionEntry transactionEntry, boolean cancelTransaction) {
BlockTransaction blockTransaction = transactionEntry.getBlockTransaction();
boolean silentPaymentTransaction = transactionEntry.getWallet().isSilentPaymentsTransaction(blockTransaction);
Map<BlockTransactionHashIndex, WalletNode> walletTxos = transactionEntry.getWallet().getWalletTxos();
List<BlockTransactionHashIndex> utxos = transactionEntry.getChildren().stream()
.filter(e -> e instanceof HashIndexEntry)
.map(e -> (HashIndexEntry)e)
.filter(e -> e.getType().equals(HashIndexEntry.Type.INPUT) && e.isSpendable())
.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())
.collect(Collectors.toList());
@ -244,13 +242,12 @@ public class EntryCell extends TreeTableCell<Entry, Entry> implements Confirmati
.collect(Collectors.toList());
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();
Transaction tx = blockTransaction.getTransaction();
double vSize = tx.getVirtualSize();
if(changeTotal == 0) {
//Add change output length to vSize if change was not present on the original transaction
TransactionOutput changeOutput = new TransactionOutput(new Transaction(), 1L, transactionEntry.getWallet().getNode(KeyPurpose.CHANGE).getOutputScript());
TransactionOutput changeOutput = new TransactionOutput(new Transaction(), 1L, transactionEntry.getWallet().getFreshNode(KeyPurpose.CHANGE).getOutputScript());
vSize += changeOutput.getLength();
}
double inputSize = tx.getInputs().get(0).getLength() + (tx.getInputs().get(0).hasWitness() ? (double)tx.getInputs().get(0).getWitness().getLength() / Transaction.WITNESS_SCALE_FACTOR : 0);
@ -259,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())
.stream().filter(outputGroup -> outputGroup.getEffectiveValue() >= 0).collect(Collectors.toList());
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
OutputGroup outputGroup = outputGroups.remove(0);
for(BlockTransactionHashIndex utxo : outputGroup.getUtxos()) {
@ -300,13 +297,9 @@ public class EntryCell extends TreeTableCell<Entry, Entry> implements Confirmati
label += " (Replaced By Fee)";
}
Address address = txOutput.getScript().getToAddress();
if(address != null) {
long value = txOutput.getValue();
if(txOutput.getScript().getToAddress() != null) {
//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;
SilentPaymentAddress silentPaymentAddress = transactionEntry.getWallet().getSilentPaymentAddress(address);
return silentPaymentAddress == null ? new Payment(address, label, value, sendMax) : new SilentPayment(silentPaymentAddress, label, value, sendMax);
return new Payment(txOutput.getScript().getToAddress(), label, txOutput.getValue(), blockTransaction.getTransaction().getOutputs().size() == 1 && rbfChange == 0);
}
return null;
@ -335,17 +328,15 @@ public class EntryCell extends TreeTableCell<Entry, Entry> implements Confirmati
if(cancelTransaction) {
Payment existing = payments.get(0);
Payment payment = transactionEntry.getWallet().getPolicyType() == PolicyType.SINGLE_SP ?
new SilentPayment(transactionEntry.getWallet().getSilentPaymentScanAddress().getChangeAddress().getSilentPaymentAddress(),
existing.getLabel(), existing.getAmount(), true) :
new Payment(transactionEntry.getWallet().getFreshNode(KeyPurpose.CHANGE).getAddress(), existing.getLabel(), existing.getAmount(), true);
Address address = transactionEntry.getWallet().getFreshNode(KeyPurpose.CHANGE).getAddress();
Payment payment = new Payment(address, existing.getLabel(), existing.getAmount(), true);
payments.clear();
payments.add(payment);
opReturns.clear();
}
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() {
@ -372,10 +363,10 @@ public class EntryCell extends TreeTableCell<Entry, Entry> implements Confirmati
}
BlockTransactionHashIndex cpfpUtxo = ourOutputs.get(0);
Address receiveAddress = transactionEntry.getWallet().getNode(KeyPurpose.RECEIVE).getAddress();
TransactionOutput txOutput = new TransactionOutput(new Transaction(), cpfpUtxo.getValue(), receiveAddress.getOutputScript());
long dustThreshold = receiveAddress.getScriptType().getDustThreshold(txOutput, Transaction.DUST_RELAY_TX_FEE);
double inputSize = receiveAddress.getScriptType().getInputVbytes();
Address freshAddress = transactionEntry.getWallet().getFreshNode(KeyPurpose.RECEIVE).getAddress();
TransactionOutput txOutput = new TransactionOutput(new Transaction(), cpfpUtxo.getValue(), freshAddress.getOutputScript());
long dustThreshold = freshAddress.getScriptType().getDustThreshold(txOutput, Transaction.DUST_RELAY_TX_FEE);
double inputSize = freshAddress.getScriptType().getInputVbytes();
double vSize = inputSize + txOutput.getLength();
List<TxoFilter> txoFilters = List.of(new ExcludeTxoFilter(List.of(cpfpUtxo)), new SpentTxoFilter(), new FrozenTxoFilter(), new CoinbaseTxoFilter(transactionEntry.getWallet()));
@ -399,23 +390,19 @@ public class EntryCell extends TreeTableCell<Entry, Entry> implements Confirmati
String label = transactionEntry.getLabel() == null ? "" : transactionEntry.getLabel();
label += (label.isEmpty() ? "" : " ") + "(CPFP)";
Payment payment = transactionEntry.getWallet().getPolicyType() == PolicyType.SINGLE_SP ?
new SilentPayment(transactionEntry.getWallet().getSilentPaymentScanAddress().getChangeAddress().getSilentPaymentAddress(),
label, inputTotal, true) :
new Payment(transactionEntry.getWallet().getFreshNode(KeyPurpose.CHANGE).getAddress(), label, inputTotal, true);
Payment payment = new Payment(freshAddress, label, inputTotal, true);
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) {
return Config.get().isMempoolFullRbf() || blockTransaction.getTransaction().isReplaceByFee() || wallet.isSilentPaymentsTransaction(blockTransaction);
private static boolean canRBF(BlockTransaction blockTransaction) {
return Config.get().isMempoolFullRbf() || blockTransaction.getTransaction().isReplaceByFee();
}
private static boolean canSignMessage(WalletNode walletNode) {
Wallet wallet = walletNode.getWallet();
PolicyType policyType = wallet.getPolicyType();
return (policyType == PolicyType.SINGLE_HD || policyType == PolicyType.SINGLE_SP) && (!wallet.isBip47() || walletNode.getKeyPurpose() == KeyPurpose.RECEIVE);
return wallet.getKeystores().size() == 1 && (!wallet.isBip47() || walletNode.getKeyPurpose() == KeyPurpose.RECEIVE);
}
private static boolean containsWalletOutputs(TransactionEntry transactionEntry) {
@ -472,7 +459,7 @@ public class EntryCell extends TreeTableCell<Entry, Entry> implements Confirmati
Double feeRate = transactionEntry.getBlockTransaction().getFeeRate();
Long vSizefromTip = transactionEntry.getVSizeFromTip();
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";
if(vSizefromTip > 1000 * 1000) {
@ -488,7 +475,7 @@ public class EntryCell extends TreeTableCell<Entry, Entry> implements Confirmati
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;
@ -556,7 +543,6 @@ public class EntryCell extends TreeTableCell<Entry, Entry> implements Confirmati
private static class UnconfirmedTransactionContextMenu extends ContextMenu {
public UnconfirmedTransactionContextMenu(TransactionEntry transactionEntry) {
Wallet wallet = transactionEntry.getWallet();
BlockTransaction blockTransaction = transactionEntry.getBlockTransaction();
MenuItem viewTransaction = new MenuItem("View Transaction");
viewTransaction.setGraphic(getViewTransactionGlyph());
@ -566,7 +552,7 @@ public class EntryCell extends TreeTableCell<Entry, Entry> implements Confirmati
});
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)");
increaseFee.setGraphic(getIncreaseFeeRBFGlyph());
increaseFee.setOnAction(AE -> {
@ -577,7 +563,7 @@ public class EntryCell extends TreeTableCell<Entry, Entry> implements Confirmati
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)");
cancelTx.setGraphic(getCancelTransactionRBFGlyph());
cancelTx.setOnAction(AE -> {
@ -620,7 +606,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) {
MenuItem viewTransaction = new MenuItem("View Transaction");
viewTransaction.setGraphic(getViewTransactionGlyph());
@ -670,7 +656,7 @@ public class EntryCell extends TreeTableCell<Entry, Entry> implements Confirmati
public static class AddressContextMenu extends ContextMenu {
public AddressContextMenu(Address address, String outputDescriptor, NodeEntry nodeEntry, boolean addUtxoItems, TreeTableView<Entry> treetable) {
if(nodeEntry == null || (!nodeEntry.getWallet().isBip47() && nodeEntry.getWallet().getPolicyType() != PolicyType.SINGLE_SP)) {
if(nodeEntry == null || !nodeEntry.getWallet().isBip47()) {
MenuItem receiveToAddress = new MenuItem("Receive To");
receiveToAddress.setGraphic(getReceiveGlyph());
receiveToAddress.setOnAction(event -> {
@ -819,17 +805,18 @@ public class EntryCell extends TreeTableCell<Entry, Entry> implements Confirmati
cell.getStyleClass().remove("utxo-row");
cell.getStyleClass().remove("unconfirmed-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("confirming");
cell.getStyleClass().remove("negative-amount");
cell.getStyleClass().remove("spent");
cell.getStyleClass().remove("unspendable");
cell.getStyleClass().remove("number-field");
if(entry != null) {
if(entry instanceof TransactionEntry transactionEntry) {
if(entry instanceof TransactionEntry) {
cell.getStyleClass().add("transaction-row");
TransactionEntry transactionEntry = (TransactionEntry)entry;
if(cell instanceof ConfirmationsListener confirmationsListener) {
if(transactionEntry.isConfirming()) {
cell.getStyleClass().add("confirming");
@ -838,36 +825,25 @@ public class EntryCell extends TreeTableCell<Entry, Entry> implements Confirmati
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) {
cell.getStyleClass().add("node-row");
} else if(entry instanceof UtxoEntry utxoEntry) {
} else if(entry instanceof UtxoEntry) {
cell.getStyleClass().add("utxo-row");
UtxoEntry utxoEntry = (UtxoEntry)entry;
if(!utxoEntry.isSpendable()) {
cell.getStyleClass().add("unspendable");
}
if(OsType.getCurrent() == OsType.MACOS && utxoEntry.getHashIndex().getHeight() > 0 && !addressCell && !cell.getStyleClass().contains("label-cell")) {
cell.getStyleClass().add("number-field");
}
} else if(entry instanceof HashIndexEntry hashIndexEntry) {
} else if(entry instanceof HashIndexEntry) {
cell.getStyleClass().add("hashindex-row");
HashIndexEntry hashIndexEntry = (HashIndexEntry)entry;
if(hashIndexEntry.isSpent()) {
cell.getStyleClass().add("spent");
}
} else if(entry instanceof WalletSummaryDialog.UnconfirmedEntry) {
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");
}
}
}
private boolean isTableSizeRecalculation() {
//As per https://bugs.openjdk.org/browse/JDK-8265669 we check for cell visibility to avoid unnecessary recalculation, but this can result in false positives
//The method releaseCell in VirtualFlow is responsible for setting accumCell visibility to false after use, so check this method is calling updateItem
return StackWalker.getInstance().walk(frames -> frames.anyMatch(frame -> frame.getClassName().equals("javafx.scene.control.skin.VirtualFlow")
&& frame.getMethodName().equals("releaseCell")));
}
}

View File

@ -1,6 +1,5 @@
package com.sparrowwallet.sparrow.control;
import com.sparrowwallet.drongo.protocol.Transaction;
import com.sparrowwallet.sparrow.AppServices;
import com.sparrowwallet.sparrow.net.FeeRatesSource;
import javafx.application.Platform;
@ -8,19 +7,14 @@ import javafx.scene.Node;
import javafx.scene.control.Slider;
import javafx.util.StringConverter;
import java.text.DecimalFormat;
import java.util.*;
import java.util.stream.Collectors;
import static com.sparrowwallet.sparrow.AppServices.*;
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() {
super(0, AppServices.getFeeRatesRange().size() - 1, 0);
super(0, FEE_RATES_RANGE.size() - 1, 0);
setMajorTickUnit(1);
setMinorTickCount(0);
setSnapToTicks(false);
@ -31,11 +25,11 @@ public class FeeRangeSlider extends Slider {
setLabelFormatter(new StringConverter<>() {
@Override
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) {
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
@ -51,94 +45,30 @@ public class FeeRangeSlider extends Slider {
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() {
return getFeeRate(AppServices.getMinimumRelayFeeRate());
}
public double getFeeRate(Double minRelayFeeRate) {
if(minRelayFeeRate >= Transaction.DEFAULT_MIN_RELAY_FEE) {
return Math.pow(2.0, getValue());
}
if(getValue() < 1.0d) {
if(minRelayFeeRate == 0.0d) {
return getValue();
}
return Math.pow(minRelayFeeRate, 1.0d - getValue());
}
return Math.pow(2.0, getValue() - 1.0d);
return Math.pow(2.0, getValue());
}
public void setFeeRate(double feeRate) {
setFeeRate(feeRate, AppServices.getMinimumRelayFeeRate());
}
public void setFeeRate(double feeRate, Double minRelayFeeRate) {
double value = getValue(feeRate, minRelayFeeRate);
double value = Math.log(feeRate) / Math.log(2);
updateMaxFeeRange(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) {
if(value >= getMax() && !isLongFeeRange()) {
if(AppServices.getMinimumRelayFeeRate() < Transaction.DEFAULT_MIN_RELAY_FEE) {
setMin(1.0d);
}
setMax(AppServices.getLongFeeRatesRange().size() - 1);
setMax(LONG_FEE_RATES_RANGE.size() - 1);
updateTrackHighlight();
} else if(value == getMin() && isLongFeeRange()) {
if(AppServices.getMinimumRelayFeeRate() < Transaction.DEFAULT_MIN_RELAY_FEE) {
setMin(0.0d);
}
setMax(AppServices.getFeeRatesRange().size() - 1);
setMax(FEE_RATES_RANGE.size() - 1);
updateTrackHighlight();
}
}
public boolean isLongFeeRange() {
return getMax() > AppServices.getFeeRatesRange().size() - 1;
private boolean isLongFeeRange() {
return getMax() > FEE_RATES_RANGE.size() - 1;
}
public void updateTrackHighlight() {
@ -193,9 +123,9 @@ public class FeeRangeSlider extends Slider {
}
private int getPercentageOfFeeRange(Double feeRate) {
double index = getValue(feeRate, AppServices.getMinimumRelayFeeRate());
double index = Math.log(feeRate) / Math.log(2);
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);
}

View File

@ -1,10 +1,8 @@
package com.sparrowwallet.sparrow.control;
import com.sparrowwallet.drongo.OsType;
import com.sparrowwallet.drongo.protocol.Transaction;
import com.sparrowwallet.sparrow.CurrencyRate;
import com.sparrowwallet.sparrow.UnitFormat;
import com.sparrowwallet.sparrow.io.Config;
import com.sparrowwallet.sparrow.wallet.Entry;
import javafx.scene.control.ContextMenu;
import javafx.scene.control.MenuItem;
@ -12,6 +10,7 @@ import javafx.scene.control.Tooltip;
import javafx.scene.control.TreeTableCell;
import javafx.scene.input.Clipboard;
import javafx.scene.input.ClipboardContent;
import org.controlsfx.tools.Platform;
import java.math.BigDecimal;
import java.util.Currency;
@ -25,7 +24,7 @@ public class FiatCell extends TreeTableCell<Entry, Number> {
tooltip = new Tooltip();
contextMenu = new FiatContextMenu();
getStyleClass().add("coin-cell");
if(OsType.getCurrent() == OsType.MACOS) {
if(Platform.getCurrent() == Platform.OSX) {
getStyleClass().add("number-field");
}
}
@ -48,27 +47,20 @@ public class FiatCell extends TreeTableCell<Entry, Number> {
CurrencyRate currencyRate = coinTreeTable.getCurrencyRate();
if(currencyRate != null && currencyRate.isAvailable()) {
if(Config.get().isHideAmounts()) {
setText(CoinLabel.HIDDEN_AMOUNT_TEXT);
setGraphic(null);
setTooltip(null);
setContextMenu(null);
} else {
Currency currency = currencyRate.getCurrency();
double btcRate = currencyRate.getBtcRate();
Currency currency = currencyRate.getCurrency();
double btcRate = currencyRate.getBtcRate();
BigDecimal satsBalance = BigDecimal.valueOf(amount.longValue());
BigDecimal btcBalance = satsBalance.divide(BigDecimal.valueOf(Transaction.SATOSHIS_PER_BITCOIN));
BigDecimal fiatBalance = btcBalance.multiply(BigDecimal.valueOf(btcRate));
BigDecimal satsBalance = BigDecimal.valueOf(amount.longValue());
BigDecimal btcBalance = satsBalance.divide(BigDecimal.valueOf(Transaction.SATOSHIS_PER_BITCOIN));
BigDecimal fiatBalance = btcBalance.multiply(BigDecimal.valueOf(btcRate));
String label = format.formatCurrencyValue(fiatBalance.doubleValue());
tooltip.setText("1 BTC = " + currency.getSymbol() + " " + format.formatCurrencyValue(btcRate));
String label = format.formatCurrencyValue(fiatBalance.doubleValue());
tooltip.setText("1 BTC = " + currency.getSymbol() + " " + format.formatCurrencyValue(btcRate));
setText(label);
setGraphic(null);
setTooltip(tooltip);
setContextMenu(contextMenu);
}
setText(label);
setGraphic(null);
setTooltip(tooltip);
setContextMenu(contextMenu);
} else {
setText(null);
setGraphic(null);

View File

@ -90,13 +90,6 @@ public class FiatLabel extends CopyableLabel {
private void setValueAsText(long balance, UnitFormat unitFormat) {
if(getCurrency() != null && getBtcRate() > 0.0) {
if(Config.get().isHideAmounts()) {
setText(CoinLabel.HIDDEN_AMOUNT_TEXT);
setTooltip(null);
setContextMenu(null);
return;
}
BigDecimal satsBalance = BigDecimal.valueOf(balance);
BigDecimal btcBalance = satsBalance.divide(BigDecimal.valueOf(Transaction.SATOSHIS_PER_BITCOIN));
BigDecimal fiatBalance = btcBalance.multiply(BigDecimal.valueOf(getBtcRate()));

View File

@ -1,13 +1,11 @@
package com.sparrowwallet.sparrow.control;
import com.google.gson.JsonParseException;
import com.sparrowwallet.drongo.OsType;
import com.sparrowwallet.drongo.crypto.InvalidPasswordException;
import com.sparrowwallet.drongo.protocol.ScriptType;
import com.sparrowwallet.drongo.wallet.Keystore;
import com.sparrowwallet.drongo.wallet.KeystoreSource;
import com.sparrowwallet.drongo.wallet.Wallet;
import com.sparrowwallet.drongo.wallet.WalletModel;
import com.sparrowwallet.sparrow.AppServices;
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
import com.sparrowwallet.sparrow.io.FileImport;
@ -26,7 +24,9 @@ import javafx.stage.FileChooser;
import javafx.stage.Stage;
import org.controlsfx.control.SegmentedButton;
import org.controlsfx.control.textfield.CustomPasswordField;
import org.controlsfx.control.textfield.TextFields;
import org.controlsfx.glyphfont.Glyph;
import org.controlsfx.tools.Platform;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ -45,8 +45,8 @@ public abstract class FileImportPane extends TitledDescriptionPane {
private final boolean fileFormatAvailable;
protected List<Wallet> wallets;
public FileImportPane(FileImport importer, String title, String description, String content, WalletModel walletModel, boolean scannable, boolean fileFormatAvailable) {
super(title, description, content, walletModel);
public FileImportPane(FileImport importer, String title, String description, String content, String imageUrl, boolean scannable, boolean fileFormatAvailable) {
super(title, description, content, imageUrl);
this.importer = importer;
this.scannable = scannable;
this.fileFormatAvailable = fileFormatAvailable;
@ -104,7 +104,7 @@ public abstract class FileImportPane extends TitledDescriptionPane {
FileChooser fileChooser = new FileChooser();
fileChooser.setTitle("Open " + importer.getWalletModel().toDisplayString() + " File");
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("TXT", "*.txt")
);
@ -240,8 +240,6 @@ public abstract class FileImportPane extends TitledDescriptionPane {
contentBox.setPadding(new Insets(10, 30, 10, 30));
contentBox.setPrefHeight(60);
javafx.application.Platform.runLater(passwordField::requestFocus);
return contentBox;
}
}

View File

@ -37,7 +37,7 @@ public class FileKeystoreExportPane extends TitledDescriptionPane {
private final boolean file;
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.exporter = exporter;
this.scannable = exporter.isKeystoreExportScannable();
@ -157,7 +157,7 @@ public class FileKeystoreExportPane extends TitledDescriptionPane {
if(exporter instanceof Bip129) {
UR ur = UR.fromBytes(baos.toByteArray());
BBQR bbqr = new BBQR(BBQRType.UNICODE, baos.toByteArray());
qrDisplayDialog = new QRDisplayDialog(ur, bbqr, false, true, QREncoding.UR);
qrDisplayDialog = new QRDisplayDialog(ur, bbqr, false, true, false);
} else {
qrDisplayDialog = new QRDisplayDialog(baos.toString(StandardCharsets.UTF_8));
}

View File

@ -16,7 +16,7 @@ public class FileKeystoreImportPane extends FileImportPane {
private final 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.importer = importer;
this.requiredDerivation = requiredDerivation;
@ -25,11 +25,11 @@ public class FileKeystoreImportPane extends FileImportPane {
protected void importFile(String fileName, InputStream inputStream, String password) throws ImportException {
Keystore keystore = getScannedKeystore(wallet.getScriptType());
if(keystore == null) {
keystore = importer.getKeystore(wallet.getPolicyType(), wallet.getScriptType(), inputStream, password);
keystore = importer.getKeystore(wallet.getScriptType(), inputStream, password);
}
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 {
EventManager.get().post(new KeystoreImportEvent(keystore));
}

View File

@ -5,7 +5,7 @@ import com.sparrowwallet.drongo.OutputDescriptor;
import com.sparrowwallet.drongo.SecureString;
import com.sparrowwallet.drongo.wallet.Wallet;
import com.sparrowwallet.hummingbird.UR;
import com.sparrowwallet.hummingbird.registry.RegistryItem;
import com.sparrowwallet.hummingbird.registry.CryptoOutput;
import com.sparrowwallet.hummingbird.registry.RegistryType;
import com.sparrowwallet.sparrow.AppServices;
import com.sparrowwallet.sparrow.EventManager;
@ -32,7 +32,7 @@ import java.nio.charset.StandardCharsets;
import java.util.Locale;
import java.util.Optional;
import static com.sparrowwallet.sparrow.wallet.SettingsController.getUROutputDescriptor;
import static com.sparrowwallet.sparrow.wallet.SettingsController.getCryptoOutput;
public class FileWalletExportPane extends TitledDescriptionPane {
private final Wallet wallet;
@ -41,7 +41,7 @@ public class FileWalletExportPane extends TitledDescriptionPane {
private final boolean file;
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.exporter = exporter;
this.scannable = exporter.isWalletExportScannable();
@ -168,21 +168,14 @@ public class FileWalletExportPane extends TitledDescriptionPane {
qrDisplayDialog = new QRDisplayDialog(RegistryType.BYTES.toString(), outputStream.toByteArray(), true);
} else if(exporter instanceof PassportMultisig || exporter instanceof KeystoneMultisig || exporter instanceof JadeMultisig) {
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());
BBQR bbqr = new BBQR(BBQRType.UNICODE, outputStream.toByteArray());
qrDisplayDialog = new QRDisplayDialog(ur, bbqr, false, false, QREncoding.UR);
qrDisplayDialog = new QRDisplayDialog(ur, bbqr, false, true, false);
} else if(exporter instanceof Descriptor) {
boolean addBbqrOption = exportWallet.getKeystores().stream().anyMatch(keystore -> keystore.getWalletModel().showBbqr());
QREncoding encoding = exportWallet.getKeystores().stream().allMatch(keystore -> keystore.getWalletModel().selectBbqr()) ? QREncoding.BBQR : QREncoding.UR;
OutputDescriptor outputDescriptor = OutputDescriptor.getOutputDescriptor(exportWallet, KeyPurpose.DEFAULT_PURPOSES, null);
RegistryItem registryItem = getUROutputDescriptor(exportWallet);
BBQR bbqr = addBbqrOption ? new BBQR(BBQRType.UNICODE, outputDescriptor.toString(true).getBytes(StandardCharsets.UTF_8)) : null;
qrDisplayDialog = new DescriptorQRDisplayDialog(exportWallet.getFullDisplayName(), outputDescriptor.toString(true), registryItem.toUR(), bbqr, encoding);
} 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, QREncoding.BBQR);
CryptoOutput cryptoOutput = getCryptoOutput(exportWallet);
qrDisplayDialog = new DescriptorQRDisplayDialog(exportWallet.getFullDisplayName(), outputDescriptor.toString(true), cryptoOutput.toUR());
} else {
qrDisplayDialog = new QRDisplayDialog(outputStream.toString(StandardCharsets.UTF_8));
}

View File

@ -12,7 +12,7 @@ public class FileWalletImportPane extends FileImportPane {
private final 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;
}

View File

@ -12,7 +12,6 @@ import com.sparrowwallet.sparrow.EventManager;
import com.sparrowwallet.sparrow.event.WalletImportEvent;
import com.sparrowwallet.sparrow.io.ImportException;
import com.sparrowwallet.sparrow.io.KeystoreFileImport;
import javafx.application.Platform;
import javafx.collections.FXCollections;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
@ -30,8 +29,8 @@ import org.slf4j.LoggerFactory;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
public class FileWalletKeystoreImportPane extends FileImportPane {
private static final Logger log = LoggerFactory.getLogger(FileWalletKeystoreImportPane.class);
@ -39,38 +38,28 @@ public class FileWalletKeystoreImportPane extends FileImportPane {
private final KeystoreFileImport importer;
private String fileName;
private byte[] fileBytes;
private String password;
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;
}
protected void importFile(String fileName, InputStream inputStream, String password) throws ImportException {
this.fileName = fileName;
this.password = password;
List<PolicyAndScriptType> types = new ArrayList<>();
for(PolicyType policyType : List.of(PolicyType.SINGLE_HD, PolicyType.SINGLE_SP)) {
for(ScriptType scriptType : ScriptType.getAddressableScriptTypes(policyType)) {
types.add(new PolicyAndScriptType(policyType, scriptType));
}
}
List<ScriptType> scriptTypes = ScriptType.getAddressableScriptTypes(PolicyType.SINGLE);
if(wallets != null && !wallets.isEmpty()) {
wallets.stream().filter(w -> w.getPolicyType() == null).forEach(w -> w.setPolicyType(PolicyType.SINGLE_HD));
List<PolicyAndScriptType> walletTypes = wallets.stream().map(w -> new PolicyAndScriptType(w.getPolicyType(), w.getScriptType())).toList();
types.retainAll(walletTypes);
if(types.isEmpty()) {
throw new ImportException("No singlesig script types present in QR code");
}
if(types.size() == 1) {
Wallet wallet = wallets.stream().filter(w -> w.getPolicyType() == types.getFirst().policyType() && w.getScriptType() == types.getFirst().scriptType()).findFirst().orElseThrow(ImportException::new);
wallet.setDefaultPolicy(Policy.getPolicy(wallet.getPolicyType(), wallet.getScriptType(), wallet.getKeystores(), null));
if(wallets.size() == 1 && scriptTypes.contains(wallets.get(0).getScriptType())) {
Wallet wallet = wallets.get(0);
wallet.setPolicyType(PolicyType.SINGLE);
wallet.setDefaultPolicy(Policy.getPolicy(PolicyType.SINGLE, wallet.getScriptType(), wallet.getKeystores(), null));
wallet.setName(importer.getName());
EventManager.get().post(new WalletImportEvent(wallet));
return;
EventManager.get().post(new WalletImportEvent(wallets.get(0)));
} else {
scriptTypes.retainAll(wallets.stream().map(Wallet::getScriptType).collect(Collectors.toList()));
if(scriptTypes.isEmpty()) {
throw new ImportException("No singlesig script types present in QR code");
}
}
} else {
try {
@ -80,61 +69,58 @@ public class FileWalletKeystoreImportPane extends FileImportPane {
}
}
setContent(getScriptTypeEntry(types));
setContent(getScriptTypeEntry(scriptTypes));
setExpanded(true);
importButton.setDisable(true);
}
private void importWallet(PolicyAndScriptType type) throws ImportException {
PolicyType policyType = type.policyType();
ScriptType scriptType = type.scriptType();
private void importWallet(ScriptType scriptType) throws ImportException {
if(wallets != null && !wallets.isEmpty()) {
Wallet wallet = wallets.stream().filter(w -> w.getPolicyType() == policyType && w.getScriptType() == scriptType).findFirst().orElseThrow(ImportException::new);
Wallet wallet = wallets.stream().filter(wallet1 -> wallet1.getScriptType() == scriptType).findFirst().orElseThrow(ImportException::new);
wallet.setName(importer.getName());
wallet.setDefaultPolicy(Policy.getPolicy(policyType, scriptType, wallet.getKeystores(), null));
wallet.setPolicyType(PolicyType.SINGLE);
wallet.setDefaultPolicy(Policy.getPolicy(PolicyType.SINGLE, wallet.getScriptType(), wallet.getKeystores(), null));
EventManager.get().post(new WalletImportEvent(wallet));
} else {
ByteArrayInputStream bais = new ByteArrayInputStream(fileBytes);
Keystore keystore = importer.getKeystore(policyType, scriptType, bais, password);
Keystore keystore = importer.getKeystore(scriptType, bais, "");
Wallet wallet = new Wallet();
wallet.setName(Files.getNameWithoutExtension(fileName));
wallet.setPolicyType(policyType);
wallet.setPolicyType(PolicyType.SINGLE);
wallet.setScriptType(scriptType);
wallet.getKeystores().add(keystore);
wallet.setDefaultPolicy(Policy.getPolicy(policyType, scriptType, wallet.getKeystores(), null));
wallet.setDefaultPolicy(Policy.getPolicy(PolicyType.SINGLE, scriptType, wallet.getKeystores(), null));
EventManager.get().post(new WalletImportEvent(wallet));
}
}
private Node getScriptTypeEntry(List<PolicyAndScriptType> types) {
Label label = new Label("Type:");
private Node getScriptTypeEntry(List<ScriptType> scriptTypes) {
Label label = new Label("Script Type:");
HBox fieldBox = new HBox(5);
fieldBox.setAlignment(Pos.CENTER_RIGHT);
ComboBox<PolicyAndScriptType> comboBox = new ComboBox<>(FXCollections.observableArrayList(types));
PolicyAndScriptType defaultType = new PolicyAndScriptType(PolicyType.SINGLE_HD, ScriptType.P2WPKH);
if(types.contains(defaultType)) {
comboBox.setValue(defaultType);
ComboBox<ScriptType> scriptTypeComboBox = new ComboBox<>(FXCollections.observableArrayList(scriptTypes));
if(scriptTypes.contains(ScriptType.P2WPKH)) {
scriptTypeComboBox.setValue(ScriptType.P2WPKH);
}
comboBox.setConverter(new StringConverter<>() {
scriptTypeComboBox.setConverter(new StringConverter<>() {
@Override
public String toString(PolicyAndScriptType type) {
return type == null ? "" : type.getDescription();
public String toString(ScriptType scriptType) {
return scriptType == null ? "" : scriptType.getDescription();
}
@Override
public PolicyAndScriptType fromString(String string) {
public ScriptType fromString(String string) {
return null;
}
});
comboBox.setMaxWidth(220);
scriptTypeComboBox.setMaxWidth(170);
HelpLabel helpLabel = new HelpLabel();
helpLabel.setHelpText("Native Segwit is usually the best choice for new wallets.\nTaproot is newer and supports both HD and SP (silent payments) wallets.\nNested Segwit and Legacy are useful for recovering older wallets.\nFor existing wallets, be sure to choose the type that matches the wallet you are importing.");
fieldBox.getChildren().addAll(comboBox, helpLabel);
helpLabel.setHelpText("P2WPKH is a Native Segwit type and is usually the best choice for new wallets.\nP2SH-P2WPKH is a Wrapped Segwit type and is a reasonable choice for the widest compatibility.\nP2PKH is a Legacy type and should be avoided for new wallets.\nFor existing wallets, be sure to choose the type that matches the wallet you are importing.");
fieldBox.getChildren().addAll(scriptTypeComboBox, helpLabel);
Region region = new Region();
HBox.setHgrow(region, Priority.SOMETIMES);
@ -144,7 +130,7 @@ public class FileWalletKeystoreImportPane extends FileImportPane {
showHideLink.setVisible(true);
setExpanded(false);
try {
importWallet(comboBox.getValue());
importWallet(scriptTypeComboBox.getValue());
} catch(ImportException e) {
log.error("Error importing file", e);
String errorMessage = e.getMessage();
@ -165,14 +151,6 @@ public class FileWalletKeystoreImportPane extends FileImportPane {
contentBox.setPadding(new Insets(10, 30, 10, 30));
contentBox.setPrefHeight(60);
Platform.runLater(comboBox::requestFocus);
return contentBox;
}
protected record PolicyAndScriptType(PolicyType policyType, ScriptType scriptType) {
public String getDescription() {
return scriptType.getDescription() + (policyType == PolicyType.SINGLE_SP ? " SP" : " HD");
}
}
}

View File

@ -1,20 +1,16 @@
package com.sparrowwallet.sparrow.control;
import com.sparrowwallet.drongo.wallet.BlockTransactionHash;
import com.sparrowwallet.drongo.wallet.Persistable;
import com.sparrowwallet.sparrow.wallet.Entry;
import javafx.animation.PauseTransition;
import javafx.application.Platform;
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.SimpleIntegerProperty;
import javafx.event.Event;
import javafx.geometry.Point2D;
import javafx.scene.control.*;
import javafx.scene.control.cell.TextFieldTreeTableCell;
import javafx.scene.input.Clipboard;
import javafx.scene.input.ClipboardContent;
import javafx.scene.input.DataFormat;
import javafx.util.Duration;
import javafx.util.converter.DefaultStringConverter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ -38,23 +34,12 @@ class LabelCell extends TextFieldTreeTableCell<Entry, String> implements Confirm
if(empty) {
setText(null);
setGraphic(null);
setTooltip(null);
} else {
Entry entry = getTreeTableView().getTreeItem(getIndex()).getValue();
EntryCell.applyRowStyles(this, entry);
setText(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) {
if(label != null) {
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
@ -132,7 +103,7 @@ class LabelCell extends TextFieldTreeTableCell<Entry, String> implements Confirm
return confirmationsProperty;
}
private class LabelContextMenu extends ContextMenu {
private static class LabelContextMenu extends ContextMenu {
public LabelContextMenu(Entry entry, String label) {
MenuItem copyLabel = new MenuItem("Copy Label");
copyLabel.setOnAction(AE -> {
@ -152,13 +123,6 @@ class LabelCell extends TextFieldTreeTableCell<Entry, String> implements Confirm
}
});
getItems().add(pasteLabel);
MenuItem editLabel = new MenuItem("Edit Label...");
editLabel.setOnAction(AE -> {
hide();
startEdit();
});
getItems().add(editLabel);
}
}
}

View File

@ -1,6 +1,5 @@
package com.sparrowwallet.sparrow.control;
import com.sparrowwallet.drongo.OsType;
import com.sparrowwallet.sparrow.AppServices;
import com.sparrowwallet.sparrow.Theme;
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
@ -58,7 +57,7 @@ public class MempoolSizeFeeRatesChart extends StackedAreaChart<String, Number> {
stage.setResizable(false);
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)));
}

View File

@ -2,7 +2,6 @@ package com.sparrowwallet.sparrow.control;
import com.google.common.eventbus.Subscribe;
import com.sparrowwallet.drongo.KeyDerivation;
import com.sparrowwallet.drongo.OsType;
import com.sparrowwallet.drongo.SecureString;
import com.sparrowwallet.drongo.address.Address;
import com.sparrowwallet.drongo.address.InvalidAddressException;
@ -10,12 +9,7 @@ import com.sparrowwallet.drongo.crypto.Bip322;
import com.sparrowwallet.drongo.crypto.ECKey;
import com.sparrowwallet.drongo.policy.PolicyType;
import com.sparrowwallet.drongo.protocol.ScriptType;
import com.sparrowwallet.drongo.psbt.PSBT;
import com.sparrowwallet.drongo.psbt.PSBTInput;
import com.sparrowwallet.drongo.wallet.*;
import com.sparrowwallet.hummingbird.registry.CryptoPSBT;
import com.sparrowwallet.sparrow.io.bbqr.BBQR;
import com.sparrowwallet.sparrow.io.bbqr.BBQRType;
import com.sparrowwallet.sparrow.AppServices;
import com.sparrowwallet.sparrow.EventManager;
import com.sparrowwallet.sparrow.event.*;
@ -23,11 +17,10 @@ import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5Brands;
import com.sparrowwallet.sparrow.io.Storage;
import javafx.application.Platform;
import javafx.scene.Node;
import javafx.scene.control.*;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.layout.VBox;
import javafx.stage.FileChooser;
import javafx.stage.Stage;
import org.controlsfx.control.SegmentedButton;
import org.controlsfx.glyphfont.Glyph;
import org.controlsfx.validation.ValidationResult;
@ -39,21 +32,17 @@ import tornadofx.control.Field;
import tornadofx.control.Fieldset;
import tornadofx.control.Form;
import java.io.*;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.security.SignatureException;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.Arrays;
import java.util.List;
import java.util.Locale;
import java.util.Optional;
import static com.sparrowwallet.sparrow.AppServices.showErrorDialog;
public class MessageSignDialog extends Dialog<ButtonBar.ButtonData> {
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 TextArea message;
private final TextArea signature;
@ -115,13 +104,19 @@ public class MessageSignDialog extends Dialog<ButtonBar.ButtonData> {
this.wallet = wallet;
this.walletNode = walletNode;
final DialogPane dialogPane = new MessageSignDialogPane();
setDialogPane(dialogPane);
final DialogPane dialogPane = getDialogPane();
dialogPane.getStylesheets().add(AppServices.class.getResource("general.css").toExternalForm());
dialogPane.getStylesheets().add(AppServices.class.getResource("dialog.css").toExternalForm());
AppServices.setStageIcon(dialogPane.getScene().getWindow());
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.setSpacing(20);
@ -137,7 +132,6 @@ public class MessageSignDialog extends Dialog<ButtonBar.ButtonData> {
address.getStyleClass().add("id");
address.setEditable(walletNode == null);
address.setTooltip(new Tooltip("Only singlesig addresses can sign"));
address.setSkin(new AddressTextFieldSkin(address));
addressField.getInputs().add(address);
if(walletNode != null) {
@ -160,17 +154,6 @@ public class MessageSignDialog extends Dialog<ButtonBar.ButtonData> {
signature.setStyle("-fx-pref-height: 80px");
signature.setWrapText(true);
signature.setOnMouseClicked(event -> signature.selectAll());
ContextMenu signatureMenu = new ContextMenu();
MenuItem copyItem = new MenuItem("Copy");
copyItem.setOnAction(e -> signature.copy());
MenuItem pasteItem = new MenuItem("Paste");
pasteItem.setOnAction(e -> signature.paste());
MenuItem clearItem = new MenuItem("Clear");
clearItem.setOnAction(e -> signature.clear());
signatureMenu.getItems().addAll(copyItem, pasteItem, clearItem);
signature.setContextMenu(signatureMenu);
signatureField.getInputs().add(signature);
Field formatField = new Field();
@ -216,7 +199,13 @@ public class MessageSignDialog extends Dialog<ButtonBar.ButtonData> {
} else {
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);
signButton.setDisable(!canSign);
@ -255,22 +244,12 @@ public class MessageSignDialog extends Dialog<ButtonBar.ButtonData> {
setFormatFromScriptType(address.getScriptType());
if(wallet != null) {
setWalletNodeFromAddress(wallet, address);
if(walletNode != null) {
setFormatFromScriptType(walletNode.getWallet().getScriptType());
}
}
} catch(InvalidAddressException e) {
//can't happen
}
}
});
formatGroup.selectedToggleProperty().addListener((_, _, newVal) -> {
if(wallet != null) {
boolean canSignSelectedFormat = canSignAllFormats(wallet) || newVal == formatElectrum;
signButton.setDisable(!isValidAddress() || !canSign || !canSignSelectedFormat);
}
});
}
EventManager.get().register(this);
@ -288,7 +267,7 @@ public class MessageSignDialog extends Dialog<ButtonBar.ButtonData> {
AppServices.onEscapePressed(dialogPane.getScene(), () -> setResult(ButtonBar.ButtonData.CANCEL_CLOSE));
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(() -> {
if(address.getText().isEmpty()) {
@ -298,7 +277,7 @@ public class MessageSignDialog extends Dialog<ButtonBar.ButtonData> {
}
if(wallet != null && walletNode != null) {
setFormatFromScriptType(walletNode.getWallet().getScriptType());
setFormatFromScriptType(wallet.getScriptType());
} else {
formatGroup.selectToggle(formatElectrum);
}
@ -306,19 +285,15 @@ public class MessageSignDialog extends Dialog<ButtonBar.ButtonData> {
}
private void checkWalletSigning(Wallet wallet) {
if(wallet.getKeystores().size() != 1 || (wallet.getPolicyType() != PolicyType.SINGLE_HD && wallet.getPolicyType() != PolicyType.SINGLE_SP)) {
throw new IllegalArgumentException("Cannot sign messages using this wallet type");
if(wallet.getKeystores().size() != 1) {
throw new IllegalArgumentException("Cannot sign messages using a wallet with multiple keystores - a single key is required");
}
}
private boolean canSign(Wallet wallet) {
return wallet.getKeystores().getFirst().hasPrivateKey()
|| wallet.getKeystores().getFirst().getSource() == KeystoreSource.HW_USB
|| wallet.getKeystores().getFirst().getWalletModel().isCard();
}
private boolean canSignAllFormats(Wallet wallet) {
return wallet.getKeystores().getFirst().hasPrivateKey();
return wallet.getKeystores().get(0).hasPrivateKey()
|| wallet.getKeystores().get(0).getSource() == KeystoreSource.HW_USB
|| wallet.getKeystores().get(0).getWalletModel().isCard();
}
private Address getAddress()throws InvalidAddressException {
@ -332,7 +307,7 @@ public class MessageSignDialog extends Dialog<ButtonBar.ButtonData> {
private boolean isValidAddress() {
try {
Address address = getAddress();
return address.getScriptType().isAllowed(PolicyType.SINGLE_HD) || address.getScriptType() == ScriptType.P2SH;
return address.getScriptType().isAllowed(PolicyType.SINGLE) || address.getScriptType() == ScriptType.P2SH;
} catch (InvalidAddressException e) {
return false;
}
@ -374,7 +349,7 @@ public class MessageSignDialog extends Dialog<ButtonBar.ButtonData> {
//Note we can expect a single keystore due to the check in the constructor
Wallet signingWallet = walletNode.getWallet();
if(signingWallet.getKeystores().getFirst().hasPrivateKey()) {
if(signingWallet.getKeystores().get(0).hasPrivateKey()) {
if(signingWallet.isEncrypted()) {
EventManager.get().post(new RequestOpenWalletsEvent());
} else {
@ -387,25 +362,19 @@ public class MessageSignDialog extends Dialog<ButtonBar.ButtonData> {
private void signUnencryptedKeystore(Wallet decryptedWallet) {
try {
Keystore keystore = decryptedWallet.getKeystores().getFirst();
Keystore keystore = decryptedWallet.getKeystores().get(0);
ECKey privKey = keystore.getKey(walletNode);
String signatureText;
if(decryptedWallet.getPolicyType() == PolicyType.SINGLE_SP) {
ECKey spendPrivKey = keystore.getSpendPrivateKey(Collections.emptyMap());
signatureText = Bip322.signMessageBip322Sp(walletNode.getAddress(), message.getText().trim(), spendPrivKey, walletNode.getSilentPaymentTweak());
spendPrivKey.clear();
if(isBip322()) {
ScriptType scriptType = decryptedWallet.getScriptType();
signatureText = Bip322.signMessageBip322(scriptType, message.getText().trim(), privKey);
} else {
ECKey privKey = keystore.getKey(walletNode);
if(isBip322()) {
ScriptType scriptType = decryptedWallet.getScriptType();
signatureText = Bip322.signMessageBip322(scriptType, message.getText().trim(), privKey);
} else {
ScriptType scriptType = isElectrumSignatureFormat() ? ScriptType.P2PKH : decryptedWallet.getScriptType();
signatureText = privKey.signMessage(message.getText().trim(), scriptType);
}
privKey.clear();
ScriptType scriptType = isElectrumSignatureFormat() ? ScriptType.P2PKH : decryptedWallet.getScriptType();
signatureText = privKey.signMessage(message.getText().trim(), scriptType);
}
signature.clear();
signature.appendText(signatureText);
privKey.clear();
} catch(Exception e) {
log.error("Could not sign message", e);
AppServices.showErrorDialog("Could not sign message", e.getMessage());
@ -413,8 +382,8 @@ public class MessageSignDialog extends Dialog<ButtonBar.ButtonData> {
}
private void signDeviceKeystore(Wallet deviceWallet) {
List<String> fingerprints = List.of(deviceWallet.getKeystores().getFirst().getKeyDerivation().getMasterFingerprint());
KeyDerivation fullDerivation = deviceWallet.getKeystores().getFirst().getKeyDerivation().extend(walletNode.getDerivation());
List<String> fingerprints = List.of(deviceWallet.getKeystores().get(0).getKeyDerivation().getMasterFingerprint());
KeyDerivation fullDerivation = deviceWallet.getKeystores().get(0).getKeyDerivation().extend(walletNode.getDerivation());
DeviceSignMessageDialog deviceSignMessageDialog = new DeviceSignMessageDialog(fingerprints, deviceWallet, message.getText().trim(), fullDerivation);
deviceSignMessageDialog.initOwner(getDialogPane().getScene().getWindow());
Optional<String> optSignature = deviceSignMessageDialog.showAndWait();
@ -481,11 +450,11 @@ public class MessageSignDialog extends Dialog<ButtonBar.ButtonData> {
if(scriptType == ScriptType.P2SH) {
scriptType = ScriptType.P2SH_P2WPKH;
}
if(!ScriptType.getScriptTypesForPolicyType(PolicyType.SINGLE_HD).contains(scriptType)) {
if(!ScriptType.getScriptTypesForPolicyType(PolicyType.SINGLE).contains(scriptType)) {
throw new IllegalArgumentException("Only single signature P2PKH, P2SH-P2WPKH or P2WPKH addresses can verify messages.");
}
Address signedMessageAddress = scriptType.getAddress(PolicyType.SINGLE_HD, signedMessageKey);
Address signedMessageAddress = scriptType.getAddress(signedMessageKey);
return providedAddress.equals(signedMessageAddress);
}
@ -495,11 +464,6 @@ public class MessageSignDialog extends Dialog<ButtonBar.ButtonData> {
return;
}
if(isBip322()) {
showBip322Qr();
return;
}
//Note we can expect a single keystore due to the check in the constructor
KeyDerivation firstDerivation = walletNode.getWallet().getKeystores().get(0).getKeyDerivation();
String derivationPath = KeyDerivation.writePath(firstDerivation.extend(walletNode.getDerivation()).getDerivation(), false);
@ -513,88 +477,13 @@ public class MessageSignDialog extends Dialog<ButtonBar.ButtonData> {
}
}
private void showBip322Qr() {
Wallet signingWallet = walletNode.getWallet();
PSBT psbt = buildBip322Psbt(signingWallet);
byte[] psbtBytes = psbt.getForExport().serialize();
CryptoPSBT cryptoPSBT = new CryptoPSBT(psbtBytes);
BBQR bbqr = new BBQR(BBQRType.PSBT, psbtBytes);
QRDisplayDialog qrDisplayDialog = new QRDisplayDialog(cryptoPSBT.toUR(), bbqr, false, true, QREncoding.UR);
qrDisplayDialog.initOwner(getDialogPane().getScene().getWindow());
Optional<ButtonType> optButtonType = qrDisplayDialog.showAndWait();
if(optButtonType.isPresent() && optButtonType.get().getButtonData() == ButtonBar.ButtonData.OK_DONE) {
scanQr();
}
}
private PSBT buildBip322Psbt(Wallet signingWallet) {
if(signingWallet.getPolicyType() == PolicyType.SINGLE_SP) {
Keystore keystore = signingWallet.getKeystores().getFirst();
ECKey spendPubKey = keystore.getSilentPaymentScanAddress().getSpendKey();
KeyDerivation spendDerivation = new KeyDerivation(keystore.getKeyDerivation().getMasterFingerprint(), KeyDerivation.writePath(KeyDerivation.getBip352SpendDerivation(keystore.getKeyDerivation().getDerivation())));
return Bip322.getBip322PsbtSp(walletNode.getAddress(), message.getText().trim(), walletNode.getSilentPaymentTweak(), Map.of(spendPubKey, spendDerivation));
}
PSBT psbt = Bip322.getBip322Psbt(signingWallet.getScriptType(), walletNode.getAddress(), message.getText().trim());
addBip322DerivationInfo(psbt, signingWallet);
return psbt;
}
private String extractBip322Signature(PSBT signedPsbt) {
String psbtMessage = signedPsbt.getGenericSignedMessage();
if(psbtMessage != null && !psbtMessage.equals(message.getText().trim())) {
Optional<ButtonType> response = AppServices.showWarningDialog("Message mismatch",
"The message in the signed PSBT does not match the message in this dialog.\n\nPSBT message: " + psbtMessage +
"\n\nContinue extracting the signature?", ButtonType.NO, ButtonType.YES);
if(response.isEmpty() || response.get() != ButtonType.YES) {
return null;
}
}
Wallet signingWallet = walletNode.getWallet();
if(signingWallet.getPolicyType() == PolicyType.SINGLE_SP) {
return Bip322.getBip322SignatureFromPsbtSp(signedPsbt);
}
ECKey pubKey = signingWallet.getKeystores().getFirst().getPubKey(walletNode);
return Bip322.getBip322SignatureFromPsbt(signingWallet.getScriptType(), signedPsbt, pubKey);
}
private void addBip322DerivationInfo(PSBT psbt, Wallet signingWallet) {
ScriptType scriptType = signingWallet.getScriptType();
PSBTInput psbtInput = psbt.getPsbtInputs().get(0);
Keystore keystore = signingWallet.getKeystores().get(0);
ECKey pubKey = keystore.getPubKey(walletNode);
KeyDerivation fullDerivation = keystore.getKeyDerivation().extend(walletNode.getDerivation());
if(scriptType == ScriptType.P2TR) {
psbtInput.setTapInternalKey(pubKey);
psbtInput.getTapDerivedPublicKeys().put(ECKey.fromPublicOnly(pubKey.getPubKeyXCoord()), Map.of(fullDerivation, Collections.emptyList()));
} else {
psbtInput.getDerivedPublicKeys().put(scriptType.getOutputKey(signingWallet.getPolicyType(), pubKey), fullDerivation);
}
}
private void scanQr() {
QRScanDialog qrScanDialog = new QRScanDialog();
qrScanDialog.initOwner(getDialogPane().getScene().getWindow());
Optional<QRScanDialog.Result> optionalResult = qrScanDialog.showAndWait();
if(optionalResult.isPresent()) {
QRScanDialog.Result result = optionalResult.get();
if(result.psbt != null) {
try {
String sig = extractBip322Signature(result.psbt);
if(sig != null) {
signature.clear();
signature.appendText(sig);
}
} catch(Exception e) {
log.error("Error extracting BIP-322 signature from PSBT", e);
AppServices.showErrorDialog("Error extracting signature", e.getMessage());
}
} else if(result.payload != null) {
if(result.payload != null) {
signature.clear();
signature.appendText(result.payload);
} else if(result.exception != null) {
@ -606,132 +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;
}
if(isBip322()) {
exportBip322File();
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 exportBip322File() {
Wallet signingWallet = walletNode.getWallet();
PSBT psbt = buildBip322Psbt(signingWallet);
Stage window = new Stage();
FileChooser fileChooser = new FileChooser();
fileChooser.setTitle("Save PSBT File");
fileChooser.setInitialFileName("bip322-signmessage.psbt");
AppServices.moveToActiveWindowScreen(window, 800, 450);
File file = fileChooser.showSaveDialog(window);
if(file != null) {
try(OutputStream os = new FileOutputStream(file)) {
os.write(psbt.getForExport().serialize());
} catch(IOException e) {
log.error("Error saving BIP-322 PSBT", e);
AppServices.showErrorDialog("Error saving PSBT", "Cannot write to " + file.getAbsolutePath());
}
}
}
private void importFile() {
Stage window = new Stage();
FileChooser fileChooser = new FileChooser();
fileChooser.setTitle("Open Signed File");
fileChooser.getExtensionFilters().addAll(
new FileChooser.ExtensionFilter("All Files", OsType.getCurrent().equals(OsType.UNIX) ? "*" : "*.*"),
new FileChooser.ExtensionFilter("Text Files", "*.txt"),
new FileChooser.ExtensionFilter("PSBT Files", "*.psbt")
);
AppServices.moveToActiveWindowScreen(window, 800, 450);
File file = fileChooser.showOpenDialog(window);
if(file != null) {
if(file.getName().toLowerCase(Locale.ROOT).endsWith(".psbt") || isBip322()) {
if(walletNode == null) {
AppServices.showErrorDialog("Address not in wallet", "The provided address is not present in the currently selected wallet.");
return;
}
try {
byte[] psbtBytes = Files.readAllBytes(file.toPath());
PSBT signedPsbt = new PSBT(psbtBytes, false);
String sig = extractBip322Signature(signedPsbt);
if(sig != null) {
signature.clear();
signature.appendText(sig);
}
return;
} catch(Exception e) {
if(file.getName().toLowerCase(Locale.ROOT).endsWith(".psbt")) {
log.error("Error loading signed PSBT", e);
AppServices.showErrorDialog("Error loading signed PSBT", e.getMessage());
return;
}
//Fall through to text handling for non-.psbt files
}
}
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() {
if(wallet != null) {
if(wallet.containsSource(KeystoreSource.HW_USB)) {
@ -776,37 +539,4 @@ public class MessageSignDialog extends Dialog<ButtonBar.ButtonData> {
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);
}
}
}

View File

@ -1,11 +1,27 @@
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.UtxoEntry;
import com.sparrowwallet.sparrow.whirlpool.Whirlpool;
import com.sparrowwallet.sparrow.whirlpool.WhirlpoolException;
import javafx.animation.Timeline;
import javafx.geometry.Pos;
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> {
private static final int ERROR_DISPLAY_MILLIS = 5 * 60 * 1000;
public MixStatusCell() {
super();
setAlignment(Pos.CENTER_RIGHT);
@ -25,9 +41,162 @@ public class MixStatusCell extends TreeTableCell<Entry, UtxoEntry.MixStatus> {
setGraphic(null);
} else {
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.isError()) {
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);
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().replaceAll("_", " ");
status = status.substring(0, 1).toUpperCase(Locale.ROOT) + status.substring(1).toLowerCase(Locale.ROOT);
tt.setText(status);
setTooltip(tt);
} 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);
}
}
}
}

View File

@ -1,6 +1,5 @@
package com.sparrowwallet.sparrow.control;
import com.sparrowwallet.drongo.OsType;
import com.sparrowwallet.drongo.Utils;
import com.sparrowwallet.drongo.wallet.Bip39MnemonicCode;
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("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.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];
Grid grid = getGrid(emptyWordGrid);
@ -256,7 +256,7 @@ public class MnemonicGridDialog extends Dialog<List<String>> {
FileChooser fileChooser = new FileChooser();
fileChooser.setTitle("Open PDF");
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")
);

View File

@ -1,27 +1,24 @@
package com.sparrowwallet.sparrow.control;
import com.sparrowwallet.drongo.wallet.DeterministicSeed;
import com.sparrowwallet.drongo.wallet.Keystore;
import com.sparrowwallet.drongo.wallet.WalletModel;
import javafx.beans.property.SimpleListProperty;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.geometry.Orientation;
import javafx.scene.Node;
import javafx.scene.layout.GridPane;
import javafx.scene.layout.StackPane;
import javafx.scene.layout.TilePane;
import javafx.scene.layout.VBox;
import java.util.ArrayList;
import java.util.List;
public class MnemonicKeystoreDisplayPane extends MnemonicKeystorePane {
private final DeterministicSeed.Type type;
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);
buttonBox.getChildren().clear();
this.type = keystore.getSeed().getType();
showWordList(keystore.getSeed());
}
@ -31,9 +28,11 @@ public class MnemonicKeystoreDisplayPane extends MnemonicKeystorePane {
VBox vBox = new VBox();
vBox.setSpacing(10);
wordsPane = new GridPane();
wordsPane = new TilePane();
wordsPane.setPrefRows(numWords / 3);
wordsPane.setHgap(10);
wordsPane.setVgap(10);
wordsPane.setOrientation(Orientation.VERTICAL);
List<String> words = new ArrayList<>();
for(int i = 0; i < numWords; i++) {
@ -44,20 +43,13 @@ public class MnemonicKeystoreDisplayPane extends MnemonicKeystorePane {
wordEntriesProperty = new SimpleListProperty<>(wordEntryList);
List<WordEntry> wordEntries = new ArrayList<>(numWords);
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++) {
wordEntries.get(i).setNextEntry(wordEntries.get(i + 1));
wordEntries.get(i).setNextField(wordEntries.get(i + 1).getEditor());
}
int numCols = 3;
int numRows = Math.ceilDiv(numWords, numCols);
for(int i = 0; i < wordEntries.size(); i++) {
int col = i / numRows;
int row = i % numRows;
wordsPane.add(wordEntries.get(i), col, row);
}
wordsPane.getChildren().addAll(wordEntries);
vBox.getChildren().add(wordsPane);
@ -65,9 +57,4 @@ public class MnemonicKeystoreDisplayPane extends MnemonicKeystorePane {
stackPane.getChildren().add(vBox);
return stackPane;
}
@Override
protected WordlistProvider getWordlistProvider() {
return getWordListProvider(type);
}
}

View File

@ -19,7 +19,7 @@ public class MnemonicKeystoreEntryPane extends MnemonicKeystorePane {
private boolean generated;
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);
buttonBox.getChildren().clear();

View File

@ -33,7 +33,6 @@ import java.util.Optional;
public class MnemonicKeystoreImportPane extends MnemonicKeystorePane {
protected final Wallet wallet;
private final KeystoreMnemonicImport importer;
private final KeyDerivation defaultDerivation;
private SplitMenuButton importButton;
@ -44,11 +43,10 @@ public class MnemonicKeystoreImportPane extends MnemonicKeystorePane {
private Button confirmButton;
private List<String> generatedMnemonicCode;
public MnemonicKeystoreImportPane(Wallet wallet, KeystoreMnemonicImport importer, KeyDerivation defaultDerivation) {
super(importer.getName(), "Create or enter seed", importer.getKeystoreImportDescription(), importer.getWalletModel());
public MnemonicKeystoreImportPane(Wallet wallet, KeystoreMnemonicImport importer) {
super(importer.getName(), "Create or enter seed", importer.getKeystoreImportDescription(), "image/" + importer.getWalletModel().getType() + ".png");
this.wallet = wallet;
this.importer = importer;
this.defaultDerivation = defaultDerivation;
createImportButton();
buttonBox.getChildren().add(importButton);
@ -58,10 +56,10 @@ public class MnemonicKeystoreImportPane extends MnemonicKeystorePane {
importButton = new SplitMenuButton();
importButton.setAlignment(Pos.CENTER_RIGHT);
importButton.setText("Import Keystore");
setDefaultButton(importButton);
importButton.getStyleClass().add("default-button");
importButton.setOnAction(event -> {
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"};
int scriptAccountsLength = ScriptType.P2SH.equals(wallet.getScriptType()) ? 1 : accounts.length;
@ -79,10 +77,6 @@ public class MnemonicKeystoreImportPane extends MnemonicKeystorePane {
importButton.setVisible(false);
}
private List<ChildNumber> getDefaultDerivation() {
return defaultDerivation == null || defaultDerivation.getDerivation().isEmpty() ? wallet.getScriptType().getDefaultDerivation() : defaultDerivation.getDerivation();
}
protected void enterMnemonic(int numWords) {
generatedMnemonicCode = null;
super.enterMnemonic(numWords);
@ -141,7 +135,7 @@ public class MnemonicKeystoreImportPane extends MnemonicKeystorePane {
protected void onWordChange(boolean empty, boolean validWords, boolean validChecksum) {
if(!empty && validWords) {
try {
importer.getKeystore(wallet.getPolicyType(), wallet.getScriptType().getDefaultDerivation(), wordEntriesProperty.get(), passphraseProperty.get());
importer.getKeystore(wallet.getScriptType().getDefaultDerivation(), wordEntriesProperty.get(), passphraseProperty.get());
validChecksum = true;
} catch(ImportException e) {
if(e.getCause() instanceof MnemonicException.MnemonicTypeException) {
@ -249,14 +243,14 @@ public class MnemonicKeystoreImportPane extends MnemonicKeystorePane {
setDescription("Ready to import");
showHideLink.setText("Show Derivation...");
showHideLink.setVisible(false);
setContent(getDerivationEntry(getDefaultDerivation()));
setContent(getDerivationEntry(wallet.getScriptType().getDefaultDerivation()));
}
}
private boolean importKeystore(List<ChildNumber> derivation, boolean dryrun) {
importButton.setDisable(true);
try {
Keystore keystore = importer.getKeystore(wallet.getPolicyType(), derivation, wordEntriesProperty.get(), passphraseProperty.get());
Keystore keystore = importer.getKeystore(derivation, wordEntriesProperty.get(), passphraseProperty.get());
if(!dryrun) {
if(passphraseProperty.get() != null && !passphraseProperty.get().isEmpty()) {
KeystorePassphraseDialog keystorePassphraseDialog = new KeystorePassphraseDialog(null, keystore, true);

View File

@ -2,11 +2,8 @@ package com.sparrowwallet.sparrow.control;
import com.sparrowwallet.drongo.wallet.Bip39MnemonicCode;
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.glyphfont.GlyphUtils;
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
import javafx.application.Platform;
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.SimpleIntegerProperty;
@ -15,15 +12,20 @@ import javafx.beans.property.SimpleStringProperty;
import javafx.collections.FXCollections;
import javafx.collections.ListChangeListener;
import javafx.collections.ObservableList;
import javafx.concurrent.ScheduledService;
import javafx.concurrent.Task;
import javafx.geometry.Insets;
import javafx.geometry.Orientation;
import javafx.geometry.Pos;
import javafx.scene.Node;
import javafx.scene.control.*;
import javafx.scene.input.Clipboard;
import javafx.scene.layout.*;
import javafx.util.Callback;
import javafx.util.Duration;
import org.controlsfx.control.textfield.AutoCompletionBinding;
import org.controlsfx.control.textfield.TextFields;
import org.controlsfx.glyphfont.Glyph;
import org.controlsfx.validation.ValidationResult;
import org.controlsfx.validation.ValidationSupport;
import org.controlsfx.validation.Validator;
@ -39,7 +41,7 @@ public class MnemonicKeystorePane extends TitledDescriptionPane {
private static final Logger log = LoggerFactory.getLogger(MnemonicKeystorePane.class);
protected SplitMenuButton enterMnemonicButton;
protected GridPane wordsPane;
protected TilePane wordsPane;
protected Label validLabel;
protected Label invalidLabel;
@ -47,8 +49,8 @@ public class MnemonicKeystorePane extends TitledDescriptionPane {
protected final SimpleStringProperty passphraseProperty = new SimpleStringProperty("");
protected IntegerProperty defaultWordSizeProperty;
public MnemonicKeystorePane(String title, String description, String content, WalletModel walletModel) {
super(title, description, content, walletModel);
public MnemonicKeystorePane(String title, String description, String content, String imageUrl) {
super(title, description, content, imageUrl);
}
@Override
@ -109,9 +111,23 @@ public class MnemonicKeystorePane extends TitledDescriptionPane {
wordEntry.getEditor().setText(words.get(i));
wordEntry.getEditor().setEditable(false);
} 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());
});
service.start();
}
}
}
@ -137,10 +153,6 @@ public class MnemonicKeystorePane extends TitledDescriptionPane {
protected void showWordList(DeterministicSeed seed) {
List<String> words = seed.getMnemonicCode();
showWordList(words);
}
protected void showWordList(List<String> words) {
setContent(getMnemonicWordsEntry(words.size(), true, true));
setExpanded(true);
@ -162,9 +174,11 @@ public class MnemonicKeystorePane extends TitledDescriptionPane {
VBox vBox = new VBox();
vBox.setSpacing(10);
wordsPane = new GridPane();
wordsPane = new TilePane();
wordsPane.setPrefRows(numWords/3);
wordsPane.setHgap(10);
wordsPane.setVgap(10);
wordsPane.setOrientation(Orientation.VERTICAL);
List<String> words = new ArrayList<>();
for(int i = 0; i < numWords; i++) {
@ -175,20 +189,13 @@ public class MnemonicKeystorePane extends TitledDescriptionPane {
wordEntriesProperty = new SimpleListProperty<>(wordEntryList);
List<WordEntry> wordEntries = new ArrayList<>(numWords);
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++) {
wordEntries.get(i).setNextEntry(wordEntries.get(i + 1));
wordEntries.get(i).setNextField(wordEntries.get(i + 1).getEditor());
}
int numCols = 3;
int numRows = Math.ceilDiv(numWords, numCols);
for(int i = 0; i < wordEntries.size(); i++) {
int col = i / numRows;
int row = i % numRows;
wordsPane.add(wordEntries.get(i), col, row);
}
wordsPane.getChildren().addAll(wordEntries);
vBox.getChildren().add(wordsPane);
@ -208,7 +215,7 @@ public class MnemonicKeystorePane extends TitledDescriptionPane {
buttonPane.getChildren().add(leftBox);
AnchorPane.setLeftAnchor(leftBox, 0.0);
validLabel = new Label("Valid checksum", GlyphUtils.getSuccessGlyph());
validLabel = new Label("Valid checksum", getValidGlyph());
validLabel.setContentDisplay(ContentDisplay.LEFT);
validLabel.setGraphicTextGap(5.0);
validLabel.managedProperty().bind(validLabel.visibleProperty());
@ -217,7 +224,7 @@ public class MnemonicKeystorePane extends TitledDescriptionPane {
AnchorPane.setTopAnchor(validLabel, 5.0);
AnchorPane.setLeftAnchor(validLabel, 0.0);
invalidLabel = new Label("Invalid checksum", GlyphUtils.getInvalidGlyph());
invalidLabel = new Label("Invalid checksum", getInvalidGlyph());
invalidLabel.setContentDisplay(ContentDisplay.LEFT);
invalidLabel.setGraphicTextGap(5.0);
invalidLabel.managedProperty().bind(invalidLabel.visibleProperty());
@ -235,7 +242,7 @@ public class MnemonicKeystorePane extends TitledDescriptionPane {
empty = false;
}
if(!getWordlistProvider().isValid(word)) {
if(!WordEntry.isValid(word)) {
validWords = false;
}
}
@ -271,20 +278,13 @@ public class MnemonicKeystorePane extends TitledDescriptionPane {
//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 {
private static List<String> wordList;
private final TextField wordField;
private WordEntry nextEntry;
private TextField nextField;
public WordEntry(int wordNumber, ObservableList<String> wordEntryList, WordlistProvider wordlistProvider) {
public WordEntry(int wordNumber, ObservableList<String> wordEntryList) {
super();
setAlignment(Pos.CENTER_RIGHT);
@ -302,7 +302,7 @@ public class MnemonicKeystorePane extends TitledDescriptionPane {
for(int i = 0; i < words.length; i++) {
String word = words[i];
if(entry.nextField != null) {
if(i == words.length - 2 && wordlistProvider.isValid(word)) {
if(i == words.length - 2 && isValid(word)) {
label.requestFocus();
} else {
entry.nextField.requestFocus();
@ -321,7 +321,6 @@ public class MnemonicKeystorePane extends TitledDescriptionPane {
}
};
wordField.setMaxWidth(100);
wordField.setAccessibleText("Word " + (wordNumber + 1));
TextFormatter<?> formatter = new TextFormatter<>((TextFormatter.Change change) -> {
String text = change.getText();
// if text was added, fix the text to fit the requirements
@ -336,7 +335,8 @@ public class MnemonicKeystorePane extends TitledDescriptionPane {
});
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.setOnAutoCompleted(event -> {
if(nextField != null) {
@ -357,7 +357,7 @@ public class MnemonicKeystorePane extends TitledDescriptionPane {
ValidationSupport validationSupport = new ValidationSupport();
validationSupport.setValidationDecorator(new StyleClassValidationDecoration());
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) -> {
@ -378,24 +378,28 @@ public class MnemonicKeystorePane extends TitledDescriptionPane {
public void setNextField(TextField field) {
this.nextField = field;
}
public static boolean isValid(String word) {
return wordList.contains(word);
}
}
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 ObservableList<String> wordEntryList;
public WordlistSuggestionProvider(WordlistProvider wordlistProvider, int wordNumber, ObservableList<String> wordEntryList) {
this.wordlistProvider = wordlistProvider;
public WordlistSuggestionProvider(List<String> wordList, int wordNumber, ObservableList<String> wordEntryList) {
this.wordList = wordList;
this.wordNumber = wordNumber;
this.wordEntryList = wordEntryList;
}
@Override
public Collection<String> call(AutoCompletionBinding.ISuggestionRequest request) {
if(wordlistProvider.supportsPossibleLastWords() && wordNumber == wordEntryList.size() - 1 && allPreviousWordsValid()) {
if(wordNumber == wordEntryList.size() - 1 && allPreviousWordsValid()) {
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()) {
possibleLastWords.removeIf(s -> !s.startsWith(request.getUserText()));
}
@ -408,7 +412,7 @@ public class MnemonicKeystorePane extends TitledDescriptionPane {
List<String> suggestions = new ArrayList<>();
if(!request.getUserText().isEmpty()) {
for(String word : wordlistProvider.getWordlist()) {
for(String word : wordList) {
if(word.startsWith(request.getUserText())) {
suggestions.add(word);
}
@ -420,7 +424,7 @@ public class MnemonicKeystorePane extends TitledDescriptionPane {
private boolean allPreviousWordsValid() {
for(int i = 0; i < wordEntryList.size() - 1; i++) {
if(!wordlistProvider.isValid(wordEntryList.get(i))) {
if(!WordEntry.isValid(wordEntryList.get(i))) {
return false;
}
}
@ -481,53 +485,17 @@ public class MnemonicKeystorePane extends TitledDescriptionPane {
}
}
protected interface WordlistProvider {
List<String> getWordlist();
boolean isValid(String word);
boolean supportsPossibleLastWords();
List<String> getPossibleLastWords(List<String> previousWords) throws MnemonicException.MnemonicLengthException, MnemonicException.MnemonicWordException;
public static Glyph getValidGlyph() {
Glyph validGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.CHECK_CIRCLE);
validGlyph.getStyleClass().add("success");
validGlyph.setFontSize(12);
return validGlyph;
}
private static class Bip39WordlistProvider implements WordlistProvider {
@Override
public List<String> getWordlist() {
return Bip39MnemonicCode.INSTANCE.getWordList();
}
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();
}
public static Glyph getInvalidGlyph() {
Glyph invalidGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.EXCLAMATION_CIRCLE);
invalidGlyph.getStyleClass().add("failure");
invalidGlyph.setFontSize(12);
return invalidGlyph;
}
}

View File

@ -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.getPolicyType(), 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");
setDefaultButton(importButton);
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(wallet.getPolicyType(), 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);
}
}

View File

@ -41,7 +41,7 @@ public class MnemonicWalletKeystoreImportPane extends MnemonicKeystorePane {
private Button importButton;
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;
}
@ -55,7 +55,7 @@ public class MnemonicWalletKeystoreImportPane extends MnemonicKeystorePane {
protected List<Node> createRightButtons() {
discoverButton = new Button("Discover Wallet");
discoverButton.setDisable(true);
discoverButton.setDefaultButton(AppServices.onlineProperty().get());
discoverButton.setDefaultButton(true);
discoverButton.managedProperty().bind(discoverButton.visibleProperty());
discoverButton.setOnAction(event -> {
discoverWallet();
@ -66,7 +66,6 @@ public class MnemonicWalletKeystoreImportPane extends MnemonicKeystorePane {
importButton = new Button("Import Wallet");
importButton.setDisable(true);
importButton.setDefaultButton(!AppServices.onlineProperty().get());
importButton.managedProperty().bind(importButton.visibleProperty());
importButton.visibleProperty().bind(discoverButton.visibleProperty().not());
importButton.setOnAction(event -> {
@ -81,7 +80,7 @@ public class MnemonicWalletKeystoreImportPane extends MnemonicKeystorePane {
protected void onWordChange(boolean empty, boolean validWords, boolean validChecksum) {
if(!empty && validWords) {
try {
importer.getKeystore(PolicyType.SINGLE_HD, ScriptType.P2WPKH.getDefaultDerivation(), wordEntriesProperty.get(), passphraseProperty.get());
importer.getKeystore(ScriptType.P2WPKH.getDefaultDerivation(), wordEntriesProperty.get(), passphraseProperty.get());
validChecksum = true;
} catch(ImportException e) {
if(e.getCause() instanceof MnemonicException.MnemonicTypeException) {
@ -108,14 +107,14 @@ public class MnemonicWalletKeystoreImportPane extends MnemonicKeystorePane {
discoverButton.setGraphic(progressIndicator);
List<Wallet> wallets = new ArrayList<>();
List<List<ChildNumber>> derivations = ScriptType.getScriptTypesForPolicyType(PolicyType.SINGLE_HD).stream().map(ScriptType::getDefaultDerivation).collect(Collectors.toList());
List<List<ChildNumber>> derivations = ScriptType.getScriptTypesForPolicyType(PolicyType.SINGLE).stream().map(ScriptType::getDefaultDerivation).collect(Collectors.toList());
derivations.add(List.of(new ChildNumber(0, true)));
derivations.add(ScriptType.P2PKH.getDefaultDerivation(1)); //Bisq segwit misderivation
for(ScriptType scriptType : ScriptType.getScriptTypesForPolicyType(PolicyType.SINGLE_HD)) {
for(ScriptType scriptType : ScriptType.getScriptTypesForPolicyType(PolicyType.SINGLE)) {
for(List<ChildNumber> derivation : derivations) {
try {
Wallet wallet = getWallet(PolicyType.SINGLE_HD, scriptType, derivation);
Wallet wallet = getWallet(scriptType, derivation);
wallets.add(wallet);
} catch(ImportException e) {
String errorMessage = e.getMessage();
@ -134,21 +133,15 @@ public class MnemonicWalletKeystoreImportPane extends MnemonicKeystorePane {
progressIndicator.progressProperty().bind(walletDiscoveryService.progressProperty());
walletDiscoveryService.setOnSucceeded(successEvent -> {
discoverButton.setGraphic(null);
Optional<List<Wallet>> optWallets = walletDiscoveryService.getValue();
if(optWallets.isPresent()) {
List<Wallet> discoveredWallets = optWallets.get();
if(discoveredWallets.size() > 1) {
for(Wallet wallet : discoveredWallets) {
wallet.setName(wallet.getKeystores().getFirst().getLabel() + " " + wallet.getScriptType().getDescription());
}
}
EventManager.get().post(new WalletImportEvent(discoveredWallets));
Optional<Wallet> optWallet = walletDiscoveryService.getValue();
if(optWallet.isPresent()) {
EventManager.get().post(new WalletImportEvent(optWallet.get()));
} else {
discoverButton.setDisable(false);
Optional<ButtonType> optButtonType = AppServices.showErrorDialog("No existing wallet found",
Config.get().getServerType() == ServerType.BITCOIN_CORE ? "The configured server type is Bitcoin Core, which does not support wallet discovery.\n\n" +
"You can however import this wallet and scan the blockchain by supplying a start date. Do you want to import this wallet?" :
"Could not find an HD wallet with existing transactions using this mnemonic. Import this wallet anyway?", ButtonType.NO, ButtonType.YES);
"Could not find a wallet with existing transactions using this mnemonic. Import this wallet anyway?", ButtonType.NO, ButtonType.YES);
if(optButtonType.isPresent() && optButtonType.get() == ButtonType.YES) {
setContent(getScriptTypeEntry());
setExpanded(true);
@ -163,61 +156,52 @@ public class MnemonicWalletKeystoreImportPane extends MnemonicKeystorePane {
walletDiscoveryService.start();
}
private Wallet getWallet(PolicyType policyType, ScriptType scriptType, List<ChildNumber> derivation) throws ImportException {
private Wallet getWallet(ScriptType scriptType, List<ChildNumber> derivation) throws ImportException {
Wallet wallet = new Wallet("");
wallet.setPolicyType(policyType);
wallet.setPolicyType(PolicyType.SINGLE);
wallet.setScriptType(scriptType);
Keystore keystore = importer.getKeystore(policyType, derivation, wordEntriesProperty.get(), passphraseProperty.get());
Keystore keystore = importer.getKeystore(derivation, wordEntriesProperty.get(), passphraseProperty.get());
wallet.getKeystores().add(keystore);
wallet.setDefaultPolicy(Policy.getPolicy(policyType, scriptType, wallet.getKeystores(), 1));
wallet.setDefaultPolicy(Policy.getPolicy(PolicyType.SINGLE, scriptType, wallet.getKeystores(), 1));
return wallet;
}
private Node getScriptTypeEntry() {
Label label = new Label("Type:");
List<PolicyAndScriptType> types = new ArrayList<>();
for(PolicyType policyType : List.of(PolicyType.SINGLE_HD, PolicyType.SINGLE_SP)) {
for(ScriptType scriptType : ScriptType.getAddressableScriptTypes(policyType)) {
types.add(new PolicyAndScriptType(policyType, scriptType));
}
}
Label label = new Label("Script Type:");
HBox fieldBox = new HBox(5);
fieldBox.setAlignment(Pos.CENTER_RIGHT);
ComboBox<PolicyAndScriptType> comboBox = new ComboBox<>(FXCollections.observableArrayList(types));
PolicyAndScriptType defaultType = new PolicyAndScriptType(PolicyType.SINGLE_HD, ScriptType.P2WPKH);
if(types.contains(defaultType)) {
comboBox.setValue(defaultType);
ComboBox<ScriptType> scriptTypeComboBox = new ComboBox<>(FXCollections.observableArrayList(ScriptType.getAddressableScriptTypes(PolicyType.SINGLE)));
if(scriptTypeComboBox.getItems().contains(ScriptType.P2WPKH)) {
scriptTypeComboBox.setValue(ScriptType.P2WPKH);
}
comboBox.setConverter(new StringConverter<>() {
scriptTypeComboBox.setConverter(new StringConverter<>() {
@Override
public String toString(PolicyAndScriptType type) {
return type == null ? "" : type.getDescription();
public String toString(ScriptType scriptType) {
return scriptType == null ? "" : scriptType.getDescription();
}
@Override
public PolicyAndScriptType fromString(String string) {
public ScriptType fromString(String string) {
return null;
}
});
comboBox.setMaxWidth(220);
scriptTypeComboBox.setMaxWidth(170);
HelpLabel helpLabel = new HelpLabel();
helpLabel.setHelpText("Native Segwit is usually the best choice for new wallets.\nTaproot is a new type useful for specific needs.\nTaproot Silent Payments creates a silent payment wallet.\nNested Segwit and Legacy are useful for recovering older wallets.\nFor existing wallets, be sure to choose the type that matches the wallet you are importing.");
fieldBox.getChildren().addAll(comboBox, helpLabel);
helpLabel.setHelpText("Native Segwit is usually the best choice for new wallets.\nTaproot is a new type useful for specific needs.\nNested Segwit and Legacy are useful for recovering older wallets.\nFor existing wallets, be sure to choose the type that matches the wallet you are importing.");
fieldBox.getChildren().addAll(scriptTypeComboBox, helpLabel);
Region region = new Region();
HBox.setHgrow(region, Priority.SOMETIMES);
Button importMnemonicButton = new Button("Import");
importMnemonicButton.setDefaultButton(true);
importMnemonicButton.setOnAction(event -> {
showHideLink.setVisible(true);
setExpanded(false);
try {
PolicyAndScriptType type = comboBox.getValue();
Wallet wallet = getWallet(type.policyType(), type.scriptType(), type.scriptType().getDefaultDerivation());
ScriptType scriptType = scriptTypeComboBox.getValue();
Wallet wallet = getWallet(scriptType, scriptType.getDefaultDerivation());
EventManager.get().post(new WalletImportEvent(wallet));
} catch(ImportException e) {
log.error("Error importing mnemonic", e);
@ -239,10 +223,4 @@ public class MnemonicWalletKeystoreImportPane extends MnemonicKeystorePane {
return contentBox;
}
protected record PolicyAndScriptType(PolicyType policyType, ScriptType scriptType) {
public String getDescription() {
return scriptType.getDescription() + (policyType == PolicyType.SINGLE_SP ? " SP" : " HD");
}
}
}

View File

@ -1,14 +1,14 @@
package com.sparrowwallet.sparrow.control;
import com.sparrowwallet.drongo.OsType;
import com.sparrowwallet.sparrow.wallet.Entry;
import javafx.scene.control.TreeTableCell;
import org.controlsfx.tools.Platform;
public class NumberCell extends TreeTableCell<Entry, Number> {
public NumberCell() {
super();
getStyleClass().add("number-cell");
if(OsType.getCurrent() == OsType.MACOS) {
if(Platform.getCurrent() == Platform.OSX) {
getStyleClass().add("number-field");
}
}

View File

@ -20,7 +20,7 @@ import org.slf4j.LoggerFactory;
import java.io.InputStream;
import java.net.Proxy;
import java.net.URI;
import java.net.URL;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
@ -79,6 +79,10 @@ public class PayNymAvatar extends StackPane {
this.paymentCodeProperty.set(paymentCode);
}
public void setPaymentCode(com.samourai.wallet.bip47.rpc.PaymentCode paymentCode) {
setPaymentCode(PaymentCode.fromString(paymentCode.toString()));
}
public void clearPaymentCode() {
this.paymentCodeProperty.set(null);
}
@ -124,11 +128,8 @@ public class PayNymAvatar extends StackPane {
log.debug("Requesting PayNym avatar from " + url);
}
try(InputStream is = (proxy == null ? new URI(url).toURL().openStream() : new URI(url).toURL().openConnection(proxy).getInputStream())) {
Image image = new Image(is, 150, 150, true, true);
if(image.getException() != null) {
throw image.getException();
}
try(InputStream is = (proxy == null ? new URL(url).openStream() : new URL(url).openConnection(proxy).getInputStream())) {
Image image = new Image(is, 150, 150, true, false);
paymentCodeCache.put(cacheId, image);
Platform.runLater(() -> EventManager.get().post(new PayNymImageLoadedEvent(paymentCode, image)));
return image;

View File

@ -81,7 +81,10 @@ public class PayNymCell extends ListCell<PayNym> {
linkButton.setDisable(true);
payNymController.linkPayNym(payNym);
});
getStyleClass().add("unlinked");
if(payNymController.isSelectLinkedOnly()) {
getStyleClass().add("unlinked");
}
}
}

View File

@ -10,6 +10,11 @@ public class PaymentCodeTextField extends CopyableTextField {
setPaymentCodeString();
}
public void setPaymentCode(com.samourai.wallet.bip47.rpc.PaymentCode paymentCode) {
this.paymentCodeStr = paymentCode.toString();
setPaymentCodeString();
}
private void setPaymentCodeString() {
String abbrevPcode = paymentCodeStr.substring(0, 12) + "..." + paymentCodeStr.substring(paymentCodeStr.length() - 5);
setText(abbrevPcode);

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