Compare commits

..
113 Commits
Author SHA1 Message Date
Craig Raw 1140a678ad followup 2025-02-08 11:38:41 +02:00
Craig Raw 6e8d44bc8c set script type import button as default after bip39 wallet discovery returns empty 2025-02-08 09:44:21 +02:00
Craig Raw ad3b384feb fix loading hex txn files with trailing whitespace 2025-02-08 09:30:21 +02:00
Craig Raw f38350b38d prefill derivation to default path for script type on watch only keystores 2025-02-07 16:22:20 +02:00
Craig Raw 62060c9839 fix unsigned byte to int conversion for ledger get_more_elements command 2025-02-07 15:56:35 +02:00
Craig Raw 8975f6f666 followup 2025-02-07 14:57:21 +02:00
Craig Raw c7351cd191 indicate historical rates support in exchange source drop-down 2025-02-07 14:52:17 +02:00
Craig Raw 62b1dc3900 support coldcard p2tr address display and show correct address for script type on message sign 2025-02-06 16:12:49 +02:00
Craig Raw f37ff47850 fix invalid claimed length error on transaction file load 2025-02-06 15:36:37 +02:00
Craig Raw cfaa1f6c6e add local network usage description for macos 15 2025-02-06 10:08:28 +02:00
Craig Raw 91c94b94eb upgrade flyway to v9.22.3 2025-02-06 09:37:51 +02:00
Craig Raw a5eb7da067 bump to v2.1.2 2025-02-05 09:21:21 +02:00
Craig Raw 195dbcef3b ensure /sys devices are writeable before calling udevadm 2025-02-04 20:53:50 +02:00
Craig Raw 24955604e2 use standard font in label cells on macos 2025-02-04 20:16:28 +02:00
Craig Raw 0305afbc02 catch and log any linkage errors while enumerating hwws 2025-02-04 19:56:07 +02:00
Craig Raw d4c3c3afa8 bump to v2.1.1 2025-02-04 13:58:47 +02:00
Craig Raw cda7835672 revert rpm spec file to use %post 2025-02-04 11:45:53 +02:00
Craig Raw b4b679dd16 fix rpm spec path reference 2025-02-04 11:22:06 +02:00
Craig Raw 3efaec2859 verbose rpm build 2025-02-04 11:14:06 +02:00
Craig Raw a53812c12f improve rpm spec and deb postinst scripts 2025-02-04 11:07:30 +02:00
Craig Raw 686c008e97 allow server urls to be pasted into the server settings host fields 2025-02-03 18:02:18 +02:00
Craig Raw 4d60a20336 add mempool.space exchange rate source 2025-02-03 14:47:23 +02:00
Craig Raw 9879889875 bump to v2.1.0 2025-02-03 08:56:11 +02:00
Craig Raw 4aee89a35b fix trezor one passphrase protection deactivation 2025-01-31 15:01:32 +02:00
Craig Raw fd9648efd1 fix trezor one passphrase behaviour, add bitbox02 non-standard path check 2025-01-31 13:50:54 +02:00
Craig Raw 8f438cd0bc rename preferences to settings 2025-01-31 11:41:20 +02:00
Craig Raw 8b47701dbe check for and delete hwi directory on macos and windows 2025-01-31 11:04:40 +02:00
Craig Raw ff571c3df4 ensure consistent key ordering in ledger multisig wallet policy 2025-01-30 16:19:49 +02:00
Craig Raw 201c9ccca3 truncate labels to persistable max label length and notify user via tooltip 2025-01-30 14:50:46 +02:00
Craig Raw cbae280cc8 add jade plus to udev rules 2025-01-30 12:05:08 +02:00
Craig Raw 1f44229e62 fix rpm spec file 2025-01-29 14:34:05 +02:00
Craig Raw 541c71a002 verbose build 2025-01-29 13:10:29 +02:00
Craig Raw bb08b5599c install rpmbuild on ubuntu arm64 2025-01-29 12:50:32 +02:00
Craig Raw fd70f03259 add arm64 runners 2025-01-29 12:36:43 +02:00
Craig Raw fbca6c691d add blackie.c3-soft.com testnet4 public electrum server 2025-01-28 13:24:10 +02:00
Craig Raw e8cd56388f upgrade to javafx 23.0.2 2025-01-28 13:20:31 +02:00
Craig Raw 3dfd8210a8 store treetable column sort on adjustment, and restore on wallet load 2025-01-28 12:53:10 +02:00
Craig Raw f9199b65f0 store treetable column widths on adjustment, and restore on wallet load 2025-01-28 10:33:46 +02:00
Craig Raw 6a001bd67f exclude taproot wallets and jade, tapsigner and satochip hwws from requiring non witness tx in psbts 2025-01-23 15:40:44 +02:00
Craig Raw ee2f387cd5 retrieve, store and use device registrations to avoid unncessary reregistration on ledger multisig wallets 2025-01-22 16:26:42 +02:00
Craig Raw 95200c7143 improve bitbox pairing flow 2025-01-22 12:59:23 +02:00
Craig Raw d7511c62bf match new behaviour in bitcoin core 28 for default windows data dir 2025-01-22 09:49:50 +02:00
Craig Raw 7a5f4ff294 reduce default tooltip show delay to 400ms 2025-01-22 07:51:04 +02:00
Craig Raw 2b145cb9cc update install udev rules dialog 2025-01-21 13:55:45 +02:00
Craig Raw 13bd05853c add udev rules installation 2025-01-21 13:11:19 +02:00
Craig Raw 59f3338842 fix coldcard last error check 2 2025-01-21 11:34:10 +02:00
Craig Raw 2cc38dc8b0 fix coldcard last error check 2025-01-21 10:41:42 +02:00
Craig Raw 0e9d97c221 replace hwi with lark 2025-01-21 10:00:38 +02:00
Craig Raw fb0fd013d9 add lark as submodule 2025-01-20 09:49:01 +02:00
Craig Raw e7510d2275 rename max block size constant for clarity 2025-01-20 09:17:05 +02:00
Craig Raw e92d0f9b58 show input label in input tooltip on transaction diagram if present 2025-01-16 14:21:42 +02:00
Craig Raw ea23bb51d9 upgrade lanterna and remove java 22 workaround 2025-01-16 12:51:10 +02:00
Craig Raw 2d3bf0b2fe skip labelled addresses when retrieving an unused address from the receive tab and send tab pay to wallet 2025-01-16 12:18:12 +02:00
Craig Raw 617ad380c0 improve socket address resolution handling 2025-01-15 15:22:15 +02:00
Craig Raw 29ac15846d disable broadcast progress bar if disconnected, and re-enable if connected again 2025-01-15 13:07:18 +02:00
Craig Raw f4acd3e587 add option to bitcoin core and private electrum server selection to scan url from a qr code 2025-01-15 11:59:45 +02:00
Craig Raw f057b92729 allow camera image mirroring to be changed from image context menu and application view menu 2025-01-15 11:07:45 +02:00
Craig Raw 4bf02f833c remove payjoin verification step to check there is no previous utxo information in the psbt as per bip78 change 2025-01-15 09:26:48 +02:00
craigrawandGitHub 7ef51e6a5d Merge pull request #1591 from Toporin/patch-satochip-multisig
default to first keystore for signing path if satochip keystore cannot be determined
2025-01-15 09:14:06 +02:00
Craig Raw fdbcea1625 enable electrum rpc batching on mempool-electrs servers 2025-01-15 09:09:25 +02:00
Craig Raw 218c2720e0 always select a new address when sending multiple payments to the same open wallet 2025-01-15 08:21:48 +02:00
Toporin 91ad82a21c Patch https://github.com/Toporin/SatochipApplet/issues/15
First try to recover derivation path from satochip keystore, otherwise from first keystore as default value.
2025-01-14 15:23:38 +01:00
Toporin f4b3b3d55a Merge branch 'master' into patch-satochip-multisig 2025-01-14 15:01:35 +01:00
Craig Raw db1b55cfa0 cormorant: report configuration error when both core data folder and user/pass is not specified 2025-01-14 15:12:39 +02:00
Craig Raw bd0aca66b5 cormorant: skip waiting for ibd to complete when networkactive is false 2025-01-14 14:15:44 +02:00
Toporin 22ad1cc5d1 Patch https://github.com/Toporin/SatochipApplet/issues/15
Null exception can be thrown when signing a multisig transaction
from a Sparrow wallet reconstructed from a Bitcoin descriptor.
This happens when the user did not configure any keystore
with the corresponding Satochip card ('import' button).
In this case, the 'fullpath' derivation path remains undefined,
leading to the exception.
2025-01-14 13:00:52 +01:00
Craig Raw d07a5f0a01 cormorant: add fee to mempool tx entries returned from get history 2025-01-14 12:23:18 +02:00
Craig Raw 947013e088 only show cpfp rate if child fee increases effective fee rate 2025-01-14 10:44:53 +02:00
Craig Raw 25f441a6a8 update javafx to 23.0.1 2025-01-13 10:22:54 +02:00
Craig Raw ee5015f0d5 update macos runner 2024-12-17 12:17:00 +02:00
Craig Raw 4f00fabd23 upgrade tern to 1.0.6 2024-11-28 11:03:49 +02:00
Craig Raw 6927423d68 switch from controlsfx platform to drongo ostype 2024-11-26 11:30:32 +02:00
Craig Raw fffa636956 followup 2024-11-26 11:00:26 +02:00
Craig Raw a02ac3dcd2 use versionless extra module info definitions where possible 2024-11-26 10:49:38 +02:00
Craig Raw e56e3d9394 switch from custom to gradlex extra-java-module-info plugin, cleanup module definitions 2024-11-26 09:31:34 +02:00
Craig Raw 119d00233d fix cast to http proxy supplier 2024-11-25 16:34:17 +02:00
Craig Raw da427610d6 move version class to drongo 2024-11-25 15:53:27 +02:00
Craig Raw 46034b8f11 repackage http client as tern library dependency 2024-11-25 13:17:39 +02:00
Craig Raw d49d5967b2 improve exception handling when loading paynym avatars 2024-11-25 10:30:28 +02:00
Craig Raw 484ef5f399 upgrade jcommander to 2.0 2024-11-20 13:09:28 +02:00
Craig Raw 740c00d1ba add output descriptor accessors and copy function 2024-11-19 10:46:48 +02:00
Craig Raw dfae39255e add equals and hashcode to output descriptor 2024-11-18 15:14:40 +02:00
Craig Raw c2bce893db fix psbtv2 output amount serialization 2024-11-18 13:05:41 +02:00
Craig Raw ef063fde75 reverse prevtxid byte ordering during serialization and deserialization 2024-11-18 12:44:14 +02:00
craigrawandGitHub adb446de3e Merge pull request #1537 from ottosch/hotkey-close-dialog
close wallet name dialog with escape key
2024-11-18 08:22:40 +01:00
ottosch d040f186a2 Close wallet name dialog with ESC 2024-11-15 18:28:52 -03:00
Craig Raw b4f9c52413 bump required java version to 22 2024-11-15 23:06:48 +02:00
Craig Raw 7527dd0909 allow hardened character selection when writing key 2024-11-15 16:33:10 +02:00
Craig Raw b0be8ca7c2 add psbt v2 support 2024-11-15 12:34:46 +02:00
Craig Raw 1e0c0c1c75 replace forward slash with underscore in file names when saving psbts 2024-11-12 08:48:50 +02:00
Craig Raw d731f7296b improve jade qr keystore import descriptions 2024-11-12 08:26:07 +02:00
Craig Raw 12034a07d7 add specter diy multisig option to wallet import menu 2024-11-05 08:49:36 +02:00
Craig Raw 60e3d4e107 be more lenient in parsing pasted btc values to send tab textfields 2024-11-04 08:03:21 +02:00
Craig Raw ad8e17a3a0 add eckey arithmetic functions 2024-10-31 17:02:42 +02:00
Craig Raw 3e676eadcb add support for x25519 and secp256r1 keys 2024-10-30 13:04:35 +02:00
Craig Raw 3640db3d3d simplify required maven build repositories 2024-10-28 13:27:44 +02:00
Craig Raw d0bf55de70 fix regression to display tabular numbers in a monospace font 2024-10-28 10:04:33 +02:00
Craig Raw ad0b6adfd8 upgrade hummingbird to v1.7.4 2024-10-28 09:48:11 +02:00
Craig Raw 92b32b0d99 drongo: fix build instructions 2024-10-21 09:28:52 +02:00
Craig Raw 233addc1b7 update fxsvgimage to v1.1 2024-10-10 09:07:12 +02:00
Craig Raw 1d8c37066e update flyway to v9.1.3 2024-10-10 09:04:01 +02:00
Craig Raw c450efe499 improve keystore import panel spacing in linux 2024-10-08 10:32:36 +02:00
craigrawandGitHub 34bcc87468 Merge pull request #1512 from dcavacec/fix-issue-1510
improve handling of spacing and links in accordion panels
2024-10-08 10:24:59 +02:00
David Cavaceci 2aac365039 PR #1510 Feedback: set min height, use AppServices url handling 2024-10-07 09:59:58 -05:00
Craig Raw 7e68ecffd3 retrieve fee rates from configured source on non-mainnet networks where available 2024-10-07 12:13:24 +02:00
David Cavaceci bf457620db Fix #1510: Handle spacing and links in content box messages. 2024-10-02 11:30:06 -05:00
Craig Raw e50fe4c68c switch from paynym.is to paynym.rs and tor equivalents, update child wallet labels on displaying paynym dialog 2024-09-30 11:31:55 +02:00
Craig Raw 1bbc586cd6 set transaction tab label to transaction label if available 2024-09-24 08:49:04 +02:00
Craig Raw e1dab3a48e update compress and jackson libs 2024-09-20 11:00:28 +02:00
Craig Raw 73b672a7ef fix arm64 architecture on server deb control file 2024-09-20 10:20:07 +02:00
Craig Raw b142c54c68 update readme for java 22 2024-09-18 15:09:04 +02:00
Craig Raw 58d09c3ba7 bump to v2.0.1 2024-09-18 14:57:23 +02:00
Craig Raw d5a7a5b855 update reproducible build instructions for java 22 2024-09-18 14:56:06 +02:00
168 changed files with 2036 additions and 3169 deletions
+3 -3
View File
@@ -10,7 +10,7 @@ jobs:
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [windows-2022, ubuntu-20.04, macos-12]
os: [windows-2022, ubuntu-20.04, ubuntu-22.04-arm, macos-13, macos-14]
steps:
- uses: actions/checkout@v4
with:
@@ -33,7 +33,7 @@ jobs:
- name: Upload Artifacts
uses: actions/upload-artifact@v4
with:
name: Sparrow Build - ${{ runner.os }}
name: Sparrow Build - ${{ runner.os }} ${{ runner.arch }}
path: |
build/jpackage/*
!build/jpackage/Sparrow/
@@ -50,7 +50,7 @@ jobs:
if: ${{ runner.os == 'Linux' }}
uses: actions/upload-artifact@v4
with:
name: Sparrow Build - ${{ runner.os }} Headless
name: Sparrow Build - ${{ runner.os }} ${{ runner.arch }} Headless
path: |
build/jpackage/*
!build/jpackage/Sparrow/
+3
View File
@@ -1,3 +1,6 @@
[submodule "drongo"]
path = drongo
url = ../../sparrowwallet/drongo.git
[submodule "lark"]
path = lark
url = ../../sparrowwallet/lark.git
+3 -3
View File
@@ -16,8 +16,8 @@ or for those without SSH credentials:
`git clone --recursive https://github.com/sparrowwallet/sparrow.git`
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).
In order to build, Sparrow requires Java 22 or higher to be installed.
The release binaries are built with [Eclipse Temurin 22.0.2+9](https://github.com/adoptium/temurin22-binaries/releases/tag/jdk-22.0.2%2B9).
Other packages may also be necessary to build depending on the platform. On Debian/Ubuntu systems:
@@ -44,7 +44,7 @@ If you prefer to run Sparrow directly from source, it can be launched from withi
`./sparrow`
Java 18 or higher must be installed.
Java 22 or higher must be installed.
## Configuration
+115 -168
View File
@@ -1,13 +1,11 @@
import java.awt.GraphicsEnvironment
plugins {
id 'application'
id 'extra-java-module-info'
id 'org-openjfx-javafxplugin'
id 'org.beryx.jlink' version '3.0.1'
id 'org.beryx.jlink' version '3.1.1'
id 'org.gradlex.extra-java-module-info' version '1.9'
}
def sparrowVersion = '2.0.0'
def sparrowVersion = '2.1.2'
def os = org.gradle.internal.os.OperatingSystem.current()
def osName = os.getFamilyName()
if(os.macOsX) {
@@ -41,10 +39,7 @@ version "${sparrowVersion}"
repositories {
mavenCentral()
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/' }
maven { url 'https://code.sparrowwallet.com/api/packages/sparrowwallet/maven' }
}
tasks.withType(AbstractArchiveTask) {
@@ -53,7 +48,7 @@ tasks.withType(AbstractArchiveTask) {
}
javafx {
version = headless ? "18" : "22"
version = headless ? "18" : "23.0.2"
modules = [ 'javafx.controls', 'javafx.fxml', 'javafx.swing', 'javafx.graphics' ]
}
@@ -64,6 +59,7 @@ 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.0.0-jre')
implementation('com.google.code.gson:gson:2.9.1')
implementation('com.h2database:h2:2.1.214')
@@ -76,13 +72,13 @@ dependencies {
implementation('org.jdbi:jdbi3-sqlobject:3.20.0') {
exclude group: 'org.slf4j'
}
implementation('org.flywaydb:flyway-core:7.10.7-SNAPSHOT')
implementation('org.flywaydb:flyway-core:9.22.3')
implementation('org.fxmisc.richtext:richtextfx:0.10.4')
implementation('no.tornado:tornadofx-controls:1.0.4')
implementation('com.google.zxing:javase:3.4.0') {
exclude group: 'com.beust', module: 'jcommander'
}
implementation('com.beust:jcommander:1.81')
implementation('org.jcommander:jcommander:2.0')
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'
@@ -90,7 +86,8 @@ dependencies {
implementation('com.github.arteam:simple-json-rpc-server:1.3') {
exclude group: 'org.slf4j'
}
implementation('com.sparrowwallet:hummingbird:1.7.3')
implementation('com.fasterxml.jackson.core:jackson-databind:2.17.2')
implementation('com.sparrowwallet:hummingbird:1.7.4')
implementation('co.nstant.in:cbor:0.9')
implementation("com.nativelibs4java:bridj${targetName}:0.7-20140918-3") {
exclude group: 'com.google.android.tools', module: 'dx'
@@ -98,15 +95,27 @@ dependencies {
implementation("com.github.sarxos:webcam-capture${targetName}:0.3.13-SNAPSHOT") {
exclude group: 'com.nativelibs4java', module: 'bridj'
}
implementation "io.matthewnelson.kotlin-components:kmp-tor:${vTor}-${vKmpTor}"
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:${vTor}-${vKmpTor}") {
exclude group: 'org.jetbrains.kotlin', module: 'kotlin-stdlib-common'
}
if(kmpOs == "linux" && kmpArch == "arm64") {
implementation("com.sparrowwallet.kmp-tor-binary:kmp-tor-binary-${kmpOs}${kmpArch}-jvm:${vTor}") {
exclude group: 'org.jetbrains.kotlin', module: 'kotlin-stdlib-common'
}
} else {
implementation("io.matthewnelson.kotlin-components:kmp-tor-binary-${kmpOs}${kmpArch}:${vTor}") {
exclude group: 'org.jetbrains.kotlin', module: 'kotlin-stdlib-common'
}
}
implementation("io.matthewnelson.kotlin-components:kmp-tor-binary-extract:${vTor}") {
exclude group: 'org.jetbrains.kotlin', module: 'kotlin-stdlib-common'
}
implementation("io.matthewnelson.kotlin-components:kmp-tor-ext-callback-manager:${vKmpTor}") {
exclude group: 'org.jetbrains.kotlin', module: 'kotlin-stdlib-common'
}
implementation('org.jetbrains.kotlinx:kotlinx-coroutines-javafx:1.7.1') {
exclude group: 'org.jetbrains.kotlin', module: 'kotlin-stdlib-common'
}
implementation("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'
@@ -124,16 +133,16 @@ dependencies {
exclude group: 'org.slf4j'
}
implementation('com.sparrowwallet.bokmakierie:bokmakierie:1.0')
implementation('org.eclipse.jetty:jetty-client:9.4.54.v20240208')
implementation('com.sparrowwallet:tern:1.0.6')
implementation('io.reactivex.rxjava2:rxjava:2.2.15')
implementation('io.reactivex.rxjava2:rxjavafx:2.2.2')
implementation('org.apache.commons:commons-lang3:3.7')
implementation('org.apache.commons:commons-compress:1.25.0')
implementation('org.apache.commons:commons-compress:1.27.1')
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('com.googlecode.lanterna:lanterna:3.1.3')
implementation('net.coobird:thumbnailator:0.4.18')
implementation('com.github.hervegirod:fxsvgimage:1.0b2')
implementation('com.github.hervegirod:fxsvgimage:1.1')
implementation('com.sparrowwallet:toucan:0.9.0')
implementation('com.jcraft:jzlib:1.1.3')
testImplementation('org.junit.jupiter:junit-jupiter-api:5.10.0')
@@ -160,7 +169,7 @@ processResources {
test {
useJUnitPlatform()
jvmArgs = ["--add-opens=java.base/java.io=ALL-UNNAMED", "--add-opens=java.base/java.io=com.google.gson"]
jvmArgs = ["--add-opens=java.base/java.io=ALL-UNNAMED", "--add-opens=java.base/java.io=com.google.gson", "--add-reads=org.flywaydb.core=java.desktop"]
}
application {
@@ -188,7 +197,8 @@ application {
"--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=kotlin.stdlib=kotlinx.coroutines.core",
"--add-reads=org.flywaydb.core=java.desktop"]
if(os.macOsX) {
applicationDefaultJvmArgs += ["-Dprism.lcdtext=false", "-Xdock:name=Sparrow", "-Xdock:icon=/Users/scy/git/sparrow/src/main/resources/sparrow-large.png",
@@ -209,8 +219,6 @@ jlink {
requires 'jdk.crypto.cryptoki'
requires 'java.management'
requires 'io.leangen.geantyref'
uses 'org.flywaydb.core.extensibility.FlywayExtension'
uses 'org.flywaydb.core.internal.database.DatabaseType'
uses 'org.eclipse.jetty.http.HttpFieldPreEncoder'
}
@@ -248,7 +256,8 @@ 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=kotlin.stdlib=kotlinx.coroutines.core"]
"--add-reads=kotlin.stdlib=kotlinx.coroutines.core",
"--add-reads=org.flywaydb.core=java.desktop"]
if(os.windows) {
jvmArgs += ["-Djavax.accessibility.assistive_technologies", "-Djavax.accessibility.screen_magnifier_present=false"]
@@ -275,7 +284,7 @@ jlink {
}
if(os.linux) {
if(headless) {
installerOptions = ['--license-file', 'LICENSE', '--resource-dir', 'src/main/deploy/package/linux-headless/']
installerOptions = ['--license-file', 'LICENSE', '--resource-dir', "src/main/deploy/package/linux-headless/${osArch}"]
} else {
installerOptions += ['--resource-dir', 'src/main/deploy/package/linux/', '--linux-shortcut', '--linux-menu-group', 'Sparrow']
}
@@ -288,11 +297,20 @@ jlink {
installerType = "dmg"
}
}
if(os.linux) {
jpackageImage {
dependsOn('prepareModulesDir', 'copyUdevRules')
}
}
}
tasks.jlink.finalizedBy "addUserWritePermission"
if(os.linux) {
tasks.jlink.finalizedBy('addUserWritePermission', 'copyUdevRules')
} else {
tasks.jlink.finalizedBy('addUserWritePermission')
}
task addUserWritePermission(type: Exec) {
tasks.register('addUserWritePermission', Exec) {
if(os.windows) {
commandLine 'icacls', "$buildDir\\image\\legal", '/grant', 'Users:(OI)(CI)F', '/T'
} else {
@@ -300,11 +318,17 @@ task addUserWritePermission(type: Exec) {
}
}
task removeGroupWritePermission(type: Exec) {
tasks.register('copyUdevRules', Copy) {
from('lark/src/main/resources/udev')
into(layout.buildDirectory.dir('image/conf/udev'))
include('*')
}
tasks.register('removeGroupWritePermission', Exec) {
commandLine 'chmod', '-R', 'g-w', "$buildDir/jpackage/Sparrow"
}
task packageZipDistribution(type: Zip) {
tasks.register('packageZipDistribution', Zip) {
archiveFileName = "Sparrow-${sparrowVersion}.zip"
destinationDirectory = file("$buildDir/jpackage")
preserveFileTimestamps = os.macOsX
@@ -314,7 +338,7 @@ task packageZipDistribution(type: Zip) {
}
}
task packageTarDistribution(type: Tar) {
tasks.register('packageTarDistribution', Tar) {
dependsOn removeGroupWritePermission
archiveFileName = "sparrow-${sparrowVersion}-${releaseArch}.tar.gz"
destinationDirectory = file("$buildDir/jpackage")
@@ -325,60 +349,11 @@ task packageTarDistribution(type: Tar) {
}
extraJavaModuleInfo {
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') {
module('no.tornado:tornadofx-controls', 'tornadofx.controls') {
exports('tornadofx.control')
requires('javafx.controls')
}
module('simple-json-rpc-core-1.3.jar', 'simple.json.rpc.core', '1.3') {
module('com.github.arteam:simple-json-rpc-core', 'simple.json.rpc.core') {
exports('com.github.arteam.simplejsonrpc.core.annotation')
exports('com.github.arteam.simplejsonrpc.core.domain')
requires('com.fasterxml.jackson.core')
@@ -386,7 +361,7 @@ extraJavaModuleInfo {
requires('com.fasterxml.jackson.databind')
requires('org.jetbrains.annotations')
}
module('simple-json-rpc-client-1.3.jar', 'simple.json.rpc.client', '1.3') {
module('com.github.arteam:simple-json-rpc-client', 'simple.json.rpc.client') {
exports('com.github.arteam.simplejsonrpc.client')
exports('com.github.arteam.simplejsonrpc.client.builder')
exports('com.github.arteam.simplejsonrpc.client.exception')
@@ -394,19 +369,19 @@ extraJavaModuleInfo {
requires('com.fasterxml.jackson.databind')
requires('simple.json.rpc.core')
}
module('simple-json-rpc-server-1.3.jar', 'simple.json.rpc.server', '1.3') {
module('com.github.arteam:simple-json-rpc-server', 'simple.json.rpc.server') {
exports('com.github.arteam.simplejsonrpc.server')
requires('simple.json.rpc.core')
requires('com.google.common')
requires('org.slf4j')
requires('com.fasterxml.jackson.databind')
}
module("bridj${targetName}-0.7-20140918-3.jar", 'com.nativelibs4java.bridj', '0.7-20140918-3') {
module("com.nativelibs4java:bridj${targetName}", 'com.nativelibs4java.bridj') {
exports('org.bridj')
exports('org.bridj.cpp')
requires('java.logging')
}
module("webcam-capture${targetName}-0.3.13-SNAPSHOT.jar", 'com.github.sarxos.webcam.capture', '0.3.13-SNAPSHOT') {
module("com.github.sarxos:webcam-capture${targetName}", 'com.github.sarxos.webcam.capture') {
exports('com.github.sarxos.webcam')
exports('com.github.sarxos.webcam.ds.buildin')
exports('com.github.sarxos.webcam.ds.buildin.natives')
@@ -414,26 +389,19 @@ extraJavaModuleInfo {
requires('com.nativelibs4java.bridj')
requires('org.slf4j')
}
module('centerdevice-nsmenufx-2.1.7.jar', 'centerdevice.nsmenufx', '2.1.7') {
module('de.codecentric.centerdevice:centerdevice-nsmenufx', 'centerdevice.nsmenufx') {
exports('de.codecentric.centerdevice')
requires('javafx.base')
requires('javafx.controls')
requires('javafx.graphics')
}
module('javacsv-2.0.jar', 'net.sourceforge.javacsv', '2.0') {
module('net.sourceforge.javacsv:javacsv', 'net.sourceforge.javacsv') {
exports('com.csvreader')
}
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('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('j2objc-annotations-2.8.jar', 'com.google.j2objc.j2objc.annotations', '2.8')
module('jdbi3-core-3.20.0.jar', 'org.jdbi.v3.core', '3.20.0') {
module('org.jdbi:jdbi3-core', 'org.jdbi.v3.core') {
exports('org.jdbi.v3.core')
exports('org.jdbi.v3.core.mapper')
exports('org.jdbi.v3.core.statement')
@@ -445,10 +413,10 @@ extraJavaModuleInfo {
requires('org.slf4j')
requires('com.github.benmanes.caffeine')
}
module('geantyref-1.3.11.jar', 'io.leangen.geantyref', '1.3.11') {
module('io.leangen.geantyref:geantyref', 'io.leangen.geantyref') {
exports('io.leangen.geantyref')
}
module('richtextfx-0.10.4.jar', 'org.fxmisc.richtext', '0.10.4') {
module('org.fxmisc.richtext:richtextfx', 'org.fxmisc.richtext') {
exports('org.fxmisc.richtext')
exports('org.fxmisc.richtext.event')
exports('org.fxmisc.richtext.model')
@@ -460,20 +428,20 @@ extraJavaModuleInfo {
requires('org.fxmisc.undo.undofx')
requires('org.fxmisc.wellbehaved')
}
module('undofx-2.1.0.jar', 'org.fxmisc.undo.undofx', '2.1.0') {
module('org.fxmisc.undo:undofx', 'org.fxmisc.undo.undofx') {
requires('javafx.base')
requires('javafx.controls')
requires('javafx.graphics')
requires('org.reactfx.reactfx')
}
module('flowless-0.6.1.jar', 'org.fxmisc.flowless', '0.6.1') {
module('org.fxmisc.flowless:flowless', 'org.fxmisc.flowless') {
exports('org.fxmisc.flowless')
requires('javafx.base')
requires('javafx.controls')
requires('javafx.graphics')
requires('org.reactfx.reactfx')
}
module('reactfx-2.0-M5.jar', 'org.reactfx.reactfx', '2.0-M5') {
module('org.reactfx:reactfx', 'org.reactfx.reactfx') {
exports('org.reactfx')
exports('org.reactfx.value')
exports('org.reactfx.collection')
@@ -482,70 +450,42 @@ extraJavaModuleInfo {
requires('javafx.graphics')
requires('javafx.controls')
}
module('rxjavafx-2.2.2.jar', 'io.reactivex.rxjava2fx', '2.2.2') {
module('io.reactivex.rxjava2:rxjavafx', 'io.reactivex.rxjava2fx') {
exports('io.reactivex.rxjavafx.schedulers')
requires('io.reactivex.rxjava2')
requires('javafx.graphics')
}
module('wellbehavedfx-0.3.3.jar', 'org.fxmisc.wellbehaved', '0.3.3') {
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') {
requires('javafx.base')
requires('javafx.graphics')
}
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('com.github.jai-imageio:jai-imageio-core', 'com.github.jai.imageio.jai.imageio.core') {
requires('java.desktop')
}
module('cbor-0.9.jar', 'co.nstant.in.cbor', '0.9') {
module('co.nstant.in:cbor', 'co.nstant.in.cbor') {
exports('co.nstant.in.cbor')
exports('co.nstant.in.cbor.model')
exports('co.nstant.in.cbor.builder')
}
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') {
module('net.sourceforge.streamsupport:streamsupport', 'net.sourceforge.streamsupport') {
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') {
module('net.coobird:thumbnailator', 'net.coobird.thumbnailator') {
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}") {
module("io.matthewnelson.kotlin-components:kmp-tor-jvm", 'kmp.tor.jvm') {
exports('io.matthewnelson.kmp.tor')
requires('kmp.tor.binary.extract.jvm')
requires('kmp.tor.manager.jvm')
@@ -556,24 +496,24 @@ extraJavaModuleInfo {
requires('java.management')
}
if(kmpOs == "linux" && kmpArch == "arm64") {
module("kmp-tor-binary-${kmpOs}${kmpArch}-jvm-${vTor}.jar", "kmp.tor.binary.${kmpOs}${kmpArch}", "${vTor}") {
module("com.sparrowwallet.kmp-tor-binary:kmp-tor-binary-${kmpOs}${kmpArch}-jvm", "kmp.tor.binary.${kmpOs}${kmpArch}") {
exports("io.matthewnelson.kmp.tor.resource.${kmpOs}.${kmpArch}")
exports("kmptor.${kmpOs}.${kmpArch}")
}
} else {
module("kmp-tor-binary-${kmpOs}${kmpArch}-jvm-${vTor}.jar", "kmp.tor.binary.${kmpOs}${kmpArch}", "${vTor}") {
module("io.matthewnelson.kotlin-components:kmp-tor-binary-${kmpOs}${kmpArch}-jvm", "kmp.tor.binary.${kmpOs}${kmpArch}") {
exports("io.matthewnelson.kmp.tor.binary.${kmpOs}.${kmpArch}")
exports("kmptor.${kmpOs}.${kmpArch}")
}
}
module("kmp-tor-binary-extract-jvm-${vTor}.jar", 'kmp.tor.binary.extract.jvm', "${vTor}") {
module("io.matthewnelson.kotlin-components:kmp-tor-binary-extract-jvm", 'kmp.tor.binary.extract.jvm') {
exports('io.matthewnelson.kmp.tor.binary.extract')
exports('io.matthewnelson.kmp.tor.binary.extract.internal')
requires('kotlin.stdlib')
requires("kmp.tor.binary.${kmpOs}${kmpArch}")
requires('kmp.tor.binary.geoip.jvm')
}
module("kmp-tor-manager-jvm-${vKmpTor}.jar", 'kmp.tor.manager.jvm', "${vKmpTor}") {
module("io.matthewnelson.kotlin-components:kmp-tor-manager-jvm", 'kmp.tor.manager.jvm') {
exports('io.matthewnelson.kmp.tor.manager')
exports('io.matthewnelson.kmp.tor.manager.util')
requires('kmp.tor.controller.common.jvm')
@@ -584,7 +524,7 @@ extraJavaModuleInfo {
requires('kmp.tor.controller.jvm')
requires('kmp.tor.common.jvm')
}
module("kmp-tor-manager-common-jvm-${vKmpTor}.jar", 'kmp.tor.manager.common.jvm', "${vKmpTor}") {
module("io.matthewnelson.kotlin-components:kmp-tor-manager-common-jvm", 'kmp.tor.manager.common.jvm') {
exports('io.matthewnelson.kmp.tor.manager.common')
exports('io.matthewnelson.kmp.tor.manager.common.event')
exports('io.matthewnelson.kmp.tor.manager.common.state')
@@ -592,7 +532,7 @@ extraJavaModuleInfo {
requires('kmp.tor.common.jvm')
requires('kotlin.stdlib')
}
module("kmp-tor-controller-common-jvm-${vKmpTor}.jar", 'kmp.tor.controller.common.jvm', "${vKmpTor}") {
module("io.matthewnelson.kotlin-components:kmp-tor-controller-common-jvm", 'kmp.tor.controller.common.jvm') {
exports('io.matthewnelson.kmp.tor.controller.common.config')
exports('io.matthewnelson.kmp.tor.controller.common.file')
exports('io.matthewnelson.kmp.tor.controller.common.control')
@@ -603,12 +543,12 @@ extraJavaModuleInfo {
requires('kotlin.stdlib')
requires('kotlinx.atomicfu')
}
module("kmp-tor-common-jvm-${vKmpTor}.jar", 'kmp.tor.common.jvm', "${vKmpTor}") {
module("io.matthewnelson.kotlin-components:kmp-tor-common-jvm", 'kmp.tor.common.jvm') {
exports('io.matthewnelson.kmp.tor.common.address')
requires('parcelize.jvm')
requires('kotlin.stdlib')
}
module("kmp-tor-controller-jvm-${vKmpTor}.jar", 'kmp.tor.controller.jvm', "${vKmpTor}") {
module("io.matthewnelson.kotlin-components:kmp-tor-controller-jvm", 'kmp.tor.controller.jvm') {
exports('io.matthewnelson.kmp.tor.controller.internal.controller')
requires('kmp.tor.common.jvm')
requires('kmp.tor.controller.common.jvm')
@@ -618,10 +558,10 @@ extraJavaModuleInfo {
requires('encoding.core.jvm')
requires('encoding.base16.jvm')
}
module("kmp-tor-ext-callback-common-jvm-${vKmpTor}.jar", 'kmp.tor.ext.callback.common.jvm', "${vKmpTor}") {
module("io.matthewnelson.kotlin-components:kmp-tor-ext-callback-common-jvm", 'kmp.tor.ext.callback.common.jvm') {
exports('io.matthewnelson.kmp.tor.ext.callback.common')
}
module("kmp-tor-ext-callback-manager-jvm-${vKmpTor}.jar", 'kmp.tor.ext.callback.manager.jvm', "${vKmpTor}") {
module("io.matthewnelson.kotlin-components:kmp-tor-ext-callback-manager-jvm", 'kmp.tor.ext.callback.manager.jvm') {
exports('io.matthewnelson.kmp.tor.ext.callback.manager')
requires('kmp.tor.manager.jvm')
requires('kmp.tor.ext.callback.common.jvm')
@@ -632,15 +572,15 @@ extraJavaModuleInfo {
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}") {
module("io.matthewnelson.kotlin-components:kmp-tor-ext-callback-manager-common-jvm", 'kmp.tor.ext.callback.manager.common.jvm') {
exports('io.matthewnelson.kmp.tor.ext.callback.manager.common')
requires('kmp.tor.ext.callback.controller.common.jvm')
}
module("kmp-tor-ext-callback-controller-common-jvm-${vKmpTor}.jar", 'kmp.tor.ext.callback.controller.common.jvm', "${vKmpTor}") {
module("io.matthewnelson.kotlin-components:kmp-tor-ext-callback-controller-common-jvm", 'kmp.tor.ext.callback.controller.common.jvm') {
exports('io.matthewnelson.kmp.tor.ext.callback.controller.common.control')
exports('io.matthewnelson.kmp.tor.ext.callback.controller.common.control.usecase')
}
module("kmp-tor-binary-geoip-jvm-${vTor}.jar", 'kmp.tor.binary.geoip.jvm', "${vTor}") {
module("io.matthewnelson.kotlin-components:kmp-tor-binary-geoip-jvm", 'kmp.tor.binary.geoip.jvm') {
exports('io.matthewnelson.kmp.tor.binary.geoip')
exports('kmptor')
}
@@ -658,11 +598,18 @@ extraJavaModuleInfo {
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') {
module('org.jcommander:jcommander', 'org.jcommander') {
exports('com.beust.jcommander')
}
module('jzlib-1.1.3.jar', 'com.jcraft.jzlib', '1.1.3') {
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') {
exports('com.jcraft.jzlib')
}
}
-6
View File
@@ -5,7 +5,6 @@ plugins {
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 {
@@ -17,11 +16,6 @@ repositories {
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"
@@ -1,54 +0,0 @@
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));
}
}
@@ -1,52 +0,0 @@
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;
}
}
@@ -1,176 +0,0 @@
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();
}
}
@@ -1,62 +0,0 @@
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;
}
}
+10 -9
View File
@@ -12,17 +12,18 @@ 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 and later, this is Eclipse Temurin 18.0.1+10.
For v1.6.6 to v1.9.1, this was Eclipse Temurin 18.0.1+10. For v2.0.0 and later, Eclipse Temurin 22.0.2+9 is used.
#### Java from Adoptium github repo
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).
It is available for all supported platforms from [Eclipse Temurin 22.0.2+9](https://github.com/adoptium/temurin22-binaries/releases/tag/jdk-22.0.2%2B9).
For reference, the downloads are as follows:
- [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)
- [Linux x64](https://github.com/adoptium/temurin22-binaries/releases/download/jdk-22.0.2%2B9/OpenJDK22U-jdk_x64_linux_hotspot_22.0.2_9.tar.gz)
- [Linux aarch64](https://github.com/adoptium/temurin22-binaries/releases/download/jdk-22.0.2%2B9/OpenJDK22U-jdk_aarch64_linux_hotspot_22.0.2_9.tar.gz)
- [MacOS x64](https://github.com/adoptium/temurin22-binaries/releases/download/jdk-22.0.2%2B9/OpenJDK22U-jdk_x64_mac_hotspot_22.0.2_9.tar.gz)
- [MacOS aarch64](https://github.com/adoptium/temurin22-binaries/releases/download/jdk-22.0.2%2B9/OpenJDK22U-jdk_aarch64_mac_hotspot_22.0.2_9.tar.gz)
- [Windows x64](https://github.com/adoptium/temurin22-binaries/releases/download/jdk-22.0.2%2B9/OpenJDK22U-jdk_x64_windows_hotspot_22.0.2_9.zip)
#### Java from Adoptium deb repo
@@ -57,7 +58,7 @@ echo "deb [signed-by=/usr/share/keyrings/adoptium.asc] https://packages.adoptium
Update cache, install the desired temurin version and configure java to be linked to this same version:
```
sudo apt update -y
sudo apt-get install -y temurin-18-jdk=18.0.1+10
sudo apt-get install -y temurin-22-jdk=22.0.2+9
sudo update-alternatives --config java
```
@@ -67,7 +68,7 @@ A alternative option for all platforms is to use the [sdkman.io](https://sdkman.
See the installation [instructions here](https://sdkman.io/install).
Once installed, run
```shell
sdk install java 18.0.1-tem
sdk install java 22.0.2-tem
```
### Other requirements
@@ -82,7 +83,7 @@ sudo apt install -y rpm fakeroot binutils
First, assign a temporary variable in your shell for the specific release you want to build. For the current one specify:
```shell
GIT_TAG="1.9.1"
GIT_TAG="2.0.0"
```
The project can then be initially cloned as follows:
+1 -1
Submodule drongo updated: f8f50c0dd9...f7d5b4fb8f
Submodule
+1
Submodule lark added at c4c7ca8da3
+1
View File
@@ -1,3 +1,4 @@
rootProject.name = 'sparrow'
include 'drongo'
include 'lark'
@@ -0,0 +1,9 @@
Package: sparrow
Version: 2.1.2-1
Section: utils
Maintainer: Craig Raw <mail@sparrowwallet.com>
Priority: optional
Architecture: arm64
Provides: sparrow
Description: Sparrow
Depends: libc6, zlib1g
@@ -1,5 +1,5 @@
Package: sparrow
Version: 2.0.0-1
Version: 2.1.2-1
Section: utils
Maintainer: Craig Raw <mail@sparrowwallet.com>
Priority: optional
+49
View File
@@ -0,0 +1,49 @@
#!/bin/sh
# postinst script for sparrow
#
# 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/sparrow/lib/sparrow-Sparrow.desktop
xdg-mime install /opt/sparrow/lib/sparrow-Sparrow-MimeInfo.xml
install -D -m 644 /opt/sparrow/lib/runtime/conf/udev/*.rules /etc/udev/rules.d
if ! getent group plugdev > /dev/null; then
groupadd plugdev
fi
if ! groups "${SUDO_USER:-$(whoami)}" | grep -q plugdev; then
usermod -aG plugdev "${SUDO_USER:-$(whoami)}"
fi
if [ -w /sys/devices ] && [ -w /sys/kernel/uevent_seqnum ] && [ -x /bin/udevadm ]; then
/bin/udevadm control --reload
/bin/udevadm trigger
fi
;;
abort-upgrade|abort-remove|abort-deconfigure)
;;
*)
echo "postinst called with unknown argument \`$1'" >&2
exit 1
;;
esac
exit 0
+259
View File
@@ -0,0 +1,259 @@
Summary: Sparrow
Name: sparrow
Version: 2.1.2
Release: 1
License: ASL 2.0
Vendor: Unknown
%if "x" != "x"
URL:
%endif
%if "x/opt" != "x"
Prefix: /opt
%endif
Provides: sparrow
%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
%global __os_install_post %{nil}
%prep
%build
%install
rm -rf %{buildroot}
install -d -m 755 %{buildroot}/opt/sparrow
cp -r %{_sourcedir}/opt/sparrow/* %{buildroot}/opt/sparrow
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/sparrow/lib/sparrow-Sparrow.desktop
xdg-mime install /opt/sparrow/lib/sparrow-Sparrow-MimeInfo.xml
install -D -m 644 /opt/sparrow/lib/runtime/conf/udev/*.rules /etc/udev/rules.d
if ! getent group plugdev > /dev/null; then
groupadd plugdev
fi
if ! groups "${SUDO_USER:-$(whoami)}" | grep -q plugdev; then
usermod -aG plugdev "${SUDO_USER:-$(whoami)}"
fi
if [ -w /sys/devices ] && [ -w /sys/kernel/uevent_seqnum ] && [ -x /bin/udevadm ]; then
/bin/udevadm control --reload
/bin/udevadm trigger
fi
%pre
package_type=rpm
file_belongs_to_single_package ()
{
if [ ! -e "$1" ]; then
false
elif [ "$package_type" = rpm ]; then
test `rpm -q --whatprovides "$1" | wc -l` = 1
elif [ "$package_type" = deb ]; then
test `dpkg -S "$1" | wc -l` = 1
else
exit 1
fi
}
do_if_file_belongs_to_single_package ()
{
local file="$1"
shift
if file_belongs_to_single_package "$file"; then
"$@"
fi
}
if [ "$1" -gt 1 ]; then
:;
fi
%preun
package_type=rpm
file_belongs_to_single_package ()
{
if [ ! -e "$1" ]; then
false
elif [ "$package_type" = rpm ]; then
test `rpm -q --whatprovides "$1" | wc -l` = 1
elif [ "$package_type" = deb ]; then
test `dpkg -S "$1" | wc -l` = 1
else
exit 1
fi
}
do_if_file_belongs_to_single_package ()
{
local file="$1"
shift
if file_belongs_to_single_package "$file"; then
"$@"
fi
}
#
# Remove $1 desktop file from the list of default handlers for $2 mime type
# in $3 file dumping output to stdout.
#
desktop_filter_out_default_mime_handler ()
{
local defaults_list="$3"
local desktop_file="$1"
local mime_type="$2"
awk -f- "$defaults_list" <<EOF
BEGIN {
mime_type="$mime_type"
mime_type_regexp="~" mime_type "="
desktop_file="$desktop_file"
}
\$0 ~ mime_type {
\$0 = substr(\$0, length(mime_type) + 2);
split(\$0, desktop_files, ";")
remaining_desktop_files
counter=0
for (idx in desktop_files) {
if (desktop_files[idx] != desktop_file) {
++counter;
}
}
if (counter) {
printf mime_type "="
for (idx in desktop_files) {
if (desktop_files[idx] != desktop_file) {
printf desktop_files[idx]
if (--counter) {
printf ";"
}
}
}
printf "\n"
}
next
}
{ print }
EOF
}
#
# Remove $2 desktop file from the list of default handlers for $@ mime types
# in $1 file.
# Result is saved in $1 file.
#
desktop_uninstall_default_mime_handler_0 ()
{
local defaults_list=$1
shift
[ -f "$defaults_list" ] || return 0
local desktop_file="$1"
shift
tmpfile1=$(mktemp)
tmpfile2=$(mktemp)
cat "$defaults_list" > "$tmpfile1"
local v
local update=
for mime in "$@"; do
desktop_filter_out_default_mime_handler "$desktop_file" "$mime" "$tmpfile1" > "$tmpfile2"
v="$tmpfile2"
tmpfile2="$tmpfile1"
tmpfile1="$v"
if ! diff -q "$tmpfile1" "$tmpfile2" > /dev/null; then
update=yes
desktop_trace Remove $desktop_file default handler for $mime mime type from $defaults_list file
fi
done
if [ -n "$update" ]; then
cat "$tmpfile1" > "$defaults_list"
desktop_trace "$defaults_list" file updated
fi
rm -f "$tmpfile1" "$tmpfile2"
}
#
# Remove $1 desktop file from the list of default handlers for $@ mime types
# in all known system defaults lists.
#
desktop_uninstall_default_mime_handler ()
{
for f in /usr/share/applications/defaults.list /usr/local/share/applications/defaults.list; do
desktop_uninstall_default_mime_handler_0 "$f" "$@"
done
}
desktop_trace ()
{
echo "$@"
}
do_if_file_belongs_to_single_package /opt/sparrow/lib/sparrow-Sparrow.desktop xdg-desktop-menu uninstall /opt/sparrow/lib/sparrow-Sparrow.desktop
do_if_file_belongs_to_single_package /opt/sparrow/lib/sparrow-Sparrow-MimeInfo.xml xdg-mime uninstall /opt/sparrow/lib/sparrow-Sparrow-MimeInfo.xml
do_if_file_belongs_to_single_package /opt/sparrow/lib/sparrow-Sparrow.desktop desktop_uninstall_default_mime_handler sparrow-Sparrow.desktop application/psbt application/bitcoin-transaction application/pgp-signature x-scheme-handler/bitcoin x-scheme-handler/auth47 x-scheme-handler/lightning
%clean
+3 -1
View File
@@ -21,7 +21,7 @@
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>2.0.0</string>
<string>2.1.2</string>
<key>CFBundleSignature</key>
<string>????</string>
<!-- See https://developer.apple.com/app-store/categories/ for list of AppStore categories -->
@@ -35,6 +35,8 @@
<string>true</string>
<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>
@@ -23,8 +23,8 @@ import com.sparrowwallet.sparrow.io.bbqr.BBQR;
import com.sparrowwallet.sparrow.io.bbqr.BBQRType;
import com.sparrowwallet.sparrow.net.ElectrumServer;
import com.sparrowwallet.sparrow.net.ServerType;
import com.sparrowwallet.sparrow.preferences.PreferenceGroup;
import com.sparrowwallet.sparrow.preferences.PreferencesDialog;
import com.sparrowwallet.sparrow.settings.SettingsGroup;
import com.sparrowwallet.sparrow.settings.SettingsDialog;
import com.sparrowwallet.sparrow.paynym.PayNymDialog;
import com.sparrowwallet.sparrow.transaction.TransactionController;
import com.sparrowwallet.sparrow.transaction.TransactionData;
@@ -150,6 +150,10 @@ public class AppController implements Initializable {
private CheckMenuItem useHdCameraResolution;
private static final BooleanProperty useHdCameraResolutionProperty = new SimpleBooleanProperty();
@FXML
private CheckMenuItem mirrorCameraImage;
private static final BooleanProperty mirrorCameraImageProperty = new SimpleBooleanProperty();
@FXML
private CheckMenuItem showLoadingLog;
private static final BooleanProperty showLoadingLogProperty = new SimpleBooleanProperty();
@@ -274,7 +278,7 @@ public class AppController implements Initializable {
}
void initializeView() {
setPlatformApplicationMenu();
Platform.runLater(this::setPlatformApplicationMenu);
rootStack.getScene().getWindow().setOnHiding(windowEvent -> {
if(searchWalletDialog != null && searchWalletDialog.isShowing()) {
@@ -379,6 +383,8 @@ public class AppController implements Initializable {
hideEmptyUsedAddresses.selectedProperty().bindBidirectional(hideEmptyUsedAddressesProperty);
useHdCameraResolutionProperty.set(Config.get().isHdCapture());
useHdCameraResolution.selectedProperty().bindBidirectional(useHdCameraResolutionProperty);
mirrorCameraImageProperty.set(Config.get().isMirrorCapture());
mirrorCameraImage.selectedProperty().bindBidirectional(mirrorCameraImageProperty);
showTxHexProperty.set(Config.get().isShowTransactionHex());
showTxHex.selectedProperty().bindBidirectional(showTxHexProperty);
showLoadingLogProperty.set(Config.get().isShowLoadingLog());
@@ -431,8 +437,8 @@ public class AppController implements Initializable {
}
private void registerShortcuts() {
org.controlsfx.tools.Platform platform = org.controlsfx.tools.Platform.getCurrent();
if(platform == org.controlsfx.tools.Platform.OSX) {
OsType osType = OsType.getCurrent();
if(osType == OsType.MACOS) {
tabs.getScene().addEventFilter(KeyEvent.KEY_PRESSED, event -> {
if(event.isShortcutDown() && event.isAltDown() && (event.getCode() == KeyCode.LEFT || event.getCode() == KeyCode.RIGHT)) {
int currentIndex = tabs.getSelectionModel().getSelectedIndex();
@@ -447,14 +453,14 @@ public class AppController implements Initializable {
}
private void setPlatformApplicationMenu() {
org.controlsfx.tools.Platform platform = org.controlsfx.tools.Platform.getCurrent();
if(platform == org.controlsfx.tools.Platform.OSX) {
OsType osType = OsType.getCurrent();
if(osType == OsType.MACOS) {
MenuToolkit tk = MenuToolkit.toolkit();
MenuItem preferences = new MenuItem("Preferences...");
preferences.setOnAction(this::openPreferences);
preferences.setAccelerator(new KeyCodeCombination(KeyCode.COMMA, KeyCombination.META_DOWN));
MenuItem settings = new MenuItem("Settings...");
settings.setOnAction(this::openSettings);
settings.setAccelerator(new KeyCodeCombination(KeyCode.COMMA, KeyCombination.META_DOWN));
Menu defaultApplicationMenu = new Menu("Apple", null, tk.createAboutMenuItem(SparrowWallet.APP_NAME, getAboutStage()), new SeparatorMenuItem(),
preferences, new SeparatorMenuItem(),
settings, new SeparatorMenuItem(),
tk.createHideMenuItem(SparrowWallet.APP_NAME), tk.createHideOthersMenuItem(), tk.createUnhideAllMenuItem(), new SeparatorMenuItem(),
tk.createQuitMenuItem(SparrowWallet.APP_NAME));
tk.setApplicationMenu(defaultApplicationMenu);
@@ -462,11 +468,11 @@ public class AppController implements Initializable {
fileMenu.getItems().removeIf(item -> item.getStyleClass().contains("osxHide"));
toolsMenu.getItems().removeIf(item -> item.getStyleClass().contains("osxHide"));
helpMenu.getItems().removeIf(item -> item.getStyleClass().contains("osxHide"));
} else if(platform == org.controlsfx.tools.Platform.WINDOWS) {
} else if(osType == OsType.WINDOWS) {
toolsMenu.getItems().removeIf(item -> item.getStyleClass().contains("windowsHide"));
}
if(platform == org.controlsfx.tools.Platform.UNIX || !TrayManager.isSupported()) {
if(osType == OsType.UNIX || !TrayManager.isSupported()) {
viewMenu.getItems().remove(minimizeToTray);
}
}
@@ -492,7 +498,7 @@ public class AppController implements Initializable {
welcomeDialog.initOwner(rootStack.getScene().getWindow());
Optional<Mode> optionalMode = welcomeDialog.showAndWait();
if(optionalMode.isPresent() && optionalMode.get().equals(Mode.ONLINE)) {
openPreferences(PreferenceGroup.SERVER);
openSettings(SettingsGroup.SERVER);
}
}
@@ -538,7 +544,7 @@ public class AppController implements Initializable {
StackPane root = loader.load();
AboutController controller = loader.getController();
if(org.controlsfx.tools.Platform.getCurrent() == org.controlsfx.tools.Platform.WINDOWS) {
if(OsType.getCurrent() == OsType.WINDOWS) {
root.setBorder(new Border(new BorderStroke(Color.DARKGRAY, BorderStrokeStyle.SOLID, CornerRadii.EMPTY, BorderWidths.DEFAULT)));
}
@@ -566,21 +572,27 @@ public class AppController implements Initializable {
}
public void installUdevRules(ActionEvent event) {
Hwi.EnumerateService enumerateService = new Hwi.EnumerateService(null);
enumerateService.setOnSucceeded(workerStateEvent -> {
Platform.runLater(this::showInstallUdevMessage);
});
enumerateService.setOnFailed(workerStateEvent -> {
Platform.runLater(this::showInstallUdevMessage);
});
enumerateService.start();
}
String commands = """
sudo install -m 644 /opt/sparrow/lib/runtime/conf/udev/*.rules /etc/udev/rules.d
sudo udevadm control --reload
sudo udevadm trigger
sudo groupadd -f plugdev
sudo usermod -aG plugdev `whoami`
""";
String home = System.getProperty(JPACKAGE_APP_PATH);
if(home != null && !home.startsWith("/opt/sparrow") && home.endsWith("bin/Sparrow")) {
home = home.replace("bin/Sparrow", "");
commands = commands.replace("/opt/sparrow/", home);
}
public void showInstallUdevMessage() {
TextAreaDialog dialog = new TextAreaDialog("sudo " + Config.get().getHwi().getAbsolutePath() + " installudevrules", false);
TextAreaDialog dialog = new TextAreaDialog(commands, false);
dialog.initOwner(rootStack.getScene().getWindow());
dialog.setTitle("Install Udev Rules");
dialog.getDialogPane().setHeaderText("Installing udev rules ensures devices can connect over USB.\nThis command requires root privileges.\nOpen a shell and enter the following:");
dialog.setTitle("Install udev Rules");
dialog.getDialogPane().setHeaderText("""
Installing udev rules ensures devices can connect over USB.
These commands require root privileges.
Open a shell and enter the following.
""");
dialog.showAndWait();
}
@@ -590,7 +602,7 @@ public class AppController implements Initializable {
FileChooser fileChooser = new FileChooser();
fileChooser.setTitle("Open Transaction");
fileChooser.getExtensionFilters().addAll(
new FileChooser.ExtensionFilter("All Files", org.controlsfx.tools.Platform.getCurrent().equals(org.controlsfx.tools.Platform.UNIX) ? "*" : "*.*"),
new FileChooser.ExtensionFilter("All Files", OsType.getCurrent().equals(OsType.UNIX) ? "*" : "*.*"),
new FileChooser.ExtensionFilter("PSBT", "*.psbt"),
new FileChooser.ExtensionFilter("TXN", "*.txn")
);
@@ -621,19 +633,10 @@ public class AppController implements Initializable {
byte[] bytes = Files.readAllBytes(file.toPath());
String name = file.getName();
try {
if(Utils.isHex(bytes) || Utils.isBase64(bytes)) {
addTransactionTab(name, file, new String(bytes, StandardCharsets.UTF_8).trim());
} else {
addTransactionTab(name, file, bytes);
} catch(ParseException e) {
ByteArrayInputStream inputStream = new ByteArrayInputStream(bytes);
ByteSource byteSource = new ByteSource() {
@Override
public InputStream openStream() {
return inputStream;
}
};
String text = byteSource.asCharSource(Charsets.UTF_8).read().trim();
addTransactionTab(name, file, text);
}
} catch(IOException e) {
showErrorDialog("Error opening file", e.getMessage());
@@ -798,6 +801,7 @@ public class AppController implements Initializable {
String fileName = ((Label)selectedTab.getGraphic()).getText();
if(fileName != null && !fileName.isEmpty()) {
fileName = fileName.replace('/', '_');
if(!fileName.endsWith(".psbt")) {
fileName += ".psbt";
}
@@ -945,6 +949,11 @@ public class AppController implements Initializable {
Config.get().setHdCapture(item.isSelected());
}
public void mirrorCameraImage(ActionEvent event) {
CheckMenuItem item = (CheckMenuItem)event.getSource();
Config.get().setMirrorCapture(item.isSelected());
}
public void showLoadingLog(ActionEvent event) {
CheckMenuItem item = (CheckMenuItem)event.getSource();
Config.get().setShowLoadingLog(item.isSelected());
@@ -1369,18 +1378,18 @@ public class AppController implements Initializable {
}
}
public void openPreferences(ActionEvent event) {
openPreferences(PreferenceGroup.GENERAL);
public void openSettings(ActionEvent event) {
openSettings(SettingsGroup.GENERAL);
}
public void openServerPreferences(ActionEvent event) {
openPreferences(PreferenceGroup.SERVER);
public void openServerSettings(ActionEvent event) {
openSettings(SettingsGroup.SERVER);
}
private void openPreferences(PreferenceGroup preferenceGroup) {
PreferencesDialog preferencesDialog = new PreferencesDialog(preferenceGroup);
preferencesDialog.initOwner(rootStack.getScene().getWindow());
preferencesDialog.showAndWait();
private void openSettings(SettingsGroup settingsGroup) {
SettingsDialog settingsDialog = new SettingsDialog(settingsGroup);
settingsDialog.initOwner(rootStack.getScene().getWindow());
settingsDialog.showAndWait();
configureSwitchServer();
serverToggle.setDisable(!Config.get().hasServer());
}
@@ -1908,7 +1917,7 @@ public class AppController implements Initializable {
}
private void addTransactionTab(BlockTransaction blockTransaction, TransactionView initialView, Integer initialIndex) {
addTransactionTab(null, null, blockTransaction.getTransaction(), null, blockTransaction, initialView, initialIndex);
addTransactionTab(blockTransaction.getLabel(), null, blockTransaction.getTransaction(), null, blockTransaction, initialView, initialIndex);
}
private void addTransactionTab(String name, File file, Transaction transaction, PSBT psbt, BlockTransaction blockTransaction, TransactionView initialView, Integer initialIndex) {
@@ -2097,7 +2106,7 @@ public class AppController implements Initializable {
FileChooser fileChooser = new FileChooser();
fileChooser.setTitle("Open Image");
fileChooser.getExtensionFilters().addAll(
new FileChooser.ExtensionFilter("All Files", org.controlsfx.tools.Platform.getCurrent().equals(org.controlsfx.tools.Platform.UNIX) ? "*" : "*.*"),
new FileChooser.ExtensionFilter("All Files", OsType.getCurrent().equals(OsType.UNIX) ? "*" : "*.*"),
new FileChooser.ExtensionFilter("Images", "*.png", "*.jpg", "*.jpeg", "*.gif")
);
@@ -2740,8 +2749,8 @@ public class AppController implements Initializable {
@Subscribe
public void connectionFailed(ConnectionFailedEvent event) {
String status = CONNECTION_FAILED_PREFIX + event.getMessage();
Hyperlink hyperlink = new Hyperlink("Server Preferences");
hyperlink.setOnAction(this::openServerPreferences);
Hyperlink hyperlink = new Hyperlink("Server Settings");
hyperlink.setOnAction(this::openServerSettings);
statusUpdated(new StatusEvent(status, hyperlink));
serverToggleStopAnimation();
setTorIcon();
@@ -3135,4 +3144,14 @@ public class AppController implements Initializable {
}
}
}
@Subscribe
public void webcamResolutionChanged(WebcamResolutionChangedEvent event) {
useHdCameraResolutionProperty.set(event.isHdResolution());
}
@Subscribe
public void webcamMirroredChanged(WebcamMirroredChangedEvent event) {
mirrorCameraImageProperty.set(event.isMirrored());
}
}
@@ -3,6 +3,7 @@ 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;
@@ -293,7 +294,7 @@ public class AppServices {
FeeRatesSource feeRatesSource = Config.get().getFeeRatesSource();
feeRatesSource = (feeRatesSource == null ? FeeRatesSource.MEMPOOL_SPACE : feeRatesSource);
if(event instanceof ConnectionEvent && Network.get().equals(Network.MAINNET) && feeRatesSource.isExternal()) {
if(event instanceof ConnectionEvent && feeRatesSource.supportsNetwork(Network.get()) && feeRatesSource.isExternal()) {
EventManager.get().post(new FeeRatesSourceChangedEvent(feeRatesSource));
}
});
@@ -379,7 +380,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(org.controlsfx.tools.Platform.getCurrent() == org.controlsfx.tools.Platform.WINDOWS ? Duration.seconds(RATES_DELAY_SECS_WINDOWS) : Duration.seconds(RATES_DELAY_SECS_DEFAULT));
ratesService.setDelay(OsType.getCurrent() == OsType.WINDOWS ? Duration.seconds(RATES_DELAY_SECS_WINDOWS) : Duration.seconds(RATES_DELAY_SECS_DEFAULT));
ratesService.setPeriod(Duration.seconds(RATES_PERIOD_SECS));
ratesService.setRestartOnFailure(true);
@@ -619,7 +620,7 @@ public class AppServices {
}
private static double getReducedWindowHeight() {
return org.controlsfx.tools.Platform.getCurrent() != org.controlsfx.tools.Platform.OSX ? 802d : 768d; //Check for menu bar of ~34px
return OsType.getCurrent() != OsType.MACOS ? 802d : 768d; //Check for menu bar of ~34px
}
public Application getApplication() {
@@ -1143,7 +1144,7 @@ public class AppServices {
}
public static boolean isOnWayland() {
if(org.controlsfx.tools.Platform.getCurrent() != org.controlsfx.tools.Platform.UNIX) {
if(OsType.getCurrent() != OsType.UNIX) {
return false;
}
@@ -1,5 +1,6 @@
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;
@@ -48,7 +49,7 @@ public class DefaultInteractionServices implements InteractionServices {
}
String[] lines = content.split("\r\n|\r|\n");
if(lines.length > 3 || org.controlsfx.tools.Platform.getCurrent() == org.controlsfx.tools.Platform.WINDOWS) {
if(lines.length > 3 || OsType.getCurrent() == OsType.WINDOWS) {
double numLines = Arrays.stream(lines).mapToDouble(line -> Math.ceil(TextUtils.computeTextWidth(Font.getDefault(), line, 0) / 300)).sum();
alert.getDialogPane().setPrefHeight(200 + numLines * 20);
}
@@ -1,6 +1,7 @@
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;
@@ -9,13 +10,12 @@ 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.preferences.PreferenceGroup;
import com.sparrowwallet.sparrow.preferences.PreferencesDialog;
import com.sparrowwallet.sparrow.settings.SettingsGroup;
import com.sparrowwallet.sparrow.settings.SettingsDialog;
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,10 +42,7 @@ public class SparrowDesktop extends Application {
public void start(Stage stage) throws Exception {
this.mainStage = stage;
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);
initializeFonts();
URL.setURLStreamHandlerFactory(protocol -> WalletIcon.PROTOCOL.equals(protocol) ? new WalletIcon.WalletIconStreamHandler() : null);
AppServices.initialize(this);
@@ -60,8 +57,8 @@ public class SparrowDesktop extends Application {
Config.get().setMode(mode);
if(mode.equals(Mode.ONLINE)) {
PreferencesDialog preferencesDialog = new PreferencesDialog(PreferenceGroup.SERVER, true);
Optional<Boolean> optNewWallet = preferencesDialog.showAndWait();
SettingsDialog settingsDialog = new SettingsDialog(SettingsGroup.SERVER, true);
Optional<Boolean> optNewWallet = settingsDialog.showAndWait();
createNewWallet = optNewWallet.isPresent() && optNewWallet.get();
} else if(Network.get() == Network.MAINNET) {
Config.get().setServerType(ServerType.PUBLIC_ELECTRUM_SERVER);
@@ -75,7 +72,7 @@ public class SparrowDesktop extends Application {
Config.get().setServerType(ServerType.ELECTRUM_SERVER);
}
if(Config.get().getHdCapture() == null && Platform.getCurrent() == Platform.OSX) {
if(Config.get().getHdCapture() == null && OsType.getCurrent() == OsType.MACOS) {
Config.get().setHdCapture(Boolean.TRUE);
}
@@ -117,6 +114,16 @@ public class SparrowDesktop extends Application {
});
}
private void 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);
if(OsType.getCurrent() == OsType.MACOS) {
Font.loadFont(AppServices.class.getResourceAsStream("/font/LiberationSans-Regular.ttf"), 13);
}
}
@Override
public void stop() throws Exception {
AppServices.get().stop();
@@ -18,7 +18,7 @@ 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.0.0";
public static final String APP_VERSION = "2.1.2";
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";
@@ -77,7 +77,7 @@ public class AddressTreeTable extends CoinTreeTable {
getColumns().forEach(col -> col.setContextMenu(contextMenu));
setEditable(true);
setEqualPreferredColumnWidths();
setupColumnWidths();
addressCol.setSortType(TreeTableColumn.SortType.ASCENDING);
getSortOrder().add(addressCol);
@@ -0,0 +1,33 @@
package com.sparrowwallet.sparrow.control;
import com.sparrowwallet.sparrow.AppServices;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.control.Alert;
import javafx.scene.control.ButtonType;
import javafx.scene.control.Label;
import javafx.scene.layout.VBox;
import static com.sparrowwallet.sparrow.AppServices.*;
public class BitBoxPairingDialog extends Alert {
public BitBoxPairingDialog(String code) {
super(AlertType.INFORMATION);
initOwner(getActiveWindow());
setStageIcon(getDialogPane().getScene().getWindow());
getDialogPane().getStylesheets().add(AppServices.class.getResource("general.css").toExternalForm());
setTitle("Confirm BitBox02 Pairing");
setHeaderText(getTitle());
VBox vBox = new VBox(20);
vBox.setAlignment(Pos.CENTER);
vBox.setPadding(new Insets(10, 20, 10, 20));
Label instructions = new Label("Confirm the following code is shown on BitBox02");
Label codeLabel = new Label(code);
codeLabel.getStyleClass().add("fixed-width");
vBox.getChildren().addAll(instructions, codeLabel);
getDialogPane().setContent(vBox);
moveToActiveWindowScreen(this);
getDialogPane().getButtonTypes().clear();
getDialogPane().getButtonTypes().add(ButtonType.CLOSE);
}
}
@@ -1,6 +1,7 @@
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;
@@ -16,7 +17,6 @@ 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(Platform.getCurrent() == Platform.OSX) {
if(OsType.getCurrent() == OsType.MACOS) {
getStyleClass().add("number-field");
}
}
@@ -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,8 +51,14 @@ public class CoinTextFormatter extends TextFormatter<String> {
commasRemoved = newText.length() - noFractionCommaText.length();
}
if(!coinValidation.matcher(noFractionCommaText).matches()) {
return null;
Matcher matcher = coinValidation.matcher(noFractionCommaText);
if(!matcher.matches()) {
matcher.reset();
if(matcher.find()) {
noFractionCommaText = matcher.group();
} else {
return null;
}
}
if(unitFormat.getGroupingSeparator().equals(change.getText())) {
@@ -1,11 +1,15 @@
package com.sparrowwallet.sparrow.control;
import com.sparrowwallet.drongo.BitcoinUnit;
import com.sparrowwallet.drongo.wallet.SortDirection;
import com.sparrowwallet.drongo.wallet.TableType;
import com.sparrowwallet.drongo.wallet.Wallet;
import com.sparrowwallet.drongo.wallet.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;
@@ -13,26 +17,39 @@ import com.sparrowwallet.sparrow.io.Config;
import com.sparrowwallet.sparrow.io.Storage;
import com.sparrowwallet.sparrow.net.ServerType;
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.Hyperlink;
import javafx.scene.control.Label;
import javafx.scene.control.TreeTableColumn;
import javafx.scene.control.TreeTableView;
import javafx.scene.control.*;
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;
}
@@ -137,19 +154,100 @@ public class CoinTreeTable extends TreeTableView<Entry> {
return stackPane;
}
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);
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);
getSortOrder().add(column);
}
}
@SuppressWarnings("deprecation")
protected void setEqualPreferredColumnWidths() {
for(TreeTableColumn<?, ?> column : getColumns()) {
column.setPrefWidth(STANDARD_WIDTH);
private WalletTable.Sort getColumnSort() {
if(getSortOrder().isEmpty() || !getColumns().contains(getSortOrder().getFirst())) {
return new WalletTable.Sort(tableType == TableType.UTXOS ? getColumns().size() - 1 : 0, SortDirection.DESCENDING);
}
return new WalletTable.Sort(getColumns().indexOf(getSortOrder().getFirst()), getSortDirection(getSortOrder().getFirst().getSortType()));
}
private SortDirection getSortDirection(TreeTableColumn.SortType sortType) {
return sortType == TreeTableColumn.SortType.ASCENDING ? SortDirection.ASCENDING : SortDirection.DESCENDING;
}
private WalletTable.Sort getSavedColumnSort() {
if(getRoot() != null && getRoot().getValue() != null && getRoot().getValue().getWallet() != null) {
Wallet wallet = getRoot().getValue().getWallet();
WalletTable walletTable = wallet.getWalletTable(tableType);
if(walletTable != null) {
return walletTable.getSort();
}
}
return null;
}
@SuppressWarnings("deprecation")
protected void setupColumnWidths() {
Double[] savedWidths = getSavedColumnWidths();
for(int i = 0; i < getColumns().size(); i++) {
TreeTableColumn<Entry, ?> column = getColumns().get(i);
column.setPrefWidth(savedWidths != null && getColumns().size() == savedWidths.length ? savedWidths[i] : STANDARD_WIDTH);
}
//TODO: Replace with TreeTableView.CONSTRAINED_RESIZE_POLICY_FLEX_LAST_COLUMN when JavaFX 20+ has headless support
setColumnResizePolicy(TreeTableView.CONSTRAINED_RESIZE_POLICY);
getColumns().getLast().widthProperty().addListener((_, _, _) -> walletTableChanged());
//Ignore initial resizes during layout
walletTableEvents.skip(3, TimeUnit.SECONDS).subscribe(event -> {
event.getWallet().getWalletTables().put(event.getTableType(), event.getWalletTable());
EventManager.get().post(event);
});
}
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;
}
}
@@ -16,11 +16,8 @@ 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.CardApi;
import com.sparrowwallet.sparrow.io.Device;
import com.sparrowwallet.sparrow.io.Hwi;
import com.sparrowwallet.sparrow.io.*;
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
import com.sparrowwallet.sparrow.io.CardAuthorizationException;
import com.sparrowwallet.sparrow.net.ElectrumServer;
import javafx.application.Platform;
import javafx.beans.property.SimpleStringProperty;
@@ -778,10 +775,12 @@ public class DevicePane extends TitledDescriptionPane {
signButton.setDisable(false);
}
} else {
Hwi.SignPSBTService signPSBTService = new Hwi.SignPSBTService(device, passphrase.get(), psbt);
Hwi.SignPSBTService signPSBTService = new Hwi.SignPSBTService(device, passphrase.get(), psbt,
OutputDescriptor.getOutputDescriptor(wallet), wallet.getFullName(), getDeviceRegistration());
signPSBTService.setOnSucceeded(workerStateEvent -> {
PSBT signedPsbt = signPSBTService.getValue();
EventManager.get().post(new PSBTSignedEvent(psbt, signedPsbt));
updateDeviceRegistrations(signPSBTService.getNewDeviceRegistrations());
});
signPSBTService.setOnFailed(workerStateEvent -> {
setError("Signing Error", signPSBTService.getException().getMessage());
@@ -820,10 +819,12 @@ public class DevicePane extends TitledDescriptionPane {
}
private void displayAddress() {
Hwi.DisplayAddressService displayAddressService = new Hwi.DisplayAddressService(device, passphrase.get(), wallet.getScriptType(), outputDescriptor);
Hwi.DisplayAddressService displayAddressService = new Hwi.DisplayAddressService(device, passphrase.get(), wallet.getScriptType(), outputDescriptor,
OutputDescriptor.getOutputDescriptor(wallet), wallet.getFullName(), getDeviceRegistration());
displayAddressService.setOnSucceeded(successEvent -> {
String address = displayAddressService.getValue();
EventManager.get().post(new AddressDisplayedEvent(address));
updateDeviceRegistrations(displayAddressService.getNewDeviceRegistrations());
});
displayAddressService.setOnFailed(failedEvent -> {
setError("Could not display address", displayAddressService.getException().getMessage());
@@ -833,6 +834,26 @@ 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 {
@@ -1,5 +1,6 @@
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;
@@ -24,7 +25,6 @@ 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;
@@ -500,9 +500,9 @@ public class DownloadVerifierDialog extends Dialog<ButtonBar.ButtonData> {
}
private List<String> getReleaseFileExtensions() {
Platform platform = Platform.getCurrent();
switch(platform) {
case OSX -> {
OsType osType = OsType.getCurrent();
switch(osType) {
case MACOS -> {
return MACOS_RELEASE_EXTENSIONS;
}
case WINDOWS -> {
@@ -515,10 +515,10 @@ public class DownloadVerifierDialog extends Dialog<ButtonBar.ButtonData> {
}
private String getReleaseFileExample(String version) {
Platform platform = Platform.getCurrent();
OsType osType = OsType.getCurrent();
String arch = System.getProperty("os.arch");
switch(platform) {
case OSX -> {
switch(osType) {
case MACOS -> {
return "Sparrow-" + version + "-" + arch;
}
case WINDOWS -> {
@@ -1,6 +1,7 @@
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.protocol.*;
@@ -459,7 +460,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);
long blocksFromTip = (long)Math.ceil((double)vSizefromTip / Transaction.MAX_BLOCK_SIZE_VBYTES);
String amount = vSizefromTip + " vB";
if(vSizefromTip > 1000 * 1000) {
@@ -805,18 +806,17 @@ public class EntryCell extends TreeTableCell<Entry, Entry> implements Confirmati
cell.getStyleClass().remove("utxo-row");
cell.getStyleClass().remove("unconfirmed-row");
cell.getStyleClass().remove("summary-row");
cell.getStyleClass().remove("address-cell");
boolean addressCell = 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) {
if(entry instanceof TransactionEntry transactionEntry) {
cell.getStyleClass().add("transaction-row");
TransactionEntry transactionEntry = (TransactionEntry)entry;
if(cell instanceof ConfirmationsListener confirmationsListener) {
if(transactionEntry.isConfirming()) {
cell.getStyleClass().add("confirming");
@@ -825,17 +825,21 @@ 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) {
} else if(entry instanceof UtxoEntry utxoEntry) {
cell.getStyleClass().add("utxo-row");
UtxoEntry utxoEntry = (UtxoEntry)entry;
if(!utxoEntry.isSpendable()) {
cell.getStyleClass().add("unspendable");
}
} else if(entry instanceof HashIndexEntry) {
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) {
cell.getStyleClass().add("hashindex-row");
HashIndexEntry hashIndexEntry = (HashIndexEntry)entry;
if(hashIndexEntry.isSpent()) {
cell.getStyleClass().add("spent");
}
@@ -1,5 +1,6 @@
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;
@@ -10,7 +11,6 @@ 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;
@@ -24,7 +24,7 @@ public class FiatCell extends TreeTableCell<Entry, Number> {
tooltip = new Tooltip();
contextMenu = new FiatContextMenu();
getStyleClass().add("coin-cell");
if(Platform.getCurrent() == Platform.OSX) {
if(OsType.getCurrent() == OsType.MACOS) {
getStyleClass().add("number-field");
}
}
@@ -1,6 +1,7 @@
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;
@@ -24,9 +25,7 @@ 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;
@@ -104,7 +103,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", Platform.getCurrent().equals(Platform.UNIX) ? "*" : "*.*"),
new FileChooser.ExtensionFilter("All Files", OsType.getCurrent().equals(OsType.UNIX) ? "*" : "*.*"),
new FileChooser.ExtensionFilter("JSON", "*.json"),
new FileChooser.ExtensionFilter("TXT", "*.txt")
);
@@ -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(), "Keystore import", importer.getKeystoreImportDescription(getAccount(wallet, requiredDerivation)), "image/" + importer.getWalletModel().getType() + ".png", importer.isKeystoreImportScannable(), importer.isFileFormatAvailable());
super(importer, importer.getName(), "Key import", importer.getKeystoreImportDescription(getAccount(wallet, requiredDerivation)), "image/" + importer.getWalletModel().getType() + ".png", importer.isKeystoreImportScannable(), importer.isFileFormatAvailable());
this.wallet = wallet;
this.importer = importer;
this.requiredDerivation = requiredDerivation;
@@ -1,16 +1,20 @@
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;
@@ -47,6 +51,20 @@ 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
@@ -1,5 +1,6 @@
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;
@@ -57,7 +58,7 @@ public class MempoolSizeFeeRatesChart extends StackedAreaChart<String, Number> {
stage.setResizable(false);
StackPane scenePane = new StackPane();
if(org.controlsfx.tools.Platform.getCurrent() == org.controlsfx.tools.Platform.WINDOWS) {
if(OsType.getCurrent() == OsType.WINDOWS) {
scenePane.setBorder(new Border(new BorderStroke(Color.DARKGRAY, BorderStrokeStyle.SOLID, CornerRadii.EMPTY, BorderWidths.DEFAULT)));
}
@@ -2,6 +2,7 @@ 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;
@@ -537,7 +538,7 @@ public class MessageSignDialog extends Dialog<ButtonBar.ButtonData> {
FileChooser fileChooser = new FileChooser();
fileChooser.setTitle("Open Signed Text File");
fileChooser.getExtensionFilters().addAll(
new FileChooser.ExtensionFilter("All Files", org.controlsfx.tools.Platform.getCurrent().equals(org.controlsfx.tools.Platform.UNIX) ? "*" : "*.*"),
new FileChooser.ExtensionFilter("All Files", OsType.getCurrent().equals(OsType.UNIX) ? "*" : "*.*"),
new FileChooser.ExtensionFilter("Text Files", "*.txt")
);
@@ -1,5 +1,6 @@
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;
@@ -256,7 +257,7 @@ public class MnemonicGridDialog extends Dialog<List<String>> {
FileChooser fileChooser = new FileChooser();
fileChooser.setTitle("Open PDF");
fileChooser.getExtensionFilters().addAll(
new FileChooser.ExtensionFilter("All Files", org.controlsfx.tools.Platform.getCurrent().equals(org.controlsfx.tools.Platform.UNIX) ? "*" : "*.*"),
new FileChooser.ExtensionFilter("All Files", OsType.getCurrent().equals(OsType.UNIX) ? "*" : "*.*"),
new FileChooser.ExtensionFilter("PDF", "*.pdf")
);
@@ -55,7 +55,7 @@ public class MnemonicWalletKeystoreImportPane extends MnemonicKeystorePane {
protected List<Node> createRightButtons() {
discoverButton = new Button("Discover Wallet");
discoverButton.setDisable(true);
discoverButton.setDefaultButton(true);
discoverButton.setDefaultButton(AppServices.onlineProperty().get());
discoverButton.managedProperty().bind(discoverButton.visibleProperty());
discoverButton.setOnAction(event -> {
discoverWallet();
@@ -66,6 +66,7 @@ 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 -> {
@@ -196,6 +197,7 @@ public class MnemonicWalletKeystoreImportPane extends MnemonicKeystorePane {
HBox.setHgrow(region, Priority.SOMETIMES);
Button importMnemonicButton = new Button("Import");
importMnemonicButton.setDefaultButton(true);
importMnemonicButton.setOnAction(event -> {
showHideLink.setVisible(true);
setExpanded(false);
@@ -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(Platform.getCurrent() == Platform.OSX) {
if(OsType.getCurrent() == OsType.MACOS) {
getStyleClass().add("number-field");
}
}
@@ -125,7 +125,10 @@ public class PayNymAvatar extends StackPane {
}
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, false);
Image image = new Image(is, 150, 150, true, true);
if(image.getException() != null) {
throw image.getException();
}
paymentCodeCache.put(cacheId, image);
Platform.runLater(() -> EventManager.get().post(new PayNymImageLoadedEvent(paymentCode, image)));
return image;
@@ -1,10 +1,7 @@
package com.sparrowwallet.sparrow.control;
import com.github.sarxos.webcam.*;
import com.sparrowwallet.drongo.ExtendedKey;
import com.sparrowwallet.drongo.KeyDerivation;
import com.sparrowwallet.drongo.OutputDescriptor;
import com.sparrowwallet.drongo.Utils;
import com.sparrowwallet.drongo.*;
import com.sparrowwallet.drongo.address.Address;
import com.sparrowwallet.drongo.address.P2PKHAddress;
import com.sparrowwallet.drongo.address.P2SHAddress;
@@ -28,6 +25,8 @@ import com.sparrowwallet.hummingbird.URDecoder;
import com.sparrowwallet.hummingbird.registry.pathcomponent.IndexPathComponent;
import com.sparrowwallet.hummingbird.registry.pathcomponent.PathComponent;
import com.sparrowwallet.sparrow.AppServices;
import com.sparrowwallet.sparrow.EventManager;
import com.sparrowwallet.sparrow.event.WebcamResolutionChangedEvent;
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
import com.sparrowwallet.sparrow.io.Config;
import com.sparrowwallet.sparrow.io.bbqr.BBQRDecoder;
@@ -95,7 +94,7 @@ public class QRScanDialog extends Dialog<QRScanDialog.Result> {
this.webcamService = new WebcamService(webcamResolutionProperty.get(), null, new QRScanListener(), new ScanDelayCalculator());
webcamService.setPeriod(Duration.millis(SCAN_PERIOD_MILLIS));
webcamService.setRestartOnFailure(false);
WebcamView webcamView = new WebcamView(webcamService);
WebcamView webcamView = new WebcamView(webcamService, Config.get().isMirrorCapture());
final DialogPane dialogPane = new QRScanDialogPane();
setDialogPane(dialogPane);
@@ -138,7 +137,7 @@ public class QRScanDialog extends Dialog<QRScanDialog.Result> {
while(nested.getCause() != null) {
nested = nested.getCause();
}
if(org.controlsfx.tools.Platform.getCurrent() == org.controlsfx.tools.Platform.WINDOWS &&
if(OsType.getCurrent() == OsType.WINDOWS &&
nested.getMessage().startsWith("Library 'OpenIMAJGrabber' was not loaded successfully from file")) {
exception = new WebcamDependencyException("Your system is missing a dependency required for the webcam. Follow the link below for more details.\n\n[https://sparrowwallet.com/docs/faq.html#your-system-is-missing-a-dependency-for-the-webcam]", exception);
} else if(nested.getMessage().startsWith("Cannot start native grabber") && Config.get().getWebcamDevice() != null) {
@@ -153,6 +152,7 @@ public class QRScanDialog extends Dialog<QRScanDialog.Result> {
webcamResolutionProperty.addListener((observable, oldValue, newResolution) -> {
if(newResolution != null) {
setHeight(newResolution == WebcamResolution.HD ? (getHeight() - 100) : (getHeight() + 100));
EventManager.get().post(new WebcamResolutionChangedEvent(newResolution == WebcamResolution.HD));
}
webcamService.cancel();
});
@@ -6,6 +6,7 @@ import com.sparrowwallet.drongo.KeyPurpose;
import com.sparrowwallet.drongo.Utils;
import com.sparrowwallet.drongo.address.Address;
import com.sparrowwallet.drongo.address.InvalidAddressException;
import com.sparrowwallet.drongo.wallet.TableType;
import com.sparrowwallet.drongo.wallet.Wallet;
import com.sparrowwallet.sparrow.AppServices;
import com.sparrowwallet.sparrow.UnitFormat;
@@ -86,10 +87,10 @@ public class SearchWalletDialog extends Dialog<Entry> {
form.getChildren().add(fieldset);
results = new CoinTreeTable();
results.setTableType(TableType.SEARCH_WALLET);
results.setShowRoot(false);
results.setPrefWidth(showWallet || showAccount ? 950 : 850);
results.setUnitFormat(walletForms.iterator().next().getWallet());
results.setEqualPreferredColumnWidths();
results.setPlaceholder(new Label("No results"));
results.setEditable(true);
@@ -169,7 +170,12 @@ public class SearchWalletDialog extends Dialog<Entry> {
searchWallets(newValue);
});
SearchWalletEntry rootEntry = new SearchWalletEntry(walletForms.getFirst().getWallet(), Collections.emptyList());
RecursiveTreeItem<Entry> rootItem = new RecursiveTreeItem<>(rootEntry, Entry::getChildren);
results.setRoot(rootItem);
setResizable(true);
results.setupColumnWidths();
AppServices.moveToActiveWindowScreen(this);
@@ -2,6 +2,7 @@ package com.sparrowwallet.sparrow.control;
import com.csvreader.CsvReader;
import com.sparrowwallet.drongo.BitcoinUnit;
import com.sparrowwallet.drongo.OsType;
import com.sparrowwallet.drongo.address.Address;
import com.sparrowwallet.drongo.address.InvalidAddressException;
import com.sparrowwallet.drongo.protocol.Transaction;
@@ -20,7 +21,6 @@ import javafx.stage.FileChooser;
import javafx.util.StringConverter;
import org.controlsfx.control.spreadsheet.*;
import org.controlsfx.glyphfont.Glyph;
import org.controlsfx.tools.Platform;
import java.io.*;
import java.nio.charset.StandardCharsets;
@@ -105,7 +105,7 @@ public class SendToManyDialog extends Dialog<List<Payment>> {
SpreadsheetCell amountCell = SpreadsheetCellType.DOUBLE.createCell(row, 1, 1, 1, amount < 0 ? null : amount);
amountCell.setFormat(bitcoinUnit == BitcoinUnit.BTC ? "0.00000000" : "###,###");
amountCell.getStyleClass().add("number-value");
if(Platform.getCurrent() == Platform.OSX) {
if(OsType.getCurrent() == OsType.MACOS) {
amountCell.getStyleClass().add("number-field");
}
list.add(amountCell);
@@ -161,7 +161,7 @@ public class SendToManyDialog extends Dialog<List<Payment>> {
FileChooser fileChooser = new FileChooser();
fileChooser.setTitle("Open CSV");
fileChooser.getExtensionFilters().addAll(
new FileChooser.ExtensionFilter("All Files", org.controlsfx.tools.Platform.getCurrent().equals(org.controlsfx.tools.Platform.UNIX) ? "*" : "*.*"),
new FileChooser.ExtensionFilter("All Files", OsType.getCurrent().equals(OsType.UNIX) ? "*" : "*.*"),
new FileChooser.ExtensionFilter("CSV", "*.csv")
);
@@ -14,8 +14,8 @@ import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority;
import javafx.scene.layout.VBox;
import java.util.Arrays;
import java.util.OptionalDouble;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class TitledDescriptionPane extends TitledPane {
private Label mainLabel;
@@ -127,25 +127,45 @@ public class TitledDescriptionPane extends TitledPane {
}
protected Node getContentBox(String message) {
Label details = new Label(message);
details.setWrapText(true);
HBox contentBox = new HBox();
// Create the VBox to hold text and Hyperlink components
VBox contentBox = new VBox();
contentBox.setAlignment(Pos.TOP_LEFT);
contentBox.getChildren().add(details);
contentBox.setPadding(new Insets(10, 30, 10, 30));
contentBox.setPrefWidth(400); // Set preferred width for wrapping
contentBox.setMinHeight(60);
double width = TextUtils.computeTextWidth(details.getFont(), message, 0.0D);
double numLines = Math.max(1, Math.ceil(width / 400d));
// Define the regex pattern to match URLs
String urlPattern = "(\\[https?://\\S+])";
Pattern pattern = Pattern.compile(urlPattern);
Matcher matcher = pattern.matcher(message);
//Handle long words like txids
OptionalDouble maxWordLength = Arrays.stream(message.split(" ")).mapToDouble(word -> TextUtils.computeTextWidth(details.getFont(), message, 0.0D)).max();
if(maxWordLength.isPresent() && maxWordLength.getAsDouble() > 300.0) {
numLines += 1.0;
// StringBuilder to track the non-URL text
int lastMatchEnd = 0;
// Iterate through the matches and build the components
while (matcher.find()) {
// Add the text before the URL as a normal Label
if (matcher.start() > lastMatchEnd) {
String nonUrlText = message.substring(lastMatchEnd, matcher.start());
Label textLabel = createWrappedLabel(nonUrlText);
contentBox.getChildren().add(textLabel);
}
// Extract the URL and create a Hyperlink for it
String url = matcher.group(1).replaceAll("\\[", "").replaceAll("\\]", "");
Hyperlink hyperlink = createHyperlink(url);
contentBox.getChildren().add(hyperlink);
// Update last match end
lastMatchEnd = matcher.end();
}
double height = Math.max(60, numLines * 20);
contentBox.setPrefHeight(height);
// Add remaining text after the last URL (if any)
if (lastMatchEnd < message.length()) {
String remainingText = message.substring(lastMatchEnd);
Label remainingLabel = createWrappedLabel(remainingText);
contentBox.getChildren().add(remainingLabel);
}
return contentBox;
}
@@ -178,4 +198,21 @@ public class TitledDescriptionPane extends TitledPane {
return account;
}
// Helper method to create a wrapped Label with a specified maxWidth
private Label createWrappedLabel(String text) {
Label label = new Label(text);
label.setWrapText(true);
label.setMaxWidth(400);
return label;
}
// Helper method to create a Hyperlink
private Hyperlink createHyperlink(String url) {
Hyperlink hyperlink = new Hyperlink(url);
hyperlink.setMaxWidth(400); // Set maximum width for wrapping
hyperlink.setWrapText(true); // Ensure text wrapping in the hyperlink
hyperlink.setOnAction(_ -> AppServices.get().getApplication().getHostServices().showDocument(url));
return hyperlink;
}
}
@@ -1,6 +1,7 @@
package com.sparrowwallet.sparrow.control;
import com.google.common.net.HostAndPort;
import com.sparrowwallet.drongo.OsType;
import com.sparrowwallet.sparrow.AppServices;
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
import com.sparrowwallet.sparrow.io.Config;
@@ -14,7 +15,6 @@ import javafx.scene.control.Label;
import javafx.scene.control.Tooltip;
import javafx.util.Duration;
import org.controlsfx.glyphfont.Glyph;
import org.controlsfx.tools.Platform;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -28,7 +28,7 @@ public class TorStatusLabel extends Label {
public TorStatusLabel() {
getStyleClass().add("tor-status");
setPadding(Platform.getCurrent() == Platform.WINDOWS ? new Insets(0, 0, 1, 3) : new Insets(1, 0, 0, 3));
setPadding(OsType.getCurrent() == OsType.WINDOWS ? new Insets(0, 0, 1, 3) : new Insets(1, 0, 0, 3));
setGraphic(getIcon());
update();
}
@@ -62,7 +62,7 @@ public class TorStatusLabel extends Label {
private Node getIcon() {
Glyph adjust = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.ADJUST);
adjust.setFontSize(Platform.getCurrent() == Platform.WINDOWS ? 14 : 15);
adjust.setFontSize(OsType.getCurrent() == OsType.WINDOWS ? 14 : 15);
adjust.setRotate(180);
Glyph bullseye = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.BULLSEYE);
@@ -1,6 +1,7 @@
package com.sparrowwallet.sparrow.control;
import com.sparrowwallet.drongo.KeyPurpose;
import com.sparrowwallet.drongo.OsType;
import com.sparrowwallet.drongo.address.Address;
import com.sparrowwallet.drongo.protocol.Sha256Hash;
import com.sparrowwallet.drongo.protocol.TransactionOutput;
@@ -44,7 +45,6 @@ import javafx.stage.Stage;
import javafx.stage.StageStyle;
import javafx.util.Duration;
import org.controlsfx.glyphfont.Glyph;
import org.controlsfx.tools.Platform;
import javax.imageio.ImageIO;
import java.awt.image.*;
@@ -89,7 +89,7 @@ public class TransactionDiagram extends GridPane {
stage.setResizable(false);
StackPane scenePane = new StackPane();
if(Platform.getCurrent() == Platform.WINDOWS || Platform.getCurrent() == Platform.UNIX) {
if(OsType.getCurrent() == OsType.WINDOWS || OsType.getCurrent() == OsType.UNIX) {
scenePane.setBorder(new Border(new BorderStroke(Color.DARKGRAY, BorderStrokeStyle.SOLID, CornerRadii.EMPTY, BorderWidths.DEFAULT)));
}
@@ -432,8 +432,14 @@ public class TransactionDiagram extends GridPane {
if(walletNode != null) {
inputValue = input.getValue();
Wallet nodeWallet = walletNode.getWallet();
tooltip.setText("Spending " + getSatsValue(inputValue) + " sats from " + (isFinal() ? nodeWallet.getFullDisplayName() : (nodeWallet.isNested() ? nodeWallet.getDisplayName() : "")) + " " + walletNode + "\n" +
input.getHashAsString() + ":" + input.getIndex() + "\n" + walletNode.getAddress());
StringJoiner joiner = new StringJoiner("\n");
joiner.add("Spending " + getSatsValue(inputValue) + " sats from " + (isFinal() ? nodeWallet.getFullDisplayName() : (nodeWallet.isNested() ? nodeWallet.getDisplayName() : "")) + " " + walletNode);
joiner.add(input.getHashAsString() + ":" + input.getIndex());
joiner.add(walletNode.getAddress().toString());
if(input.getLabel() != null) {
joiner.add(input.getLabel());
}
tooltip.setText(joiner.toString());
tooltip.getStyleClass().add("input-label");
if(input.getLabel() == null || input.getLabel().isEmpty()) {
@@ -1,11 +1,11 @@
package com.sparrowwallet.sparrow.control;
import com.sparrowwallet.drongo.wallet.TableType;
import com.sparrowwallet.sparrow.wallet.Entry;
import com.sparrowwallet.sparrow.wallet.TransactionEntry;
import com.sparrowwallet.sparrow.wallet.WalletTransactionsEntry;
import javafx.beans.property.ReadOnlyObjectWrapper;
import javafx.scene.control.TreeTableColumn;
import javafx.scene.control.TreeTableView;
public class TransactionsTreeTable extends CoinTreeTable {
public void initialize(WalletTransactionsEntry rootEntry) {
@@ -49,8 +49,8 @@ public class TransactionsTreeTable extends CoinTreeTable {
setPlaceholder(getDefaultPlaceholder(rootEntry.getWallet()));
setEditable(true);
setEqualPreferredColumnWidths();
setSortColumn(0, TreeTableColumn.SortType.DESCENDING);
setupColumnWidths();
setupColumnSort(0, TreeTableColumn.SortType.DESCENDING);
}
public void updateAll(WalletTransactionsEntry rootEntry) {
@@ -60,13 +60,13 @@ public class TransactionsTreeTable extends CoinTreeTable {
setRoot(rootItem);
rootItem.setExpanded(true);
setSortColumn(0, TreeTableColumn.SortType.DESCENDING);
resetSortColumn();
}
public void updateHistory() {
//Transaction entries should have already been updated using WalletTransactionsEntry.updateHistory, so only a resort required
sort();
setSortColumn(0, TreeTableColumn.SortType.DESCENDING);
resetSortColumn();
}
public void updateLabel(Entry entry) {
@@ -1,5 +1,6 @@
package com.sparrowwallet.sparrow.control;
import com.sparrowwallet.drongo.OsType;
import javafx.application.Platform;
import javafx.stage.Stage;
import org.slf4j.Logger;
@@ -31,7 +32,7 @@ public class TrayManager {
try {
List<Image> imgList = new ArrayList<>();
if(org.controlsfx.tools.Platform.getCurrent() == org.controlsfx.tools.Platform.WINDOWS) {
if(OsType.getCurrent() == OsType.WINDOWS) {
imgList.add(ImageIO.read(getClass().getResource("/image/sparrow-black-small.png")));
imgList.add(ImageIO.read(getClass().getResource("/image/sparrow-black-small@2x.png")));
imgList.add(ImageIO.read(getClass().getResource("/image/sparrow-black-small@3x.png")));
@@ -1,10 +1,10 @@
package com.sparrowwallet.sparrow.control;
import com.sparrowwallet.drongo.wallet.TableType;
import com.sparrowwallet.sparrow.wallet.*;
import javafx.beans.property.ReadOnlyObjectWrapper;
import javafx.scene.control.SelectionMode;
import javafx.scene.control.TreeTableColumn;
import javafx.scene.control.TreeTableView;
import java.util.Comparator;
@@ -82,8 +82,8 @@ public class UtxosTreeTable extends CoinTreeTable {
setPlaceholder(getDefaultPlaceholder(rootEntry.getWallet()));
setEditable(true);
setEqualPreferredColumnWidths();
setSortColumn(getColumns().size() - 1, TreeTableColumn.SortType.DESCENDING);
setupColumnWidths();
setupColumnSort(getColumns().size() - 1, TreeTableColumn.SortType.DESCENDING);
getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE);
}
@@ -95,14 +95,14 @@ public class UtxosTreeTable extends CoinTreeTable {
setRoot(rootItem);
rootItem.setExpanded(true);
setSortColumn(getColumns().size() - 1, TreeTableColumn.SortType.DESCENDING);
resetSortColumn();
}
public void updateHistory() {
//Utxo entries should have already been updated, so only a resort required
if(!getRoot().getChildren().isEmpty()) {
sort();
setSortColumn(getColumns().size() - 1, TreeTableColumn.SortType.DESCENDING);
resetSortColumn();
}
}
@@ -61,7 +61,7 @@ public class WalletImportDialog extends Dialog<Wallet> {
}
List<WalletImport> walletImporters = new ArrayList<>(List.of(new Bip129(), new CaravanMultisig(), new ColdcardMultisig(), new CoboVaultMultisig(), new Electrum(),
new KeystoneMultisig(), new Descriptor(), new SpecterDesktop(), new BlueWalletMultisig(), new Sparrow(), new JadeMultisig(), new PassportMultisig()));
new KeystoneMultisig(), new Descriptor(), new SpecterDesktop(), new BlueWalletMultisig(), new Sparrow(), new JadeMultisig(), new PassportMultisig(), new SpecterDIYMultisig()));
if(!selectedWalletForms.isEmpty()) {
walletImporters.add(new WalletLabels(selectedWalletForms));
}
@@ -10,6 +10,8 @@ import javafx.beans.binding.Bindings;
import javafx.beans.binding.BooleanBinding;
import javafx.geometry.Insets;
import javafx.scene.control.*;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyEvent;
import javafx.scene.layout.HBox;
import javafx.scene.layout.VBox;
import org.controlsfx.control.textfield.CustomTextField;
@@ -56,6 +58,11 @@ public class WalletNameDialog extends Dialog<WalletNameDialog.NameAndBirthDate>
dialogPane.getButtonTypes().addAll(ButtonType.CANCEL);
dialogPane.setPrefWidth(460);
dialogPane.setPrefHeight(requestBirthDate ? 250 : 200);
dialogPane.getScene().addEventFilter(KeyEvent.KEY_PRESSED, event -> {
if (event.getCode() == KeyCode.ESCAPE) {
WalletNameDialog.this.close();
}
});
AppServices.moveToActiveWindowScreen(this);
Glyph wallet = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.WALLET);
@@ -1,5 +1,6 @@
package com.sparrowwallet.sparrow.control;
import com.sparrowwallet.drongo.wallet.TableType;
import com.sparrowwallet.drongo.wallet.Wallet;
import com.sparrowwallet.sparrow.AppServices;
import com.sparrowwallet.sparrow.CurrencyRate;
@@ -54,6 +55,7 @@ public class WalletSummaryDialog extends Dialog<Void> {
HBox hBox = new HBox(40);
CoinTreeTable table = new CoinTreeTable();
table.setTableType(TableType.WALLET_SUMMARY);
TreeTableColumn<Entry, String> nameColumn = new TreeTableColumn<>("Wallet");
nameColumn.setCellValueFactory((TreeTableColumn.CellDataFeatures<Entry, String> param) -> {
@@ -101,7 +103,7 @@ public class WalletSummaryDialog extends Dialog<Void> {
table.setRoot(rootItem);
rootItem.setExpanded(true);
table.setEqualPreferredColumnWidths();
table.setupColumnWidths();
table.setPrefWidth(450);
VBox vBox = new VBox();
@@ -1,8 +1,13 @@
package com.sparrowwallet.sparrow.control;
import com.sparrowwallet.sparrow.EventManager;
import com.sparrowwallet.sparrow.event.WebcamMirroredChangedEvent;
import com.sparrowwallet.sparrow.io.Config;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.scene.Node;
import javafx.scene.control.CheckMenuItem;
import javafx.scene.control.ContextMenu;
import javafx.scene.control.Label;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
@@ -21,12 +26,26 @@ public class WebcamView {
private final ObjectProperty<Image> imageProperty = new SimpleObjectProperty<>(null);
public WebcamView(WebcamService service) {
public WebcamView(WebcamService service, boolean mirrored) {
this.service = service ;
this.imageView = new ImageView();
imageView.setPreserveRatio(true);
// make the cam behave like a mirror:
imageView.setScaleX(-1);
if(mirrored) {
setMirrored(true);
}
ContextMenu contextMenu = new ContextMenu();
CheckMenuItem mirrorItem = new CheckMenuItem("Mirror Camera");
mirrorItem.setSelected(mirrored);
mirrorItem.selectedProperty().addListener((observable, oldValue, newValue) -> {
setMirrored(newValue);
Config.get().setMirrorCapture(newValue);
EventManager.get().post(new WebcamMirroredChangedEvent(newValue));
});
contextMenu.getItems().add(mirrorItem);
imageView.setOnContextMenuRequested(event -> {
contextMenu.show(imageView, event.getScreenX(), event.getScreenY());
});
service.valueProperty().addListener((observable, oldValue, newValue) -> {
if(newValue != null) {
@@ -112,4 +131,8 @@ public class WebcamView {
public Node getView() {
return view;
}
public void setMirrored(boolean mirrored) {
imageView.setScaleX(mirrored ? -1 : 1);
}
}
@@ -0,0 +1,19 @@
package com.sparrowwallet.sparrow.event;
import com.sparrowwallet.drongo.wallet.Keystore;
import com.sparrowwallet.drongo.wallet.Wallet;
import java.util.List;
public class KeystoreDeviceRegistrationsChangedEvent extends WalletChangedEvent {
private final List<Keystore> changedKeystores;
public KeystoreDeviceRegistrationsChangedEvent(Wallet wallet, List<Keystore> changedKeystores) {
super(wallet);
this.changedKeystores = changedKeystores;
}
public List<Keystore> getChangedKeystores() {
return changedKeystores;
}
}
@@ -0,0 +1,22 @@
package com.sparrowwallet.sparrow.event;
import com.sparrowwallet.drongo.wallet.TableType;
import com.sparrowwallet.drongo.wallet.Wallet;
import com.sparrowwallet.drongo.wallet.WalletTable;
public class WalletTableChangedEvent extends WalletChangedEvent {
private final WalletTable walletTable;
public WalletTableChangedEvent(Wallet wallet, WalletTable walletTable) {
super(wallet);
this.walletTable = walletTable;
}
public WalletTable getWalletTable() {
return walletTable;
}
public TableType getTableType() {
return walletTable == null ? null : walletTable.getTableType();
}
}
@@ -0,0 +1,13 @@
package com.sparrowwallet.sparrow.event;
public class WebcamMirroredChangedEvent {
private final boolean mirrored;
public WebcamMirroredChangedEvent(boolean mirrored) {
this.mirrored = mirrored;
}
public boolean isMirrored() {
return mirrored;
}
}
@@ -0,0 +1,13 @@
package com.sparrowwallet.sparrow.event;
public class WebcamResolutionChangedEvent {
private final boolean hdResolution;
public WebcamResolutionChangedEvent(boolean hdResolution) {
this.hdResolution = hdResolution;
}
public boolean isHdResolution() {
return hdResolution;
}
}
@@ -1,6 +1,7 @@
package com.sparrowwallet.sparrow.io;
import com.google.common.base.Throwables;
import com.sparrowwallet.drongo.OsType;
import com.sparrowwallet.drongo.address.Address;
import com.sparrowwallet.drongo.crypto.ChildNumber;
import com.sparrowwallet.drongo.crypto.ECKey;
@@ -13,7 +14,6 @@ import com.sparrowwallet.sparrow.io.ckcard.CkCardApi;
import com.sparrowwallet.sparrow.io.satochip.SatoCardApi;
import javafx.beans.property.StringProperty;
import javafx.concurrent.Service;
import org.controlsfx.tools.Platform;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -171,7 +171,7 @@ public abstract class CardApi {
}
private static void setLibrary() {
if(!initialized && Platform.getCurrent() == Platform.UNIX) {
if(!initialized && OsType.getCurrent() == OsType.UNIX) {
for(File lib : LINUX_PCSC_LIBS) {
if(lib.exists()) {
System.setProperty("sun.security.smartcardio.library", lib.getAbsolutePath());
@@ -54,10 +54,10 @@ public class Config {
private List<File> recentWalletFiles;
private Integer keyDerivationPeriod;
private long dustAttackThreshold = DUST_ATTACK_THRESHOLD_SATS;
private File hwi;
private int enumerateHwPeriod = ENUMERATE_HW_PERIOD_SECS;
private QRDensity qrDensity;
private Boolean hdCapture;
private boolean mirrorCapture = true;
private boolean useZbar = true;
private String webcamDevice;
private ServerType serverType;
@@ -370,15 +370,6 @@ public class Config {
return dustAttackThreshold;
}
public File getHwi() {
return hwi;
}
public void setHwi(File hwi) {
this.hwi = hwi;
flush();
}
public int getEnumerateHwPeriod() {
return enumerateHwPeriod;
}
@@ -405,6 +396,15 @@ public class Config {
flush();
}
public boolean isMirrorCapture() {
return mirrorCapture;
}
public void setMirrorCapture(boolean mirrorCapture) {
this.mirrorCapture = mirrorCapture;
flush();
}
public boolean isUseZbar() {
return useZbar;
}
@@ -1,6 +1,7 @@
package com.sparrowwallet.sparrow.io;
import com.sparrowwallet.drongo.wallet.WalletModel;
import com.sparrowwallet.lark.HardwareClient;
import java.util.Objects;
@@ -122,4 +123,18 @@ public class Device {
public int hashCode() {
return Objects.hash(type, path);
}
public static Device fromHardwareClient(HardwareClient hardwareClient) {
Device device = new Device();
device.type = hardwareClient.getType();
device.path = hardwareClient.getPath();
device.model = hardwareClient.getModel();
device.needsPinSent = hardwareClient.needsPinSent();
device.needsPassphraseSent = hardwareClient.needsPassphraseSent();
device.fingerprint = hardwareClient.fingerprint();
device.card = hardwareClient.card();
device.warnings = hardwareClient.warnings();
device.error = hardwareClient.error();
return device;
}
}
@@ -1,48 +1,47 @@
package com.sparrowwallet.sparrow.io;
import com.google.common.io.ByteStreams;
import com.google.common.io.CharStreams;
import com.google.gson.*;
import com.sparrowwallet.drongo.Network;
import com.sparrowwallet.drongo.ExtendedKey;
import com.sparrowwallet.drongo.OsType;
import com.sparrowwallet.drongo.OutputDescriptor;
import com.sparrowwallet.drongo.protocol.ScriptType;
import com.sparrowwallet.drongo.psbt.PSBT;
import com.sparrowwallet.drongo.psbt.PSBTParseException;
import com.sparrowwallet.drongo.wallet.StandardAccount;
import com.sparrowwallet.drongo.wallet.WalletModel;
import com.sparrowwallet.lark.DeviceException;
import com.sparrowwallet.lark.Lark;
import com.sparrowwallet.lark.bitbox02.BitBoxFileNoiseConfig;
import com.sparrowwallet.sparrow.AppServices;
import com.sparrowwallet.sparrow.control.BitBoxPairingDialog;
import javafx.application.Platform;
import javafx.concurrent.ScheduledService;
import javafx.concurrent.Service;
import javafx.concurrent.Task;
import org.apache.commons.compress.archivers.tar.TarArchiveEntry;
import org.apache.commons.compress.archivers.tar.TarArchiveInputStream;
import org.apache.commons.compress.compressors.bzip2.BZip2CompressorInputStream;
import org.controlsfx.tools.Platform;
import javafx.scene.control.ButtonType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.smartcardio.CardException;
import javax.smartcardio.CardNotPresentException;
import java.io.*;
import java.lang.reflect.Type;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.io.File;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.attribute.PosixFilePermission;
import java.nio.file.attribute.PosixFilePermissions;
import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicBoolean;
public class Hwi {
private static final Logger log = LoggerFactory.getLogger(Hwi.class);
private static final String HWI_HOME_DIR = "hwi";
private static final String HWI_VERSION_PREFIX = "hwi-";
private static final String HWI_VERSION = "3.1.0";
private static final String HWI_VERSION_DIR = HWI_VERSION_PREFIX + HWI_VERSION;
private static final String LARK_HOME_DIR = "lark";
private static final String BITBOX_FILENAME = "bitbox02.json";
private static boolean isPromptActive = false;
private final Set<byte[]> newDeviceRegistrations = new HashSet<>();
static {
deleteHwiDir();
}
public List<Device> enumerate(String passphrase) throws ImportException {
List<Device> devices = new ArrayList<>();
devices.addAll(enumerateUsb(passphrase));
@@ -51,45 +50,13 @@ public class Hwi {
}
private List<Device> enumerateUsb(String passphrase) throws ImportException {
String output = null;
try {
List<String> command;
if(passphrase != null) {
command = new ArrayList<>(List.of(getHwiExecutable(Command.ENUMERATE).getAbsolutePath(), "--password", escape(passphrase), Command.ENUMERATE.toString()));
} else {
command = new ArrayList<>(List.of(getHwiExecutable(Command.ENUMERATE).getAbsolutePath(), Command.ENUMERATE.toString()));
}
addChainType(command, true);
Lark lark = getLark(passphrase);
isPromptActive = true;
output = execute(command);
Device[] devices = getGson().fromJson(output, Device[].class);
if(devices == null) {
throw new ImportException("Error scanning, check devices are ready");
}
//Restore previous (pre v2.2.0) behaviour for Trezor One - don't default to an empty passphrase if one is not supplied
Arrays.stream(devices).filter(device -> device.containsWarning("Using default passphrase of the empty string")).forEach(device -> {
device.setFingerprint(null);
device.setNeedsPassphraseSent(true);
device.setError("Passphrase needs to be specified before the fingerprint information can be retrieved");
});
return Arrays.stream(devices).filter(device -> device != null && device.getModel() != null).collect(Collectors.toList());
} catch(IOException e) {
log.error("Error executing " + HWI_VERSION_DIR, e);
throw new ImportException("Error executing HWI", e);
} catch(Exception e) {
if(output != null) {
try {
JsonObject result = JsonParser.parseString(output).getAsJsonObject();
JsonElement error = result.get("error");
throw new ImportException(error.getAsString());
} catch(Exception ex) {
log.error("Error parsing JSON: " + output, e);
throw new ImportException("Error scanning" + (output.isEmpty() ? ", check devices are ready" : ": " + output), e);
}
}
throw e;
return lark.enumerate().stream().map(Device::fromHardwareClient).toList();
} catch(Throwable e) {
log.error("Error enumerating USB devices", e);
throw new ImportException("Error scanning" + (e.getMessage() == null || e.getMessage().isEmpty() ? ", check devices are ready" : ": " + e.getMessage()), e);
} finally {
isPromptActive = false;
}
@@ -124,31 +91,43 @@ public class Hwi {
public boolean promptPin(Device device) throws ImportException {
try {
String output = execute(getDeviceCommand(device, Command.PROMPT_PIN));
Lark lark = getLark();
boolean result = lark.promptPin(device.getType(), device.getPath());
isPromptActive = true;
return wasSuccessful(output);
} catch(IOException e) {
throw new ImportException(e);
return result;
} catch(DeviceException e) {
throw new ImportException(e.getMessage(), e);
} catch(RuntimeException e) {
log.error("Error prompting pin", e);
throw e;
}
}
public boolean sendPin(Device device, String pin) throws ImportException {
try {
String output = execute(getDeviceCommand(device, Command.SEND_PIN, pin));
Lark lark = getLark();
boolean result = lark.sendPin(device.getType(), device.getPath(), pin);
isPromptActive = false;
return wasSuccessful(output);
} catch(IOException e) {
throw new ImportException(e);
return result;
} catch(DeviceException e) {
throw new ImportException(e.getMessage(), e);
} catch(RuntimeException e) {
log.error("Error sending pin", e);
throw e;
}
}
public boolean togglePassphrase(Device device) throws ImportException {
try {
String output = execute(getDeviceCommand(device, Command.TOGGLE_PASSPHRASE));
Lark lark = getLark();
boolean result = lark.togglePassphrase(device.getType(), device.getPath());
isPromptActive = false;
return wasSuccessful(output);
} catch(IOException e) {
throw new ImportException(e);
return result;
} catch(DeviceException e) {
throw new ImportException(e.getMessage(), e);
} catch(RuntimeException e) {
log.error("Error toggling passphrase", e);
throw e;
}
}
@@ -163,59 +142,36 @@ public class Hwi {
public String getXpub(Device device, String passphrase, String derivationPath) throws ImportException {
try {
String output;
if(passphrase != null && device.getModel().externalPassphraseEntry()) {
output = execute(getDeviceCommand(device, passphrase, Command.GET_XPUB, derivationPath));
} else {
output = execute(getDeviceCommand(device, Command.GET_XPUB, derivationPath));
}
Lark lark = getLark(passphrase);
ExtendedKey xpub = lark.getPubKeyAtPath(device.getType(), device.getPath(), derivationPath);
isPromptActive = false;
JsonObject result = JsonParser.parseString(output).getAsJsonObject();
if(result.get("xpub") != null) {
return result.get("xpub").getAsString();
} else {
JsonElement error = result.get("error");
if(error != null) {
throw new ImportException(error.getAsString());
} else {
throw new ImportException("Could not retrieve xpub - reconnect your device and try again.");
}
}
} catch(IOException e) {
throw new ImportException(e);
return xpub.toString();
} catch(DeviceException e) {
throw new ImportException(e.getMessage(), e);
} catch(RuntimeException e) {
log.error("Error retrieving xpub", e);
throw e;
}
}
public String displayAddress(Device device, String passphrase, ScriptType scriptType, OutputDescriptor outputDescriptor) throws DisplayAddressException {
public String displayAddress(Device device, String passphrase, ScriptType scriptType, OutputDescriptor addressDescriptor,
OutputDescriptor walletDescriptor, String walletName, byte[] walletRegistration) throws DisplayAddressException {
try {
if(!Arrays.asList(ScriptType.ADDRESSABLE_TYPES).contains(scriptType)) {
throw new IllegalArgumentException("Cannot display address for script type " + scriptType + ": Only addressable types supported");
}
String descriptor = outputDescriptor.toString();
isPromptActive = true;
String output;
if(passphrase != null && device.getModel().externalPassphraseEntry()) {
output = execute(getDeviceCommand(device, passphrase, Command.DISPLAY_ADDRESS, "--desc", descriptor));
} else {
output = execute(getDeviceCommand(device, Command.DISPLAY_ADDRESS, "--desc", descriptor));
}
JsonObject result = JsonParser.parseString(output).getAsJsonObject();
if(result.get("address") != null) {
return result.get("address").getAsString();
} else {
JsonElement error = result.get("error");
if(error != null) {
throw new DisplayAddressException(error.getAsString());
} else {
throw new DisplayAddressException("Could not retrieve address");
}
}
} catch(IOException e) {
throw new DisplayAddressException(e);
Lark lark = getLark(passphrase, walletDescriptor, walletName, walletRegistration);
String address = lark.displayAddress(device.getType(), device.getPath(), addressDescriptor);
newDeviceRegistrations.addAll(lark.getWalletRegistrations().values());
newDeviceRegistrations.remove(walletRegistration);
return address;
} catch(DeviceException e) {
throw new DisplayAddressException(e.getMessage(), e);
} catch(RuntimeException e) {
log.error("Error displaying address", e);
throw e;
} finally {
isPromptActive = false;
}
@@ -224,406 +180,76 @@ public class Hwi {
public String signMessage(Device device, String passphrase, String message, String derivationPath) throws SignMessageException {
try {
isPromptActive = true;
String output;
if(passphrase != null && device.getModel().externalPassphraseEntry()) {
output = execute(getDeviceArguments(device, passphrase, Command.SIGN_MESSAGE), Command.SIGN_MESSAGE, message, derivationPath);
} else {
output = execute(getDeviceArguments(device, Command.SIGN_MESSAGE), Command.SIGN_MESSAGE, message, derivationPath);
}
JsonObject result = JsonParser.parseString(output).getAsJsonObject();
if(result.get("signature") != null) {
return result.get("signature").getAsString();
} else {
JsonElement error = result.get("error");
if(error != null) {
throw new SignMessageException(error.getAsString());
} else {
throw new SignMessageException("Could not sign message");
}
}
} catch(IOException e) {
throw new SignMessageException(e);
Lark lark = getLark(passphrase);
return lark.signMessage(device.getType(), device.getPath(), message, derivationPath);
} catch(DeviceException e) {
throw new SignMessageException(e.getMessage(), e);
} catch(RuntimeException e) {
log.error("Error signing message", e);
throw e;
} finally {
isPromptActive = false;
}
}
public PSBT signPSBT(Device device, String passphrase, PSBT psbt) throws SignTransactionException {
public PSBT signPSBT(Device device, String passphrase, PSBT psbt,
OutputDescriptor walletDescriptor, String walletName, byte[] walletRegistration) throws SignTransactionException {
try {
String psbtBase64 = psbt.toBase64String();
isPromptActive = true;
String output;
if(passphrase != null && device.getModel().externalPassphraseEntry()) {
output = execute(getDeviceArguments(device, passphrase, Command.SIGN_TX), Command.SIGN_TX, psbtBase64);
} else {
output = execute(getDeviceArguments(device, Command.SIGN_TX), Command.SIGN_TX, psbtBase64);
}
JsonObject result = JsonParser.parseString(output).getAsJsonObject();
if(result.get("psbt") != null) {
String strPsbt = result.get("psbt").getAsString();
return PSBT.fromString(strPsbt);
} else {
JsonElement error = result.get("error");
if(error != null && error.getAsString().equals("sign_tx canceled")) {
throw new SignTransactionException("Signing cancelled");
} else if(error != null && error.getAsString().equals("open failed")) {
throw new SignTransactionException("Please reconnect your USB device");
} else if(error != null) {
throw new SignTransactionException(error.getAsString());
} else {
throw new SignTransactionException("Could not retrieve PSBT");
}
}
} catch(IOException e) {
throw new SignTransactionException("Could not sign PSBT", e);
} catch(PSBTParseException e) {
throw new SignTransactionException("Could not parse signed PSBT" + (e.getMessage() != null ? ": " + e.getMessage() : ""), e);
Lark lark = getLark(passphrase, walletDescriptor, walletName, walletRegistration);
PSBT signed = lark.signTransaction(device.getType(), device.getPath(), psbt);
newDeviceRegistrations.addAll(lark.getWalletRegistrations().values());
newDeviceRegistrations.remove(walletRegistration);
return signed;
} catch(DeviceException e) {
throw new SignTransactionException(e.getMessage(), e);
} catch(RuntimeException e) {
log.error("Error signing PSBT", e);
throw e;
} finally {
isPromptActive = false;
}
}
private String execute(List<String> command) throws IOException {
long start = System.currentTimeMillis();
Process process = null;
try {
ProcessBuilder processBuilder = new ProcessBuilder(command);
process = processBuilder.start();
return getProcessOutput(process);
} finally {
deleteExtractionOnFailure(process, start);
}
private Lark getLark() {
return getLark(null);
}
private String execute(List<String> arguments, Command command, String... commandArguments) throws IOException {
long start = System.currentTimeMillis();
Process process = null;
try {
List<String> processArguments = new ArrayList<>(arguments);
private Lark getLark(String passphrase) {
return getLark(passphrase, null, null, null);
}
boolean useStdin = Arrays.stream(commandArguments).noneMatch(arg -> arg.contains("\n"));
if(useStdin) {
processArguments.add("--stdin");
private Lark getLark(String passphrase, OutputDescriptor walletDescriptor, String walletName, byte[] walletRegistration) {
Lark lark = new Lark(AppServices.getHttpClientService());
lark.setBitBoxNoiseConfig(new BitBoxFxNoiseConfig());
if(passphrase != null) {
lark.setPassphrase(passphrase);
}
if(walletDescriptor != null && walletName != null) {
if(walletRegistration != null) {
lark.addWalletRegistration(walletDescriptor, walletName, walletRegistration);
} else {
processArguments.add(command.toString());
processArguments.addAll(Arrays.asList(commandArguments));
lark.addWalletName(walletDescriptor, walletName);
}
ProcessBuilder processBuilder = new ProcessBuilder(processArguments);
process = processBuilder.start();
if(useStdin) {
try(BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(process.getOutputStream(), StandardCharsets.UTF_8))) {
writer.write(command.toString());
for(String commandArgument : commandArguments) {
writer.write(" \"");
writer.write(commandArgument.replace("\\", "\\\\").replace("\"", "\\\""));
writer.write("\"");
}
writer.flush();
}
}
return getProcessOutput(process);
} finally {
deleteExtractionOnFailure(process, start);
}
return lark;
}
private String getProcessOutput(Process process) throws IOException {
String output;
try(InputStreamReader reader = new InputStreamReader(process.getInputStream(), StandardCharsets.UTF_8)) {
output = CharStreams.toString(reader);
}
if(output.isEmpty() && process.getErrorStream() != null) {
try(InputStreamReader reader = new InputStreamReader(process.getErrorStream(), StandardCharsets.UTF_8)) {
String errorOutput = CharStreams.toString(reader);
if(!errorOutput.isEmpty()) {
throw new IOException(errorOutput);
}
}
}
return output;
}
private synchronized File getHwiExecutable(Command command) {
File hwiExecutable = Config.get().getHwi();
if(hwiExecutable != null && hwiExecutable.exists()) {
File homeDir = getHwiHomeDir();
String tmpDir = System.getProperty("java.io.tmpdir");
String hwiPath = hwiExecutable.getAbsolutePath();
if(command.isTestFirst() && (hwiPath.contains(tmpDir) || hwiPath.startsWith(homeDir.getAbsolutePath())) && (!hwiPath.contains(HWI_VERSION_DIR) || !testHwi(hwiExecutable))) {
if(Platform.getCurrent() == Platform.OSX) {
IOUtils.deleteDirectory(hwiExecutable.getParentFile());
} else {
hwiExecutable.delete();
}
}
}
if(hwiExecutable == null || !hwiExecutable.exists()) {
try {
Platform platform = Platform.getCurrent();
String osArch = System.getProperty("os.arch");
Set<PosixFilePermission> ownerExecutableWritable = PosixFilePermissions.fromString("rwxr--r--");
//A PyInstaller --onefile expands into a new directory on every run triggering OSX Gatekeeper checks.
//To avoid doing these with every invocation, use a --onedir packaging and expand into a temp folder on OSX
//The check will still happen on first invocation, but will not thereafter
//See https://github.com/bitcoin-core/HWI/issues/327 for details
if(platform == Platform.OSX) {
InputStream inputStream;
if(osArch.equals("aarch64")) {
inputStream = Hwi.class.getResourceAsStream("/native/osx/aarch64/" + HWI_VERSION_DIR + "-mac-aarch64-signed.tar.bz2");
} else {
inputStream = Hwi.class.getResourceAsStream("/native/osx/x64/" + HWI_VERSION_DIR + "-mac-amd64-signed.tar.bz2");
}
if(inputStream == null) {
throw new IllegalStateException("Cannot load " + HWI_VERSION_DIR + " from classpath");
}
File hwiHomeDir = getHwiHomeDir();
File hwiVersionDir = new File(hwiHomeDir, HWI_VERSION_DIR);
IOUtils.deleteDirectory(hwiVersionDir);
if(!hwiVersionDir.exists()) {
Files.createDirectories(hwiVersionDir.toPath(), PosixFilePermissions.asFileAttribute(ownerExecutableWritable));
}
log.debug("Using HWI path: " + hwiVersionDir.getAbsolutePath());
File hwiExec = null;
try(TarArchiveInputStream tarInput = new TarArchiveInputStream(new BZip2CompressorInputStream(inputStream))) {
TarArchiveEntry tarEntry = tarInput.getNextEntry();
while(tarEntry != null) {
if(tarEntry.isDirectory()) {
newDirectory(hwiVersionDir, tarEntry, ownerExecutableWritable);
} else if(tarEntry.isSymbolicLink()) {
newSymlink(hwiVersionDir, tarEntry, ownerExecutableWritable);
} else {
File newFile = newFile(hwiVersionDir, tarEntry, ownerExecutableWritable);
try(FileOutputStream fos = new FileOutputStream(newFile)) {
ByteStreams.copy(tarInput, new FileOutputStream(newFile));
fos.flush();
}
if(tarEntry.getName().equals("hwi")) {
hwiExec = newFile;
}
}
tarEntry = tarInput.getNextEntry();
}
}
hwiExecutable = hwiExec;
} else {
InputStream inputStream;
Path tempExecPath;
if(platform == Platform.WINDOWS) {
Files.createDirectories(getHwiHomeDir().toPath());
inputStream = Hwi.class.getResourceAsStream("/native/windows/x64/hwi.exe");
tempExecPath = Files.createTempFile(getHwiHomeDir().toPath(), HWI_VERSION_DIR, null);
} else if(osArch.equals("aarch64")) {
inputStream = Hwi.class.getResourceAsStream("/native/linux/aarch64/hwi");
tempExecPath = Files.createTempFile(HWI_VERSION_DIR, null, PosixFilePermissions.asFileAttribute(ownerExecutableWritable));
} else {
inputStream = Hwi.class.getResourceAsStream("/native/linux/x64/hwi");
tempExecPath = Files.createTempFile(HWI_VERSION_DIR, null, PosixFilePermissions.asFileAttribute(ownerExecutableWritable));
}
if(inputStream == null) {
throw new IllegalStateException("Cannot load " + HWI_VERSION_DIR + " from classpath");
}
File tempExec = tempExecPath.toFile();
tempExec.deleteOnExit();
try(OutputStream tempExecStream = new BufferedOutputStream(new FileOutputStream(tempExec))) {
ByteStreams.copy(inputStream, tempExecStream);
inputStream.close();
tempExecStream.flush();
};
hwiExecutable = tempExec;
}
} catch(Exception e) {
log.error("Error initializing HWI", e);
}
Config.get().setHwi(hwiExecutable);
}
return hwiExecutable;
}
private File getHwiHomeDir() {
if(Platform.getCurrent() == Platform.OSX || Platform.getCurrent() == Platform.WINDOWS) {
return new File(Storage.getSparrowDir(), HWI_HOME_DIR);
}
return new File(System.getProperty("java.io.tmpdir"));
}
private boolean testHwi(File hwiExecutable) {
private static void deleteHwiDir() {
try {
List<String> command = List.of(hwiExecutable.getAbsolutePath(), "--version");
ProcessBuilder processBuilder = new ProcessBuilder(command);
Process process = processBuilder.start();
int exitValue = process.waitFor();
return exitValue == 0;
} catch (Exception e) {
return false;
}
}
public static File newDirectory(File destinationDir, TarArchiveEntry tarEntry, Set<PosixFilePermission> setFilePermissions) throws IOException {
String destDirPath = destinationDir.getCanonicalPath();
Path path = Path.of(destDirPath, tarEntry.getName());
File destDir = Files.createDirectory(path, PosixFilePermissions.asFileAttribute(setFilePermissions)).toFile();
String destSubDirPath = destDir.getCanonicalPath();
if(!destSubDirPath.startsWith(destDirPath + File.separator)) {
throw new IOException("Entry is outside of the target dir: " + tarEntry.getName());
}
return destDir;
}
public static File newFile(File destinationDir, TarArchiveEntry tarEntry, Set<PosixFilePermission> setFilePermissions) throws IOException {
String destDirPath = destinationDir.getCanonicalPath();
Path path = Path.of(destDirPath, tarEntry.getName());
File destFile = Files.createFile(path, PosixFilePermissions.asFileAttribute(setFilePermissions)).toFile();
String destFilePath = destFile.getCanonicalPath();
if(!destFilePath.startsWith(destDirPath + File.separator)) {
throw new IOException("Entry is outside of the target dir: " + tarEntry.getName());
}
return destFile;
}
public static File newSymlink(File destinationDir, TarArchiveEntry tarEntry, Set<PosixFilePermission> setFilePermissions) throws IOException {
String destDirPath = destinationDir.getCanonicalPath();
Path path = Path.of(destDirPath, tarEntry.getName());
File destFile = Files.createSymbolicLink(path, Paths.get(tarEntry.getLinkName())).toFile();
String destFilePath = destFile.getCanonicalPath();
if(!destFilePath.startsWith(destDirPath + File.separator)) {
throw new IOException("Entry is outside of the target dir: " + tarEntry.getName());
}
return destFile;
}
private boolean wasSuccessful(String output) throws ImportException {
JsonObject result = JsonParser.parseString(output).getAsJsonObject();
if(result.get("error") != null) {
throw new ImportException(result.get("error").getAsString());
}
return result.get("success").getAsBoolean();
}
private void deleteExtractionOnFailure(Process process, long after) {
try {
if(Platform.getCurrent() != Platform.OSX && process != null && process.waitFor(100, TimeUnit.MILLISECONDS) && process.exitValue() != 0) {
File extraction = getTemporaryExtraction(after);
if(extraction != null) {
IOUtils.deleteDirectory(extraction);
if(OsType.getCurrent() == OsType.MACOS || OsType.getCurrent() == OsType.WINDOWS) {
File hwiHomeDir = new File(Storage.getSparrowDir(), HWI_HOME_DIR);
if(hwiHomeDir.exists()) {
IOUtils.deleteDirectory(hwiHomeDir);
}
}
} catch(Exception e) {
log.debug("Error deleting temporary extraction", e);
log.error("Error deleting hwi directory", e);
}
}
private File getTemporaryExtraction(long after) {
File tmpDir = new File(System.getProperty("java.io.tmpdir"));
if(!tmpDir.exists()) {
return null;
}
File[] tmps = tmpDir.listFiles(file -> {
if(!file.isDirectory() || file.lastModified() < after) {
return false;
}
String name = file.getName();
if(name.length() < 9 || !name.startsWith("_MEI")) {
return false;
}
File hwilib = new File(file, "hwilib");
return hwilib.exists();
});
return tmps == null || tmps.length == 0 ? null : Arrays.stream(tmps).sorted(Comparator.comparingLong(File::lastModified)).findFirst().orElse(null);
}
private List<String> getDeviceArguments(Device device, Command command) throws IOException {
List<String> elements = new ArrayList<>(List.of(getHwiExecutable(command).getAbsolutePath(), "--device-path", device.getPath(), "--device-type", device.getType()));
addChainType(elements, false);
return elements;
}
private List<String> getDeviceArguments(Device device, String passphrase, Command command) throws IOException {
List<String> elements = new ArrayList<>(List.of(getHwiExecutable(command).getAbsolutePath(), "--device-path", device.getPath(), "--device-type", device.getType(), "--password", escape(passphrase)));
addChainType(elements, false);
return elements;
}
private List<String> getDeviceCommand(Device device, Command command) throws IOException {
List<String> elements = new ArrayList<>(List.of(getHwiExecutable(command).getAbsolutePath(), "--device-path", device.getPath(), "--device-type", device.getType(), command.toString()));
addChainType(elements, true);
return elements;
}
private List<String> getDeviceCommand(Device device, Command command, String... commandData) throws IOException {
List<String> elements = new ArrayList<>(List.of(getHwiExecutable(command).getAbsolutePath(), "--device-path", device.getPath(), "--device-type", device.getType(), command.toString()));
addChainType(elements, true);
elements.addAll(Arrays.stream(commandData).filter(Objects::nonNull).collect(Collectors.toList()));
return elements;
}
private List<String> getDeviceCommand(Device device, String passphrase, Command command, String... commandData) throws IOException {
List<String> elements = new ArrayList<>(List.of(getHwiExecutable(command).getAbsolutePath(), "--device-path", device.getPath(), "--device-type", device.getType(), "--password", escape(passphrase), command.toString()));
addChainType(elements, true);
elements.addAll(Arrays.stream(commandData).filter(Objects::nonNull).collect(Collectors.toList()));
return elements;
}
private void addChainType(List<String> elements, boolean commandPresent) {
if(Network.get() != Network.MAINNET) {
elements.add(elements.size() - (commandPresent ? 1 : 0), "--chain");
elements.add(elements.size() - (commandPresent ? 1 : 0), getChainName(Network.getCanonical()));
}
}
private String getChainName(Network network) {
if(network == Network.MAINNET) {
return "main";
} else if(network == Network.TESTNET) {
return "test";
}
return network.toString();
}
private String escape(String passphrase) {
Platform platform = Platform.getCurrent();
if(platform == Platform.WINDOWS) {
return passphrase.replace("\"", "\\\"");
}
return passphrase;
}
public static class EnumerateService extends Service<List<Device>> {
private final String passphrase;
@@ -724,13 +350,24 @@ public class Hwi {
private final Device device;
private final String passphrase;
private final ScriptType scriptType;
private final OutputDescriptor outputDescriptor;
private final OutputDescriptor addressDescriptor;
private final OutputDescriptor walletDescriptor;
private final String walletName;
private final byte[] walletRegistration;
private final Set<byte[]> newDeviceRegistrations = new HashSet<>();
public DisplayAddressService(Device device, String passphrase, ScriptType scriptType, OutputDescriptor outputDescriptor) {
public DisplayAddressService(Device device, String passphrase, ScriptType scriptType, OutputDescriptor addressDescriptor, OutputDescriptor walletDescriptor, String walletName, byte[] walletRegistration) {
this.device = device;
this.passphrase = passphrase;
this.scriptType = scriptType;
this.outputDescriptor = outputDescriptor;
this.addressDescriptor = addressDescriptor;
this.walletDescriptor = walletDescriptor;
this.walletName = walletName;
this.walletRegistration = walletRegistration;
}
public Set<byte[]> getNewDeviceRegistrations() {
return newDeviceRegistrations;
}
@Override
@@ -738,7 +375,9 @@ public class Hwi {
return new Task<>() {
protected String call() throws DisplayAddressException {
Hwi hwi = new Hwi();
return hwi.displayAddress(device, passphrase, scriptType, outputDescriptor);
String address = hwi.displayAddress(device, passphrase, scriptType, addressDescriptor, walletDescriptor, walletName, walletRegistration);
newDeviceRegistrations.addAll(hwi.newDeviceRegistrations);
return address;
}
};
}
@@ -816,11 +455,22 @@ public class Hwi {
private final Device device;
private final String passphrase;
private final PSBT psbt;
private final OutputDescriptor walletDescriptor;
private final String walletName;
private final byte[] walletRegistration;
private final Set<byte[]> newDeviceRegistrations = new HashSet<>();
public SignPSBTService(Device device, String passphrase, PSBT psbt) {
public SignPSBTService(Device device, String passphrase, PSBT psbt, OutputDescriptor walletDescriptor, String walletName, byte[] walletRegistration) {
this.device = device;
this.passphrase = passphrase;
this.psbt = psbt;
this.walletDescriptor = walletDescriptor;
this.walletName = walletName;
this.walletRegistration = walletRegistration;
}
public Set<byte[]> getNewDeviceRegistrations() {
return newDeviceRegistrations;
}
@Override
@@ -828,72 +478,61 @@ public class Hwi {
return new Task<>() {
protected PSBT call() throws SignTransactionException {
Hwi hwi = new Hwi();
return hwi.signPSBT(device, passphrase, psbt);
PSBT signed = hwi.signPSBT(device, passphrase, psbt, walletDescriptor, walletName, walletRegistration);
newDeviceRegistrations.addAll(hwi.newDeviceRegistrations);
return signed;
}
};
}
}
public Gson getGson() {
GsonBuilder gsonBuilder = new GsonBuilder().setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES);
gsonBuilder.registerTypeAdapter(WalletModel.class, new DeviceModelSerializer());
gsonBuilder.registerTypeAdapter(WalletModel.class, new DeviceModelDeserializer());
return gsonBuilder.create();
}
private static final class BitBoxFxNoiseConfig extends BitBoxFileNoiseConfig {
private BitBoxPairingDialog pairingDialog;
private static class DeviceModelSerializer implements JsonSerializer<WalletModel> {
@Override
public JsonElement serialize(WalletModel src, Type typeOfSrc, JsonSerializationContext context) {
return new JsonPrimitive(src.toString().toLowerCase(Locale.ROOT));
public BitBoxFxNoiseConfig() {
super(Path.of(Storage.getSparrowHome().getAbsolutePath(), LARK_HOME_DIR, BITBOX_FILENAME).toFile());
}
}
private static class DeviceModelDeserializer implements JsonDeserializer<WalletModel> {
@Override
public WalletModel deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
String modelStr = json.getAsJsonPrimitive().getAsString();
try {
return WalletModel.valueOf(modelStr.toUpperCase(Locale.ROOT).replace(' ', '_'));
} catch(Exception e) {
for(WalletModel model : WalletModel.values()) {
if(modelStr.startsWith(model.getType())) {
return model;
}
public boolean showPairing(String code, DeviceResponse response) throws DeviceException {
CountDownLatch latch = new CountDownLatch(1);
AtomicBoolean confirmedDevice = new AtomicBoolean(false);
Thread showPairingDeviceThread = new Thread(() -> {
try {
isPromptActive = true;
confirmedDevice.set(response.call());
latch.countDown();
} catch(DeviceException e) {
throw new RuntimeException(e);
} finally {
isPromptActive = false;
}
});
showPairingDeviceThread.start();
Platform.runLater(() -> {
pairingDialog = new BitBoxPairingDialog(code);
pairingDialog.initOwner(AppServices.getActiveWindow());
pairingDialog.show();
});
try {
latch.await();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
throw new IllegalArgumentException("Could not determine wallet model for " + modelStr);
}
}
private enum Command {
ENUMERATE("enumerate", true),
PROMPT_PIN("promptpin", true),
SEND_PIN("sendpin", false),
TOGGLE_PASSPHRASE("togglepassphrase", true),
DISPLAY_ADDRESS("displayaddress", true),
SIGN_MESSAGE("signmessage", true),
GET_XPUB("getxpub", true),
SIGN_TX("signtx", true);
Platform.runLater(() -> {
if(pairingDialog != null && pairingDialog.isShowing()) {
pairingDialog.setResult(ButtonType.APPLY);
}
if(!confirmedDevice.get()) {
AppServices.showWarningDialog("Pairing Refused", "Pairing was refused on the device.");
}
});
private final String command;
private final boolean testFirst;
Command(String command, boolean testFirst) {
this.command = command;
this.testFirst = testFirst;
}
public String getCommand() {
return command;
}
public boolean isTestFirst() {
return testFirst;
}
@Override
public String toString() {
return command;
return confirmedDevice.get();
}
}
}
@@ -30,7 +30,7 @@ public class Jade implements KeystoreFileImport {
@Override
public String getKeystoreImportDescription(int account) {
return "Import QR created on your Jade by selecting Xpub Export from the Settings menu once you have loaded your seed.";
return "Import QR created on your Jade by selecting Options > Wallet > Export Xpub once you have loaded your seed. Make sure to select Singlesig as the Wallet type in the Options menu there.";
}
@Override
@@ -1,5 +1,6 @@
package com.sparrowwallet.sparrow.io;
import com.sparrowwallet.drongo.protocol.ScriptType;
import com.sparrowwallet.drongo.wallet.Keystore;
import com.sparrowwallet.drongo.wallet.Wallet;
import com.sparrowwallet.drongo.wallet.WalletModel;
@@ -67,4 +68,24 @@ public class JadeMultisig extends ColdcardMultisig {
return wallet;
}
@Override
public Keystore getKeystore(ScriptType scriptType, InputStream inputStream, String password) throws ImportException {
throw new ImportException("Failed to detect a valid " + scriptType.getDescription() + " keystore.");
}
@Override
public String getKeystoreImportDescription(int account) {
return "Import QR created on your Jade by selecting Options > Wallet > Export Xpub once you have loaded your seed. Make sure to select Multisig as the Wallet type in the Options menu there.";
}
@Override
public boolean isKeystoreImportScannable() {
return true;
}
@Override
public boolean isFileFormatAvailable() {
return false;
}
}
@@ -0,0 +1,19 @@
package com.sparrowwallet.sparrow.io;
import com.sparrowwallet.drongo.wallet.WalletModel;
public class SpecterDIYMultisig extends SpecterDesktop {
@Override
public String getWalletImportDescription() {
return "Import file or QR created by using the Multisig Wallet > Settings > Export Wallet Descriptor feature on your Specter DIY device.";
}
public String getName() {
return "Specter DIY Multisig";
}
@Override
public WalletModel getWalletModel() {
return WalletModel.SPECTER_DIY;
}
}
@@ -12,7 +12,6 @@ import javafx.concurrent.ScheduledService;
import javafx.concurrent.Service;
import javafx.concurrent.Task;
import org.apache.commons.lang3.concurrent.BasicThreadFactory;
import org.controlsfx.tools.Platform;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -646,7 +645,7 @@ public class Storage {
}
private static boolean isWindows() {
return Platform.getCurrent() == Platform.WINDOWS;
return OsType.getCurrent() == OsType.WINDOWS;
}
public static class LoadWalletService extends Service<WalletAndKey> {
@@ -61,6 +61,6 @@ public interface BlockTransactionDao {
}
default String truncate(String label) {
return (label != null && label.length() > 255 ? label.substring(0, 255) : label);
return (label != null && label.length() > BlockTransaction.MAX_LABEL_LENGTH ? label.substring(0, BlockTransaction.MAX_LABEL_LENGTH) : label);
}
}
@@ -268,7 +268,7 @@ public class DbPersistence implements Persistence {
}
if(dirtyPersistables.label != null) {
walletDao.updateLabel(wallet.getId(), dirtyPersistables.label.length() > 255 ? dirtyPersistables.label.substring(0, 255) : dirtyPersistables.label);
walletDao.updateLabel(wallet.getId(), dirtyPersistables.label.length() > Wallet.MAX_LABEL_LENGTH ? dirtyPersistables.label.substring(0, Wallet.MAX_LABEL_LENGTH) : dirtyPersistables.label);
}
if(dirtyPersistables.blockHeight != null) {
@@ -323,6 +323,11 @@ public class DbPersistence implements Persistence {
walletConfigDao.addOrUpdate(wallet, wallet.getWalletConfig());
}
if(dirtyPersistables.walletTable != null) {
WalletTableDao walletTableDao = handle.attach(WalletTableDao.class);
walletTableDao.addOrUpdate(wallet, dirtyPersistables.walletTable.getTableType(), dirtyPersistables.walletTable);
}
if(dirtyPersistables.mixConfig) {
MixConfigDao mixConfigDao = handle.attach(MixConfigDao.class);
mixConfigDao.addOrUpdate(wallet, wallet.getMixConfig());
@@ -355,6 +360,13 @@ public class DbPersistence implements Persistence {
}
}
if(!dirtyPersistables.registrationKeystores.isEmpty()) {
KeystoreDao keystoreDao = handle.attach(KeystoreDao.class);
for(Keystore keystore : dirtyPersistables.registrationKeystores) {
keystoreDao.updateDeviceRegistration(keystore.getDeviceRegistration(), keystore.getId());
}
}
dirtyPersistablesMap.remove(wallet);
} finally {
walletDao.setSchema(DEFAULT_SCHEMA);
@@ -624,7 +636,7 @@ public class DbPersistence implements Persistence {
}
private Flyway getFlyway(Storage storage, String schema, String password, File resourcesDir) throws StorageException {
return Flyway.configure().dataSource(getDataSource(storage, password)).locations("filesystem:" + resourcesDir.getAbsolutePath()).schemas(schema).failOnMissingLocations(true).load();
return Flyway.configure().cleanDisabled(false).dataSource(getDataSource(storage, password)).locations("filesystem:" + resourcesDir.getAbsolutePath()).schemas(schema).failOnMissingLocations(true).load();
}
//Flyway does not support JPMS yet, so the migration files are extracted to a temp dir in order to avoid classloader encapsulation issues
@@ -761,6 +773,13 @@ public class DbPersistence implements Persistence {
}
}
@Subscribe
public void walletTableChanged(WalletTableChangedEvent event) {
if(persistsFor(event.getWallet()) && event.getTableType() != null && event.getWallet().getWalletTable(event.getTableType()) != null) {
updateExecutor.execute(() -> dirtyPersistablesMap.computeIfAbsent(event.getWallet(), key -> new DirtyPersistables()).walletTable = event.getWalletTable());
}
}
@Subscribe
public void walletMixConfigChanged(WalletMixConfigChangedEvent event) {
if(persistsFor(event.getWallet()) && event.getWallet().getMixConfig() != null) {
@@ -792,6 +811,13 @@ public class DbPersistence implements Persistence {
}
}
@Subscribe
public void keystoreDeviceRegistrationsChanged(KeystoreDeviceRegistrationsChangedEvent event) {
if(persistsFor(event.getWallet())) {
updateExecutor.execute(() -> dirtyPersistablesMap.computeIfAbsent(event.getWallet(), key -> new DirtyPersistables()).registrationKeystores.addAll(event.getChangedKeystores()));
}
}
@Subscribe
public void walletWatchLastChanged(WalletWatchLastChangedEvent event) {
if(persistsFor(event.getWallet())) {
@@ -810,11 +836,13 @@ public class DbPersistence implements Persistence {
public final List<Entry> labelEntries = new ArrayList<>();
public final List<BlockTransactionHashIndex> utxoStatuses = new ArrayList<>();
public boolean walletConfig;
public WalletTable walletTable = null;
public boolean mixConfig;
public final Map<Sha256Hash, UtxoMixData> changedUtxoMixes = new HashMap<>();
public final Map<Sha256Hash, UtxoMixData> removedUtxoMixes = new HashMap<>();
public final List<Keystore> labelKeystores = new ArrayList<>();
public final List<Keystore> encryptionKeystores = new ArrayList<>();
public final List<Keystore> registrationKeystores = new ArrayList<>();
public String toString() {
return "Dirty Persistables" +
@@ -830,11 +858,13 @@ public class DbPersistence implements Persistence {
"\nUTXO labels:" + labelEntries.stream().filter(entry -> entry instanceof HashIndexEntry).map(entry -> ((HashIndexEntry)entry).getHashIndex().toString()).collect(Collectors.toList()) +
"\nUTXO statuses:" + utxoStatuses +
"\nWallet config:" + walletConfig +
"\nWallet table:" + walletTable +
"\nMix config:" + mixConfig +
"\nUTXO mixes changed:" + changedUtxoMixes +
"\nUTXO mixes removed:" + removedUtxoMixes +
"\nKeystore labels:" + labelKeystores.stream().map(Keystore::getLabel).collect(Collectors.toList()) +
"\nKeystore encryptions:" + encryptionKeystores.stream().map(Keystore::getLabel).collect(Collectors.toList());
"\nKeystore encryptions:" + encryptionKeystores.stream().map(Keystore::getLabel).collect(Collectors.toList()) +
"\nKeystore registrations:" + registrationKeystores.stream().map(Keystore::getDeviceRegistration).collect(Collectors.toList());
}
}
}
@@ -1,5 +1,6 @@
package com.sparrowwallet.sparrow.io.db;
import com.sparrowwallet.drongo.wallet.Persistable;
import com.sparrowwallet.drongo.wallet.Wallet;
import org.jdbi.v3.sqlobject.config.RegisterRowMapper;
import org.jdbi.v3.sqlobject.statement.SqlBatch;
@@ -26,7 +27,7 @@ public interface DetachedLabelDao {
List<String> labels = new ArrayList<>();
for(Map.Entry<String, String> labelEntry : new HashSet<>(wallet.getDetachedLabels().entrySet())) {
entries.add(truncate(labelEntry.getKey(), 80));
labels.add(truncate(labelEntry.getValue(), 255));
labels.add(truncate(labelEntry.getValue(), Persistable.MAX_LABEL_LENGTH));
}
insertDetachedLabels(entries, labels);
@@ -1,10 +1,7 @@
package com.sparrowwallet.sparrow.io.db;
import com.sparrowwallet.drongo.crypto.EncryptedData;
import com.sparrowwallet.drongo.wallet.DeterministicSeed;
import com.sparrowwallet.drongo.wallet.Keystore;
import com.sparrowwallet.drongo.wallet.MasterPrivateExtendedKey;
import com.sparrowwallet.drongo.wallet.Wallet;
import com.sparrowwallet.drongo.wallet.*;
import org.jdbi.v3.sqlobject.config.RegisterRowMapper;
import org.jdbi.v3.sqlobject.statement.GetGeneratedKeys;
import org.jdbi.v3.sqlobject.statement.SqlQuery;
@@ -13,16 +10,16 @@ import org.jdbi.v3.sqlobject.statement.SqlUpdate;
import java.util.List;
public interface KeystoreDao {
@SqlQuery("select keystore.id, keystore.label, keystore.source, keystore.walletModel, keystore.masterFingerprint, keystore.derivationPath, keystore.extendedPublicKey, keystore.externalPaymentCode, " +
@SqlQuery("select keystore.id, keystore.label, keystore.source, keystore.walletModel, keystore.masterFingerprint, keystore.derivationPath, keystore.extendedPublicKey, keystore.externalPaymentCode, keystore.deviceRegistration, " +
"masterPrivateExtendedKey.id, masterPrivateExtendedKey.privateKey, masterPrivateExtendedKey.chainCode, masterPrivateExtendedKey.initialisationVector, masterPrivateExtendedKey.encryptedBytes, masterPrivateExtendedKey.keySalt, masterPrivateExtendedKey.deriver, masterPrivateExtendedKey.crypter, " +
"seed.id, seed.type, seed.mnemonicString, seed.initialisationVector, seed.encryptedBytes, seed.keySalt, seed.deriver, seed.crypter, seed.needsPassphrase, seed.creationTimeSeconds " +
"from keystore left join masterPrivateExtendedKey on keystore.masterPrivateExtendedKey = masterPrivateExtendedKey.id left join seed on keystore.seed = seed.id where keystore.wallet = ? order by keystore.index asc")
@RegisterRowMapper(KeystoreMapper.class)
List<Keystore> getForWalletId(Long id);
@SqlUpdate("insert into keystore (label, source, walletModel, masterFingerprint, derivationPath, extendedPublicKey, externalPaymentCode, masterPrivateExtendedKey, seed, wallet, index) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)")
@SqlUpdate("insert into keystore (label, source, walletModel, masterFingerprint, derivationPath, extendedPublicKey, externalPaymentCode, deviceRegistration, masterPrivateExtendedKey, seed, wallet, index) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)")
@GetGeneratedKeys("id")
long insert(String label, int source, int walletModel, String masterFingerprint, String derivationPath, String extendedPublicKey, String externalPaymentCode, Long masterPrivateExtendedKey, Long seed, long wallet, int index);
long insert(String label, int source, int walletModel, String masterFingerprint, String derivationPath, String extendedPublicKey, String externalPaymentCode, byte[] deviceRegistration, Long masterPrivateExtendedKey, Long seed, long wallet, int index);
@SqlUpdate("insert into masterPrivateExtendedKey (privateKey, chainCode, initialisationVector, encryptedBytes, keySalt, deriver, crypter, creationTimeSeconds) values (?, ?, ?, ?, ?, ?, ?, ?)")
@GetGeneratedKeys("id")
@@ -41,6 +38,9 @@ public interface KeystoreDao {
@SqlUpdate("update keystore set label = ? where id = ?")
void updateLabel(String label, long id);
@SqlUpdate("update keystore set deviceRegistration = ? where id = ?")
void updateDeviceRegistration(byte[] deviceRegistration, long id);
default void addKeystores(Wallet wallet) {
for(int i = 0; i < wallet.getKeystores().size(); i++) {
Keystore keystore = wallet.getKeystores().get(i);
@@ -73,6 +73,7 @@ public interface KeystoreDao {
keystore.getKeyDerivation().getDerivationPath(),
keystore.hasMasterPrivateKey() || wallet.isBip47() ? null : keystore.getExtendedPublicKey().toString(),
keystore.getExternalPaymentCode() == null ? null : keystore.getExternalPaymentCode().toString(),
keystore.getDeviceRegistration(),
keystore.getMasterPrivateExtendedKey() == null ? null : keystore.getMasterPrivateExtendedKey().getId(),
keystore.getSeed() == null ? null : keystore.getSeed().getId(), wallet.getId(), i);
keystore.setId(id);
@@ -102,6 +103,6 @@ public interface KeystoreDao {
}
default String truncate(String label) {
return (label != null && label.length() > 255 ? label.substring(0, 255) : label);
return (label != null && label.length() > Persistable.MAX_LABEL_LENGTH ? label.substring(0, Persistable.MAX_LABEL_LENGTH) : label);
}
}
@@ -25,6 +25,7 @@ public class KeystoreMapper implements RowMapper<Keystore> {
keystore.setKeyDerivation(new KeyDerivation(rs.getString("keystore.masterFingerprint"), rs.getString("keystore.derivationPath")));
keystore.setExtendedPublicKey(rs.getString("keystore.extendedPublicKey") == null ? null : ExtendedKey.fromDescriptor(rs.getString("keystore.extendedPublicKey")));
keystore.setExternalPaymentCode(rs.getString("keystore.externalPaymentCode") == null ? null : PaymentCode.fromString(rs.getString("keystore.externalPaymentCode")));
keystore.setDeviceRegistration(rs.getBytes("keystore.deviceRegistration"));
if(rs.getBytes("masterPrivateExtendedKey.privateKey") != null) {
MasterPrivateExtendedKey masterPrivateExtendedKey = new MasterPrivateExtendedKey(rs.getBytes("masterPrivateExtendedKey.privateKey"), rs.getBytes("masterPrivateExtendedKey.chainCode"));
@@ -7,7 +7,7 @@ import org.jdbi.v3.core.statement.StatementContext;
import java.sql.ResultSet;
import java.sql.SQLException;
public class WalletConfigMapper implements RowMapper<WalletConfig> {
public class WalletConfigMapper implements RowMapper<WalletConfig> {
@Override
public WalletConfig map(ResultSet rs, StatementContext ctx) throws SQLException {
byte[] iconData = rs.getBytes("iconData");
@@ -33,6 +33,9 @@ public interface WalletDao {
@CreateSqlObject
WalletConfigDao createWalletConfigDao();
@CreateSqlObject
WalletTableDao createWalletTableDao();
@CreateSqlObject
MixConfigDao createMixConfigDao();
@@ -119,6 +122,10 @@ public interface WalletDao {
wallet.getDetachedLabels().putAll(detachedLabels);
wallet.setWalletConfig(createWalletConfigDao().getForWalletId(wallet.getId()));
Map<TableType, WalletTable> walletTables = createWalletTableDao().getForWalletId(wallet.getId());
wallet.getWalletTables().putAll(walletTables);
wallet.setMixConfig(createMixConfigDao().getForWalletId(wallet.getId()));
Map<Sha256Hash, UtxoMixData> utxoMixes = createUtxoMixDataDao().getForWalletId(wallet.getId());
@@ -138,6 +145,7 @@ public interface WalletDao {
createBlockTransactionDao().addBlockTransactions(wallet);
createDetachedLabelDao().clearAndAddAll(wallet);
createWalletConfigDao().addWalletConfig(wallet);
createWalletTableDao().addWalletTables(wallet);
createMixConfigDao().addMixConfig(wallet);
createUtxoMixDataDao().addUtxoMixData(wallet);
} finally {
@@ -146,6 +154,6 @@ public interface WalletDao {
}
default String truncate(String label) {
return (label != null && label.length() > 255 ? label.substring(0, 255) : label);
return (label != null && label.length() > Wallet.MAX_LABEL_LENGTH ? label.substring(0, Wallet.MAX_LABEL_LENGTH) : label);
}
}
@@ -121,6 +121,6 @@ public interface WalletNodeDao {
}
default String truncate(String label) {
return (label != null && label.length() > 255 ? label.substring(0, 255) : label);
return (label != null && label.length() > WalletNode.MAX_LABEL_LENGTH ? label.substring(0, WalletNode.MAX_LABEL_LENGTH) : label);
}
}
@@ -0,0 +1,52 @@
package com.sparrowwallet.sparrow.io.db;
import com.sparrowwallet.drongo.wallet.TableType;
import com.sparrowwallet.drongo.wallet.Wallet;
import com.sparrowwallet.drongo.wallet.WalletTable;
import org.jdbi.v3.sqlobject.config.RegisterRowMapper;
import org.jdbi.v3.sqlobject.statement.GetGeneratedKeys;
import org.jdbi.v3.sqlobject.statement.SqlQuery;
import org.jdbi.v3.sqlobject.statement.SqlUpdate;
import java.util.HashMap;
import java.util.Map;
public interface WalletTableDao {
@SqlQuery("select id, type, widths, sortColumn, sortDirection from walletTable where wallet = ?")
@RegisterRowMapper(WalletTableMapper.class)
Map<TableType, WalletTable> getForWalletId(Long id);
@SqlQuery("select id, type, widths, sortColumn, sortDirection from walletTable where type = ?")
@RegisterRowMapper(WalletTableMapper.class)
Map<TableType, WalletTable> getForTypeId(int tableTypeId);
@SqlUpdate("insert into walletTable (type, widths, sortColumn, sortDirection, wallet) values (?, ?, ?, ?, ?)")
@GetGeneratedKeys("id")
long insertWalletTable(int tableType, Double[] widths, int sortColumn, int sortDirection, long wallet);
@SqlUpdate("update walletTable set type = ?, widths = ?, sortColumn = ?, sortDirection = ?, wallet = ? where id = ?")
void updateWalletTable(int tableType, Double[] widths, int sortColumn, int sortDirection, long wallet, long id);
default void addWalletTables(Wallet wallet) {
Map<TableType, WalletTable> walletTables = new HashMap<>(wallet.getWalletTables());
for(Map.Entry<TableType, WalletTable> tableEntry : walletTables.entrySet()) {
tableEntry.getValue().setId(null);
addOrUpdate(wallet, tableEntry.getKey(), tableEntry.getValue());
}
}
default void addOrUpdate(Wallet wallet, TableType tableType, WalletTable walletTable) {
Map<TableType, WalletTable> existing = getForTypeId(tableType.ordinal());
if(existing.isEmpty() && walletTable.getId() == null) {
long id = insertWalletTable(walletTable.getTableType().ordinal(), walletTable.getWidths(),
walletTable.getSortColumn(), walletTable.getSortDirection().ordinal(), wallet.getId());
walletTable.setId(id);
} else {
Long existingId = existing.get(tableType) != null ? existing.get(tableType).getId() : walletTable.getId();
updateWalletTable(walletTable.getTableType().ordinal(), walletTable.getWidths(),
walletTable.getSortColumn(), walletTable.getSortDirection().ordinal(), wallet.getId(), existingId);
walletTable.setId(existingId);
}
}
}
@@ -0,0 +1,43 @@
package com.sparrowwallet.sparrow.io.db;
import com.sparrowwallet.drongo.wallet.SortDirection;
import com.sparrowwallet.drongo.wallet.TableType;
import com.sparrowwallet.drongo.wallet.WalletTable;
import org.jdbi.v3.core.mapper.RowMapper;
import org.jdbi.v3.core.statement.StatementContext;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.Arrays;
import java.util.Map;
public class WalletTableMapper implements RowMapper<Map.Entry<TableType, WalletTable>> {
@Override
public Map.Entry<TableType, WalletTable> map(ResultSet rs, StatementContext ctx) throws SQLException {
TableType tableType = TableType.values()[rs.getInt("type")];
Object[] objWidths = (Object[])rs.getArray("widths").getArray();
Double[] widths = Arrays.copyOf(objWidths, objWidths.length, Double[].class);
int sortColumn = rs.getInt("sortColumn");
SortDirection sortDirection = SortDirection.values()[rs.getInt("sortDirection")];
WalletTable walletTable = new WalletTable(tableType, widths, sortColumn, sortDirection);
walletTable.setId(rs.getLong("id"));
return new Map.Entry<>() {
@Override
public TableType getKey() {
return tableType;
}
@Override
public WalletTable getValue() {
return walletTable;
}
@Override
public WalletTable setValue(WalletTable value) {
return null;
}
};
}
}
@@ -164,8 +164,9 @@ public class SatoCardApi extends CardApi {
for(PSBTInput psbtInput : psbt.getPsbtInputs()) {
if(!psbtInput.isSigned()) {
WalletNode signingNode = signingNodes.get(psbtInput);
String fullPath = null;
List<Keystore> keystores = wallet.getKeystores();
// recover derivation path from Satochip keystore
String fullPath = null;
for(int i = 0; i < keystores.size(); i++) {
Keystore keystore = keystores.get(i);
WalletModel walletModel = keystore.getWalletModel();
@@ -173,11 +174,16 @@ public class SatoCardApi extends CardApi {
String basePath = keystore.getKeyDerivation().getDerivationPath();
String extendedPath = signingNode.getDerivationPath().substring(1);
fullPath = basePath + extendedPath;
keystore.getPubKey(signingNode);
break;
}
}
if (fullPath == null) {
// recover a default derivation path from first keystore
Keystore keystore = keystores.get(0);
String basePath = keystore.getKeyDerivation().getDerivationPath();
String extendedPath = signingNode.getDerivationPath().substring(1);
fullPath = basePath + extendedPath;
}
psbtInput.sign(new CardPSBTInputSigner(signingNode, fullPath));
}
}
@@ -28,7 +28,7 @@ public class HwAirgappedController extends KeystoreImportDetailController {
if(getMasterController().getWallet().getPolicyType().equals(PolicyType.SINGLE)) {
fileImporters = List.of(new ColdcardSinglesig(), new CoboVaultSinglesig(), new Jade(), new KeystoneSinglesig(), new PassportSinglesig(), new SeedSigner(), new GordianSeedTool(), new SpecterDIY(), new Krux(), new AirGapVault());
} else if(getMasterController().getWallet().getPolicyType().equals(PolicyType.MULTI)) {
fileImporters = List.of(new Bip129(), new ColdcardMultisig(), new CoboVaultMultisig(), new Jade(), new KeystoneMultisig(), new PassportMultisig(), new SeedSigner(), new GordianSeedTool(), new SpecterDIY(), new Krux());
fileImporters = List.of(new Bip129(), new ColdcardMultisig(), new CoboVaultMultisig(), new JadeMultisig(), new KeystoneMultisig(), new PassportMultisig(), new SeedSigner(), new GordianSeedTool(), new SpecterDIY(), new Krux());
}
for(KeystoreFileImport importer : fileImporters) {
@@ -25,13 +25,11 @@ public class BatchedElectrumServerRpc implements ElectrumServerRpc {
static final int RETRY_DELAY_SECS = 1;
private final AtomicLong idCounter;
private final int maxTargetBlocks;
public BatchedElectrumServerRpc() {
this.idCounter = new AtomicLong();
}
public BatchedElectrumServerRpc(long idCounterValue) {
public BatchedElectrumServerRpc(long idCounterValue, int maxTargetBlocks) {
this.idCounter = new AtomicLong(idCounterValue);
this.maxTargetBlocks = maxTargetBlocks;
}
@Override
@@ -222,11 +220,19 @@ public class BatchedElectrumServerRpc implements ElectrumServerRpc {
public Map<Integer, Double> getFeeEstimates(Transport transport, List<Integer> targetBlocks) {
PagedBatchRequestBuilder<Integer, Double> batchRequest = PagedBatchRequestBuilder.create(transport, idCounter).keysType(Integer.class).returnType(Double.class);
for(Integer targetBlock : targetBlocks) {
batchRequest.add(targetBlock, "blockchain.estimatefee", targetBlock);
if(targetBlock <= maxTargetBlocks) {
batchRequest.add(targetBlock, "blockchain.estimatefee", targetBlock);
}
}
try {
return batchRequest.execute();
Map<Integer, Double> result = batchRequest.execute();
for(Integer targetBlock : targetBlocks) {
if(targetBlock > maxTargetBlocks) {
result.put(targetBlock, result.values().stream().mapToDouble(v -> v).min().orElse(0.0001d));
}
}
return result;
} catch(JsonRpcBatchException e) {
throw new ElectrumServerRpcException("Error getting fee estimates from connected server: " + e.getErrors(), e);
} catch(Exception e) {
@@ -6,7 +6,7 @@ import com.sparrowwallet.drongo.Utils;
import com.sparrowwallet.drongo.protocol.Sha256Hash;
import com.sparrowwallet.drongo.protocol.Transaction;
import com.sparrowwallet.sparrow.AppServices;
import com.sparrowwallet.sparrow.net.http.client.HttpResponseException;
import com.sparrowwallet.tern.http.client.HttpResponseException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -5,6 +5,7 @@ import com.google.gson.Gson;
import com.google.gson.annotations.SerializedName;
import com.sparrowwallet.drongo.KeyPurpose;
import com.sparrowwallet.drongo.Network;
import com.sparrowwallet.drongo.OsType;
import com.sparrowwallet.drongo.OutputDescriptor;
import com.sparrowwallet.drongo.protocol.ScriptType;
import com.sparrowwallet.drongo.wallet.BlockTransactionHash;
@@ -42,13 +43,13 @@ public class Bwt {
public synchronized static void initialize() {
if(!initialized) {
try {
org.controlsfx.tools.Platform platform = org.controlsfx.tools.Platform.getCurrent();
OsType osType = OsType.getCurrent();
String osArch = System.getProperty("os.arch");
if(platform == org.controlsfx.tools.Platform.OSX && osArch.equals("aarch64")) {
if(osType == OsType.MACOS && osArch.equals("aarch64")) {
NativeUtils.loadLibraryFromJar("/native/osx/aarch64/libbwt_jni.dylib");
} else if(platform == org.controlsfx.tools.Platform.OSX) {
} else if(osType == OsType.MACOS) {
NativeUtils.loadLibraryFromJar("/native/osx/x64/libbwt_jni.dylib");
} else if(platform == org.controlsfx.tools.Platform.WINDOWS) {
} else if(osType == OsType.WINDOWS) {
NativeUtils.loadLibraryFromJar("/native/windows/x64/bwt_jni.dll");
} else if(osArch.equals("aarch64")) {
NativeUtils.loadLibraryFromJar("/native/linux/aarch64/libbwt_jni.so");
@@ -0,0 +1,11 @@
package com.sparrowwallet.sparrow.net;
public class ConfigurationException extends RuntimeException {
public ConfigurationException(String message) {
super(message);
}
public ConfigurationException(String message, Throwable cause) {
super(message, cause);
}
}
@@ -5,6 +5,7 @@ import com.google.common.net.HostAndPort;
import com.sparrowwallet.drongo.KeyPurpose;
import com.sparrowwallet.drongo.Network;
import com.sparrowwallet.drongo.Utils;
import com.sparrowwallet.drongo.Version;
import com.sparrowwallet.drongo.address.Address;
import com.sparrowwallet.drongo.bip47.InvalidPaymentCodeException;
import com.sparrowwallet.drongo.bip47.PaymentCode;
@@ -46,6 +47,8 @@ public class ElectrumServer {
private static final Version FULCRUM_MIN_BATCHING_VERSION = new Version("1.6.0");
private static final Version MEMPOOL_ELECTRS_MIN_BATCHING_VERSION = new Version("3.1.0");
public static final String CORE_ELECTRUM_HOST = "127.0.0.1";
private static final int MINIMUM_BROADCASTS = 2;
@@ -823,7 +826,7 @@ public class ElectrumServer {
if(AppServices.getTargetBlockFeeRates() != null) {
targetBlocksFeeRatesSats.putAll(AppServices.getTargetBlockFeeRates());
}
} else if(Network.get().equals(Network.MAINNET)) {
} else if(feeRatesSource.supportsNetwork(Network.get())) {
targetBlocksFeeRatesSats.putAll(feeRatesSource.getBlockTargetFeeRates(targetBlocksFeeRatesSats));
}
@@ -1020,15 +1023,15 @@ public class ElectrumServer {
existingStatuses.add(status);
}
public static boolean supportsBatching(List<String> serverVersion) {
if(serverVersion.size() > 0) {
String server = serverVersion.get(0).toLowerCase(Locale.ROOT);
public static ServerCapability getServerCapability(List<String> serverVersion) {
if(!serverVersion.isEmpty()) {
String server = serverVersion.getFirst().toLowerCase(Locale.ROOT);
if(server.contains("electrumx")) {
return true;
return new ServerCapability(true);
}
if(server.startsWith("cormorant")) {
return true;
return new ServerCapability(true);
}
if(server.startsWith("electrs/")) {
@@ -1040,7 +1043,7 @@ public class ElectrumServer {
try {
Version version = new Version(electrsVersion);
if(version.compareTo(ELECTRS_MIN_BATCHING_VERSION) >= 0) {
return true;
return new ServerCapability(true);
}
} catch(Exception e) {
//ignore
@@ -1056,7 +1059,26 @@ public class ElectrumServer {
try {
Version version = new Version(fulcrumVersion);
if(version.compareTo(FULCRUM_MIN_BATCHING_VERSION) >= 0) {
return true;
return new ServerCapability(true);
}
} catch(Exception e) {
//ignore
}
}
if(server.startsWith("mempool-electrs")) {
String mempoolElectrsVersion = server.substring("mempool-electrs".length()).trim();
int dashIndex = mempoolElectrsVersion.indexOf('-');
String mempoolElectrsSuffix = "";
if(dashIndex > -1) {
mempoolElectrsSuffix = mempoolElectrsVersion.substring(dashIndex);
mempoolElectrsVersion = mempoolElectrsVersion.substring(0, dashIndex);
}
try {
Version version = new Version(mempoolElectrsVersion);
if(version.compareTo(MEMPOOL_ELECTRS_MIN_BATCHING_VERSION) > 0 ||
(version.compareTo(MEMPOOL_ELECTRS_MIN_BATCHING_VERSION) == 0 && (!mempoolElectrsSuffix.contains("dev") || mempoolElectrsSuffix.contains("dev-249848d")))) {
return new ServerCapability(true, 25);
}
} catch(Exception e) {
//ignore
@@ -1064,7 +1086,7 @@ public class ElectrumServer {
}
}
return false;
return new ServerCapability(false);
}
public static class ServerVersionService extends Service<List<String>> {
@@ -1156,7 +1178,7 @@ public class ElectrumServer {
if(bwtStartException != null) {
Matcher walletLoadingMatcher = RPC_WALLET_LOADING_PATTERN.matcher(bwtStartException.getMessage());
if(bwtStartException.getMessage().contains("Wallet file not specified")) {
throw new ServerException("Bitcoin Core requires Multi-Wallet to be enabled in the Server Preferences");
throw new ServerException("Bitcoin Core requires Multi-Wallet to be enabled in the Server Settings");
} else if(bwtStartException.getMessage().contains("Upgrade Bitcoin Core to v24 or later for Taproot wallet support")) {
throw new ServerException(bwtStartException.getMessage());
} else if(bwtStartException.getMessage().contains("Wallet file verification failed. Refusing to load database.")) {
@@ -1195,9 +1217,10 @@ public class ElectrumServer {
firstCall = false;
//If electrumx is detected, we can upgrade to batched RPC. Electrs/EPS do not support batching.
if(supportsBatching(serverVersion)) {
ServerCapability serverCapability = getServerCapability(serverVersion);
if(serverCapability.supportsBatching()) {
log.debug("Upgrading to batched JSON-RPC");
electrumServerRpc = new BatchedElectrumServerRpc(electrumServerRpc.getIdCounterValue());
electrumServerRpc = new BatchedElectrumServerRpc(electrumServerRpc.getIdCounterValue(), serverCapability.getMaxTargetBlocks());
}
BlockHeaderTip tip;
@@ -1,8 +1,10 @@
package com.sparrowwallet.sparrow.net;
import com.fasterxml.jackson.annotation.JsonAnyGetter;
import com.fasterxml.jackson.annotation.JsonAnySetter;
import com.sparrowwallet.sparrow.AppServices;
import com.sparrowwallet.sparrow.event.ExchangeRatesUpdatedEvent;
import com.sparrowwallet.sparrow.net.http.client.HttpResponseException;
import com.sparrowwallet.tern.http.client.HttpResponseException;
import javafx.concurrent.ScheduledService;
import javafx.concurrent.Service;
import javafx.concurrent.Task;
@@ -19,7 +21,7 @@ import java.util.*;
import java.util.stream.Collectors;
public enum ExchangeSource {
NONE("None") {
NONE("None", null) {
@Override
public List<Currency> getSupportedCurrencies() {
return Collections.emptyList();
@@ -35,7 +37,7 @@ public enum ExchangeSource {
return Collections.emptyMap();
}
},
COINBASE("Coinbase") {
COINBASE("Coinbase", "No historical rates") {
@Override
public List<Currency> getSupportedCurrencies() {
return getRates().data.rates.keySet().stream().filter(code -> isValidISO4217Code(code.toUpperCase(Locale.ROOT)))
@@ -119,7 +121,7 @@ public enum ExchangeSource {
return historicalRates;
}
},
COINGECKO("Coingecko") {
COINGECKO("Coingecko", "No historical rates") {
@Override
public List<Currency> getSupportedCurrencies() {
return getRates().rates.entrySet().stream().filter(rate -> "fiat".equals(rate.getValue().type) && isValidISO4217Code(rate.getKey().toUpperCase(Locale.ROOT)))
@@ -184,6 +186,75 @@ public enum ExchangeSource {
}
}
return historicalRates;
}
},
MEMPOOL_SPACE("mempool.space", "Historical rates from Apr 2023") {
@Override
public List<Currency> getSupportedCurrencies() {
return getRates().rates.entrySet().stream().filter(price -> isValidISO4217Code(price.getKey().toUpperCase(Locale.ROOT)))
.map(rate -> Currency.getInstance(rate.getKey().toUpperCase(Locale.ROOT))).collect(Collectors.toList());
}
@Override
public Double getExchangeRate(Currency currency) {
String currencyCode = currency.getCurrencyCode();
OptionalDouble optRate = getRates().rates.entrySet().stream().filter(price -> currencyCode.equalsIgnoreCase(price.getKey())).mapToDouble(Map.Entry::getValue).findFirst();
if(optRate.isPresent()) {
return optRate.getAsDouble();
}
return null;
}
private MempoolSpaceRates getRates() {
String url = AppServices.isUsingProxy() ? "http://mempoolhqx4isw62xs7abwphsq7ldayuidyx2v2oethdhhj6mlo2r6ad.onion/api/v1/prices" : "https://mempool.space/api/v1/prices";
if(log.isInfoEnabled()) {
log.info("Requesting exchange rates from " + url);
}
HttpClientService httpClientService = AppServices.getHttpClientService();
try {
return httpClientService.requestJson(url, MempoolSpaceRates.class, null);
} catch(Exception e) {
if(log.isDebugEnabled()) {
log.warn("Error retrieving currency rates", e);
} else {
log.warn("Error retrieving currency rates (" + e.getMessage() + ")");
}
return new MempoolSpaceRates();
}
}
@Override
public Map<Date, Double> getHistoricalExchangeRates(Currency currency, Date start, Date end) {
String url = AppServices.isUsingProxy() ? "http://mempoolhqx4isw62xs7abwphsq7ldayuidyx2v2oethdhhj6mlo2r6ad.onion/api/v1/historical-price?currency=" + currency.getCurrencyCode() :
"https://mempool.space/api/v1/historical-price?currency=" + currency.getCurrencyCode();
if(log.isInfoEnabled()) {
log.info("Requesting historical exchange rates from " + url);
}
Map<Date, Double> historicalRates = new TreeMap<>();
HttpClientService httpClientService = AppServices.getHttpClientService();
try {
MempoolSpaceHistoricalRates mempoolSpaceHistoricalRates = httpClientService.requestJson(url, MempoolSpaceHistoricalRates.class, null);
Collections.reverse(mempoolSpaceHistoricalRates.prices); //Use "closing" rates
for(MempoolSpaceRates historicalRate : mempoolSpaceHistoricalRates.prices) {
Date date = new Date(historicalRate.time * 1000);
if(date.after(start) && date.before(end) && historicalRate.rates.containsKey(currency.getCurrencyCode())) {
historicalRates.put(DateUtils.truncate(date, Calendar.DAY_OF_MONTH), historicalRate.rates.get(currency.getCurrencyCode()));
}
}
} catch(Exception e) {
if(log.isDebugEnabled()) {
log.warn("Error retrieving historical currency rates", e);
} else {
log.warn("Error retrieving historical currency rates (" + e.getMessage() + ")");
}
}
return historicalRates;
}
};
@@ -191,9 +262,11 @@ public enum ExchangeSource {
private static final Logger log = LoggerFactory.getLogger(ExchangeSource.class);
private final String name;
private final String description;
ExchangeSource(String name) {
ExchangeSource(String name, String description) {
this.name = name;
this.description = description;
}
public abstract List<Currency> getSupportedCurrencies();
@@ -211,6 +284,14 @@ public enum ExchangeSource {
}
}
public String getName() {
return name;
}
public String getDescription() {
return description;
}
@Override
public String toString() {
return name;
@@ -279,4 +360,24 @@ public enum ExchangeSource {
private static class CoinGeckoHistoricalRates {
public List<List<Number>> prices = new ArrayList<>();
}
private static class MempoolSpaceRates {
public long time;
public final Map<String, Double> rates = new LinkedHashMap<>();
// Capture all other fields that Jackson do not match other members
@JsonAnyGetter
public Map<String, Double> getPrices() {
return rates;
}
@JsonAnySetter
public void setPrice(String name, Double value) {
rates.put(name, value);
}
}
private static class MempoolSpaceHistoricalRates {
public List<MempoolSpaceRates> prices = new ArrayList<>();
}
}
@@ -1,5 +1,6 @@
package com.sparrowwallet.sparrow.net;
import com.sparrowwallet.drongo.Network;
import com.sparrowwallet.sparrow.AppServices;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -14,13 +15,26 @@ public enum FeeRatesSource {
public Map<Integer, Double> getBlockTargetFeeRates(Map<Integer, Double> defaultblockTargetFeeRates) {
return Collections.emptyMap();
}
@Override
public boolean supportsNetwork(Network network) {
return true;
}
},
MEMPOOL_SPACE("mempool.space", true) {
@Override
public Map<Integer, Double> getBlockTargetFeeRates(Map<Integer, Double> defaultblockTargetFeeRates) {
String url = AppServices.isUsingProxy() ? "http://mempoolhqx4isw62xs7abwphsq7ldayuidyx2v2oethdhhj6mlo2r6ad.onion/api/v1/fees/recommended" : "https://mempool.space/api/v1/fees/recommended";
if(Network.get() != Network.MAINNET && supportsNetwork(Network.get())) {
url = url.replace("/api/", "/" + Network.get().getName() + "/api/");
}
return getThreeTierFeeRates(this, defaultblockTargetFeeRates, url);
}
@Override
public boolean supportsNetwork(Network network) {
return network == Network.MAINNET || network == Network.TESTNET || network == Network.TESTNET4 || network == Network.SIGNET;
}
},
BITCOINFEES_EARN_COM("bitcoinfees.earn.com", true) {
@Override
@@ -28,6 +42,11 @@ public enum FeeRatesSource {
String url = "https://bitcoinfees.earn.com/api/v1/fees/recommended";
return getThreeTierFeeRates(this, defaultblockTargetFeeRates, url);
}
@Override
public boolean supportsNetwork(Network network) {
return network == Network.MAINNET;
}
},
MINIMUM("Minimum (1 sat/vB)", false) {
@Override
@@ -39,6 +58,11 @@ public enum FeeRatesSource {
return blockTargetFeeRates;
}
@Override
public boolean supportsNetwork(Network network) {
return true;
}
},
OXT_ME("oxt.me", true) {
@Override
@@ -47,6 +71,11 @@ public enum FeeRatesSource {
return getThreeTierFeeRates(this, defaultblockTargetFeeRates, url);
}
@Override
public boolean supportsNetwork(Network network) {
return network == Network.MAINNET;
}
@Override
protected ThreeTierRates getThreeTierRates(String url, HttpClientService httpClientService) throws Exception {
OxtRates oxtRates = httpClientService.requestJson(url, OxtRates.class, null);
@@ -72,6 +101,8 @@ public enum FeeRatesSource {
public abstract Map<Integer, Double> getBlockTargetFeeRates(Map<Integer, Double> defaultblockTargetFeeRates);
public abstract boolean supportsNetwork(Network network);
public String getName() {
return name;
}
@@ -132,7 +163,7 @@ public enum FeeRatesSource {
return name;
}
private record ThreeTierRates(Double fastestFee, Double halfHourFee, Double hourFee, Double minimumFee) {}
protected record ThreeTierRates(Double fastestFee, Double halfHourFee, Double hourFee, Double minimumFee) {}
private record OxtRates(OxtRatesData[] data) {}
@@ -1,50 +1,28 @@
package com.sparrowwallet.sparrow.net;
import com.google.common.net.HostAndPort;
import com.sparrowwallet.sparrow.net.http.client.AsyncUtil;
import com.sparrowwallet.sparrow.net.http.client.HttpUsage;
import com.sparrowwallet.sparrow.net.http.client.IHttpClient;
import com.sparrowwallet.sparrow.net.http.client.JettyHttpClientService;
import io.reactivex.Observable;
import javafx.concurrent.Service;
import javafx.concurrent.Task;
import java.util.Map;
import java.util.Optional;
public class HttpClientService extends JettyHttpClientService {
private static final int REQUEST_TIMEOUT = 120000;
public class HttpClientService extends com.sparrowwallet.tern.http.client.HttpClientService {
public HttpClientService(HostAndPort torProxy) {
super(REQUEST_TIMEOUT, new HttpProxySupplier(torProxy));
}
public <T> T requestJson(String url, Class<T> responseType, Map<String, String> headers) throws Exception {
return getHttpClient(HttpUsage.DEFAULT).getJson(url, responseType, headers);
}
public <T> Observable<Optional<T>> postJson(String url, Class<T> responseType, Map<String, String> headers, Object body) {
return getHttpClient(HttpUsage.DEFAULT).postJson(url, responseType, headers, body).toObservable();
}
public String postString(String url, Map<String, String> headers, String contentType, String content) throws Exception {
IHttpClient httpClient = getHttpClient(HttpUsage.DEFAULT);
return AsyncUtil.getInstance().blockingGet(httpClient.postString(url, headers, contentType, content)).get();
super(new TorHttpProxySupplier(torProxy));
}
public HostAndPort getTorProxy() {
return getHttpProxySupplier().getTorProxy();
if(getHttpProxySupplier() instanceof TorHttpProxySupplier torHttpProxySupplier) {
return torHttpProxySupplier.getTorProxy();
}
return null;
}
public void setTorProxy(HostAndPort torProxy) {
//Ensure all http clients are shutdown first
stop();
getHttpProxySupplier()._setTorProxy(torProxy);
}
@Override
public HttpProxySupplier getHttpProxySupplier() {
return (HttpProxySupplier)super.getHttpProxySupplier();
if(getHttpProxySupplier() instanceof TorHttpProxySupplier torHttpProxySupplier) {
//Ensure all http clients are shutdown first
stop();
torHttpProxySupplier._setTorProxy(torProxy);
}
}
public static class ShutdownService extends Service<Boolean> {
@@ -32,7 +32,7 @@ public class ProxyTcpOverTlsTransport extends TcpOverTlsTransport {
protected void createSocket() throws IOException {
InetSocketAddress proxyAddr = new InetSocketAddress(proxy.getHost(), proxy.getPortOrDefault(DEFAULT_PROXY_PORT));
socket = new Socket(new Proxy(Proxy.Type.SOCKS, proxyAddr));
socket.connect(new InetSocketAddress(server.getHost(), server.getPortOrDefault(getDefaultPort())));
socket.connect(InetSocketAddress.createUnresolved(server.getHost(), server.getPortOrDefault(getDefaultPort())));
socket = sslSocketFactory.createSocket(socket, proxy.getHost(), proxy.getPortOrDefault(DEFAULT_PROXY_PORT), true);
startHandshake((SSLSocket)socket);
}
@@ -18,7 +18,8 @@ public enum PublicElectrumServer {
TESTNET_ARANGUREN_ORG("testnet.aranguren.org", "ssl://testnet.aranguren.org:51002", Network.TESTNET),
TESTNET_QTORNADO_COM("testnet.qtornado.com", "ssl://testnet.qtornado.com:51002", Network.TESTNET),
SIGNET_MEMPOOL_SPACE("mempool.space", "ssl://mempool.space:60602", Network.SIGNET),
TESTNET4_MEMPOOL_SPACE("mempool.space", "ssl://mempool.space:40002", Network.TESTNET4);
TESTNET4_MEMPOOL_SPACE("mempool.space", "ssl://mempool.space:40002", Network.TESTNET4),
TESTNET4_C3_SOFT("blackie.c3-soft.com", "ssl://blackie.c3-soft.com:57010", Network.TESTNET4);
PublicElectrumServer(String name, String url, Network network) {
this.server = new Server(url, name);
@@ -0,0 +1,26 @@
package com.sparrowwallet.sparrow.net;
import com.sparrowwallet.sparrow.AppServices;
public class ServerCapability {
private final boolean supportsBatching;
private final int maxTargetBlocks;
public ServerCapability(boolean supportsBatching) {
this.supportsBatching = supportsBatching;
this.maxTargetBlocks = AppServices.TARGET_BLOCKS_RANGE.getLast();
}
public ServerCapability(boolean supportsBatching, int maxTargetBlocks) {
this.supportsBatching = supportsBatching;
this.maxTargetBlocks = maxTargetBlocks;
}
public boolean supportsBatching() {
return supportsBatching;
}
public int getMaxTargetBlocks() {
return maxTargetBlocks;
}
}
@@ -268,7 +268,9 @@ public class TcpTransport implements CloseableTransport, TimeoutCounter {
protected void createSocket() throws IOException {
socket = socketFactory.createSocket();
socket.connect(new InetSocketAddress(server.getHost(), server.getPortOrDefault(getDefaultPort())));
socket.connect(socketFactory instanceof ProxySocketFactory ?
InetSocketAddress.createUnresolved(server.getHost(), server.getPortOrDefault(getDefaultPort())) :
new InetSocketAddress(server.getHost(), server.getPortOrDefault(getDefaultPort())));
}
protected int getDefaultPort() {
@@ -1,6 +1,7 @@
package com.sparrowwallet.sparrow.net;
import com.google.common.net.HostAndPort;
import com.sparrowwallet.drongo.OsType;
import com.sparrowwallet.sparrow.EventManager;
import com.sparrowwallet.sparrow.event.TorStatusEvent;
import com.sparrowwallet.sparrow.io.Storage;
@@ -16,7 +17,6 @@ import io.matthewnelson.kmp.tor.ext.callback.manager.CallbackTorManager;
import io.matthewnelson.kmp.tor.manager.TorManager;
import io.matthewnelson.kmp.tor.manager.common.event.TorManagerEvent;
import io.matthewnelson.kmp.tor.manager.util.PortUtil;
import org.controlsfx.tools.Platform;
import org.jetbrains.annotations.NotNull;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -38,20 +38,20 @@ public class Tor {
private ProxyAddress socksAddress;
public Tor() {
Platform platform = Platform.getCurrent();
OsType osType = OsType.getCurrent();
String arch = System.getProperty("os.arch");
PlatformInstaller installer;
PlatformInstaller.InstallOption installOption = PlatformInstaller.InstallOption.CleanInstallIfMissing;
if(platform == Platform.OSX) {
if(osType == OsType.MACOS) {
if(arch.equals("aarch64")) {
installer = PlatformInstaller.macosArm64(installOption);
} else {
installer = PlatformInstaller.macosX64(installOption);
}
} else if(platform == Platform.WINDOWS) {
} else if(osType == OsType.WINDOWS) {
installer = PlatformInstaller.mingwX64(installOption);
} else if(platform == Platform.UNIX) {
} else if(osType == OsType.UNIX) {
if(arch.equals("aarch64")) {
TorBinaryResource linuxArm64 = TorBinaryResource.from(TorBinaryResource.OS.Linux, "arm64",
"588496f3164d52b91f17e4db3372d8dfefa6366a8df265eebd4a28d4128992aa",
@@ -61,7 +61,7 @@ public class Tor {
installer = PlatformInstaller.linuxX64(installOption);
}
} else {
throw new UnsupportedOperationException("Sparrow's bundled Tor is not supported on " + platform + " " + arch);
throw new UnsupportedOperationException("Sparrow's bundled Tor is not supported on " + osType + " " + arch);
}
TorConfigProviderJvm torConfigProviderJvm = new TorConfigProviderJvm() {
@@ -1,18 +1,15 @@
package com.sparrowwallet.sparrow.net;
import com.google.common.net.HostAndPort;
import com.sparrowwallet.sparrow.net.http.client.HttpProxy;
import com.sparrowwallet.sparrow.net.http.client.HttpProxyProtocol;
import com.sparrowwallet.sparrow.net.http.client.HttpUsage;
import com.sparrowwallet.sparrow.net.http.client.IHttpProxySupplier;
import com.sparrowwallet.tern.http.client.*;
import java.util.Optional;
public class HttpProxySupplier implements IHttpProxySupplier {
public class TorHttpProxySupplier implements IHttpProxySupplier {
private HostAndPort torProxy;
private HttpProxy httpProxy;
public HttpProxySupplier(HostAndPort torProxy) {
public TorHttpProxySupplier(HostAndPort torProxy) {
this.torProxy = torProxy;
this.httpProxy = computeHttpProxy(torProxy);
}
@@ -29,7 +26,7 @@ public class HttpProxySupplier implements IHttpProxySupplier {
return torProxy;
}
// shouldnt call directly but use httpClientService.setTorProxy()
// shouldn't call directly but use httpClientService.setTorProxy()
public void _setTorProxy(HostAndPort hostAndPort) {
// set proxy
this.torProxy = hostAndPort;
@@ -1,54 +0,0 @@
package com.sparrowwallet.sparrow.net;
public class Version implements Comparable<Version> {
private final String version;
public final String get() {
return this.version;
}
public Version(String version) {
if(version == null) {
throw new IllegalArgumentException("Version can not be null");
}
if(!version.matches("[0-9]+(\\.[0-9]+)*")) {
throw new IllegalArgumentException("Invalid version format");
}
this.version = version;
}
@Override
public int compareTo(Version that) {
if(that == null) {
return 1;
}
String[] thisParts = this.get().split("\\.");
String[] thatParts = that.get().split("\\.");
int length = Math.max(thisParts.length, thatParts.length);
for(int i = 0; i < length; i++) {
int thisPart = i < thisParts.length ? Integer.parseInt(thisParts[i]) : 0;
int thatPart = i < thatParts.length ? Integer.parseInt(thatParts[i]) : 0;
if(thisPart < thatPart) {
return -1;
}
if(thisPart > thatPart) {
return 1;
}
}
return 0;
}
@Override
public boolean equals(Object that) {
if(this == that) {
return true;
}
if(that == null) {
return false;
}
if(this.getClass() != that.getClass()) {
return false;
}
return this.compareTo((Version) that) == 0;
}
}
@@ -1,5 +1,6 @@
package com.sparrowwallet.sparrow.net;
import com.sparrowwallet.drongo.Version;
import com.sparrowwallet.drongo.address.Address;
import com.sparrowwallet.drongo.address.InvalidAddressException;
import com.sparrowwallet.drongo.crypto.ECKey;
@@ -17,6 +17,7 @@ import com.sparrowwallet.sparrow.event.CormorantScanStatusEvent;
import com.sparrowwallet.sparrow.event.CormorantSyncStatusEvent;
import com.sparrowwallet.sparrow.io.Config;
import com.sparrowwallet.sparrow.net.Bwt;
import com.sparrowwallet.sparrow.net.ConfigurationException;
import com.sparrowwallet.sparrow.net.CoreAuthType;
import com.sparrowwallet.sparrow.net.cormorant.Cormorant;
import com.sparrowwallet.drongo.address.Address;
@@ -94,8 +95,10 @@ public class BitcoindClient {
Config config = Config.get();
if((config.getCoreAuthType() == CoreAuthType.COOKIE || config.getCoreAuth() == null || config.getCoreAuth().length() < 2) && config.getCoreDataDir() != null) {
bitcoindTransport = new BitcoindTransport(config.getCoreServer(), CORE_WALLET_NAME, config.getCoreDataDir());
} else {
} else if(config.getCoreAuth() != null) {
bitcoindTransport = new BitcoindTransport(config.getCoreServer(), CORE_WALLET_NAME, config.getCoreAuth());
} else {
throw new ConfigurationException("Bitcoin Core data folder or user and password is required");
}
this.jsonRpcClient = new JsonRpcClient(bitcoindTransport);
@@ -114,7 +117,7 @@ public class BitcoindClient {
tip = blockHeader.getBlockHeader();
timer.schedule(new PollTask(), 5000, 5000);
if(blockchainInfo.initialblockdownload()) {
if(blockchainInfo.initialblockdownload() && networkInfo.networkactive()) {
syncingLock.lock();
try {
syncing = true;
@@ -10,7 +10,7 @@ public record MempoolEntry(int vsize, int ancestorsize, boolean bip125_replaceab
}
public TxEntry getTxEntry(String txid) {
return new TxEntry(hasUnconfirmedParents() ? -1 : 0, 0, txid);
return new TxEntry(hasUnconfirmedParents() ? -1 : 0, 0, txid, fees().base());
}
public VsizeFeerate getVsizeFeerate() {
@@ -3,6 +3,6 @@ package com.sparrowwallet.sparrow.net.cormorant.bitcoind;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
@JsonIgnoreProperties(ignoreUnknown = true)
public record NetworkInfo(int version, String subversion) {
public record NetworkInfo(int version, String subversion, boolean networkactive) {
}
@@ -9,7 +9,7 @@ import com.sparrowwallet.drongo.protocol.Sha256Hash;
import com.sparrowwallet.sparrow.EventManager;
import com.sparrowwallet.sparrow.SparrowWallet;
import com.sparrowwallet.sparrow.event.MempoolEntriesInitializedEvent;
import com.sparrowwallet.sparrow.net.Version;
import com.sparrowwallet.drongo.Version;
import com.sparrowwallet.sparrow.net.cormorant.Cormorant;
import com.sparrowwallet.sparrow.net.cormorant.bitcoind.*;
import com.sparrowwallet.sparrow.net.cormorant.index.TxEntry;
@@ -45,7 +45,7 @@ public class ElectrumServerService {
@JsonRpcMethod("server.banner")
public String getServerBanner() {
return Cormorant.SERVER_NAME + " " + SparrowWallet.APP_VERSION + "\n" + bitcoindClient.getNetworkInfo().subversion();
return Cormorant.SERVER_NAME + " " + SparrowWallet.APP_VERSION + "\n" + bitcoindClient.getNetworkInfo().subversion() + (bitcoindClient.getNetworkInfo().networkactive() ? "" : " (disconnected)");
}
@JsonRpcMethod("blockchain.estimatefee")
@@ -35,7 +35,7 @@ public class Store {
mempoolEntries.put(txid, null);
}
entries.removeIf(txe -> txe.height > 0 && txe.tx_hash.equals(listTransaction.txid()));
txEntry = new TxEntry(0, 0, listTransaction.txid());
txEntry = new TxEntry(0, 0, listTransaction.txid(), listTransaction.fee());
} else {
mempoolEntries.remove(txid);
entries.removeIf(txe -> txe.height != listTransaction.blockheight() && txe.tx_hash.equals(listTransaction.txid()));
@@ -1,14 +1,29 @@
package com.sparrowwallet.sparrow.net.cormorant.index;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.sparrowwallet.drongo.protocol.Transaction;
import java.util.Objects;
@JsonInclude(JsonInclude.Include.NON_NULL)
public class TxEntry implements Comparable<TxEntry> {
public final int height;
private final transient int index;
public final String tx_hash;
public final Long fee;
public TxEntry(int height, int index, String tx_hash) {
this.height = height;
this.index = index;
this.tx_hash = tx_hash;
this.fee = null;
}
public TxEntry(int height, int index, String tx_hash, double btcFee) {
this.height = height;
this.index = index;
this.tx_hash = tx_hash;
this.fee = btcFee > 0.0 ? (long)(btcFee * Transaction.SATOSHIS_PER_BITCOIN) : null;
}
@Override
@@ -16,22 +31,18 @@ public class TxEntry implements Comparable<TxEntry> {
if(this == o) {
return true;
}
if(o == null || getClass() != o.getClass()) {
if(!(o instanceof TxEntry txEntry)) {
return false;
}
TxEntry txEntry = (TxEntry) o;
if(height != txEntry.height) {
return false;
}
return tx_hash.equals(txEntry.tx_hash);
return height == txEntry.height && tx_hash.equals(txEntry.tx_hash) && Objects.equals(fee, txEntry.fee);
}
@Override
public int hashCode() {
int result = height;
result = 31 * result + tx_hash.hashCode();
result = 31 * result + Objects.hashCode(fee);
return result;
}
@@ -62,6 +73,7 @@ public class TxEntry implements Comparable<TxEntry> {
"height=" + height +
", index=" + index +
", tx_hash='" + tx_hash + '\'' +
", fee=" + fee +
'}';
}
}

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