mirror of
https://github.com/sparrowwallet/sparrow.git
synced 2026-07-30 19:46:16 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2b9c3eb737 | ||
|
|
ef2c2cde2b | ||
|
|
0dce4783ef | ||
|
|
cf797ea0f4 | ||
|
|
969dd72d6d | ||
|
|
2d667d118a | ||
|
|
1b8e9b5229 | ||
|
|
aeb0eeb11a | ||
|
|
febece9e9b | ||
|
|
432758d22a | ||
|
|
8b410ad005 | ||
|
|
abc0a4f2b7 | ||
|
|
6b30e6a3ec | ||
|
|
a5f705f185 | ||
|
|
d7c726782c | ||
|
|
b3d51bce34 | ||
|
|
d8e7b54cae | ||
|
|
078af174af | ||
|
|
4b8a4594b0 | ||
|
|
9a870ba522 | ||
|
|
b0591d7afe | ||
|
|
e3db43679f | ||
|
|
fb10ac3aa2 | ||
|
|
08b36abb6a | ||
|
|
fa9194a032 | ||
|
|
73897fc53a | ||
|
|
72c1d822a4 | ||
|
|
a13016609d | ||
|
|
c7798d920e | ||
|
|
5ae36a549f | ||
|
|
f7f6b59d6d | ||
|
|
771f2dc9bc | ||
|
|
a9e144bb0c | ||
|
|
0b8199948d | ||
|
|
f697aa610b | ||
|
|
37bab9f321 | ||
|
|
3dc50682a3 | ||
|
|
d5b119e338 | ||
|
|
b457caa5d2 | ||
|
|
61ed816c87 | ||
|
|
9a603d7547 | ||
|
|
ac666545be | ||
|
|
c4c77d84e0 | ||
|
|
8d126869a6 | ||
|
|
464fade68f | ||
|
|
79517da131 | ||
|
|
086297436e | ||
|
|
d69e274b18 | ||
|
|
d1e67ad4a0 | ||
|
|
37ca98c2b0 | ||
|
|
287c943b44 | ||
|
|
cb92f76546 | ||
|
|
24e9c39cb8 | ||
|
|
754ebf7bbf | ||
|
|
bc7a0be87e | ||
|
|
87af1ed9f5 | ||
|
|
da476c9d77 | ||
|
|
d8e4582b3c | ||
|
|
ad5c695946 | ||
|
|
3150a96aab | ||
|
|
28521cbc1a | ||
|
|
79bbe7df9f | ||
|
|
e339c9f51a | ||
|
|
6dd1bda0cb | ||
|
|
9fe0d17aac | ||
|
|
a2eb937fce | ||
|
|
e985a03a58 | ||
|
|
273d2aacf3 | ||
|
|
97383a4e35 | ||
|
|
712750a25c | ||
|
|
1be9ac1072 | ||
|
|
a035767e38 | ||
|
|
1ad237c623 | ||
|
|
7e4aaacd2f | ||
|
|
60d4cce15b | ||
|
|
7afb27b3e5 | ||
|
|
8f56e95048 | ||
|
|
1a5f97cecf | ||
|
|
db3ebdb300 | ||
|
|
0b012995a0 | ||
|
|
9b12361724 | ||
|
|
201d4b8376 | ||
|
|
ab6416f30a | ||
|
|
e64069f02f | ||
|
|
e96a7113b0 | ||
|
|
abe1f9c9e5 | ||
|
|
69aef9c228 | ||
|
|
630ce0f3ed | ||
|
|
723b004e5f | ||
|
|
6cefec5cec | ||
|
|
f5ee7bf277 | ||
|
|
0459f4ca45 | ||
|
|
c23dbeedcf | ||
|
|
0bb5e9319f | ||
|
|
1c5602aa9d | ||
|
|
c67008840d | ||
|
|
17a04510fd | ||
|
|
dd50f6973b | ||
|
|
8780e515d5 | ||
|
|
110f887952 | ||
|
|
1143eaa55f | ||
|
|
3068ba3988 | ||
|
|
58ca52e4c7 | ||
|
|
883b7cf3b2 | ||
|
|
ee5b502a00 | ||
|
|
4d7baa070e | ||
|
|
fb48643466 | ||
|
|
e2c38c2ac4 | ||
|
|
e55dd4401b | ||
|
|
53b6e2532f | ||
|
|
98ae891898 | ||
|
|
019b11c95e | ||
|
|
186eacc245 | ||
|
|
32a35ed2c0 | ||
|
|
21f9f9fe25 | ||
|
|
6c6664f29a | ||
|
|
ea0509ca3d |
@@ -0,0 +1,110 @@
|
||||
name: Update Gradle verification metadata
|
||||
description: Restore optional previous Gradle verification files, update them, validate them, and upload the result.
|
||||
|
||||
inputs:
|
||||
output-artifact:
|
||||
description: Name of the artifact to upload with the updated verification files.
|
||||
required: true
|
||||
previous-artifact:
|
||||
description: Optional previous verification artifact to restore before running Gradle.
|
||||
required: false
|
||||
default: ''
|
||||
clean-start:
|
||||
description: Remove existing verification files before bootstrapping metadata.
|
||||
required: false
|
||||
default: 'false'
|
||||
cache-gradle-wrapper-distribution:
|
||||
description: Cache only the Gradle wrapper distribution, not Gradle dependency artifacts.
|
||||
required: false
|
||||
default: 'true'
|
||||
|
||||
runs:
|
||||
using: composite
|
||||
steps:
|
||||
- name: Download previous verification files
|
||||
if: ${{ inputs.previous-artifact != '' }}
|
||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||
with:
|
||||
name: ${{ inputs.previous-artifact }}
|
||||
path: build/previous-verification
|
||||
|
||||
- name: Restore previous verification files
|
||||
if: ${{ inputs.previous-artifact != '' }}
|
||||
shell: bash
|
||||
run: cp build/previous-verification/gradle/verification-* gradle/
|
||||
|
||||
- name: Bootstrap verification metadata
|
||||
shell: bash
|
||||
run: |
|
||||
mkdir -p gradle
|
||||
if [ "${{ inputs.clean-start }}" = "true" ]; then
|
||||
rm -f gradle/verification-metadata.xml gradle/verification-keyring.keys
|
||||
fi
|
||||
if [ ! -f gradle/verification-metadata.xml ]; then
|
||||
{
|
||||
printf '%s\n' '<?xml version="1.0" encoding="UTF-8"?>'
|
||||
printf '%s\n' '<verification-metadata xmlns="https://schema.gradle.org/dependency-verification" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="https://schema.gradle.org/dependency-verification https://schema.gradle.org/dependency-verification/dependency-verification-1.3.xsd">'
|
||||
printf '%s\n' ' <configuration>'
|
||||
printf '%s\n' ' <verify-metadata>true</verify-metadata>'
|
||||
printf '%s\n' ' <verify-signatures>true</verify-signatures>'
|
||||
printf '%s\n' ' <keyring-format>armored</keyring-format>'
|
||||
printf '%s\n' ' <key-servers>'
|
||||
printf '%s\n' ' <key-server uri="https://keyserver.ubuntu.com"/>'
|
||||
printf '%s\n' ' <key-server uri="https://keys.openpgp.org"/>'
|
||||
printf '%s\n' ' </key-servers>'
|
||||
printf '%s\n' ' <trusted-artifacts>'
|
||||
printf '%s\n' ' <trust file=".*-javadoc[.]jar" regex="true"/>'
|
||||
printf '%s\n' ' <trust file=".*-sources[.]jar" regex="true"/>'
|
||||
printf '%s\n' ' </trusted-artifacts>'
|
||||
printf '%s\n' ' </configuration>'
|
||||
printf '%s\n' ' <components>'
|
||||
printf '%s\n' ' </components>'
|
||||
printf '%s\n' '</verification-metadata>'
|
||||
} > gradle/verification-metadata.xml
|
||||
fi
|
||||
|
||||
- name: Clear Java tool-cache for reproducibility
|
||||
shell: bash
|
||||
run: rm -rf "$RUNNER_TOOL_CACHE"/Java_*
|
||||
|
||||
- name: Set up JDK 25.0.2
|
||||
uses: actions/setup-java@0f481fcb613427c0f801b606911222b5b6f3083a # v5.5.0
|
||||
with:
|
||||
distribution: temurin
|
||||
java-version: '25.0.2'
|
||||
|
||||
- name: Cache Gradle wrapper distribution
|
||||
if: ${{ inputs.cache-gradle-wrapper-distribution == 'true' }}
|
||||
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
|
||||
with:
|
||||
path: ~/.gradle/wrapper/dists
|
||||
key: gradle-wrapper-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('gradle/wrapper/gradle-wrapper.properties') }}
|
||||
|
||||
- name: Generate dependency verification metadata
|
||||
shell: bash
|
||||
run: |
|
||||
./gradlew --write-verification-metadata sha512 --refresh-keys \
|
||||
dependencies :drongo:dependencies :lark:dependencies jpackageImage
|
||||
./gradlew --write-verification-metadata pgp,sha512 --refresh-keys \
|
||||
dependencies :drongo:dependencies :lark:dependencies jpackageImage
|
||||
./gradlew --export-keys help
|
||||
|
||||
- name: Validate dependency verification metadata
|
||||
shell: bash
|
||||
run: |
|
||||
./gradlew help dependencies :drongo:dependencies :lark:dependencies \
|
||||
--dependency-verification strict --refresh-keys
|
||||
|
||||
- name: Stage verification files
|
||||
shell: bash
|
||||
run: |
|
||||
mkdir -p build/dependency-verification/gradle
|
||||
cp gradle/verification-metadata.xml build/dependency-verification/gradle/
|
||||
cp gradle/verification-keyring.keys build/dependency-verification/gradle/
|
||||
|
||||
- name: Upload verification files
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: ${{ inputs.output-artifact }}
|
||||
path: build/dependency-verification
|
||||
if-no-files-found: error
|
||||
@@ -0,0 +1,187 @@
|
||||
name: Dependency Verification
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
schedule:
|
||||
- cron: '17 3 * * *'
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
env:
|
||||
ALLOW_ADDITIONAL_ARTIFACT_CHECKSUMS: 'false'
|
||||
CLEAN_STALE_VERIFICATION_METADATA: 'false'
|
||||
CACHE_GRADLE_WRAPPER_DISTRIBUTION: 'true'
|
||||
JDK_JAVAC_OPTIONS: '-Xlint:-module'
|
||||
|
||||
concurrency:
|
||||
group: dependency-verification-${{ github.ref }}
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
linux-x64:
|
||||
name: Generate (Linux x64)
|
||||
runs-on: ubuntu-22.04
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
- uses: ./.github/actions/update-gradle-verification
|
||||
with:
|
||||
cache-gradle-wrapper-distribution: ${{ env.CACHE_GRADLE_WRAPPER_DISTRIBUTION }}
|
||||
clean-start: ${{ env.CLEAN_STALE_VERIFICATION_METADATA }}
|
||||
output-artifact: gradle-verification-linux-x64
|
||||
|
||||
linux-arm64:
|
||||
name: Generate (Linux arm64)
|
||||
runs-on: ubuntu-22.04-arm
|
||||
needs: linux-x64
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
- uses: ./.github/actions/update-gradle-verification
|
||||
with:
|
||||
cache-gradle-wrapper-distribution: ${{ env.CACHE_GRADLE_WRAPPER_DISTRIBUTION }}
|
||||
previous-artifact: gradle-verification-linux-x64
|
||||
output-artifact: gradle-verification-linux-arm64
|
||||
|
||||
windows-x64:
|
||||
name: Generate (Windows x64)
|
||||
runs-on: windows-2022
|
||||
needs: linux-arm64
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
- uses: ./.github/actions/update-gradle-verification
|
||||
with:
|
||||
cache-gradle-wrapper-distribution: ${{ env.CACHE_GRADLE_WRAPPER_DISTRIBUTION }}
|
||||
previous-artifact: gradle-verification-linux-arm64
|
||||
output-artifact: gradle-verification-windows-x64
|
||||
|
||||
macos-x64:
|
||||
name: Generate (macOS x64)
|
||||
runs-on: macos-15-intel
|
||||
needs: windows-x64
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
- uses: ./.github/actions/update-gradle-verification
|
||||
with:
|
||||
cache-gradle-wrapper-distribution: ${{ env.CACHE_GRADLE_WRAPPER_DISTRIBUTION }}
|
||||
previous-artifact: gradle-verification-windows-x64
|
||||
output-artifact: gradle-verification-macos-x64
|
||||
|
||||
macos-arm64:
|
||||
name: Generate (macOS arm64)
|
||||
runs-on: macos-14
|
||||
needs: macos-x64
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
- uses: ./.github/actions/update-gradle-verification
|
||||
with:
|
||||
cache-gradle-wrapper-distribution: ${{ env.CACHE_GRADLE_WRAPPER_DISTRIBUTION }}
|
||||
previous-artifact: gradle-verification-macos-x64
|
||||
output-artifact: gradle-verification-final
|
||||
|
||||
update-pr:
|
||||
name: Open update PR
|
||||
runs-on: ubuntu-22.04
|
||||
needs: macos-arm64
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
- name: Download final verification files
|
||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||
with:
|
||||
name: gradle-verification-final
|
||||
path: build/final-verification
|
||||
|
||||
- name: Create or update pull request
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
BRANCH_NAME: automation/update-gradle-verification-metadata
|
||||
run: |
|
||||
verification_metadata_diff() {
|
||||
if git ls-files --error-unmatch gradle/verification-metadata.xml >/dev/null 2>&1; then
|
||||
git diff -- gradle/verification-metadata.xml
|
||||
else
|
||||
git diff --no-index -- /dev/null gradle/verification-metadata.xml || true
|
||||
fi
|
||||
}
|
||||
|
||||
reset_verification_files() {
|
||||
for file in gradle/verification-metadata.xml gradle/verification-keyring.keys; do
|
||||
if git ls-files --error-unmatch "$file" >/dev/null 2>&1; then
|
||||
git restore "$file"
|
||||
else
|
||||
rm -f "$file"
|
||||
fi
|
||||
done
|
||||
}
|
||||
|
||||
mkdir -p /tmp/dependency-verification/gradle
|
||||
cp build/final-verification/gradle/verification-* /tmp/dependency-verification/gradle/
|
||||
cp /tmp/dependency-verification/gradle/verification-* gradle/
|
||||
|
||||
if [ -z "$(git status --porcelain -- gradle/verification-metadata.xml gradle/verification-keyring.keys)" ]; then
|
||||
echo "No dependency verification changes detected."
|
||||
exit 0
|
||||
fi
|
||||
if [ "$ALLOW_ADDITIONAL_ARTIFACT_CHECKSUMS" != "true" ] && verification_metadata_diff | grep -q '^[+] *<also-trust '; then
|
||||
echo "Generated verification metadata adds also-trust checksums."
|
||||
echo "This indicates an existing artifact coordinate resolved to a checksum not currently trusted by the repository."
|
||||
echo "Set ALLOW_ADDITIONAL_ARTIFACT_CHECKSUMS to 'true' to allow opening an automated metadata update PR for this case."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
git config user.name "github-actions[bot]"
|
||||
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
|
||||
if git fetch origin "+refs/heads/$BRANCH_NAME:refs/remotes/origin/$BRANCH_NAME"; then
|
||||
mkdir -p /tmp/dependency-verification/existing/gradle
|
||||
if git show "refs/remotes/origin/$BRANCH_NAME:gradle/verification-metadata.xml" > /tmp/dependency-verification/existing/gradle/verification-metadata.xml &&
|
||||
git show "refs/remotes/origin/$BRANCH_NAME:gradle/verification-keyring.keys" > /tmp/dependency-verification/existing/gradle/verification-keyring.keys &&
|
||||
cmp -s /tmp/dependency-verification/gradle/verification-metadata.xml /tmp/dependency-verification/existing/gradle/verification-metadata.xml &&
|
||||
cmp -s /tmp/dependency-verification/gradle/verification-keyring.keys /tmp/dependency-verification/existing/gradle/verification-keyring.keys &&
|
||||
git merge-base --is-ancestor HEAD "refs/remotes/origin/$BRANCH_NAME"; then
|
||||
echo "Existing automation branch already contains these verification changes."
|
||||
exit 0
|
||||
fi
|
||||
fi
|
||||
|
||||
reset_verification_files
|
||||
git checkout -B "$BRANCH_NAME"
|
||||
cp /tmp/dependency-verification/gradle/verification-* gradle/
|
||||
git add gradle/verification-metadata.xml gradle/verification-keyring.keys
|
||||
git commit -m "Update Gradle dependency verification metadata"
|
||||
git push --force-with-lease origin "HEAD:$BRANCH_NAME"
|
||||
|
||||
if gh pr list --base master --head "$BRANCH_NAME" --json number --jq '.[0].number' | grep -q .; then
|
||||
echo "Existing dependency verification PR found; branch was updated."
|
||||
else
|
||||
gh pr create \
|
||||
--base master \
|
||||
--head "$BRANCH_NAME" \
|
||||
--title "Update Gradle dependency verification metadata" \
|
||||
--body "Regenerates Gradle dependency verification metadata across Linux, Windows, and macOS using Gradle's incremental verification metadata update flow."
|
||||
fi
|
||||
@@ -5,6 +5,9 @@ on: workflow_dispatch
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
env:
|
||||
JDK_JAVAC_OPTIONS: '-Xlint:-module'
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ${{ matrix.os }}
|
||||
@@ -12,14 +15,14 @@ jobs:
|
||||
matrix:
|
||||
os: [windows-2022, ubuntu-22.04, ubuntu-22.04-arm, macos-15-intel, macos-14]
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
with:
|
||||
submodules: recursive
|
||||
- name: Clear Java tool-cache for reproducibility
|
||||
shell: bash
|
||||
run: rm -rf "$RUNNER_TOOL_CACHE"/Java_*
|
||||
- name: Set up JDK 25.0.2
|
||||
uses: actions/setup-java@v5
|
||||
uses: actions/setup-java@0f481fcb613427c0f801b606911222b5b6f3083a # v5.5.0
|
||||
with:
|
||||
distribution: 'temurin'
|
||||
java-version: '25.0.2'
|
||||
@@ -29,7 +32,7 @@ jobs:
|
||||
run: ./gradlew jpackage
|
||||
- name: Codesign, package and notarize macOS distribution
|
||||
if: ${{ runner.os == 'macOS' }}
|
||||
uses: sparrowwallet/github-actions/codesign-macos@v1
|
||||
uses: sparrowwallet/github-actions/codesign-macos@6eeb7bf9b882cf89ff96b77f99bcf487b9d33992 # v1
|
||||
with:
|
||||
app-name: Sparrow
|
||||
certificate: ${{ secrets.MACOS_CERTIFICATE }}
|
||||
@@ -47,7 +50,7 @@ jobs:
|
||||
if: ${{ runner.os == 'Linux' }}
|
||||
run: ./repackage.sh
|
||||
- name: Upload Artifact
|
||||
uses: actions/upload-artifact@v6
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: Sparrow Build - ${{ runner.os }} ${{ runner.arch }}
|
||||
path: |
|
||||
@@ -65,7 +68,7 @@ jobs:
|
||||
run: ./repackage.sh
|
||||
- name: Upload Headless Artifact
|
||||
if: ${{ runner.os == 'Linux' }}
|
||||
uses: actions/upload-artifact@v6
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: Sparrow Build - ${{ runner.os }} ${{ runner.arch }} Headless
|
||||
path: |
|
||||
|
||||
@@ -81,12 +81,24 @@ When not explicitly configured using the command line argument above, Sparrow st
|
||||
|
||||
| Platform | Location |
|
||||
|----------| -------- |
|
||||
| OSX | ~/.sparrow |
|
||||
| macOS | ~/.sparrow |
|
||||
| Linux | ~/.sparrow |
|
||||
| Windows | %APPDATA%/Sparrow |
|
||||
|
||||
Testnet3, testnet4, regtest and signet configurations (along with their wallets) are stored in subfolders to allow easy switching between networks.
|
||||
|
||||
On macOS and Linux, Sparrow also supports the [XDG Base Directory Specification](https://specifications.freedesktop.org/basedir-spec/latest/).
|
||||
This is opt in: for each category below, if the corresponding directory already exists, Sparrow uses it, otherwise it continues to use the home folder above. Categories are resolved independently, so files can be moved across one at a time.
|
||||
|
||||
| Category | Location | Contents |
|
||||
|----------| -------- |---------------------------------------|
|
||||
| Config | `$XDG_CONFIG_HOME/sparrow` (default `~/.config/sparrow`) | `config`, `network-*` markers |
|
||||
| Data | `$XDG_DATA_HOME/sparrow` (default `~/.local/share/sparrow`) | `wallets`, `certs`, `lark` |
|
||||
| State | `$XDG_STATE_HOME/sparrow` (default `~/.local/state/sparrow`) | `sparrow.log`, `tor/work`, lock files |
|
||||
| Cache | `$XDG_CACHE_HOME/sparrow` (default `~/.cache/sparrow`) | `tor/cache` |
|
||||
|
||||
Specifying a home folder with the `-d` argument disables XDG resolution entirely, and stores all files in the given folder.
|
||||
|
||||
## Reporting Issues
|
||||
|
||||
Please use the [Issues](https://github.com/sparrowwallet/sparrow/issues) tab above to report an issue. If possible, look in the sparrow.log file in the configuration directory for information helpful in debugging.
|
||||
|
||||
+124
-38
@@ -1,7 +1,7 @@
|
||||
plugins {
|
||||
id 'application'
|
||||
id 'org-openjfx-javafxplugin'
|
||||
id 'org.beryx.jlink' version '3.2.1'
|
||||
id 'org.openjfx.javafxplugin' version '0.1.0'
|
||||
id 'org.beryx.jlink' version '4.0.2'
|
||||
id 'org.gradlex.extra-java-module-info' version '1.13.1'
|
||||
id 'io.matthewnelson.kmp.tor.resource-filterjar' version '408.21.0'
|
||||
}
|
||||
@@ -20,7 +20,7 @@ if(System.getProperty("os.arch") == "aarch64") {
|
||||
def headless = "true".equals(System.getProperty("java.awt.headless"))
|
||||
|
||||
group = 'com.sparrowwallet'
|
||||
version = '2.4.2'
|
||||
version = '2.5.4'
|
||||
|
||||
repositories {
|
||||
mavenCentral()
|
||||
@@ -32,7 +32,7 @@ tasks.withType(AbstractArchiveTask).configureEach {
|
||||
}
|
||||
|
||||
javafx {
|
||||
version = headless ? "18" : "25.0.2"
|
||||
version = "26"
|
||||
modules = [ 'javafx.controls', 'javafx.fxml', 'javafx.swing', 'javafx.graphics' ]
|
||||
}
|
||||
|
||||
@@ -73,7 +73,7 @@ dependencies {
|
||||
implementation('com.fasterxml.jackson.core:jackson-databind:2.21.1')
|
||||
implementation('com.sparrowwallet:hummingbird:1.7.4')
|
||||
implementation('co.nstant.in:cbor:0.9')
|
||||
implementation('org.openpnp:openpnp-capture-java:0.0.30-1')
|
||||
implementation('io.github.doblon8:openpnp-capture-java:0.0.3')
|
||||
implementation("io.matthewnelson.kmp-tor:runtime:2.5.0")
|
||||
implementation("io.matthewnelson.kmp-tor:resource-exec-tor-gpl:408.21.0")
|
||||
implementation('org.jetbrains.kotlinx:kotlinx-coroutines-javafx:1.10.2') {
|
||||
@@ -104,12 +104,12 @@ dependencies {
|
||||
implementation('org.apache.commons:commons-lang3:3.20.0')
|
||||
implementation('org.apache.commons:commons-compress:1.28.0')
|
||||
implementation('com.github.librepdf:openpdf:1.3.43')
|
||||
implementation('com.googlecode.lanterna:lanterna:3.1.3')
|
||||
implementation('com.googlecode.lanterna:lanterna:3.1.5')
|
||||
implementation('net.coobird:thumbnailator:0.4.21')
|
||||
implementation('com.github.hervegirod:fxsvgimage:1.1')
|
||||
implementation('com.sparrowwallet:toucan:0.9.0')
|
||||
implementation('com.jcraft:jzlib:1.1.3')
|
||||
implementation('io.github.doblon8:jzbar:0.3.0')
|
||||
implementation('io.github.doblon8:jzbar:0.4.0')
|
||||
testImplementation('org.junit.jupiter:junit-jupiter-api:5.14.1')
|
||||
testRuntimeOnly('org.junit.jupiter:junit-jupiter-engine:5.14.1')
|
||||
testRuntimeOnly('org.junit.platform:junit-platform-launcher')
|
||||
@@ -124,17 +124,9 @@ compileJava {
|
||||
}
|
||||
}
|
||||
|
||||
processResources {
|
||||
doLast {
|
||||
delete fileTree("$buildDir/resources/main/native").matching {
|
||||
exclude "${osName}/${osArch}/**"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
test {
|
||||
useJUnitPlatform()
|
||||
jvmArgs = ["--add-opens=java.base/java.io=ALL-UNNAMED", "--enable-native-access=ALL-UNNAMED"]
|
||||
jvmArgs = ["--enable-native-access=ALL-UNNAMED"]
|
||||
}
|
||||
|
||||
application {
|
||||
@@ -148,6 +140,7 @@ application {
|
||||
"--enable-native-access=com.fazecast.jSerialComm",
|
||||
"--enable-native-access=org.usb4java",
|
||||
"--enable-native-access=io.github.doblon8.jzbar",
|
||||
"--enable-native-access=io.github.doblon8.openpnp.capture",
|
||||
"--add-opens=javafx.graphics/com.sun.javafx.css=org.controlsfx.controls",
|
||||
"--add-opens=javafx.graphics/javafx.scene=org.controlsfx.controls",
|
||||
"--add-opens=javafx.controls/com.sun.javafx.scene.control.behavior=org.controlsfx.controls",
|
||||
@@ -161,7 +154,6 @@ application {
|
||||
"--add-opens=javafx.graphics/com.sun.javafx.application=com.sparrowwallet.sparrow",
|
||||
"--add-opens=javafx.graphics/javafx.scene.input=com.sparrowwallet.sparrow",
|
||||
"--add-opens=java.base/java.net=com.sparrowwallet.sparrow",
|
||||
"--add-opens=java.base/java.io=com.google.gson",
|
||||
"--add-opens=java.smartcardio/sun.security.smartcardio=com.sparrowwallet.sparrow",
|
||||
"--add-reads=kotlin.stdlib=kotlinx.coroutines.core",
|
||||
"--add-reads=org.flywaydb.core=java.desktop"]
|
||||
@@ -170,10 +162,15 @@ application {
|
||||
applicationDefaultJvmArgs += ["-Dprism.lcdtext=false", "-Xdock:name=Sparrow"]
|
||||
}
|
||||
if(headless) {
|
||||
applicationDefaultJvmArgs += ["-Dglass.platform=Monocle", "-Dmonocle.platform=Headless", "-Dprism.order=sw"]
|
||||
applicationDefaultJvmArgs += ["-Dglass.platform=Headless"]
|
||||
}
|
||||
}
|
||||
|
||||
run {
|
||||
//Add jdk.unsupported for IDE agents requiring sun.misc
|
||||
jvmArgs += ["--add-modules=jdk.unsupported"]
|
||||
}
|
||||
|
||||
jlink {
|
||||
mergedModule {
|
||||
requires 'javafx.graphics'
|
||||
@@ -188,7 +185,32 @@ jlink {
|
||||
uses 'org.eclipse.jetty.http.HttpFieldPreEncoder'
|
||||
}
|
||||
|
||||
options = ['--strip-native-commands', '--strip-java-debug-attributes', '--compress', 'zip-6', '--no-header-files', '--no-man-pages', '--ignore-signing-information', '--exclude-files', '**.png', '--exclude-resources', 'glob:/com.sparrowwallet.merged.module/META-INF/*']
|
||||
options = ['--strip-native-commands', '--strip-java-debug-attributes', '--compress', 'zip-6',
|
||||
'--no-header-files', '--no-man-pages', '--ignore-signing-information',
|
||||
'--exclude-files', '**.png',
|
||||
'--exclude-resources',
|
||||
'glob:/com.sparrowwallet.merged.module/META-INF/*,' +
|
||||
'glob:/javafx.graphics/*.dylib,' +
|
||||
'glob:/javafx.graphics/*.so,' +
|
||||
'glob:/javafx.graphics/*.dll,' +
|
||||
'glob:/com.sparrowwallet.drongo/native/**,' +
|
||||
'glob:/com.sparrowwallet.sparrow/native/**,' +
|
||||
'glob:/com.sparrowwallet.merged.module/com/sun/jna/**/*.so,' +
|
||||
'glob:/com.sparrowwallet.merged.module/com/sun/jna/**/*.dylib,' +
|
||||
'glob:/com.sparrowwallet.merged.module/com/sun/jna/**/*.jnilib,' +
|
||||
'glob:/com.sparrowwallet.merged.module/com/sun/jna/**/*.dll,' +
|
||||
'glob:/com.sparrowwallet.merged.module/com/sun/jna/**/*.a,' +
|
||||
'glob:/com.sparrowwallet.merged.module/darwin-*/**,' +
|
||||
'glob:/com.sparrowwallet.merged.module/linux-*/**,' +
|
||||
'glob:/com.sparrowwallet.merged.module/win32-*/**,' +
|
||||
'glob:/org.usb4java/org/usb4java/darwin-*/**,' +
|
||||
'glob:/org.usb4java/org/usb4java/linux-*/**,' +
|
||||
'glob:/org.usb4java/org/usb4java/win32-*/**,' +
|
||||
'glob:/org.hid4java/darwin-*/**,' +
|
||||
'glob:/org.hid4java/linux-*/**,' +
|
||||
'glob:/org.hid4java/win32-*/**,' +
|
||||
'glob:/io.github.doblon8.jzbar/native/**,' +
|
||||
'glob:/io.github.doblon8.openpnp.capture/native/**']
|
||||
launcher {
|
||||
name = 'sparrow'
|
||||
jvmArgs = ["--enable-native-access=com.sparrowwallet.drongo",
|
||||
@@ -198,6 +220,8 @@ jlink {
|
||||
"--enable-native-access=com.fazecast.jSerialComm",
|
||||
"--enable-native-access=org.usb4java",
|
||||
"--enable-native-access=io.github.doblon8.jzbar",
|
||||
"--enable-native-access=io.github.doblon8.openpnp.capture",
|
||||
"--enable-native-access=com.sparrowwallet.sparrow",
|
||||
"--add-opens=javafx.graphics/com.sun.javafx.css=org.controlsfx.controls",
|
||||
"--add-opens=javafx.graphics/javafx.scene=org.controlsfx.controls",
|
||||
"--add-opens=javafx.controls/com.sun.javafx.scene.control.behavior=org.controlsfx.controls",
|
||||
@@ -211,7 +235,6 @@ jlink {
|
||||
"--add-opens=javafx.graphics/javafx.scene.input=com.sparrowwallet.sparrow",
|
||||
"--add-opens=javafx.graphics/com.sun.javafx.application=com.sparrowwallet.sparrow",
|
||||
"--add-opens=java.base/java.net=com.sparrowwallet.sparrow",
|
||||
"--add-opens=java.base/java.io=com.google.gson",
|
||||
"--add-opens=java.smartcardio/sun.security.smartcardio=com.sparrowwallet.sparrow",
|
||||
"--add-reads=com.sparrowwallet.merged.module=java.desktop",
|
||||
"--add-reads=com.sparrowwallet.merged.module=java.sql",
|
||||
@@ -230,13 +253,13 @@ jlink {
|
||||
"--add-reads=org.flywaydb.core=java.desktop"]
|
||||
|
||||
if(os.windows) {
|
||||
jvmArgs += ["-Djavax.accessibility.assistive_technologies", "-Djavax.accessibility.screen_magnifier_present=false"]
|
||||
jvmArgs.addAll(["-Djavax.accessibility.assistive_technologies", "-Djavax.accessibility.screen_magnifier_present=false"])
|
||||
}
|
||||
if(os.macOsX) {
|
||||
jvmArgs += ["-Dprism.lcdtext=false", "--add-opens=javafx.graphics/com.sun.glass.ui.mac=com.sparrowwallet.merged.module"]
|
||||
jvmArgs.addAll(["-Dprism.lcdtext=false", "--add-opens=javafx.graphics/com.sun.glass.ui.mac=com.sparrowwallet.merged.module"])
|
||||
}
|
||||
if(headless) {
|
||||
jvmArgs += ["-Dglass.platform=Monocle", "-Dmonocle.platform=Headless", "-Dprism.order=sw"]
|
||||
jvmArgs.addAll(["-Dglass.platform=Headless"])
|
||||
}
|
||||
}
|
||||
addExtraDependencies("javafx")
|
||||
@@ -248,8 +271,8 @@ jlink {
|
||||
imageOptions = []
|
||||
installerOptions = ['--file-associations', 'src/main/deploy/psbt.properties', '--file-associations', 'src/main/deploy/txn.properties', '--file-associations', 'src/main/deploy/asc.properties', '--license-file', 'LICENSE']
|
||||
if(os.windows) {
|
||||
installerOptions += ['--win-per-user-install', '--win-dir-chooser', '--win-menu', '--win-menu-group', 'Sparrow', '--win-shortcut', '--resource-dir', 'src/main/deploy/package/windows/']
|
||||
imageOptions += ['--icon', 'src/main/deploy/package/windows/sparrow.ico']
|
||||
installerOptions.addAll(['--win-per-user-install', '--win-menu', '--win-menu-group', 'Sparrow', '--win-shortcut', '--resource-dir', 'src/main/deploy/package/windows/'])
|
||||
imageOptions.addAll(['--icon', 'src/main/deploy/package/windows/sparrow.ico'])
|
||||
installerType = "msi"
|
||||
}
|
||||
if(os.linux) {
|
||||
@@ -258,14 +281,14 @@ jlink {
|
||||
installerOptions = ['--license-file', 'LICENSE']
|
||||
} else {
|
||||
installerName = "sparrowwallet"
|
||||
installerOptions += ['--linux-shortcut', '--linux-menu-group', 'Sparrow']
|
||||
installerOptions.addAll(['--linux-shortcut', '--linux-menu-group', 'Sparrow'])
|
||||
}
|
||||
installerOptions += ['--resource-dir', layout.buildDirectory.dir('deploy/package').get().asFile.toString(), '--linux-app-category', 'utils', '--linux-app-release', '1', '--linux-rpm-license-type', 'ASL 2.0', '--linux-deb-maintainer', 'mail@sparrowwallet.com']
|
||||
imageOptions += ['--icon', 'src/main/deploy/package/linux/Sparrow.png', '--resource-dir', 'src/main/deploy/package/linux/']
|
||||
installerOptions.addAll(['--resource-dir', layout.buildDirectory.dir('deploy/package').get().asFile.toString(), '--linux-app-category', 'utils', '--linux-app-release', '1', '--linux-rpm-license-type', 'ASL 2.0', '--linux-deb-maintainer', 'mail@sparrowwallet.com'])
|
||||
imageOptions.addAll(['--icon', 'src/main/deploy/package/linux/Sparrow.png', '--resource-dir', 'src/main/deploy/package/linux/'])
|
||||
}
|
||||
if(os.macOsX) {
|
||||
installerOptions += ['--mac-sign', '--mac-signing-key-user-name', 'Craig Raw (UPLVMSK9D7)']
|
||||
imageOptions += ['--icon', 'src/main/deploy/package/macos/sparrow.icns', '--resource-dir', 'src/main/deploy/package/macos/']
|
||||
installerOptions.addAll(['--mac-sign', '--mac-signing-key-user-name', 'Craig Raw (UPLVMSK9D7)'])
|
||||
imageOptions.addAll(['--icon', 'src/main/deploy/package/macos/sparrow.icns', '--resource-dir', 'src/main/deploy/package/macos/'])
|
||||
installerType = "dmg"
|
||||
}
|
||||
}
|
||||
@@ -277,13 +300,13 @@ jlink {
|
||||
}
|
||||
|
||||
if(os.linux) {
|
||||
tasks.jlink.finalizedBy('addUserWritePermission', 'copyUdevRules')
|
||||
tasks.jlink.finalizedBy('addUserWritePermission', 'copyUdevRules', 'extractNativeLibraries')
|
||||
tasks.jpackageImage.finalizedBy('prepareResourceDir')
|
||||
if(!headless) {
|
||||
tasks.jpackage.dependsOn('copyMimeInfo')
|
||||
}
|
||||
} else {
|
||||
tasks.jlink.finalizedBy('addUserWritePermission')
|
||||
tasks.jlink.finalizedBy('addUserWritePermission', 'extractNativeLibraries')
|
||||
}
|
||||
|
||||
tasks.register('addUserWritePermission', Exec) {
|
||||
@@ -362,6 +385,75 @@ tasks.register('packageTarDistribution', Tar) {
|
||||
}
|
||||
}
|
||||
|
||||
def jnaPlatform
|
||||
if(os.macOsX) {
|
||||
jnaPlatform = "darwin-${osArch == 'aarch64' ? 'aarch64' : 'x86-64'}"
|
||||
} else if(os.windows) {
|
||||
jnaPlatform = "win32-x86-64"
|
||||
} else {
|
||||
jnaPlatform = "linux-${osArch == 'aarch64' ? 'aarch64' : 'x86-64'}"
|
||||
}
|
||||
|
||||
def serialOs = os.macOsX ? "OSX" : (os.windows ? "Windows" : "Linux")
|
||||
def serialArch = osArch == "aarch64" ? "aarch64" : "x86_64"
|
||||
|
||||
// Map of JAR name prefix to the include glob for platform-specific natives inside the JAR.
|
||||
def nativeLibJars = [
|
||||
'jna-' : "com/sun/jna/${jnaPlatform}/*",
|
||||
'argon2-jvm-2' : "${jnaPlatform}/*",
|
||||
'hid4java-' : "${jnaPlatform}/*",
|
||||
'jSerialComm-' : "${serialOs}/${serialArch}/*",
|
||||
'usb4java-' : "org/usb4java/${jnaPlatform}/*",
|
||||
'jzbar-' : "native/${osName}/${osArch}/*",
|
||||
'openpnp-capture-java-': "native/${osName}/${osArch}/*",
|
||||
]
|
||||
|
||||
tasks.register('extractNativeLibraries') {
|
||||
dependsOn 'jlink'
|
||||
def drongoNativeDir = "${project(':drongo').projectDir}/src/main/resources/native/${osName}/${osArch}"
|
||||
doLast {
|
||||
def imageLib = file("$buildDir/image/lib")
|
||||
|
||||
// Project-owned natives
|
||||
copy {
|
||||
from drongoNativeDir, "src/main/resources/native/${osName}/${osArch}"
|
||||
into imageLib
|
||||
eachFile { it.permissions { unix('rw-r--r--') } }
|
||||
}
|
||||
|
||||
// JavaFX natives
|
||||
def javafxClassifier = ""
|
||||
if(os.macOsX) {
|
||||
javafxClassifier = osArch == "aarch64" ? "mac-aarch64" : "mac"
|
||||
} else if(os.windows) {
|
||||
javafxClassifier = "win"
|
||||
} else {
|
||||
javafxClassifier = osArch == "aarch64" ? "linux-aarch64" : "linux"
|
||||
}
|
||||
def javafxJar = configurations.runtimeClasspath.find { it.name == "javafx-graphics-${javafx.version}-${javafxClassifier}.jar" }
|
||||
if(javafxJar) {
|
||||
copy {
|
||||
from(zipTree(javafxJar)) { include "*.dylib", "*.so", "*.dll" }
|
||||
into imageLib
|
||||
eachFile { it.permissions { unix('rw-r--r--') } }
|
||||
}
|
||||
}
|
||||
|
||||
// Third-party natives
|
||||
nativeLibJars.each { prefix, includePattern ->
|
||||
def jar = configurations.runtimeClasspath.find { it.name.startsWith(prefix) }
|
||||
if(jar) {
|
||||
copy {
|
||||
from(zipTree(jar)) { include includePattern }
|
||||
into imageLib
|
||||
eachFile { it.path = it.name; it.permissions { unix('rw-r--r--') } }
|
||||
includeEmptyDirs = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extraJavaModuleInfo {
|
||||
module('no.tornado:tornadofx-controls', 'tornadofx.controls') {
|
||||
exports('tornadofx.control')
|
||||
@@ -390,12 +482,6 @@ extraJavaModuleInfo {
|
||||
requires('org.slf4j')
|
||||
requires('com.fasterxml.jackson.databind')
|
||||
}
|
||||
module('org.openpnp:openpnp-capture-java', 'openpnp.capture.java') {
|
||||
exports('org.openpnp.capture')
|
||||
exports('org.openpnp.capture.library')
|
||||
requires('java.desktop')
|
||||
requires('com.sun.jna')
|
||||
}
|
||||
module('net.sourceforge.javacsv:javacsv', 'net.sourceforge.javacsv') {
|
||||
exports('com.csvreader')
|
||||
}
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
plugins {
|
||||
id 'java-gradle-plugin' // so we can assign and ID to our plugin
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation 'com.google.gradle:osdetector-gradle-plugin:1.7.3'
|
||||
}
|
||||
|
||||
repositories {
|
||||
mavenCentral()
|
||||
maven {
|
||||
url = uri("https://plugins.gradle.org/m2/")
|
||||
}
|
||||
}
|
||||
|
||||
gradlePlugin {
|
||||
plugins {
|
||||
register("org-openjfx-javafxplugin") {
|
||||
id = "org-openjfx-javafxplugin"
|
||||
implementationClass = "org.openjfx.gradle.JavaFXPlugin"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,114 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2018, 2020, Gluon
|
||||
* All rights reserved.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without
|
||||
* modification, are permitted provided that the following conditions are met:
|
||||
*
|
||||
* * Redistributions of source code must retain the above copyright notice, this
|
||||
* list of conditions and the following disclaimer.
|
||||
*
|
||||
* * Redistributions in binary form must reproduce the above copyright notice,
|
||||
* this list of conditions and the following disclaimer in the documentation
|
||||
* and/or other materials provided with the distribution.
|
||||
*
|
||||
* * Neither the name of the copyright holder nor the names of its
|
||||
* contributors may be used to endorse or promote products derived from
|
||||
* this software without specific prior written permission.
|
||||
*
|
||||
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
||||
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*/
|
||||
package org.openjfx.gradle;
|
||||
|
||||
import org.gradle.api.GradleException;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.regex.Pattern;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
public enum JavaFXModule {
|
||||
|
||||
BASE,
|
||||
GRAPHICS(BASE),
|
||||
CONTROLS(BASE, GRAPHICS),
|
||||
FXML(BASE, GRAPHICS),
|
||||
MEDIA(BASE, GRAPHICS),
|
||||
SWING(BASE, GRAPHICS),
|
||||
WEB(BASE, CONTROLS, GRAPHICS, MEDIA);
|
||||
|
||||
static final String PREFIX_MODULE = "javafx.";
|
||||
private static final String PREFIX_ARTIFACT = "javafx-";
|
||||
|
||||
private List<JavaFXModule> dependentModules;
|
||||
|
||||
JavaFXModule(JavaFXModule...dependentModules) {
|
||||
this.dependentModules = List.of(dependentModules);
|
||||
}
|
||||
|
||||
public static Optional<JavaFXModule> fromModuleName(String moduleName) {
|
||||
return Stream.of(JavaFXModule.values())
|
||||
.filter(javaFXModule -> moduleName.equals(javaFXModule.getModuleName()))
|
||||
.findFirst();
|
||||
}
|
||||
|
||||
public String getModuleName() {
|
||||
return PREFIX_MODULE + name().toLowerCase(Locale.ROOT);
|
||||
}
|
||||
|
||||
public String getModuleJarFileName() {
|
||||
return getModuleName() + ".jar";
|
||||
}
|
||||
|
||||
public String getArtifactName() {
|
||||
return PREFIX_ARTIFACT + name().toLowerCase(Locale.ROOT);
|
||||
}
|
||||
|
||||
public boolean compareJarFileName(JavaFXPlatform platform, String jarFileName) {
|
||||
Pattern p = Pattern.compile(getArtifactName() + "-.+-" + platform.getClassifier() + "\\.jar");
|
||||
return p.matcher(jarFileName).matches();
|
||||
}
|
||||
|
||||
public static Set<JavaFXModule> getJavaFXModules(List<String> moduleNames) {
|
||||
validateModules(moduleNames);
|
||||
|
||||
return moduleNames.stream()
|
||||
.map(JavaFXModule::fromModuleName)
|
||||
.flatMap(Optional::stream)
|
||||
.flatMap(javaFXModule -> javaFXModule.getMavenDependencies().stream())
|
||||
.collect(Collectors.toSet());
|
||||
}
|
||||
|
||||
public static void validateModules(List<String> moduleNames) {
|
||||
var invalidModules = moduleNames.stream()
|
||||
.filter(module -> JavaFXModule.fromModuleName(module).isEmpty())
|
||||
.collect(Collectors.toList());
|
||||
|
||||
if (! invalidModules.isEmpty()) {
|
||||
throw new GradleException("Found one or more invalid JavaFX module names: " + invalidModules);
|
||||
}
|
||||
}
|
||||
|
||||
public List<JavaFXModule> getDependentModules() {
|
||||
return dependentModules;
|
||||
}
|
||||
|
||||
public List<JavaFXModule> getMavenDependencies() {
|
||||
List<JavaFXModule> dependencies = new ArrayList<>(dependentModules);
|
||||
dependencies.add(0, this);
|
||||
return dependencies;
|
||||
}
|
||||
}
|
||||
@@ -1,164 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2018, Gluon
|
||||
* All rights reserved.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without
|
||||
* modification, are permitted provided that the following conditions are met:
|
||||
*
|
||||
* * Redistributions of source code must retain the above copyright notice, this
|
||||
* list of conditions and the following disclaimer.
|
||||
*
|
||||
* * Redistributions in binary form must reproduce the above copyright notice,
|
||||
* this list of conditions and the following disclaimer in the documentation
|
||||
* and/or other materials provided with the distribution.
|
||||
*
|
||||
* * Neither the name of the copyright holder nor the names of its
|
||||
* contributors may be used to endorse or promote products derived from
|
||||
* this software without specific prior written permission.
|
||||
*
|
||||
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
||||
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*/
|
||||
package org.openjfx.gradle;
|
||||
|
||||
import org.gradle.api.Project;
|
||||
import org.gradle.api.artifacts.repositories.FlatDirectoryArtifactRepository;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import static org.openjfx.gradle.JavaFXModule.PREFIX_MODULE;
|
||||
|
||||
public class JavaFXOptions {
|
||||
|
||||
private static final String MAVEN_JAVAFX_ARTIFACT_GROUP_ID = "org.openjfx";
|
||||
private static final String JAVAFX_SDK_LIB_FOLDER = "lib";
|
||||
|
||||
private final Project project;
|
||||
private final JavaFXPlatform platform;
|
||||
|
||||
private String version = "16";
|
||||
private String sdk;
|
||||
private String configuration = "implementation";
|
||||
private String lastUpdatedConfiguration;
|
||||
private List<String> modules = new ArrayList<>();
|
||||
private FlatDirectoryArtifactRepository customSDKArtifactRepository;
|
||||
|
||||
public JavaFXOptions(Project project) {
|
||||
this.project = project;
|
||||
this.platform = JavaFXPlatform.detect(project);
|
||||
}
|
||||
|
||||
public JavaFXPlatform getPlatform() {
|
||||
return platform;
|
||||
}
|
||||
|
||||
public String getVersion() {
|
||||
return version;
|
||||
}
|
||||
|
||||
public void setVersion(String version) {
|
||||
this.version = version;
|
||||
updateJavaFXDependencies();
|
||||
}
|
||||
|
||||
/**
|
||||
* If set, the JavaFX modules will be taken from this local
|
||||
* repository, and not from Maven Central
|
||||
* @param sdk, the path to the local JavaFX SDK folder
|
||||
*/
|
||||
public void setSdk(String sdk) {
|
||||
this.sdk = sdk;
|
||||
updateJavaFXDependencies();
|
||||
}
|
||||
|
||||
public String getSdk() {
|
||||
return sdk;
|
||||
}
|
||||
|
||||
/** Set the configuration name for dependencies, e.g.
|
||||
* 'implementation', 'compileOnly' etc.
|
||||
* @param configuration The configuration name for dependencies
|
||||
*/
|
||||
public void setConfiguration(String configuration) {
|
||||
this.configuration = configuration;
|
||||
updateJavaFXDependencies();
|
||||
}
|
||||
|
||||
public String getConfiguration() {
|
||||
return configuration;
|
||||
}
|
||||
|
||||
public List<String> getModules() {
|
||||
return modules;
|
||||
}
|
||||
|
||||
public void setModules(List<String> modules) {
|
||||
this.modules = modules;
|
||||
updateJavaFXDependencies();
|
||||
}
|
||||
|
||||
public void modules(String...moduleNames) {
|
||||
setModules(List.of(moduleNames));
|
||||
}
|
||||
|
||||
private void updateJavaFXDependencies() {
|
||||
clearJavaFXDependencies();
|
||||
|
||||
String configuration = getConfiguration();
|
||||
JavaFXModule.getJavaFXModules(this.modules).stream()
|
||||
.sorted()
|
||||
.forEach(javaFXModule -> {
|
||||
if (customSDKArtifactRepository != null) {
|
||||
project.getDependencies().add(configuration, Map.of("name", javaFXModule.getModuleName()));
|
||||
} else {
|
||||
project.getDependencies().add(configuration,
|
||||
String.format("%s:%s:%s:%s", MAVEN_JAVAFX_ARTIFACT_GROUP_ID, javaFXModule.getArtifactName(),
|
||||
getVersion(), getPlatform().getClassifier()));
|
||||
}
|
||||
});
|
||||
lastUpdatedConfiguration = configuration;
|
||||
}
|
||||
|
||||
private void clearJavaFXDependencies() {
|
||||
if (customSDKArtifactRepository != null) {
|
||||
project.getRepositories().remove(customSDKArtifactRepository);
|
||||
customSDKArtifactRepository = null;
|
||||
}
|
||||
|
||||
if (sdk != null && ! sdk.isEmpty()) {
|
||||
Map<String, String> dirs = new HashMap<>();
|
||||
dirs.put("name", "customSDKArtifactRepository");
|
||||
if (sdk.endsWith(File.separator)) {
|
||||
dirs.put("dirs", sdk + JAVAFX_SDK_LIB_FOLDER);
|
||||
} else {
|
||||
dirs.put("dirs", sdk + File.separator + JAVAFX_SDK_LIB_FOLDER);
|
||||
}
|
||||
customSDKArtifactRepository = project.getRepositories().flatDir(dirs);
|
||||
}
|
||||
|
||||
if (lastUpdatedConfiguration == null) {
|
||||
return;
|
||||
}
|
||||
var configuration = project.getConfigurations().findByName(lastUpdatedConfiguration);
|
||||
if (configuration != null) {
|
||||
if (customSDKArtifactRepository != null) {
|
||||
configuration.getDependencies()
|
||||
.removeIf(dependency -> dependency.getName().startsWith(PREFIX_MODULE));
|
||||
}
|
||||
configuration.getDependencies()
|
||||
.removeIf(dependency -> MAVEN_JAVAFX_ARTIFACT_GROUP_ID.equals(dependency.getGroup()));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,91 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2018, Gluon
|
||||
* All rights reserved.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without
|
||||
* modification, are permitted provided that the following conditions are met:
|
||||
*
|
||||
* * Redistributions of source code must retain the above copyright notice, this
|
||||
* list of conditions and the following disclaimer.
|
||||
*
|
||||
* * Redistributions in binary form must reproduce the above copyright notice,
|
||||
* this list of conditions and the following disclaimer in the documentation
|
||||
* and/or other materials provided with the distribution.
|
||||
*
|
||||
* * Neither the name of the copyright holder nor the names of its
|
||||
* contributors may be used to endorse or promote products derived from
|
||||
* this software without specific prior written permission.
|
||||
*
|
||||
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
||||
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*/
|
||||
package org.openjfx.gradle;
|
||||
|
||||
import com.google.gradle.osdetector.OsDetector;
|
||||
import org.gradle.api.GradleException;
|
||||
import org.gradle.api.Project;
|
||||
|
||||
import java.awt.*;
|
||||
import java.util.Arrays;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public enum JavaFXPlatform {
|
||||
|
||||
LINUX("linux", "linux-x86_64"),
|
||||
LINUX_MONOCLE("linux-monocle", "linux-x86_64-monocle"),
|
||||
LINUX_AARCH64("linux-aarch64", "linux-aarch_64"),
|
||||
LINUX_AARCH64_MONOCLE("linux-aarch64-monocle", "linux-aarch_64-monocle"),
|
||||
WINDOWS("win", "windows-x86_64"),
|
||||
WINDOWS_MONOCLE("win-monocle", "windows-x86_64-monocle"),
|
||||
OSX("mac", "osx-x86_64"),
|
||||
OSX_MONOCLE("mac-monocle", "osx-x86_64-monocle"),
|
||||
OSX_AARCH64("mac-aarch64", "osx-aarch_64"),
|
||||
OSX_AARCH64_MONOCLE("mac-aarch64-monocle", "osx-aarch_64-monocle");
|
||||
|
||||
private final String classifier;
|
||||
private final String osDetectorClassifier;
|
||||
|
||||
JavaFXPlatform( String classifier, String osDetectorClassifier ) {
|
||||
this.classifier = classifier;
|
||||
this.osDetectorClassifier = osDetectorClassifier;
|
||||
}
|
||||
|
||||
public String getClassifier() {
|
||||
return classifier;
|
||||
}
|
||||
|
||||
public static JavaFXPlatform detect(Project project) {
|
||||
|
||||
String osClassifier = project.getExtensions().getByType(OsDetector.class).getClassifier();
|
||||
|
||||
if("true".equals(System.getProperty("java.awt.headless"))) {
|
||||
osClassifier += "-monocle";
|
||||
}
|
||||
|
||||
for ( JavaFXPlatform platform: values()) {
|
||||
if ( platform.osDetectorClassifier.equals(osClassifier)) {
|
||||
return platform;
|
||||
}
|
||||
}
|
||||
|
||||
String supportedPlatforms = Arrays.stream(values())
|
||||
.map(p->p.osDetectorClassifier)
|
||||
.collect(Collectors.joining("', '", "'", "'"));
|
||||
|
||||
throw new GradleException(
|
||||
String.format(
|
||||
"Unsupported JavaFX platform found: '%s'! " +
|
||||
"This plugin is designed to work on supported platforms only." +
|
||||
"Current supported platforms are %s.", osClassifier, supportedPlatforms )
|
||||
);
|
||||
|
||||
}
|
||||
}
|
||||
@@ -1,47 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2018, Gluon
|
||||
* All rights reserved.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without
|
||||
* modification, are permitted provided that the following conditions are met:
|
||||
*
|
||||
* * Redistributions of source code must retain the above copyright notice, this
|
||||
* list of conditions and the following disclaimer.
|
||||
*
|
||||
* * Redistributions in binary form must reproduce the above copyright notice,
|
||||
* this list of conditions and the following disclaimer in the documentation
|
||||
* and/or other materials provided with the distribution.
|
||||
*
|
||||
* * Neither the name of the copyright holder nor the names of its
|
||||
* contributors may be used to endorse or promote products derived from
|
||||
* this software without specific prior written permission.
|
||||
*
|
||||
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
||||
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*/
|
||||
package org.openjfx.gradle;
|
||||
|
||||
import com.google.gradle.osdetector.OsDetectorPlugin;
|
||||
import org.gradle.api.Plugin;
|
||||
import org.gradle.api.Project;
|
||||
import org.openjfx.gradle.tasks.ExecTask;
|
||||
|
||||
public class JavaFXPlugin implements Plugin<Project> {
|
||||
|
||||
@Override
|
||||
public void apply(Project project) {
|
||||
project.getPlugins().apply(OsDetectorPlugin.class);
|
||||
|
||||
project.getExtensions().create("javafx", JavaFXOptions.class, project);
|
||||
|
||||
project.getTasks().register("configJavafxRun", ExecTask.class, project);
|
||||
}
|
||||
}
|
||||
@@ -1,90 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2019, 2021, Gluon
|
||||
* All rights reserved.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without
|
||||
* modification, are permitted provided that the following conditions are met:
|
||||
*
|
||||
* * Redistributions of source code must retain the above copyright notice, this
|
||||
* list of conditions and the following disclaimer.
|
||||
*
|
||||
* * Redistributions in binary form must reproduce the above copyright notice,
|
||||
* this list of conditions and the following disclaimer in the documentation
|
||||
* and/or other materials provided with the distribution.
|
||||
*
|
||||
* * Neither the name of the copyright holder nor the names of its
|
||||
* contributors may be used to endorse or promote products derived from
|
||||
* this software without specific prior written permission.
|
||||
*
|
||||
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
||||
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*/
|
||||
package org.openjfx.gradle.tasks;
|
||||
|
||||
import org.gradle.api.DefaultTask;
|
||||
import org.gradle.api.GradleException;
|
||||
import org.gradle.api.Project;
|
||||
import org.gradle.api.file.FileCollection;
|
||||
import org.gradle.api.plugins.ApplicationPlugin;
|
||||
import org.gradle.api.tasks.JavaExec;
|
||||
import org.gradle.api.tasks.TaskAction;
|
||||
import org.openjfx.gradle.JavaFXModule;
|
||||
import org.openjfx.gradle.JavaFXOptions;
|
||||
import org.openjfx.gradle.JavaFXPlatform;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import java.io.File;
|
||||
import java.util.Arrays;
|
||||
import java.util.TreeSet;
|
||||
|
||||
public class ExecTask extends DefaultTask {
|
||||
private final Project project;
|
||||
private JavaExec execTask;
|
||||
|
||||
@Inject
|
||||
public ExecTask(Project project) {
|
||||
this.project = project;
|
||||
project.getPluginManager().withPlugin(ApplicationPlugin.APPLICATION_PLUGIN_NAME, e -> {
|
||||
execTask = (JavaExec) project.getTasks().findByName(ApplicationPlugin.TASK_RUN_NAME);
|
||||
if (execTask != null) {
|
||||
execTask.dependsOn(this);
|
||||
} else {
|
||||
throw new GradleException("Run task not found.");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@TaskAction
|
||||
public void action() {
|
||||
if (execTask != null) {
|
||||
JavaFXOptions javaFXOptions = project.getExtensions().getByType(JavaFXOptions.class);
|
||||
JavaFXModule.validateModules(javaFXOptions.getModules());
|
||||
|
||||
var definedJavaFXModuleNames = new TreeSet<>(javaFXOptions.getModules());
|
||||
if (!definedJavaFXModuleNames.isEmpty()) {
|
||||
final FileCollection classpathWithoutJavaFXJars = execTask.getClasspath().filter(
|
||||
jar -> Arrays.stream(JavaFXModule.values()).noneMatch(javaFXModule -> jar.getName().contains(javaFXModule.getArtifactName()))
|
||||
);
|
||||
final FileCollection javaFXPlatformJars = execTask.getClasspath().filter(jar -> isJavaFXJar(jar, javaFXOptions.getPlatform()));
|
||||
execTask.setClasspath(classpathWithoutJavaFXJars.plus(javaFXPlatformJars));
|
||||
}
|
||||
} else {
|
||||
throw new GradleException("Run task not found. Please, make sure the Application plugin is applied");
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean isJavaFXJar(File jar, JavaFXPlatform platform) {
|
||||
return jar.isFile() &&
|
||||
Arrays.stream(JavaFXModule.values()).anyMatch(javaFXModule ->
|
||||
javaFXModule.compareJarFileName(platform, jar.getName()) ||
|
||||
javaFXModule.getModuleJarFileName().equals(jar.getName()));
|
||||
}
|
||||
}
|
||||
@@ -56,7 +56,7 @@ sudo apt install -y rpm fakeroot binutils
|
||||
First, assign a temporary variable in your shell for the specific release you want to build. For the current one specify:
|
||||
|
||||
```shell
|
||||
GIT_TAG="2.4.1"
|
||||
GIT_TAG="2.5.3"
|
||||
```
|
||||
|
||||
The project can then be initially cloned as follows:
|
||||
|
||||
+1
-1
Submodule drongo updated: 4049ebcdda...fa1bb4eec2
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
+1
-1
Submodule lark updated: 7f79ddee6b...00fdebc715
@@ -21,7 +21,7 @@
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>2.4.2</string>
|
||||
<string>2.5.4</string>
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
<!-- See https://developer.apple.com/app-store/categories/ for list of AppStore categories -->
|
||||
|
||||
@@ -1,4 +1,10 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
|
||||
<!--
|
||||
This file is a copy of the jpackage WiX template (src/jdk.jpackage/windows/classes/jdk/jpackage/internal/resources/main.wxs in the JDK sources),
|
||||
customized only to add the RegistryEntries and CapabilitiesEntries components registering the auth47, bitcoin and lightning URI schemes, and their ComponentRefs in the Feature.
|
||||
When upgrading the build JDK, re-diff this file against that JDK's template and re-apply those blocks, keeping the component GUIDs unchanged.
|
||||
-->
|
||||
<Wix xmlns="http://schemas.microsoft.com/wix/2006/wi"
|
||||
xmlns:util="http://schemas.microsoft.com/wix/UtilExtension">
|
||||
|
||||
@@ -14,6 +20,17 @@
|
||||
|
||||
<?include $(var.JpConfigDir)/overrides.wxi ?>
|
||||
|
||||
<?ifdef JpAllowUpgrades ?>
|
||||
<?define JpUpgradeVersionOnlyDetectUpgrade="no"?>
|
||||
<?else?>
|
||||
<?define JpUpgradeVersionOnlyDetectUpgrade="yes"?>
|
||||
<?endif?>
|
||||
<?ifdef JpAllowDowngrades ?>
|
||||
<?define JpUpgradeVersionOnlyDetectDowngrade="no"?>
|
||||
<?else?>
|
||||
<?define JpUpgradeVersionOnlyDetectDowngrade="yes"?>
|
||||
<?endif?>
|
||||
|
||||
<Product
|
||||
Id="$(var.JpProductCode)"
|
||||
Name="$(var.JpAppName)"
|
||||
@@ -32,45 +49,31 @@
|
||||
|
||||
<Media Id="1" Cabinet="Data.cab" EmbedCab="yes" />
|
||||
|
||||
<?ifdef JpAllowDowngrades ?>
|
||||
<?ifdef JpAllowUpgrades ?>
|
||||
<MajorUpgrade
|
||||
AllowDowngrades="yes"
|
||||
Disallow="no"
|
||||
/>
|
||||
<?endif?>
|
||||
<?endif?>
|
||||
<Upgrade Id="$(var.JpProductUpgradeCode)">
|
||||
<UpgradeVersion
|
||||
OnlyDetect="$(var.JpUpgradeVersionOnlyDetectUpgrade)"
|
||||
Property="JP_UPGRADABLE_FOUND"
|
||||
Maximum="$(var.JpAppVersion)"
|
||||
MigrateFeatures="yes"
|
||||
IncludeMaximum="$(var.JpUpgradeVersionOnlyDetectUpgrade)" />
|
||||
<UpgradeVersion
|
||||
OnlyDetect="$(var.JpUpgradeVersionOnlyDetectDowngrade)"
|
||||
Property="JP_DOWNGRADABLE_FOUND"
|
||||
Minimum="$(var.JpAppVersion)"
|
||||
MigrateFeatures="yes"
|
||||
IncludeMinimum="$(var.JpUpgradeVersionOnlyDetectDowngrade)" />
|
||||
</Upgrade>
|
||||
|
||||
<?ifdef JpAllowDowngrades ?>
|
||||
<?ifndef JpAllowUpgrades ?>
|
||||
<MajorUpgrade
|
||||
AllowDowngrades="yes"
|
||||
Disallow="yes"
|
||||
DisallowUpgradeErrorMessage="!(loc.DisallowUpgradeErrorMessage)"
|
||||
/>
|
||||
<CustomAction Id="JpDisallowUpgrade" Error="!(loc.DisallowUpgradeErrorMessage)" />
|
||||
<?endif?>
|
||||
<?ifndef JpAllowDowngrades ?>
|
||||
<CustomAction Id="JpDisallowDowngrade" Error="!(loc.DowngradeErrorMessage)" />
|
||||
<?endif?>
|
||||
|
||||
<?ifndef JpAllowDowngrades ?>
|
||||
<?ifdef JpAllowUpgrades ?>
|
||||
<MajorUpgrade
|
||||
AllowDowngrades="no"
|
||||
Disallow="no"
|
||||
DowngradeErrorMessage="!(loc.DowngradeErrorMessage)"
|
||||
/>
|
||||
<?endif?>
|
||||
<?endif?>
|
||||
<Binary Id="JpCaDll" SourceFile="wixhelper.dll"/>
|
||||
|
||||
<?ifndef JpAllowDowngrades ?>
|
||||
<?ifndef JpAllowUpgrades ?>
|
||||
<MajorUpgrade
|
||||
AllowDowngrades="no"
|
||||
Disallow="yes"
|
||||
DowngradeErrorMessage="!(loc.DowngradeErrorMessage)"
|
||||
DisallowUpgradeErrorMessage="!(loc.DisallowUpgradeErrorMessage)"
|
||||
/>
|
||||
<?endif?>
|
||||
<?endif?>
|
||||
<CustomAction Id="JpFindRelatedProducts" BinaryKey="JpCaDll" DllEntry="FindRelatedProductsEx" />
|
||||
|
||||
<!-- Standard required root -->
|
||||
<Directory Id="TARGETDIR" Name="SourceDir"/>
|
||||
@@ -81,7 +84,7 @@
|
||||
<RegistryValue Type="string" Name="URL Protocol" Value=""/>
|
||||
<RegistryValue Type="string" Value="URL:Auth47 Authentication URI"/>
|
||||
<RegistryKey Key="DefaultIcon">
|
||||
<RegistryValue Type="string" Value="$(var.JpAppName).exe" />
|
||||
<RegistryValue Type="string" Value="[INSTALLDIR]$(var.JpAppName).exe" />
|
||||
</RegistryKey>
|
||||
<RegistryKey Key="shell\open\command">
|
||||
<RegistryValue Type="string" Value=""[INSTALLDIR]$(var.JpAppName).exe" "%1"" />
|
||||
@@ -91,7 +94,7 @@
|
||||
<RegistryValue Type="string" Name="URL Protocol" Value=""/>
|
||||
<RegistryValue Type="string" Value="URL:Bitcoin Payment URL"/>
|
||||
<RegistryKey Key="DefaultIcon">
|
||||
<RegistryValue Type="string" Value="$(var.JpAppName).exe" />
|
||||
<RegistryValue Type="string" Value="[INSTALLDIR]$(var.JpAppName).exe" />
|
||||
</RegistryKey>
|
||||
<RegistryKey Key="shell\open\command">
|
||||
<RegistryValue Type="string" Value=""[INSTALLDIR]$(var.JpAppName).exe" "%1"" />
|
||||
@@ -101,73 +104,89 @@
|
||||
<RegistryValue Type="string" Name="URL Protocol" Value=""/>
|
||||
<RegistryValue Type="string" Value="URL:LNURL URI"/>
|
||||
<RegistryKey Key="DefaultIcon">
|
||||
<RegistryValue Type="string" Value="$(var.JpAppName).exe" />
|
||||
<RegistryValue Type="string" Value="[INSTALLDIR]$(var.JpAppName).exe" />
|
||||
</RegistryKey>
|
||||
<RegistryKey Key="shell\open\command">
|
||||
<RegistryValue Type="string" Value=""[INSTALLDIR]$(var.JpAppName).exe" "%1"" />
|
||||
</RegistryKey>
|
||||
</RegistryKey>
|
||||
</Component>
|
||||
<Component Id="CapabilitiesEntries" Guid="{BE92DD88-F799-47CC-A256-4165F9183A46}">
|
||||
<RegistryKey Root="HKMU" Key="Software\$(var.JpAppName)\Capabilities" Action="createAndRemoveOnUninstall">
|
||||
<RegistryValue Type="string" Name="ApplicationName" Value="$(var.JpAppName)"/>
|
||||
<RegistryValue Type="string" Name="ApplicationDescription" Value="$(var.JpAppDescription)"/>
|
||||
<RegistryKey Key="URLAssociations">
|
||||
<RegistryValue Type="string" Name="auth47" Value="auth47"/>
|
||||
<RegistryValue Type="string" Name="bitcoin" Value="bitcoin"/>
|
||||
<RegistryValue Type="string" Name="lightning" Value="lightning"/>
|
||||
</RegistryKey>
|
||||
</RegistryKey>
|
||||
<RegistryValue Root="HKMU" Key="Software\RegisteredApplications" Name="$(var.JpAppName)" Type="string" Value="Software\$(var.JpAppName)\Capabilities"/>
|
||||
</Component>
|
||||
</DirectoryRef>
|
||||
|
||||
<Feature Id="DefaultFeature" Title="!(loc.MainFeatureTitle)" Level="1">
|
||||
<ComponentGroupRef Id="Shortcuts"/>
|
||||
<ComponentGroupRef Id="Files"/>
|
||||
<ComponentGroupRef Id="FileAssociations"/>
|
||||
<ComponentGroupRef Id="FragmentOsCondition"/>
|
||||
<ComponentRef Id="RegistryEntries"/>
|
||||
<ComponentRef Id="CapabilitiesEntries"/>
|
||||
</Feature>
|
||||
|
||||
<?ifdef JpInstallDirChooser ?>
|
||||
<Binary Id="JpCaDll" SourceFile="wixhelper.dll"/>
|
||||
<CustomAction Id="JpCheckInstallDir" BinaryKey="JpCaDll" DllEntry="CheckInstallDir" />
|
||||
<CustomAction Id="JpSetARPINSTALLLOCATION" Property="ARPINSTALLLOCATION" Value="[INSTALLDIR]" />
|
||||
<CustomAction Id="JpSetARPCOMMENTS" Property="ARPCOMMENTS" Value="$(var.JpAppDescription)" />
|
||||
<CustomAction Id="JpSetARPCONTACT" Property="ARPCONTACT" Value="$(var.JpAppVendor)" />
|
||||
<CustomAction Id="JpSetARPSIZE" Property="ARPSIZE" Value="$(var.JpAppSizeKb)" />
|
||||
|
||||
<?ifdef JpHelpURL ?>
|
||||
<CustomAction Id="JpSetARPHELPLINK" Property="ARPHELPLINK" Value="$(var.JpHelpURL)" />
|
||||
<?endif?>
|
||||
|
||||
<UI>
|
||||
<?ifdef JpInstallDirChooser ?>
|
||||
<Dialog Id="JpInvalidInstallDir" Width="300" Height="85" Title="[ProductName] Setup" NoMinimize="yes">
|
||||
<Control Id="JpInvalidInstallDirYes" Type="PushButton" X="100" Y="55" Width="50" Height="15" Default="no" Cancel="no" Text="Yes">
|
||||
<Publish Event="NewDialog" Value="VerifyReadyDlg">1</Publish>
|
||||
</Control>
|
||||
<Control Id="JpInvalidInstallDirNo" Type="PushButton" X="150" Y="55" Width="50" Height="15" Default="yes" Cancel="yes" Text="No">
|
||||
<Publish Event="NewDialog" Value="InstallDirDlg">1</Publish>
|
||||
</Control>
|
||||
<Control Id="Text" Type="Text" X="25" Y="15" Width="250" Height="30" TabSkip="no">
|
||||
<Text>!(loc.message.install.dir.exist)</Text>
|
||||
</Control>
|
||||
</Dialog>
|
||||
|
||||
<!--
|
||||
Run WixUI_InstallDir dialog in the default install directory.
|
||||
-->
|
||||
<Property Id="WIXUI_INSTALLDIR" Value="INSTALLDIR"/>
|
||||
<UIRef Id="WixUI_InstallDir" />
|
||||
|
||||
<Publish Dialog="InstallDirDlg" Control="Next" Event="DoAction" Value="JpCheckInstallDir" Order="3">1</Publish>
|
||||
<Publish Dialog="InstallDirDlg" Control="Next" Event="NewDialog" Value="JpInvalidInstallDir" Order="5">INSTALLDIR_VALID="0"</Publish>
|
||||
<Publish Dialog="InstallDirDlg" Control="Next" Event="NewDialog" Value="VerifyReadyDlg" Order="5">INSTALLDIR_VALID="1"</Publish>
|
||||
|
||||
<?ifndef JpLicenseRtf ?>
|
||||
<!--
|
||||
No license file provided.
|
||||
Override the dialog sequence in built-in dialog set "WixUI_InstallDir"
|
||||
to exclude license dialog.
|
||||
-->
|
||||
<Publish Dialog="WelcomeDlg" Control="Next" Event="NewDialog" Value="InstallDirDlg" Order="2">1</Publish>
|
||||
<Publish Dialog="InstallDirDlg" Control="Back" Event="NewDialog" Value="WelcomeDlg" Order="2">1</Publish>
|
||||
<?endif?>
|
||||
|
||||
<?else?>
|
||||
|
||||
<?ifdef JpLicenseRtf ?>
|
||||
<UIRef Id="WixUI_Minimal" />
|
||||
<?endif?>
|
||||
|
||||
<?endif?>
|
||||
</UI>
|
||||
|
||||
<?ifdef JpLicenseRtf ?>
|
||||
<WixVariable Id="WixUILicenseRtf" Value="$(var.JpLicenseRtf)"/>
|
||||
<?ifdef JpAboutURL ?>
|
||||
<CustomAction Id="JpSetARPURLINFOABOUT" Property="ARPURLINFOABOUT" Value="$(var.JpAboutURL)" />
|
||||
<?endif?>
|
||||
|
||||
<?ifdef JpUpdateURL ?>
|
||||
<CustomAction Id="JpSetARPURLUPDATEINFO" Property="ARPURLUPDATEINFO" Value="$(var.JpUpdateURL)" />
|
||||
<?endif?>
|
||||
|
||||
<?ifdef JpIcon ?>
|
||||
<Property Id="ARPPRODUCTICON" Value="JpARPPRODUCTICON"/>
|
||||
<Icon Id="JpARPPRODUCTICON" SourceFile="$(var.JpIcon)"/>
|
||||
<?endif?>
|
||||
|
||||
<UIRef Id="JpUI"/>
|
||||
|
||||
<InstallExecuteSequence>
|
||||
<Custom Action="JpSetARPINSTALLLOCATION" After="CostFinalize">Not Installed</Custom>
|
||||
<Custom Action="JpSetARPCOMMENTS" After="CostFinalize">Not Installed</Custom>
|
||||
<Custom Action="JpSetARPCONTACT" After="CostFinalize">Not Installed</Custom>
|
||||
<Custom Action="JpSetARPSIZE" After="CostFinalize">Not Installed</Custom>
|
||||
<?ifdef JpHelpURL ?>
|
||||
<Custom Action="JpSetARPHELPLINK" After="CostFinalize">Not Installed</Custom>
|
||||
<?endif?>
|
||||
<?ifdef JpAboutURL ?>
|
||||
<Custom Action="JpSetARPURLINFOABOUT" After="CostFinalize">Not Installed</Custom>
|
||||
<?endif?>
|
||||
<?ifdef JpUpdateURL ?>
|
||||
<Custom Action="JpSetARPURLUPDATEINFO" After="CostFinalize">Not Installed</Custom>
|
||||
<?endif?>
|
||||
|
||||
<?ifndef JpAllowUpgrades ?>
|
||||
<Custom Action="JpDisallowUpgrade" After="JpFindRelatedProducts">JP_UPGRADABLE_FOUND</Custom>
|
||||
<?endif?>
|
||||
<?ifndef JpAllowDowngrades ?>
|
||||
<Custom Action="JpDisallowDowngrade" After="JpFindRelatedProducts">JP_DOWNGRADABLE_FOUND</Custom>
|
||||
<?endif?>
|
||||
<LaunchConditions Before="AppSearch"/>
|
||||
<RemoveExistingProducts Before="CostInitialize"/>
|
||||
<Custom Action="JpFindRelatedProducts" After="FindRelatedProducts"/>
|
||||
</InstallExecuteSequence>
|
||||
|
||||
<InstallUISequence>
|
||||
<Custom Action="JpFindRelatedProducts" After="FindRelatedProducts"/>
|
||||
</InstallUISequence>
|
||||
|
||||
</Product>
|
||||
</Wix>
|
||||
</Wix>
|
||||
|
||||
@@ -88,7 +88,6 @@ public class AppController implements Initializable {
|
||||
public static final String LOADING_TRANSACTIONS_MESSAGE = "Loading wallet, select Transactions tab to view...";
|
||||
public static final String CONNECTION_FAILED_PREFIX = "Connection failed: ";
|
||||
public static final String TRYING_ANOTHER_SERVER_MESSAGE = "trying another server...";
|
||||
public static final String JPACKAGE_APP_PATH = "jpackage.app-path";
|
||||
|
||||
@FXML
|
||||
private VBox rootBox;
|
||||
@@ -420,7 +419,7 @@ public class AppController implements Initializable {
|
||||
networkItem.setOnAction(event -> restart(event, network));
|
||||
restart.getItems().add(networkItem);
|
||||
}
|
||||
restart.setVisible(System.getProperty(JPACKAGE_APP_PATH) != null);
|
||||
restart.setVisible(System.getProperty(SparrowWallet.JPACKAGE_APP_PATH) != null);
|
||||
|
||||
saveTransaction.setDisable(true);
|
||||
showTransaction.visibleProperty().bind(Bindings.and(saveTransaction.visibleProperty(), saveTransaction.disableProperty().not()));
|
||||
@@ -472,7 +471,7 @@ public class AppController implements Initializable {
|
||||
|
||||
private void setPlatformApplicationMenu() {
|
||||
OsType osType = OsType.getCurrent();
|
||||
if(osType == OsType.MACOS) {
|
||||
if(osType == OsType.MACOS && Interface.get() == Interface.DESKTOP) {
|
||||
MenuToolkit tk = MenuToolkit.toolkit();
|
||||
MenuItem settings = new MenuItem("Settings...");
|
||||
settings.setOnAction(this::openSettings);
|
||||
@@ -525,7 +524,7 @@ public class AppController implements Initializable {
|
||||
}
|
||||
|
||||
public void showLogFile(ActionEvent event) throws IOException {
|
||||
File logFile = new File(Storage.getSparrowHome(), "sparrow.log");
|
||||
File logFile = new File(Storage.getStateHome(), "sparrow.log");
|
||||
if(logFile.exists()) {
|
||||
AppServices.get().getApplication().getHostServices().showDocument(logFile.toPath().toUri().toString());
|
||||
} else {
|
||||
@@ -597,7 +596,7 @@ public class AppController implements Initializable {
|
||||
sudo groupadd -f -r plugdev
|
||||
sudo usermod -aG plugdev `whoami`
|
||||
""";
|
||||
String home = System.getProperty(JPACKAGE_APP_PATH);
|
||||
String home = System.getProperty(SparrowWallet.JPACKAGE_APP_PATH);
|
||||
if(home != null && !home.startsWith("/opt/sparrowwallet") && home.endsWith("bin/Sparrow")) {
|
||||
home = home.replace("bin/Sparrow", "");
|
||||
commands = commands.replace("/opt/sparrowwallet/", home);
|
||||
@@ -615,6 +614,10 @@ public class AppController implements Initializable {
|
||||
}
|
||||
|
||||
public void openTransactionFromFile(ActionEvent event) {
|
||||
openTransactionFromFile(event, null);
|
||||
}
|
||||
|
||||
private void openTransactionFromFile(ActionEvent event, PSBT contextPsbt) {
|
||||
Stage window = new Stage();
|
||||
|
||||
FileChooser fileChooser = new FileChooser();
|
||||
@@ -629,19 +632,21 @@ public class AppController implements Initializable {
|
||||
List<File> files = fileChooser.showOpenMultipleDialog(window);
|
||||
if(files != null) {
|
||||
for(File file : files) {
|
||||
openTransactionFile(file);
|
||||
openTransactionFile(file, contextPsbt);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void openTransactionFile(File file) {
|
||||
for(Tab tab : tabs.getTabs()) {
|
||||
TabData tabData = (TabData)tab.getUserData();
|
||||
if(tabData instanceof TransactionTabData) {
|
||||
TransactionTabData transactionTabData = (TransactionTabData)tabData;
|
||||
if(file.equals(transactionTabData.getFile())) {
|
||||
tabs.getSelectionModel().select(tab);
|
||||
return;
|
||||
private void openTransactionFile(File file, PSBT contextPsbt) {
|
||||
if(contextPsbt == null) {
|
||||
for(Tab tab : tabs.getTabs()) {
|
||||
TabData tabData = (TabData)tab.getUserData();
|
||||
if(tabData instanceof TransactionTabData) {
|
||||
TransactionTabData transactionTabData = (TransactionTabData)tabData;
|
||||
if(file.equals(transactionTabData.getFile())) {
|
||||
tabs.getSelectionModel().select(tab);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -652,9 +657,9 @@ public class AppController implements Initializable {
|
||||
String name = file.getName();
|
||||
|
||||
if(Utils.isHex(bytes) || Utils.isBase64(bytes)) {
|
||||
addTransactionTab(name, file, new String(bytes, StandardCharsets.UTF_8).trim());
|
||||
addTransactionTab(name, file, new String(bytes, StandardCharsets.UTF_8).trim(), contextPsbt);
|
||||
} else {
|
||||
addTransactionTab(name, file, bytes);
|
||||
addTransactionTab(name, file, bytes, contextPsbt);
|
||||
}
|
||||
} catch(IOException e) {
|
||||
showErrorDialog("Error opening file", e.getMessage());
|
||||
@@ -676,7 +681,7 @@ public class AppController implements Initializable {
|
||||
Optional<String> text = dialog.showAndWait();
|
||||
if(text.isPresent() && !text.get().isEmpty()) {
|
||||
try {
|
||||
addTransactionTab(null, null, text.get().trim());
|
||||
addTransactionTab(null, null, text.get().trim(), null);
|
||||
} catch(PSBTParseException e) {
|
||||
showErrorDialog("Invalid PSBT", e.getMessage());
|
||||
} catch(TransactionParseException e) {
|
||||
@@ -1035,7 +1040,7 @@ public class AppController implements Initializable {
|
||||
Stage window = new Stage();
|
||||
DirectoryChooser directoryChooser = new DirectoryChooser();
|
||||
directoryChooser.setTitle("Choose Sparrow Home Folder");
|
||||
directoryChooser.setInitialDirectory(initialDir == null || !initialDir.exists() ? Storage.getSparrowHome() : initialDir);
|
||||
directoryChooser.setInitialDirectory(initialDir == null || !initialDir.exists() ? Storage.getDefaultHome() : initialDir);
|
||||
File newHome = directoryChooser.showDialog(window);
|
||||
|
||||
if(newHome != null) {
|
||||
@@ -1045,8 +1050,8 @@ public class AppController implements Initializable {
|
||||
}
|
||||
|
||||
public void restart(ActionEvent event, Network network) {
|
||||
if(System.getProperty(JPACKAGE_APP_PATH) == null) {
|
||||
throw new IllegalStateException("Property " + JPACKAGE_APP_PATH + " is not present");
|
||||
if(System.getProperty(SparrowWallet.JPACKAGE_APP_PATH) == null) {
|
||||
throw new IllegalStateException("Property " + SparrowWallet.JPACKAGE_APP_PATH + " is not present");
|
||||
}
|
||||
|
||||
Args args = getRestartArgs();
|
||||
@@ -1067,7 +1072,7 @@ public class AppController implements Initializable {
|
||||
private void restart(ActionEvent event, Args args) {
|
||||
try {
|
||||
List<String> cmd = new ArrayList<>();
|
||||
cmd.add(System.getProperty(JPACKAGE_APP_PATH));
|
||||
cmd.add(System.getProperty(SparrowWallet.JPACKAGE_APP_PATH));
|
||||
cmd.addAll(args.toParams());
|
||||
final ProcessBuilder builder = new ProcessBuilder(cmd);
|
||||
if(OsType.getCurrent() == OsType.UNIX) {
|
||||
@@ -1092,7 +1097,7 @@ public class AppController implements Initializable {
|
||||
verifyOpened = true;
|
||||
}
|
||||
} else {
|
||||
openTransactionFile(file);
|
||||
openTransactionFile(file, null);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1122,7 +1127,7 @@ public class AppController implements Initializable {
|
||||
WalletNameDialog.NameAndBirthDate nameAndBirthDate = optNameAndBirthDate.get();
|
||||
File walletFile = Storage.getWalletFile(nameAndBirthDate.getName());
|
||||
Storage storage = new Storage(walletFile);
|
||||
Wallet wallet = new Wallet(nameAndBirthDate.getName(), PolicyType.SINGLE, ScriptType.P2WPKH, nameAndBirthDate.getBirthDate());
|
||||
Wallet wallet = new Wallet(nameAndBirthDate.getName(), PolicyType.SINGLE_HD, ScriptType.P2WPKH, nameAndBirthDate.getBirthDate());
|
||||
addWalletTabOrWindow(storage, wallet, false);
|
||||
}
|
||||
}
|
||||
@@ -1272,7 +1277,7 @@ public class AppController implements Initializable {
|
||||
List<ExtendedKey> xpubs = wallet.getKeystores().stream().map(Keystore::getExtendedPublicKey).collect(Collectors.toList());
|
||||
Optional<WalletForm> optNewWalletForm = walletTabData.stream()
|
||||
.map(WalletTabData::getWalletForm)
|
||||
.filter(wf -> wf.getSettingsWalletForm() != null && wf.getSettingsWalletForm().getWallet().getPolicyType() == PolicyType.MULTI &&
|
||||
.filter(wf -> wf.getSettingsWalletForm() != null && wf.getSettingsWalletForm().getWallet().getPolicyType() == PolicyType.MULTI_HD &&
|
||||
wf.getSettingsWalletForm().getWallet().getScriptType() == wallet.getScriptType() && !wf.getSettingsWalletForm().getWallet().isValid() &&
|
||||
wf.getSettingsWalletForm().getWallet().getKeystores().stream().map(Keystore::getExtendedPublicKey).anyMatch(xpubs::contains)).findFirst();
|
||||
if(optNewWalletForm.isPresent()) {
|
||||
@@ -1287,6 +1292,7 @@ public class AppController implements Initializable {
|
||||
|
||||
private boolean attemptImportWallet(File file, SecureString password) {
|
||||
List<WalletImport> walletImporters = List.of(new ColdcardSinglesig(), new ColdcardMultisig(),
|
||||
new Bip129(),
|
||||
new Electrum(),
|
||||
new SpecterDesktop(),
|
||||
new Descriptor(),
|
||||
@@ -1328,13 +1334,16 @@ public class AppController implements Initializable {
|
||||
return;
|
||||
}
|
||||
|
||||
WalletNameDialog nameDlg = new WalletNameDialog(wallet.getName(), true, wallet.getBirthDate());
|
||||
WalletNameDialog nameDlg = new WalletNameDialog(wallet.getName(), true, wallet.getPolicyType(), wallet.getBirthDate(), false);
|
||||
nameDlg.initOwner(rootStack.getScene().getWindow());
|
||||
Optional<WalletNameDialog.NameAndBirthDate> optNameAndBirthDate = nameDlg.showAndWait();
|
||||
if(optNameAndBirthDate.isPresent()) {
|
||||
WalletNameDialog.NameAndBirthDate nameAndBirthDate = optNameAndBirthDate.get();
|
||||
wallet.setName(nameAndBirthDate.getName());
|
||||
wallet.setBirthDate(nameAndBirthDate.getBirthDate());
|
||||
if(wallet.getPolicyType() == PolicyType.SINGLE_SP && wallet.getBirthDate() == null) {
|
||||
wallet.setBirthDate(new Date());
|
||||
}
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
@@ -1473,7 +1482,7 @@ public class AppController implements Initializable {
|
||||
WalletForm selectedWalletForm = getSelectedWalletForm();
|
||||
if(selectedWalletForm != null) {
|
||||
Wallet wallet = selectedWalletForm.getWallet();
|
||||
if(wallet.getKeystores().size() == 1) {
|
||||
if(wallet.getPolicyType() == PolicyType.SINGLE_HD || wallet.getPolicyType() == PolicyType.SINGLE_SP) {
|
||||
//Can sign and verify
|
||||
messageSignDialog = new MessageSignDialog(wallet);
|
||||
}
|
||||
@@ -1508,7 +1517,7 @@ public class AppController implements Initializable {
|
||||
bitcoinUnit = wallet.getAutoUnit();
|
||||
}
|
||||
|
||||
sendToManyDialog = new SendToManyDialog(bitcoinUnit, initialPayments);
|
||||
sendToManyDialog = new SendToManyDialog(bitcoinUnit, Config.get().getUnitFormat(), initialPayments);
|
||||
sendToManyDialog.initModality(Modality.NONE);
|
||||
Optional<List<Payment>> optPayments = sendToManyDialog.showAndWait();
|
||||
sendToManyDialog = null;
|
||||
@@ -1928,25 +1937,29 @@ public class AppController implements Initializable {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
private void addTransactionTab(String name, File file, String string) throws ParseException, PSBTParseException, TransactionParseException {
|
||||
private void addTransactionTab(String name, File file, String string, PSBT contextPsbt) throws ParseException, PSBTParseException, TransactionParseException {
|
||||
if(Utils.isBase64(string) && !Utils.isHex(string)) {
|
||||
addTransactionTab(name, file, Base64.getDecoder().decode(string));
|
||||
addTransactionTab(name, file, Base64.getDecoder().decode(string), contextPsbt);
|
||||
} else if(Utils.isHex(string)) {
|
||||
addTransactionTab(name, file, Utils.hexToBytes(string));
|
||||
addTransactionTab(name, file, Utils.hexToBytes(string), contextPsbt);
|
||||
} else {
|
||||
throw new ParseException("Input is not base64 or hex", 0);
|
||||
}
|
||||
}
|
||||
|
||||
private void addTransactionTab(String name, File file, byte[] bytes) throws PSBTParseException, ParseException, TransactionParseException {
|
||||
private void addTransactionTab(String name, File file, byte[] bytes, PSBT contextPsbt) throws PSBTParseException, ParseException, TransactionParseException {
|
||||
if(PSBT.isPSBT(bytes)) {
|
||||
//Don't verify signatures here - provided PSBT may omit UTXO data that can be found when combining with an existing PSBT
|
||||
PSBT psbt = new PSBT(bytes, false);
|
||||
addTransactionTab(name, file, psbt);
|
||||
if(verifyTransactionContext(contextPsbt, null, psbt, "loaded")) {
|
||||
addTransactionTab(name, file, psbt);
|
||||
}
|
||||
} else if(Transaction.isTransaction(bytes)) {
|
||||
try {
|
||||
Transaction transaction = new Transaction(bytes);
|
||||
addTransactionTab(name, file, transaction);
|
||||
if(verifyTransactionContext(contextPsbt, transaction, null, "loaded")) {
|
||||
addTransactionTab(name, file, transaction);
|
||||
}
|
||||
} catch(Exception e) {
|
||||
throw new TransactionParseException(e.getMessage());
|
||||
}
|
||||
@@ -2055,6 +2068,34 @@ public class AppController implements Initializable {
|
||||
AppServices.showErrorDialog("Invalid PSBT", e.getMessage());
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
psbt.verifySigHashes();
|
||||
} catch(PSBTSignatureException e) {
|
||||
Optional<ButtonType> result = AppServices.showWarningDialog("Non-Default Sighash",
|
||||
e.getMessage() + "\n\nReview this PSBT carefully before signing.\n\nOpen the transaction?", ButtonType.YES, ButtonType.NO);
|
||||
if(result.isEmpty() || result.get() != ButtonType.YES) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//Skip the warning for already-confirmed transactions loaded for inspection
|
||||
if(blockTransaction == null) {
|
||||
List<TransactionOutput> unknownScriptOutputs = new ArrayList<>();
|
||||
for(int i = 0; i < transaction.getOutputs().size(); i++) {
|
||||
TransactionOutput txOutput = transaction.getOutputs().get(i);
|
||||
if(txOutput.getValue() > 0 && txOutput.getScript().getToAddress() == null) {
|
||||
//Silent payment outputs have an empty script and non-zero value until the recipient script is computed
|
||||
if(psbt != null && i < psbt.getPsbtOutputs().size() && psbt.getPsbtOutputs().get(i).getSilentPaymentAddress() != null) {
|
||||
continue;
|
||||
}
|
||||
unknownScriptOutputs.add(txOutput);
|
||||
}
|
||||
}
|
||||
if(!unknownScriptOutputs.isEmpty() && !confirmUnknownScriptOutputs(unknownScriptOutputs)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
@@ -2151,6 +2192,37 @@ public class AppController implements Initializable {
|
||||
tabs.getSelectionModel().select(tab);
|
||||
}
|
||||
|
||||
private boolean verifyTransactionContext(PSBT contextPsbt, Transaction transaction, PSBT psbt, String source) {
|
||||
if(contextPsbt == null || matchesOpenTransactionTab(transaction, psbt)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if(psbt == null && contextPsbt.possibleUnverifiableSilentPaymentsTransaction(transaction)) {
|
||||
AppServices.showErrorDialog("Silent Payments Transaction", "This transaction pays a silent payment address.\n\nThe signing device must return the PSBT rather than the final transaction, so the silent payment outputs can be verified.");
|
||||
} else {
|
||||
AppServices.showErrorDialog("Mismatched Transaction", "The " + source + " transaction does not match the transaction in this or any other open tab.\n\nCheck that the correct transaction was signed and exported from the signing device.");
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private boolean matchesOpenTransactionTab(Transaction transaction, PSBT psbt) {
|
||||
for(Tab tab : tabs.getTabs()) {
|
||||
TabData tabData = (TabData)tab.getUserData();
|
||||
if(tabData instanceof TransactionTabData transactionTabData) {
|
||||
if(transactionTabData.getPsbt() != null) {
|
||||
if(psbt != null ? transactionTabData.getPsbt().matches(psbt) : transactionTabData.getPsbt().matches(transaction)) {
|
||||
return true;
|
||||
}
|
||||
} else if(transactionTabData.getTransaction().calculateTxId(false).equals(psbt != null ? psbt.getTransaction().getTxId() : transaction.getTxId())) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private boolean openUnverifiableTransaction(String tabName) {
|
||||
Optional<ButtonType> result = AppServices.showWarningDialog(
|
||||
"Unverifiable Silent Payments Transaction",
|
||||
@@ -2161,6 +2233,23 @@ public class AppController implements Initializable {
|
||||
return result.isPresent() && result.get() == ButtonType.YES;
|
||||
}
|
||||
|
||||
private boolean confirmUnknownScriptOutputs(List<TransactionOutput> unknownScriptOutputs) {
|
||||
long totalAmount = unknownScriptOutputs.stream().mapToLong(TransactionOutput::getValue).sum();
|
||||
UnitFormat format = Config.get().getUnitFormat() == null ? UnitFormat.DOT : Config.get().getUnitFormat();
|
||||
BitcoinUnit unit = Config.get().getBitcoinUnit();
|
||||
if(unit == null || unit.equals(BitcoinUnit.AUTO)) {
|
||||
unit = totalAmount >= BitcoinUnit.getAutoThreshold() ? BitcoinUnit.BTC : BitcoinUnit.SATOSHIS;
|
||||
}
|
||||
String amount = unit.equals(BitcoinUnit.BTC) ? format.formatBtcValue(totalAmount) + " BTC" : format.formatSatsValue(totalAmount) + " sats";
|
||||
String outputDesc = unknownScriptOutputs.size() == 1 ? "an output" : unknownScriptOutputs.size() + " outputs";
|
||||
Optional<ButtonType> result = AppServices.showWarningDialog("Unknown Script Type",
|
||||
"This transaction contains " + outputDesc + " of a non-standard or unrecognised script type, totalling " + amount + ".\n\n" +
|
||||
"Sparrow cannot resolve these outputs to addresses, so they will not appear in the transaction diagram. " +
|
||||
"Review the individual output(s) in the transaction tree carefully before signing or broadcasting.\n\n" +
|
||||
"Open the transaction?", ButtonType.YES, ButtonType.NO);
|
||||
return result.isPresent() && result.get() == ButtonType.YES;
|
||||
}
|
||||
|
||||
private String getTabName(Tab tab) {
|
||||
return ((Label)tab.getGraphic()).getText();
|
||||
}
|
||||
@@ -3172,7 +3261,7 @@ public class AppController implements Initializable {
|
||||
if(tabs.getScene().getWindow().equals(event.getWindow())) {
|
||||
if(event.getBlockTransaction() != null) {
|
||||
addTransactionTab(event.getBlockTransaction(), event.getInitialView(), event.getInitialIndex());
|
||||
} else {
|
||||
} else if(verifyTransactionContext(event.getContextPsbt(), event.getTransaction(), null, "scanned")) {
|
||||
addTransactionTab(event.getTransaction(), event.getInitialView(), event.getInitialIndex());
|
||||
}
|
||||
}
|
||||
@@ -3181,7 +3270,9 @@ public class AppController implements Initializable {
|
||||
@Subscribe
|
||||
public void viewPSBT(ViewPSBTEvent event) {
|
||||
if(tabs.getScene().getWindow().equals(event.getWindow())) {
|
||||
addTransactionTab(event.getLabel(), event.getFile(), event.getPsbt());
|
||||
if(verifyTransactionContext(event.getContextPsbt(), null, event.getPsbt(), "scanned")) {
|
||||
addTransactionTab(event.getLabel(), event.getFile(), event.getPsbt());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3242,9 +3333,9 @@ public class AppController implements Initializable {
|
||||
public void requestTransactionOpen(RequestTransactionOpenEvent event) {
|
||||
if(tabs.getScene().getWindow().equals(event.getWindow())) {
|
||||
if(event.getFile() != null) {
|
||||
openTransactionFile(event.getFile());
|
||||
openTransactionFile(event.getFile(), event.getContextPsbt());
|
||||
} else {
|
||||
openTransactionFromFile(null);
|
||||
openTransactionFromFile(null, event.getContextPsbt());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -69,9 +69,11 @@ import java.time.temporal.ChronoUnit;
|
||||
import java.util.*;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.ThreadLocalRandom;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static com.sparrowwallet.sparrow.AppController.CONNECTION_FAILED_PREFIX;
|
||||
import static com.sparrowwallet.sparrow.control.DownloadVerifierDialog.*;
|
||||
|
||||
public class AppServices {
|
||||
@@ -367,15 +369,18 @@ public class AppServices {
|
||||
onlineProperty.setValue(false);
|
||||
onlineProperty.addListener(onlineServicesListener);
|
||||
|
||||
log.debug("Connection failed", failEvent.getSource().getException());
|
||||
if(Config.get().getServerType() == ServerType.PUBLIC_ELECTRUM_SERVER) {
|
||||
Config.get().changePublicServer();
|
||||
connectionService.setPeriod(Duration.seconds(PUBLIC_SERVER_RETRY_PERIOD_SECS));
|
||||
boolean changed = changePublicServer();
|
||||
connectionService.setPeriod(changed ? Duration.seconds(PUBLIC_SERVER_RETRY_PERIOD_SECS) : Duration.seconds(PRIVATE_SERVER_RETRY_PERIOD_SECS));
|
||||
EventManager.get().post(new ConnectionFailedEvent(failEvent.getSource().getException()));
|
||||
if(!changed) {
|
||||
Platform.runLater(() -> EventManager.get().post(new StatusEvent(CONNECTION_FAILED_PREFIX + "No public servers available that can serve the open wallets, retrying later...")));
|
||||
}
|
||||
} else {
|
||||
connectionService.setPeriod(Duration.seconds(PRIVATE_SERVER_RETRY_PERIOD_SECS));
|
||||
EventManager.get().post(new ConnectionFailedEvent(failEvent.getSource().getException()));
|
||||
}
|
||||
|
||||
log.debug("Connection failed", failEvent.getSource().getException());
|
||||
EventManager.get().post(new ConnectionFailedEvent(failEvent.getSource().getException()));
|
||||
});
|
||||
|
||||
return connectionService;
|
||||
@@ -851,6 +856,10 @@ public class AppServices {
|
||||
public static void clearTransactionHistoryCache(Wallet wallet) {
|
||||
ElectrumServer.clearRetrievedScriptHashes(wallet);
|
||||
|
||||
if(wallet.getPolicyType() == PolicyType.SINGLE_SP && wallet.isValid()) {
|
||||
ElectrumServer.releaseSilentPaymentSubscription(wallet.getSilentPaymentScanAddress());
|
||||
}
|
||||
|
||||
for(Wallet childWallet : wallet.getChildWallets()) {
|
||||
if(childWallet.isNested()) {
|
||||
AppServices.clearTransactionHistoryCache(childWallet);
|
||||
@@ -862,6 +871,22 @@ public class AppServices {
|
||||
return Storage.isWalletFile(file);
|
||||
}
|
||||
|
||||
public boolean changePublicServer() {
|
||||
List<PolicyType> policyTypes = getOpenWallets().keySet().stream().map(Wallet::getPolicyType).filter(Objects::nonNull).collect(Collectors.toList());
|
||||
return changePublicServer(policyTypes.isEmpty() ? List.of(PolicyType.SINGLE_HD) : policyTypes);
|
||||
}
|
||||
|
||||
private boolean changePublicServer(List<PolicyType> policyTypes) {
|
||||
Config config = Config.get();
|
||||
List<Server> otherServers = PublicElectrumServer.getServers().stream().filter(pes -> pes.supportsAllPolicyTypes(policyTypes))
|
||||
.map(PublicElectrumServer::getServer).filter(server -> !server.equals(config.getPublicElectrumServer())).collect(Collectors.toList());
|
||||
if(!otherServers.isEmpty()) {
|
||||
config.setPublicElectrumServer(otherServers.get(ThreadLocalRandom.current().nextInt(otherServers.size())));
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public static Optional<ButtonType> showWarningDialog(String title, String content, ButtonType... buttons) {
|
||||
return showAlertDialog(title, content, Alert.AlertType.WARNING, buttons);
|
||||
}
|
||||
@@ -1077,7 +1102,7 @@ public class AppServices {
|
||||
try {
|
||||
Auth47 auth47 = new Auth47(uri);
|
||||
List<ScriptType> scriptTypes = PaymentCode.SEGWIT_SCRIPT_TYPES;
|
||||
Wallet wallet = selectWallet(List.of(PolicyType.SINGLE), scriptTypes, false, true, "login to " + auth47.getCallback().getHost(), true);
|
||||
Wallet wallet = selectWallet(List.of(PolicyType.SINGLE_HD), scriptTypes, false, true, "login to " + auth47.getCallback().getHost(), true);
|
||||
|
||||
if(wallet != null) {
|
||||
try {
|
||||
@@ -1097,8 +1122,8 @@ public class AppServices {
|
||||
private static void openLnurlAuthUri(URI uri) {
|
||||
try {
|
||||
LnurlAuth lnurlAuth = new LnurlAuth(uri);
|
||||
List<ScriptType> scriptTypes = ScriptType.getAddressableScriptTypes(PolicyType.SINGLE);
|
||||
Wallet wallet = selectWallet(List.of(PolicyType.SINGLE), scriptTypes, true, true, lnurlAuth.getLoginMessage(), true);
|
||||
List<ScriptType> scriptTypes = ScriptType.getAddressableScriptTypes(PolicyType.SINGLE_HD);
|
||||
Wallet wallet = selectWallet(List.of(PolicyType.SINGLE_HD), scriptTypes, true, true, lnurlAuth.getLoginMessage(), true);
|
||||
|
||||
if(wallet != null) {
|
||||
if(wallet.isEncrypted()) {
|
||||
@@ -1212,6 +1237,7 @@ public class AppServices {
|
||||
|
||||
public static boolean isWhirlpoolCompatible(Wallet wallet) {
|
||||
return WHIRLPOOL_NETWORKS.contains(Network.get())
|
||||
&& wallet.getPolicyType() == PolicyType.SINGLE_HD
|
||||
&& wallet.getScriptType() != ScriptType.P2TR //Taproot not yet supported
|
||||
&& wallet.getKeystores().size() == 1
|
||||
&& wallet.getKeystores().get(0).hasSeed()
|
||||
@@ -1222,6 +1248,7 @@ public class AppServices {
|
||||
|
||||
public static boolean isWhirlpoolPostmixCompatible(Wallet wallet) {
|
||||
return WHIRLPOOL_NETWORKS.contains(Network.get())
|
||||
&& wallet.getPolicyType() == PolicyType.SINGLE_HD
|
||||
&& wallet.getScriptType() != ScriptType.P2TR //Taproot not yet supported
|
||||
&& wallet.getKeystores().size() == 1
|
||||
&& wallet.getKeystores().getFirst().getWalletModel() != WalletModel.BITBOX_02; //BitBox02 does not support high account numbers
|
||||
@@ -1457,10 +1484,28 @@ public class AppServices {
|
||||
@Subscribe
|
||||
public void walletHistoryFailed(WalletHistoryFailedEvent event) {
|
||||
if(Config.get().getServerType() == ServerType.PUBLIC_ELECTRUM_SERVER && isConnected()) {
|
||||
String currentName = Config.get().getServerDisplayName();
|
||||
onlineProperty.set(false);
|
||||
log.warn("Failed to fetch wallet history from " + Config.get().getServerDisplayName() + ", reconnecting to another server...");
|
||||
Config.get().changePublicServer();
|
||||
boolean changed = changePublicServer();
|
||||
if(changed) {
|
||||
log.warn("Failed to fetch wallet history from " + currentName + ", reconnecting to another server...");
|
||||
} else {
|
||||
log.warn("Failed to fetch wallet history from " + currentName + ", retrying later");
|
||||
connectionService.setDelay(Duration.seconds(PRIVATE_SERVER_RETRY_PERIOD_SECS));
|
||||
EventManager.get().post(new StatusEvent("Wallet load failed: No other public servers available that can serve the open wallets, retrying later..."));
|
||||
}
|
||||
onlineProperty.set(true);
|
||||
}
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void silentPaymentsUnsubscribe(SilentPaymentsUnsubscribeEvent event) {
|
||||
if(isConnected()) {
|
||||
ElectrumServer.SilentPaymentsUnsubscribeService unsubscribeService = new ElectrumServer.SilentPaymentsUnsubscribeService(event.getScanAddress());
|
||||
unsubscribeService.setOnFailed(workerStateEvent -> {
|
||||
log.warn("Failed to unsubscribe silent payments for " + event.getScanAddress().getAddress(), workerStateEvent.getSource().getException());
|
||||
});
|
||||
unsubscribeService.start();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -55,7 +55,7 @@ public abstract class BaseController {
|
||||
descriptorArea.setMouseOverTextDelay(Duration.ofMillis(150));
|
||||
descriptorArea.addEventHandler(MouseOverTextEvent.MOUSE_OVER_TEXT_BEGIN, e -> {
|
||||
TwoDimensional.Position position = descriptorArea.getParagraph(0).getStyleSpans().offsetToPosition(e.getCharacterIndex(), Backward);
|
||||
int index = descriptorArea.getWallet().getPolicyType() == PolicyType.SINGLE ? position.getMajor() - 1 : ((position.getMajor() - 1) / 2);
|
||||
int index = descriptorArea.getWallet().getPolicyType() == PolicyType.SINGLE_HD || descriptorArea.getWallet().getPolicyType() == PolicyType.SINGLE_SP ? position.getMajor() - 1 : ((position.getMajor() - 1) / 2);
|
||||
if(position.getMajor() > 0 && index >= 0 && index < descriptorArea.getWallet().getKeystores().size()) {
|
||||
Keystore hoverKeystore = descriptorArea.getWallet().getKeystores().get(index);
|
||||
Point2D pos = e.getScreenPosition();
|
||||
@@ -75,7 +75,11 @@ public abstract class BaseController {
|
||||
builder.append(keystore.getKeyDerivation().getMasterFingerprint());
|
||||
builder.append(KeyDerivation.writePath(KeyDerivation.parsePath(keystore.getKeyDerivation().getDerivationPath())).substring(1));
|
||||
builder.append("]");
|
||||
builder.append(keystore.getExtendedPublicKey().toString());
|
||||
if(keystore.getExtendedPublicKey() != null) {
|
||||
builder.append(keystore.getExtendedPublicKey().toString());
|
||||
} else if(keystore.getSilentPaymentScanAddress() != null) {
|
||||
builder.append(keystore.getSilentPaymentScanAddress().toKeyString());
|
||||
}
|
||||
|
||||
return builder.toString();
|
||||
}
|
||||
|
||||
@@ -8,13 +8,13 @@ public enum Interface {
|
||||
public static Interface get() {
|
||||
if(currentInterface == null) {
|
||||
boolean headless = java.awt.GraphicsEnvironment.isHeadless();
|
||||
boolean monocle = "Monocle".equalsIgnoreCase(System.getProperty("glass.platform"));
|
||||
boolean headlessPlatform = "Headless".equalsIgnoreCase(System.getProperty("glass.platform"));
|
||||
|
||||
if(headless || monocle) {
|
||||
if(headless || headlessPlatform) {
|
||||
currentInterface = TERMINAL;
|
||||
|
||||
if(headless && !monocle) {
|
||||
throw new UnsupportedOperationException("Headless environment detected but Monocle platform not found");
|
||||
if(headless && !headlessPlatform) {
|
||||
throw new UnsupportedOperationException("Headless environment detected but headless glass platform not found");
|
||||
}
|
||||
} else {
|
||||
currentInterface = DESKTOP;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package com.sparrowwallet.sparrow;
|
||||
|
||||
import com.beust.jcommander.JCommander;
|
||||
import com.sparrowwallet.drongo.ApplicationDir;
|
||||
import com.sparrowwallet.drongo.Drongo;
|
||||
import com.sparrowwallet.drongo.Network;
|
||||
import com.sparrowwallet.sparrow.io.Storage;
|
||||
@@ -18,14 +19,24 @@ 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.4.2";
|
||||
public static final String APP_VERSION = "2.5.4";
|
||||
public static final String APP_VERSION_SUFFIX = "";
|
||||
public static final String APP_HOME_PROPERTY = "sparrow.home";
|
||||
public static final String APP_HOME_PROPERTY = ApplicationDir.getHomeProperty(APP_NAME);
|
||||
public static final String NETWORK_ENV_PROPERTY = "SPARROW_NETWORK";
|
||||
public static final String JPACKAGE_APP_PATH = "jpackage.app-path";
|
||||
|
||||
private static Instance instance;
|
||||
|
||||
public static void main(String[] argv) {
|
||||
if(System.getProperty(JPACKAGE_APP_PATH) != null) {
|
||||
String libDir = System.getProperty("java.home") + File.separator + "lib";
|
||||
System.setProperty("jna.boot.library.path", libDir);
|
||||
System.setProperty("jna.library.path", libDir);
|
||||
System.setProperty("jSerialComm.library.path", libDir);
|
||||
System.setProperty("org.usb4java.LibraryName", "usb4java");
|
||||
System.setProperty("java.library.path", libDir);
|
||||
}
|
||||
|
||||
Args args = new Args();
|
||||
JCommander jCommander = JCommander.newBuilder().addObject(args).programName(APP_NAME.toLowerCase(Locale.ROOT)).acceptUnknownOptions(true).build();
|
||||
jCommander.parse(argv);
|
||||
@@ -61,17 +72,19 @@ public class SparrowWallet {
|
||||
}
|
||||
}
|
||||
|
||||
File testnetFlag = new File(Storage.getSparrowHome(), "network-" + Network.TESTNET.getName());
|
||||
Storage.logApplicationDirs();
|
||||
|
||||
File testnetFlag = new File(Storage.getConfigHome(), "network-" + Network.TESTNET.getName());
|
||||
if(testnetFlag.exists()) {
|
||||
Network.set(Network.TESTNET);
|
||||
}
|
||||
|
||||
File testnet4Flag = new File(Storage.getSparrowHome(), "network-" + Network.TESTNET4.getName());
|
||||
File testnet4Flag = new File(Storage.getConfigHome(), "network-" + Network.TESTNET4.getName());
|
||||
if(testnet4Flag.exists()) {
|
||||
Network.set(Network.TESTNET4);
|
||||
}
|
||||
|
||||
File signetFlag = new File(Storage.getSparrowHome(), "network-" + Network.SIGNET.getName());
|
||||
File signetFlag = new File(Storage.getConfigHome(), "network-" + Network.SIGNET.getName());
|
||||
if(signetFlag.exists()) {
|
||||
Network.set(Network.SIGNET);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.sparrowwallet.sparrow.control;
|
||||
|
||||
import com.sparrowwallet.drongo.policy.PolicyType;
|
||||
import com.sparrowwallet.drongo.wallet.KeystoreSource;
|
||||
import com.sparrowwallet.drongo.wallet.StandardAccount;
|
||||
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||
@@ -68,7 +69,7 @@ public class AddAccountDialog extends Dialog<List<StandardAccount>> {
|
||||
|
||||
final ButtonType discoverButtonType = new javafx.scene.control.ButtonType("Discover", ButtonBar.ButtonData.LEFT);
|
||||
if(!availableAccounts.isEmpty() && (masterWallet.getKeystores().stream().allMatch(ks -> ks.getSource() == KeystoreSource.SW_SEED)
|
||||
|| (masterWallet.getKeystores().size() == 1 && masterWallet.getKeystores().stream().allMatch(ks -> ks.getSource() == KeystoreSource.HW_USB)))) {
|
||||
|| (masterWallet.getPolicyType() == PolicyType.SINGLE_HD && masterWallet.getKeystores().stream().allMatch(ks -> ks.getSource() == KeystoreSource.HW_USB)))) {
|
||||
dialogPane.getButtonTypes().add(discoverButtonType);
|
||||
Button discoverButton = (Button)dialogPane.lookupButton(discoverButtonType);
|
||||
discoverButton.disableProperty().bind(AppServices.onlineProperty().not());
|
||||
|
||||
@@ -80,6 +80,7 @@ public class AddressTreeTable extends CoinTreeTable {
|
||||
contextMenu.getItems().add(showCountItem);
|
||||
getColumns().forEach(col -> col.setContextMenu(contextMenu));
|
||||
|
||||
setPlaceholder(getDefaultPlaceholder(rootEntry.getWallet()));
|
||||
setEditable(true);
|
||||
setupColumnWidths();
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ package com.sparrowwallet.sparrow.control;
|
||||
import com.google.common.base.Throwables;
|
||||
import com.sparrowwallet.drongo.KeyDerivation;
|
||||
import com.sparrowwallet.drongo.crypto.ChildNumber;
|
||||
import com.sparrowwallet.drongo.policy.PolicyType;
|
||||
import com.sparrowwallet.drongo.protocol.ScriptType;
|
||||
import com.sparrowwallet.drongo.protocol.Sha256Hash;
|
||||
import com.sparrowwallet.drongo.wallet.*;
|
||||
@@ -44,6 +45,7 @@ public class CardImportPane extends TitledDescriptionPane {
|
||||
private static final Logger log = LoggerFactory.getLogger(CardImportPane.class);
|
||||
|
||||
private final KeystoreCardImport importer;
|
||||
private final PolicyType policyType;
|
||||
private List<ChildNumber> derivation;
|
||||
protected Button importButton;
|
||||
private final SimpleStringProperty pin = new SimpleStringProperty("");
|
||||
@@ -51,6 +53,7 @@ public class CardImportPane extends TitledDescriptionPane {
|
||||
public CardImportPane(Wallet wallet, KeystoreCardImport importer, KeyDerivation defaultDerivation, KeyDerivation requiredDerivation) {
|
||||
super(importer.getName(), "Place card on reader", importer.getKeystoreImportDescription(getAccount(wallet, requiredDerivation)), importer.getWalletModel());
|
||||
this.importer = importer;
|
||||
this.policyType = wallet.getPolicyType();
|
||||
this.derivation = requiredDerivation == null ? getDefaultDerivation(wallet, defaultDerivation) : requiredDerivation.getDerivation();
|
||||
}
|
||||
|
||||
@@ -59,7 +62,7 @@ public class CardImportPane extends TitledDescriptionPane {
|
||||
return defaultDerivation.getDerivation();
|
||||
}
|
||||
|
||||
return wallet == null || wallet.getScriptType() == null ? ScriptType.P2WPKH.getDefaultDerivation() : wallet.getScriptType().getDefaultDerivation();
|
||||
return wallet.getScriptType() == null ? ScriptType.P2WPKH.getDefaultDerivation() : wallet.getScriptType().getDefaultDerivation();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -111,7 +114,7 @@ public class CardImportPane extends TitledDescriptionPane {
|
||||
return;
|
||||
}
|
||||
|
||||
CardImportService cardImportService = new CardImportService(importer, pin.get(), derivation, messageProperty);
|
||||
CardImportService cardImportService = new CardImportService(importer, policyType, pin.get(), derivation, messageProperty);
|
||||
cardImportService.setOnSucceeded(event -> {
|
||||
EventManager.get().post(new KeystoreImportEvent(cardImportService.getValue()));
|
||||
});
|
||||
@@ -352,12 +355,14 @@ public class CardImportPane extends TitledDescriptionPane {
|
||||
|
||||
public static class CardImportService extends Service<Keystore> {
|
||||
private final KeystoreCardImport cardImport;
|
||||
private final PolicyType policyType;
|
||||
private final String pin;
|
||||
private final List<ChildNumber> derivation;
|
||||
private final StringProperty messageProperty;
|
||||
|
||||
public CardImportService(KeystoreCardImport cardImport, String pin, List<ChildNumber> derivation, StringProperty messageProperty) {
|
||||
public CardImportService(KeystoreCardImport cardImport, PolicyType policyType, String pin, List<ChildNumber> derivation, StringProperty messageProperty) {
|
||||
this.cardImport = cardImport;
|
||||
this.policyType = policyType;
|
||||
this.pin = pin;
|
||||
this.derivation = derivation;
|
||||
this.messageProperty = messageProperty;
|
||||
@@ -368,7 +373,7 @@ public class CardImportPane extends TitledDescriptionPane {
|
||||
return new Task<>() {
|
||||
@Override
|
||||
protected Keystore call() throws Exception {
|
||||
return cardImport.getKeystore(pin, derivation, messageProperty);
|
||||
return cardImport.getKeystore(policyType, pin, derivation, messageProperty);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -81,7 +81,7 @@ public class CodexKeystoreImportPane extends TitledDescriptionPane {
|
||||
private void importKeystore(List<ChildNumber> derivation) {
|
||||
importButton.setDisable(true);
|
||||
try {
|
||||
Keystore keystore = importer.getKeystore(derivation, secretShareProperty.get());
|
||||
Keystore keystore = importer.getKeystore(wallet.getPolicyType(), derivation, secretShareProperty.get());
|
||||
EventManager.get().post(new KeystoreImportEvent(keystore));
|
||||
} catch(ImportException e) {
|
||||
String errorMessage = e.getMessage();
|
||||
@@ -125,7 +125,7 @@ public class CodexKeystoreImportPane extends TitledDescriptionPane {
|
||||
private void onInputChange(boolean empty, boolean validChecksum) {
|
||||
if(!empty) {
|
||||
try {
|
||||
importer.getKeystore(ScriptType.P2WPKH.getDefaultDerivation(), secretShareProperty.get());
|
||||
importer.getKeystore(wallet.getPolicyType(), ScriptType.P2WPKH.getDefaultDerivation(), secretShareProperty.get());
|
||||
validChecksum = true;
|
||||
} catch(ImportException e) {
|
||||
invalidLabel.setText("Invalid checksum");
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package com.sparrowwallet.sparrow.control;
|
||||
|
||||
import com.sparrowwallet.drongo.BitcoinUnit;
|
||||
import com.sparrowwallet.drongo.policy.PolicyType;
|
||||
import com.sparrowwallet.drongo.wallet.SortDirection;
|
||||
import com.sparrowwallet.drongo.wallet.TableType;
|
||||
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||
@@ -15,7 +16,10 @@ import com.sparrowwallet.sparrow.event.WalletDataChangedEvent;
|
||||
import com.sparrowwallet.sparrow.event.WalletHistoryStatusEvent;
|
||||
import com.sparrowwallet.sparrow.io.Config;
|
||||
import com.sparrowwallet.sparrow.io.Storage;
|
||||
import com.sparrowwallet.sparrow.net.ElectrumServer;
|
||||
import com.sparrowwallet.sparrow.net.ServerType;
|
||||
import com.sparrowwallet.sparrow.net.cormorant.Cormorant;
|
||||
import com.sparrowwallet.sparrow.net.cormorant.bitcoind.BitcoindClient;
|
||||
import com.sparrowwallet.sparrow.wallet.Entry;
|
||||
import io.reactivex.Observable;
|
||||
import io.reactivex.subjects.PublishSubject;
|
||||
@@ -106,7 +110,7 @@ public class CoinTreeTable extends TreeTableView<Entry> {
|
||||
setPlaceholder(new Label("Error loading transactions: " + event.getErrorMessage()));
|
||||
} else if(event.isLoading()) {
|
||||
if(event.getStatusMessage() != null) {
|
||||
setPlaceholder(new Label(event.getStatusMessage() + "..."));
|
||||
setPlaceholder(new Label(event.getStatusMessage() + (event.getStatusMessage().contains("...") ? "" : "...")));
|
||||
} else {
|
||||
setPlaceholder(new Label("Loading transactions..."));
|
||||
}
|
||||
@@ -122,7 +126,7 @@ public class CoinTreeTable extends TreeTableView<Entry> {
|
||||
StackPane stackPane = new StackPane();
|
||||
stackPane.getChildren().add(AppServices.isConnecting() ? new Label("Loading transactions...") : new Label("No transactions"));
|
||||
|
||||
if(Config.get().getServerType() == ServerType.BITCOIN_CORE && !AppServices.isConnecting()) {
|
||||
if((Config.get().getServerType() == ServerType.BITCOIN_CORE || wallet.getPolicyType() == PolicyType.SINGLE_SP) && !AppServices.isConnecting() && !isFullyScanned(wallet)) {
|
||||
Hyperlink hyperlink = new Hyperlink();
|
||||
hyperlink.setTranslateY(30);
|
||||
hyperlink.setOnAction(event -> {
|
||||
@@ -133,6 +137,7 @@ public class CoinTreeTable extends TreeTableView<Entry> {
|
||||
Storage storage = AppServices.get().getOpenWallets().get(wallet);
|
||||
Wallet pastWallet = wallet.copy();
|
||||
wallet.setBirthDate(optDate.get());
|
||||
wallet.setBirthHeight(null);
|
||||
//Trigger background save of birthdate
|
||||
EventManager.get().post(new WalletDataChangedEvent(wallet));
|
||||
//Trigger full wallet rescan
|
||||
@@ -148,12 +153,47 @@ public class CoinTreeTable extends TreeTableView<Entry> {
|
||||
}
|
||||
|
||||
stackPane.getChildren().add(hyperlink);
|
||||
} else if(!AppServices.isConnecting() && Config.get().getServerType() == ServerType.BITCOIN_CORE && isFullyScanned(wallet)) {
|
||||
Date prunedDate = getPrunedDate();
|
||||
if(prunedDate != null) {
|
||||
DateFormat dateFormat = new SimpleDateFormat(DateStringConverter.FORMAT_PATTERN);
|
||||
Label prunedLabel = new Label("Scanned to pruned start date of " + dateFormat.format(prunedDate));
|
||||
prunedLabel.setTranslateY(30);
|
||||
stackPane.getChildren().add(prunedLabel);
|
||||
}
|
||||
}
|
||||
|
||||
stackPane.setAlignment(Pos.CENTER);
|
||||
return stackPane;
|
||||
}
|
||||
|
||||
private boolean isFullyScanned(Wallet wallet) {
|
||||
if(wallet.getPolicyType() == PolicyType.SINGLE_SP) {
|
||||
return wallet.isValid() && ElectrumServer.isSilentPaymentsFullyCovered(wallet.getSilentPaymentScanAddress());
|
||||
}
|
||||
|
||||
if(Config.get().getServerType() == ServerType.BITCOIN_CORE) {
|
||||
Date prunedDate = getPrunedDate();
|
||||
return prunedDate != null && wallet.getBirthDate() != null && !wallet.getBirthDate().after(prunedDate);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static Date getPrunedDate() {
|
||||
Cormorant cormorant = ElectrumServer.getCormorant();
|
||||
if(cormorant == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
BitcoindClient bitcoindClient = cormorant.getBitcoindClient();
|
||||
if(bitcoindClient == null || !bitcoindClient.isPruned()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return bitcoindClient.getCachedPrunedDate();
|
||||
}
|
||||
|
||||
protected void setupColumnSort(int defaultColumnIndex, TreeTableColumn.SortType defaultSortType) {
|
||||
WalletTable.Sort columnSort = getSavedColumnSort();
|
||||
if(columnSort == null) {
|
||||
@@ -208,7 +248,6 @@ public class CoinTreeTable extends TreeTableView<Entry> {
|
||||
return null;
|
||||
}
|
||||
|
||||
@SuppressWarnings("deprecation")
|
||||
protected void setupColumnWidths() {
|
||||
Double[] savedWidths = getSavedColumnWidths();
|
||||
for(int i = 0; i < getColumns().size(); i++) {
|
||||
@@ -216,8 +255,7 @@ public class CoinTreeTable extends TreeTableView<Entry> {
|
||||
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);
|
||||
setColumnResizePolicy(TreeTableView.CONSTRAINED_RESIZE_POLICY_FLEX_LAST_COLUMN);
|
||||
|
||||
getColumns().getLast().widthProperty().addListener((_, _, _) -> walletTableChanged());
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ import java.util.Date;
|
||||
public class DateAxisFormatter extends StringConverter<Number> {
|
||||
private static final DateFormat HOUR_FORMAT = new SimpleDateFormat("HH:mm");
|
||||
private static final DateFormat DAY_FORMAT = new SimpleDateFormat("d MMM");
|
||||
private static final DateFormat MONTH_FORMAT = new SimpleDateFormat("MMM yy");
|
||||
private static final DateFormat MONTH_FORMAT = new SimpleDateFormat("MMM yyyy");
|
||||
|
||||
private final DateFormat dateFormat;
|
||||
private int oddCounter;
|
||||
|
||||
@@ -14,8 +14,7 @@ import org.fxmisc.richtext.CodeArea;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import static com.sparrowwallet.drongo.policy.PolicyType.MULTI;
|
||||
import static com.sparrowwallet.drongo.policy.PolicyType.SINGLE;
|
||||
import static com.sparrowwallet.drongo.policy.PolicyType.*;
|
||||
import static com.sparrowwallet.drongo.protocol.ScriptType.MULTISIG;
|
||||
|
||||
public class DescriptorArea extends CodeArea {
|
||||
@@ -33,13 +32,13 @@ public class DescriptorArea extends CodeArea {
|
||||
List<Keystore> keystores = wallet.getKeystores();
|
||||
int threshold = wallet.getDefaultPolicy().getNumSignaturesRequired();
|
||||
|
||||
if(SINGLE.equals(policyType)) {
|
||||
if(SINGLE_HD.equals(policyType)) {
|
||||
append(scriptType.getDescriptor(), "descriptor-text");
|
||||
replace(getLength(), getLength(), keystores.get(0).getScriptName(), List.of(keystores.get(0).isValid() ? "descriptor-text" : "descriptor-error", keystores.get(0).getScriptName()));
|
||||
append(scriptType.getCloseDescriptor(), "descriptor-text");
|
||||
}
|
||||
|
||||
if(MULTI.equals(policyType)) {
|
||||
if(MULTI_HD.equals(policyType)) {
|
||||
append(scriptType.getDescriptor(), "descriptor-text");
|
||||
append(MULTISIG.getDescriptor(), "descriptor-text");
|
||||
append(Integer.toString(threshold), "descriptor-text");
|
||||
@@ -52,6 +51,12 @@ public class DescriptorArea extends CodeArea {
|
||||
append(MULTISIG.getCloseDescriptor(), "descriptor-text");
|
||||
append(scriptType.getCloseDescriptor(), "descriptor-text");
|
||||
}
|
||||
|
||||
if(SINGLE_SP.equals(policyType)) {
|
||||
append("sp(", "descriptor-text");
|
||||
replace(getLength(), getLength(), keystores.get(0).getScriptName(), List.of(keystores.get(0).isValid() ? "descriptor-text" : "descriptor-error", keystores.get(0).getScriptName()));
|
||||
append(")", "descriptor-text");
|
||||
}
|
||||
}
|
||||
|
||||
public Wallet getWallet() {
|
||||
|
||||
@@ -23,5 +23,10 @@ public class DescriptorQRDisplayDialog extends QRDisplayDialog {
|
||||
PdfUtils.saveOutputDescriptor(walletName, outputDescriptor, ur, getEncoding() == QREncoding.BBQR ? bbqr : null);
|
||||
event.consume();
|
||||
});
|
||||
|
||||
ButtonBar buttonBar = (ButtonBar)dialogPane.lookup(".button-bar");
|
||||
if(buttonBar != null) {
|
||||
buttonBar.setButtonOrder("E+L+B+C+O");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ import com.sparrowwallet.drongo.policy.PolicyType;
|
||||
import com.sparrowwallet.drongo.protocol.ScriptType;
|
||||
import com.sparrowwallet.drongo.protocol.Sha256Hash;
|
||||
import com.sparrowwallet.drongo.psbt.PSBT;
|
||||
import com.sparrowwallet.drongo.silentpayments.SilentPaymentScanAddress;
|
||||
import com.sparrowwallet.drongo.wallet.*;
|
||||
import com.sparrowwallet.sparrow.AppServices;
|
||||
import com.sparrowwallet.sparrow.EventManager;
|
||||
@@ -304,15 +305,34 @@ public class DevicePane extends TitledDescriptionPane {
|
||||
|
||||
if(importButton instanceof SplitMenuButton importMenuButton) {
|
||||
if(wallet.getScriptType() == null) {
|
||||
ScriptType[] scriptTypes = new ScriptType[] {ScriptType.P2WPKH, ScriptType.P2SH_P2WPKH, ScriptType.P2PKH, ScriptType.P2TR};
|
||||
for(ScriptType scriptType : scriptTypes) {
|
||||
MenuItem item = new MenuItem(scriptType.getDescription());
|
||||
final List<ChildNumber> derivation = scriptType.getDefaultDerivation();
|
||||
item.setOnAction(event -> {
|
||||
importMenuButton.setDisable(true);
|
||||
importKeystore(derivation);
|
||||
});
|
||||
importMenuButton.getItems().add(item);
|
||||
if(wallet.getPolicyType() == null) {
|
||||
List<PolicyAndScriptType> types = new ArrayList<>();
|
||||
for(PolicyType policyType : List.of(PolicyType.SINGLE_HD, PolicyType.SINGLE_SP)) {
|
||||
for(ScriptType scriptType : ScriptType.getAddressableScriptTypes(policyType)) {
|
||||
types.add(new PolicyAndScriptType(policyType, scriptType));
|
||||
}
|
||||
}
|
||||
for(PolicyAndScriptType type : types) {
|
||||
MenuItem item = new MenuItem(type.getDescription());
|
||||
final List<ChildNumber> derivation = type.scriptType().getDefaultDerivation();
|
||||
item.setOnAction(event -> {
|
||||
importMenuButton.setDisable(true);
|
||||
wallet.setPolicyType(type.policyType());
|
||||
importKeystore(derivation);
|
||||
});
|
||||
importMenuButton.getItems().add(item);
|
||||
}
|
||||
} else {
|
||||
List<ScriptType> scriptTypes = ScriptType.getScriptTypesForPolicyType(wallet.getPolicyType());
|
||||
for(ScriptType scriptType : scriptTypes) {
|
||||
MenuItem item = new MenuItem(scriptType.getDescription());
|
||||
final List<ChildNumber> derivation = scriptType.getDefaultDerivation();
|
||||
item.setOnAction(event -> {
|
||||
importMenuButton.setDisable(true);
|
||||
importKeystore(derivation);
|
||||
});
|
||||
importMenuButton.getItems().add(item);
|
||||
}
|
||||
}
|
||||
importMenuButton.getItems().add(new SeparatorMenuItem());
|
||||
MenuItem discoverItem = new MenuItem("Discover Wallet...");
|
||||
@@ -711,7 +731,7 @@ public class DevicePane extends TitledDescriptionPane {
|
||||
return;
|
||||
}
|
||||
|
||||
Service<Keystore> importService = cardApi.getImportService(derivation, messageProperty);
|
||||
Service<Keystore> importService = cardApi.getImportService(wallet.getPolicyType(), derivation, messageProperty);
|
||||
handleCardOperation(importService, importButton, "Import", true, event -> {
|
||||
importKeystore(derivation, importService.getValue());
|
||||
});
|
||||
@@ -730,13 +750,21 @@ public class DevicePane extends TitledDescriptionPane {
|
||||
}
|
||||
}
|
||||
|
||||
importXpub(derivation);
|
||||
importKey(derivation);
|
||||
});
|
||||
enumerateService.setOnFailed(workerStateEvent -> {
|
||||
setError("Error", enumerateService.getException().getMessage());
|
||||
importButton.setDisable(false);
|
||||
});
|
||||
enumerateService.start();
|
||||
} else {
|
||||
importKey(derivation);
|
||||
}
|
||||
}
|
||||
|
||||
private void importKey(List<ChildNumber> derivation) {
|
||||
if(wallet != null && wallet.getPolicyType() == PolicyType.SINGLE_SP) {
|
||||
importSpscan(derivation);
|
||||
} else {
|
||||
importXpub(derivation);
|
||||
}
|
||||
@@ -747,7 +775,7 @@ public class DevicePane extends TitledDescriptionPane {
|
||||
|
||||
Hwi.GetXpubService getXpubService = new Hwi.GetXpubService(device, passphrase.get(), derivationPath);
|
||||
getXpubService.setOnSucceeded(workerStateEvent -> {
|
||||
String xpub = getXpubService.getValue();
|
||||
ExtendedKey xpub = getXpubService.getValue();
|
||||
|
||||
try {
|
||||
Keystore keystore = new Keystore();
|
||||
@@ -755,7 +783,7 @@ public class DevicePane extends TitledDescriptionPane {
|
||||
keystore.setSource(KeystoreSource.HW_USB);
|
||||
keystore.setWalletModel(device.getModel());
|
||||
keystore.setKeyDerivation(new KeyDerivation(device.getFingerprint(), derivationPath));
|
||||
keystore.setExtendedPublicKey(ExtendedKey.fromDescriptor(xpub));
|
||||
keystore.setExtendedPublicKey(xpub);
|
||||
|
||||
importKeystore(derivation, keystore);
|
||||
} catch(Exception e) {
|
||||
@@ -771,14 +799,44 @@ public class DevicePane extends TitledDescriptionPane {
|
||||
getXpubService.start();
|
||||
}
|
||||
|
||||
private void importSpscan(List<ChildNumber> derivation) {
|
||||
String derivationPath = KeyDerivation.writePath(derivation);
|
||||
|
||||
Hwi.GetSpscanService getSpscanService = new Hwi.GetSpscanService(device, passphrase.get(), derivationPath);
|
||||
getSpscanService.setOnSucceeded(workerStateEvent -> {
|
||||
SilentPaymentScanAddress spscan = getSpscanService.getValue();
|
||||
|
||||
try {
|
||||
Keystore keystore = new Keystore();
|
||||
keystore.setLabel(device.getModel().toDisplayString());
|
||||
keystore.setSource(KeystoreSource.HW_USB);
|
||||
keystore.setWalletModel(device.getModel());
|
||||
keystore.setKeyDerivation(new KeyDerivation(device.getFingerprint(), derivationPath));
|
||||
keystore.setSilentPaymentScanAddress(spscan);
|
||||
|
||||
importKeystore(derivation, keystore);
|
||||
} catch(Exception e) {
|
||||
setError("Could not retrieve spscan", e.getMessage());
|
||||
}
|
||||
});
|
||||
getSpscanService.setOnFailed(workerStateEvent -> {
|
||||
setError("Could not retrieve spscan", getSpscanService.getException().getMessage());
|
||||
importButton.setDisable(false);
|
||||
});
|
||||
setDescription("Importing...");
|
||||
showHideLink.setVisible(false);
|
||||
getSpscanService.start();
|
||||
}
|
||||
|
||||
private void importKeystore(List<ChildNumber> derivation, Keystore keystore) {
|
||||
if(wallet.getScriptType() == null) {
|
||||
ScriptType scriptType = Arrays.stream(ScriptType.ADDRESSABLE_TYPES).filter(type -> type.getDefaultDerivation().get(0).equals(derivation.get(0))).findFirst().orElse(ScriptType.P2PKH);
|
||||
ScriptType scriptType = Arrays.stream(ScriptType.ADDRESSABLE_TYPES).filter(type -> type.getDefaultDerivation().getFirst().equals(derivation.getFirst())).findFirst().orElse(ScriptType.P2PKH);
|
||||
PolicyType policyType = wallet.getPolicyType() != null ? wallet.getPolicyType() : PolicyType.SINGLE_HD;
|
||||
wallet.setName(device.getModel().toDisplayString());
|
||||
wallet.setPolicyType(PolicyType.SINGLE);
|
||||
wallet.setPolicyType(policyType);
|
||||
wallet.setScriptType(scriptType);
|
||||
wallet.getKeystores().add(keystore);
|
||||
wallet.setDefaultPolicy(Policy.getPolicy(PolicyType.SINGLE, scriptType, wallet.getKeystores(), null));
|
||||
wallet.setDefaultPolicy(Policy.getPolicy(policyType, scriptType, wallet.getKeystores(), null));
|
||||
|
||||
EventManager.get().post(new WalletImportEvent(wallet));
|
||||
} else {
|
||||
@@ -926,7 +984,7 @@ public class DevicePane extends TitledDescriptionPane {
|
||||
|
||||
List<StandardAccount> discoveryAccounts = new ArrayList<>(Arrays.asList(StandardAccount.values()).subList(0, optRange.get() + 1));
|
||||
Map<Hwi.WalletType, String> derivationPaths = new LinkedHashMap<>();
|
||||
List<ScriptType> scriptTypes = new ArrayList<>(ScriptType.getAddressableScriptTypes(PolicyType.SINGLE));
|
||||
List<ScriptType> scriptTypes = new ArrayList<>(ScriptType.getAddressableScriptTypes(PolicyType.SINGLE_HD));
|
||||
if(device.getModel() == WalletModel.BITBOX_02) {
|
||||
scriptTypes.remove(ScriptType.P2PKH);
|
||||
}
|
||||
@@ -938,21 +996,21 @@ public class DevicePane extends TitledDescriptionPane {
|
||||
|
||||
Hwi.GetXpubsService getXpubsService = new Hwi.GetXpubsService(device, passphrase.get(), derivationPaths);
|
||||
getXpubsService.setOnSucceeded(_ -> {
|
||||
Map<Hwi.WalletType, String> accountXpubs = getXpubsService.getValue();
|
||||
Map<Hwi.WalletType, ExtendedKey> accountXpubs = getXpubsService.getValue();
|
||||
|
||||
for(Map.Entry<Hwi.WalletType, String> entry : accountXpubs.entrySet()) {
|
||||
for(Map.Entry<Hwi.WalletType, ExtendedKey> entry : accountXpubs.entrySet()) {
|
||||
try {
|
||||
Wallet wallet = new Wallet(device.getModel().toDisplayString());
|
||||
wallet.setPolicyType(PolicyType.SINGLE);
|
||||
wallet.setPolicyType(PolicyType.SINGLE_HD);
|
||||
wallet.setScriptType(entry.getKey().scriptType());
|
||||
Keystore keystore = new Keystore();
|
||||
keystore.setLabel(device.getModel().toDisplayString());
|
||||
keystore.setSource(KeystoreSource.HW_USB);
|
||||
keystore.setWalletModel(device.getModel());
|
||||
keystore.setKeyDerivation(new KeyDerivation(device.getFingerprint(), derivationPaths.get(entry.getKey())));
|
||||
keystore.setExtendedPublicKey(ExtendedKey.fromDescriptor(entry.getValue()));
|
||||
keystore.setExtendedPublicKey(entry.getValue());
|
||||
wallet.getKeystores().add(keystore);
|
||||
wallet.setDefaultPolicy(Policy.getPolicy(PolicyType.SINGLE, entry.getKey().scriptType(), wallet.getKeystores(), 1));
|
||||
wallet.setDefaultPolicy(Policy.getPolicy(PolicyType.SINGLE_HD, entry.getKey().scriptType(), wallet.getKeystores(), 1));
|
||||
if(entry.getKey().standardAccount().equals(StandardAccount.ACCOUNT_0)) {
|
||||
wallets.add(wallet);
|
||||
} else {
|
||||
@@ -982,7 +1040,7 @@ public class DevicePane extends TitledDescriptionPane {
|
||||
AppServices.showErrorDialog("No existing wallet found",
|
||||
Config.get().getServerType() == ServerType.BITCOIN_CORE ? "The configured server type is Bitcoin Core, which does not support wallet discovery.\n\n" +
|
||||
"You can however import the " + device.getModel().toDisplayString() + " and scan the blockchain by supplying a start date." :
|
||||
"Could not find a wallet with existing transactions using the " + device.getModel().toDisplayString() + ".");
|
||||
"Could not find an HD wallet with existing transactions using the " + device.getModel().toDisplayString() + ".");
|
||||
setDefaultStatus();
|
||||
importButton.setDisable(false);
|
||||
}
|
||||
@@ -1032,16 +1090,16 @@ public class DevicePane extends TitledDescriptionPane {
|
||||
Map<StandardAccount, Keystore> importedKeystores = new LinkedHashMap<>();
|
||||
Hwi.GetXpubsService getXpubsService = new Hwi.GetXpubsService(device, passphrase.get(), accountDerivationPaths);
|
||||
getXpubsService.setOnSucceeded(workerStateEvent -> {
|
||||
Map<Hwi.WalletType, String> accountXpubs = getXpubsService.getValue();
|
||||
Map<Hwi.WalletType, ExtendedKey> accountXpubs = getXpubsService.getValue();
|
||||
|
||||
for(Map.Entry<Hwi.WalletType, String> entry : accountXpubs.entrySet()) {
|
||||
for(Map.Entry<Hwi.WalletType, ExtendedKey> entry : accountXpubs.entrySet()) {
|
||||
try {
|
||||
Keystore keystore = new Keystore();
|
||||
keystore.setLabel(device.getModel().toDisplayString());
|
||||
keystore.setSource(KeystoreSource.HW_USB);
|
||||
keystore.setWalletModel(device.getModel());
|
||||
keystore.setKeyDerivation(new KeyDerivation(masterFingerprint, accountDerivationPaths.get(entry.getKey())));
|
||||
keystore.setExtendedPublicKey(ExtendedKey.fromDescriptor(entry.getValue()));
|
||||
keystore.setExtendedPublicKey(entry.getValue());
|
||||
importedKeystores.put(entry.getKey().standardAccount(), keystore);
|
||||
} catch(Exception e) {
|
||||
setError("Could not retrieve xpub", e.getMessage());
|
||||
@@ -1179,7 +1237,7 @@ public class DevicePane extends TitledDescriptionPane {
|
||||
showHideLink.setVisible(true);
|
||||
setExpanded(false);
|
||||
List<ChildNumber> importDerivation = KeyDerivation.parsePath(derivationField.getText());
|
||||
importXpub(importDerivation);
|
||||
importKey(importDerivation);
|
||||
});
|
||||
|
||||
derivationField.textProperty().addListener((observable, oldValue, newValue) -> {
|
||||
@@ -1355,4 +1413,10 @@ public class DevicePane extends TitledDescriptionPane {
|
||||
public enum DeviceOperation {
|
||||
IMPORT, SIGN, DISPLAY_ADDRESS, SIGN_MESSAGE, DISCOVER_KEYSTORES, GET_PRIVATE_KEY, GET_ADDRESS;
|
||||
}
|
||||
|
||||
protected record PolicyAndScriptType(PolicyType policyType, ScriptType scriptType) {
|
||||
public String getDescription() {
|
||||
return scriptType.getDescription() + (policyType == PolicyType.SINGLE_SP ? " SP" : " HD");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,6 +24,7 @@ public class DialogImage extends StackPane {
|
||||
public ObjectProperty<DialogImage.Type> typeProperty = new SimpleObjectProperty<>();
|
||||
|
||||
public DialogImage() {
|
||||
getStyleClass().add("dialog-image");
|
||||
setPrefSize(WIDTH, HEIGHT);
|
||||
this.typeProperty.addListener((observable, oldValue, type) -> {
|
||||
refresh(type);
|
||||
|
||||
@@ -4,6 +4,7 @@ import com.sparrowwallet.drongo.KeyPurpose;
|
||||
import com.sparrowwallet.drongo.OsType;
|
||||
import com.sparrowwallet.drongo.Utils;
|
||||
import com.sparrowwallet.drongo.address.Address;
|
||||
import com.sparrowwallet.drongo.policy.PolicyType;
|
||||
import com.sparrowwallet.drongo.protocol.*;
|
||||
import com.sparrowwallet.drongo.silentpayments.SilentPayment;
|
||||
import com.sparrowwallet.drongo.silentpayments.SilentPaymentAddress;
|
||||
@@ -135,7 +136,7 @@ public class EntryCell extends TreeTableCell<Entry, Entry> implements Confirmati
|
||||
HBox actionBox = new HBox();
|
||||
actionBox.getStyleClass().add("cell-actions");
|
||||
|
||||
if(!nodeEntry.getNode().getWallet().isBip47()) {
|
||||
if(!nodeEntry.getNode().getWallet().isBip47() && nodeEntry.getNode().getWallet().getPolicyType() != PolicyType.SINGLE_SP) {
|
||||
Button receiveButton = new Button("");
|
||||
receiveButton.setGraphic(getReceiveGlyph());
|
||||
receiveButton.setOnAction(event -> {
|
||||
@@ -249,7 +250,7 @@ public class EntryCell extends TreeTableCell<Entry, Entry> implements Confirmati
|
||||
double vSize = tx.getVirtualSize();
|
||||
if(changeTotal == 0) {
|
||||
//Add change output length to vSize if change was not present on the original transaction
|
||||
TransactionOutput changeOutput = new TransactionOutput(new Transaction(), 1L, transactionEntry.getWallet().getFreshNode(KeyPurpose.CHANGE).getOutputScript());
|
||||
TransactionOutput changeOutput = new TransactionOutput(new Transaction(), 1L, transactionEntry.getWallet().getNode(KeyPurpose.CHANGE).getOutputScript());
|
||||
vSize += changeOutput.getLength();
|
||||
}
|
||||
double inputSize = tx.getInputs().get(0).getLength() + (tx.getInputs().get(0).hasWitness() ? (double)tx.getInputs().get(0).getWitness().getLength() / Transaction.WITNESS_SCALE_FACTOR : 0);
|
||||
@@ -334,8 +335,10 @@ public class EntryCell extends TreeTableCell<Entry, Entry> implements Confirmati
|
||||
|
||||
if(cancelTransaction) {
|
||||
Payment existing = payments.get(0);
|
||||
Address address = transactionEntry.getWallet().getFreshNode(KeyPurpose.CHANGE).getAddress();
|
||||
Payment payment = new Payment(address, existing.getLabel(), existing.getAmount(), true);
|
||||
Payment payment = transactionEntry.getWallet().getPolicyType() == PolicyType.SINGLE_SP ?
|
||||
new SilentPayment(transactionEntry.getWallet().getSilentPaymentScanAddress().getChangeAddress().getSilentPaymentAddress(),
|
||||
existing.getLabel(), existing.getAmount(), true) :
|
||||
new Payment(transactionEntry.getWallet().getFreshNode(KeyPurpose.CHANGE).getAddress(), existing.getLabel(), existing.getAmount(), true);
|
||||
payments.clear();
|
||||
payments.add(payment);
|
||||
opReturns.clear();
|
||||
@@ -369,10 +372,10 @@ public class EntryCell extends TreeTableCell<Entry, Entry> implements Confirmati
|
||||
}
|
||||
|
||||
BlockTransactionHashIndex cpfpUtxo = ourOutputs.get(0);
|
||||
Address freshAddress = transactionEntry.getWallet().getFreshNode(KeyPurpose.RECEIVE).getAddress();
|
||||
TransactionOutput txOutput = new TransactionOutput(new Transaction(), cpfpUtxo.getValue(), freshAddress.getOutputScript());
|
||||
long dustThreshold = freshAddress.getScriptType().getDustThreshold(txOutput, Transaction.DUST_RELAY_TX_FEE);
|
||||
double inputSize = freshAddress.getScriptType().getInputVbytes();
|
||||
Address receiveAddress = transactionEntry.getWallet().getNode(KeyPurpose.RECEIVE).getAddress();
|
||||
TransactionOutput txOutput = new TransactionOutput(new Transaction(), cpfpUtxo.getValue(), receiveAddress.getOutputScript());
|
||||
long dustThreshold = receiveAddress.getScriptType().getDustThreshold(txOutput, Transaction.DUST_RELAY_TX_FEE);
|
||||
double inputSize = receiveAddress.getScriptType().getInputVbytes();
|
||||
double vSize = inputSize + txOutput.getLength();
|
||||
|
||||
List<TxoFilter> txoFilters = List.of(new ExcludeTxoFilter(List.of(cpfpUtxo)), new SpentTxoFilter(), new FrozenTxoFilter(), new CoinbaseTxoFilter(transactionEntry.getWallet()));
|
||||
@@ -396,7 +399,10 @@ public class EntryCell extends TreeTableCell<Entry, Entry> implements Confirmati
|
||||
|
||||
String label = transactionEntry.getLabel() == null ? "" : transactionEntry.getLabel();
|
||||
label += (label.isEmpty() ? "" : " ") + "(CPFP)";
|
||||
Payment payment = new Payment(freshAddress, label, inputTotal, true);
|
||||
Payment payment = transactionEntry.getWallet().getPolicyType() == PolicyType.SINGLE_SP ?
|
||||
new SilentPayment(transactionEntry.getWallet().getSilentPaymentScanAddress().getChangeAddress().getSilentPaymentAddress(),
|
||||
label, inputTotal, true) :
|
||||
new Payment(transactionEntry.getWallet().getFreshNode(KeyPurpose.CHANGE).getAddress(), label, inputTotal, true);
|
||||
|
||||
EventManager.get().post(new SendActionEvent(transactionEntry.getWallet(), utxos));
|
||||
Platform.runLater(() -> EventManager.get().post(new SpendUtxoEvent(transactionEntry.getWallet(), utxos, List.of(payment), null, blockTransaction.getFee(), true, null, true)));
|
||||
@@ -408,7 +414,8 @@ public class EntryCell extends TreeTableCell<Entry, Entry> implements Confirmati
|
||||
|
||||
private static boolean canSignMessage(WalletNode walletNode) {
|
||||
Wallet wallet = walletNode.getWallet();
|
||||
return wallet.getKeystores().size() == 1 && (!wallet.isBip47() || walletNode.getKeyPurpose() == KeyPurpose.RECEIVE);
|
||||
PolicyType policyType = wallet.getPolicyType();
|
||||
return (policyType == PolicyType.SINGLE_HD || policyType == PolicyType.SINGLE_SP) && (!wallet.isBip47() || walletNode.getKeyPurpose() == KeyPurpose.RECEIVE);
|
||||
}
|
||||
|
||||
private static boolean containsWalletOutputs(TransactionEntry transactionEntry) {
|
||||
@@ -663,7 +670,7 @@ public class EntryCell extends TreeTableCell<Entry, Entry> implements Confirmati
|
||||
|
||||
public static class AddressContextMenu extends ContextMenu {
|
||||
public AddressContextMenu(Address address, String outputDescriptor, NodeEntry nodeEntry, boolean addUtxoItems, TreeTableView<Entry> treetable) {
|
||||
if(nodeEntry == null || !nodeEntry.getWallet().isBip47()) {
|
||||
if(nodeEntry == null || (!nodeEntry.getWallet().isBip47() && nodeEntry.getWallet().getPolicyType() != PolicyType.SINGLE_SP)) {
|
||||
MenuItem receiveToAddress = new MenuItem("Receive To");
|
||||
receiveToAddress.setGraphic(getReceiveGlyph());
|
||||
receiveToAddress.setOnAction(event -> {
|
||||
|
||||
@@ -203,7 +203,9 @@ public abstract class FileImportPane extends TitledDescriptionPane {
|
||||
for(Wallet wallet : wallets) {
|
||||
if(scriptType.equals(wallet.getScriptType()) && !wallet.getKeystores().isEmpty()) {
|
||||
Keystore keystore = wallet.getKeystores().get(0);
|
||||
keystore.setLabel(importer.getName().replace(" Multisig", ""));
|
||||
if(Keystore.DEFAULT_LABEL.equals(keystore.getLabel())) {
|
||||
keystore.setLabel(importer.getName().replace(" Multisig", ""));
|
||||
}
|
||||
keystore.setSource(KeystoreSource.HW_AIRGAPPED);
|
||||
keystore.setWalletModel(importer.getWalletModel());
|
||||
return keystore;
|
||||
|
||||
@@ -25,7 +25,7 @@ public class FileKeystoreImportPane extends FileImportPane {
|
||||
protected void importFile(String fileName, InputStream inputStream, String password) throws ImportException {
|
||||
Keystore keystore = getScannedKeystore(wallet.getScriptType());
|
||||
if(keystore == null) {
|
||||
keystore = importer.getKeystore(wallet.getScriptType(), inputStream, password);
|
||||
keystore = importer.getKeystore(wallet.getPolicyType(), wallet.getScriptType(), inputStream, password);
|
||||
}
|
||||
|
||||
if(requiredDerivation != null && !requiredDerivation.getDerivation().equals(keystore.getKeyDerivation().getDerivation())) {
|
||||
|
||||
@@ -5,7 +5,7 @@ import com.sparrowwallet.drongo.OutputDescriptor;
|
||||
import com.sparrowwallet.drongo.SecureString;
|
||||
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||
import com.sparrowwallet.hummingbird.UR;
|
||||
import com.sparrowwallet.hummingbird.registry.CryptoOutput;
|
||||
import com.sparrowwallet.hummingbird.registry.RegistryItem;
|
||||
import com.sparrowwallet.hummingbird.registry.RegistryType;
|
||||
import com.sparrowwallet.sparrow.AppServices;
|
||||
import com.sparrowwallet.sparrow.EventManager;
|
||||
@@ -32,7 +32,7 @@ import java.nio.charset.StandardCharsets;
|
||||
import java.util.Locale;
|
||||
import java.util.Optional;
|
||||
|
||||
import static com.sparrowwallet.sparrow.wallet.SettingsController.getCryptoOutput;
|
||||
import static com.sparrowwallet.sparrow.wallet.SettingsController.getUROutputDescriptor;
|
||||
|
||||
public class FileWalletExportPane extends TitledDescriptionPane {
|
||||
private final Wallet wallet;
|
||||
@@ -176,9 +176,9 @@ public class FileWalletExportPane extends TitledDescriptionPane {
|
||||
boolean addBbqrOption = exportWallet.getKeystores().stream().anyMatch(keystore -> keystore.getWalletModel().showBbqr());
|
||||
QREncoding encoding = exportWallet.getKeystores().stream().allMatch(keystore -> keystore.getWalletModel().selectBbqr()) ? QREncoding.BBQR : QREncoding.UR;
|
||||
OutputDescriptor outputDescriptor = OutputDescriptor.getOutputDescriptor(exportWallet, KeyPurpose.DEFAULT_PURPOSES, null);
|
||||
CryptoOutput cryptoOutput = getCryptoOutput(exportWallet);
|
||||
RegistryItem registryItem = getUROutputDescriptor(exportWallet);
|
||||
BBQR bbqr = addBbqrOption ? new BBQR(BBQRType.UNICODE, outputDescriptor.toString(true).getBytes(StandardCharsets.UTF_8)) : null;
|
||||
qrDisplayDialog = new DescriptorQRDisplayDialog(exportWallet.getFullDisplayName(), outputDescriptor.toString(true), cryptoOutput.toUR(), bbqr, encoding);
|
||||
qrDisplayDialog = new DescriptorQRDisplayDialog(exportWallet.getFullDisplayName(), outputDescriptor.toString(true), registryItem.toUR(), bbqr, encoding);
|
||||
} else if(exporter.getClass().equals(ColdcardMultisig.class)) {
|
||||
UR ur = UR.fromBytes(outputStream.toByteArray());
|
||||
BBQR bbqr = new BBQR(BBQRType.UNICODE, outputStream.toByteArray());
|
||||
|
||||
@@ -7,6 +7,7 @@ import com.sparrowwallet.drongo.policy.Policy;
|
||||
import com.sparrowwallet.drongo.policy.PolicyType;
|
||||
import com.sparrowwallet.drongo.protocol.ScriptType;
|
||||
import com.sparrowwallet.drongo.wallet.Keystore;
|
||||
import com.sparrowwallet.drongo.wallet.KeystoreSource;
|
||||
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||
import com.sparrowwallet.sparrow.EventManager;
|
||||
import com.sparrowwallet.sparrow.event.WalletImportEvent;
|
||||
@@ -30,8 +31,8 @@ import org.slf4j.LoggerFactory;
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public class FileWalletKeystoreImportPane extends FileImportPane {
|
||||
private static final Logger log = LoggerFactory.getLogger(FileWalletKeystoreImportPane.class);
|
||||
@@ -50,19 +51,25 @@ public class FileWalletKeystoreImportPane extends FileImportPane {
|
||||
this.fileName = fileName;
|
||||
this.password = password;
|
||||
|
||||
List<ScriptType> scriptTypes = ScriptType.getAddressableScriptTypes(PolicyType.SINGLE);
|
||||
List<PolicyAndScriptType> types = new ArrayList<>();
|
||||
for(PolicyType policyType : List.of(PolicyType.SINGLE_HD, PolicyType.SINGLE_SP)) {
|
||||
for(ScriptType scriptType : ScriptType.getAddressableScriptTypes(policyType)) {
|
||||
types.add(new PolicyAndScriptType(policyType, scriptType));
|
||||
}
|
||||
}
|
||||
|
||||
if(wallets != null && !wallets.isEmpty()) {
|
||||
if(wallets.size() == 1 && scriptTypes.contains(wallets.get(0).getScriptType())) {
|
||||
Wallet wallet = wallets.get(0);
|
||||
wallet.setPolicyType(PolicyType.SINGLE);
|
||||
wallet.setDefaultPolicy(Policy.getPolicy(PolicyType.SINGLE, wallet.getScriptType(), wallet.getKeystores(), null));
|
||||
wallet.setName(importer.getName());
|
||||
EventManager.get().post(new WalletImportEvent(wallets.get(0)));
|
||||
} else {
|
||||
scriptTypes.retainAll(wallets.stream().map(Wallet::getScriptType).collect(Collectors.toList()));
|
||||
if(scriptTypes.isEmpty()) {
|
||||
throw new ImportException("No singlesig script types present in QR code");
|
||||
}
|
||||
wallets.stream().filter(w -> w.getPolicyType() == null).forEach(w -> w.setPolicyType(PolicyType.SINGLE_HD));
|
||||
List<PolicyAndScriptType> walletTypes = wallets.stream().map(w -> new PolicyAndScriptType(w.getPolicyType(), w.getScriptType())).toList();
|
||||
types.retainAll(walletTypes);
|
||||
if(types.isEmpty()) {
|
||||
throw new ImportException("No singlesig script types present in QR code");
|
||||
}
|
||||
|
||||
if(types.size() == 1) {
|
||||
Wallet wallet = wallets.stream().filter(w -> w.getPolicyType() == types.getFirst().policyType() && w.getScriptType() == types.getFirst().scriptType()).findFirst().orElseThrow(ImportException::new);
|
||||
importScannedWallet(wallet);
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
@@ -72,58 +79,75 @@ public class FileWalletKeystoreImportPane extends FileImportPane {
|
||||
}
|
||||
}
|
||||
|
||||
setContent(getScriptTypeEntry(scriptTypes));
|
||||
setContent(getScriptTypeEntry(types));
|
||||
setExpanded(true);
|
||||
importButton.setDisable(true);
|
||||
}
|
||||
|
||||
private void importWallet(ScriptType scriptType) throws ImportException {
|
||||
private void importWallet(PolicyAndScriptType type) throws ImportException {
|
||||
PolicyType policyType = type.policyType();
|
||||
ScriptType scriptType = type.scriptType();
|
||||
|
||||
if(wallets != null && !wallets.isEmpty()) {
|
||||
Wallet wallet = wallets.stream().filter(wallet1 -> wallet1.getScriptType() == scriptType).findFirst().orElseThrow(ImportException::new);
|
||||
wallet.setName(importer.getName());
|
||||
wallet.setPolicyType(PolicyType.SINGLE);
|
||||
wallet.setDefaultPolicy(Policy.getPolicy(PolicyType.SINGLE, wallet.getScriptType(), wallet.getKeystores(), null));
|
||||
EventManager.get().post(new WalletImportEvent(wallet));
|
||||
Wallet wallet = wallets.stream().filter(w -> w.getPolicyType() == policyType && w.getScriptType() == scriptType).findFirst().orElseThrow(ImportException::new);
|
||||
importScannedWallet(wallet);
|
||||
} else {
|
||||
ByteArrayInputStream bais = new ByteArrayInputStream(fileBytes);
|
||||
Keystore keystore = importer.getKeystore(scriptType, bais, password);
|
||||
Keystore keystore = importer.getKeystore(policyType, scriptType, bais, password);
|
||||
|
||||
Wallet wallet = new Wallet();
|
||||
wallet.setName(Files.getNameWithoutExtension(fileName));
|
||||
wallet.setPolicyType(PolicyType.SINGLE);
|
||||
wallet.setPolicyType(policyType);
|
||||
wallet.setScriptType(scriptType);
|
||||
wallet.getKeystores().add(keystore);
|
||||
wallet.setDefaultPolicy(Policy.getPolicy(PolicyType.SINGLE, scriptType, wallet.getKeystores(), null));
|
||||
wallet.setDefaultPolicy(Policy.getPolicy(policyType, scriptType, wallet.getKeystores(), null));
|
||||
|
||||
EventManager.get().post(new WalletImportEvent(wallet));
|
||||
}
|
||||
}
|
||||
|
||||
private Node getScriptTypeEntry(List<ScriptType> scriptTypes) {
|
||||
Label label = new Label("Script Type:");
|
||||
private void importScannedWallet(Wallet wallet) {
|
||||
wallet.setName(importer.getName());
|
||||
wallet.setDefaultPolicy(Policy.getPolicy(wallet.getPolicyType(), wallet.getScriptType(), wallet.getKeystores(), null));
|
||||
|
||||
Keystore keystore = wallet.getKeystores().getFirst();
|
||||
if(keystore.getSource() == KeystoreSource.SW_WATCH) {
|
||||
if(Keystore.DEFAULT_LABEL.equals(keystore.getLabel())) {
|
||||
keystore.setLabel(importer.getName());
|
||||
}
|
||||
keystore.setSource(KeystoreSource.HW_AIRGAPPED);
|
||||
keystore.setWalletModel(importer.getWalletModel());
|
||||
}
|
||||
|
||||
EventManager.get().post(new WalletImportEvent(wallet));
|
||||
}
|
||||
|
||||
private Node getScriptTypeEntry(List<PolicyAndScriptType> types) {
|
||||
Label label = new Label("Type:");
|
||||
|
||||
HBox fieldBox = new HBox(5);
|
||||
fieldBox.setAlignment(Pos.CENTER_RIGHT);
|
||||
ComboBox<ScriptType> scriptTypeComboBox = new ComboBox<>(FXCollections.observableArrayList(scriptTypes));
|
||||
if(scriptTypes.contains(ScriptType.P2WPKH)) {
|
||||
scriptTypeComboBox.setValue(ScriptType.P2WPKH);
|
||||
ComboBox<PolicyAndScriptType> comboBox = new ComboBox<>(FXCollections.observableArrayList(types));
|
||||
PolicyAndScriptType defaultType = new PolicyAndScriptType(PolicyType.SINGLE_HD, ScriptType.P2WPKH);
|
||||
if(types.contains(defaultType)) {
|
||||
comboBox.setValue(defaultType);
|
||||
}
|
||||
scriptTypeComboBox.setConverter(new StringConverter<>() {
|
||||
comboBox.setConverter(new StringConverter<>() {
|
||||
@Override
|
||||
public String toString(ScriptType scriptType) {
|
||||
return scriptType == null ? "" : scriptType.getDescription();
|
||||
public String toString(PolicyAndScriptType type) {
|
||||
return type == null ? "" : type.getDescription();
|
||||
}
|
||||
|
||||
@Override
|
||||
public ScriptType fromString(String string) {
|
||||
public PolicyAndScriptType fromString(String string) {
|
||||
return null;
|
||||
}
|
||||
});
|
||||
scriptTypeComboBox.setMaxWidth(170);
|
||||
comboBox.setMaxWidth(220);
|
||||
|
||||
HelpLabel helpLabel = new HelpLabel();
|
||||
helpLabel.setHelpText("P2WPKH is a Native Segwit type and is usually the best choice for new wallets.\nP2SH-P2WPKH is a Wrapped Segwit type and is a reasonable choice for the widest compatibility.\nP2PKH is a Legacy type and should be avoided for new wallets.\nFor existing wallets, be sure to choose the type that matches the wallet you are importing.");
|
||||
fieldBox.getChildren().addAll(scriptTypeComboBox, helpLabel);
|
||||
helpLabel.setHelpText("Native Segwit is usually the best choice for new wallets.\nTaproot is newer and supports both HD and SP (silent payments) wallets.\nNested Segwit and Legacy are useful for recovering older wallets.\nFor existing wallets, be sure to choose the type that matches the wallet you are importing.");
|
||||
fieldBox.getChildren().addAll(comboBox, helpLabel);
|
||||
|
||||
Region region = new Region();
|
||||
HBox.setHgrow(region, Priority.SOMETIMES);
|
||||
@@ -133,7 +157,7 @@ public class FileWalletKeystoreImportPane extends FileImportPane {
|
||||
showHideLink.setVisible(true);
|
||||
setExpanded(false);
|
||||
try {
|
||||
importWallet(scriptTypeComboBox.getValue());
|
||||
importWallet(comboBox.getValue());
|
||||
} catch(ImportException e) {
|
||||
log.error("Error importing file", e);
|
||||
String errorMessage = e.getMessage();
|
||||
@@ -154,8 +178,14 @@ public class FileWalletKeystoreImportPane extends FileImportPane {
|
||||
contentBox.setPadding(new Insets(10, 30, 10, 30));
|
||||
contentBox.setPrefHeight(60);
|
||||
|
||||
Platform.runLater(scriptTypeComboBox::requestFocus);
|
||||
Platform.runLater(comboBox::requestFocus);
|
||||
|
||||
return contentBox;
|
||||
}
|
||||
|
||||
protected record PolicyAndScriptType(PolicyType policyType, ScriptType scriptType) {
|
||||
public String getDescription() {
|
||||
return scriptType.getDescription() + (policyType == PolicyType.SINGLE_SP ? " SP" : " HD");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,8 +25,6 @@ import com.sparrowwallet.sparrow.io.Storage;
|
||||
import javafx.application.Platform;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.control.*;
|
||||
import javafx.scene.image.Image;
|
||||
import javafx.scene.image.ImageView;
|
||||
import javafx.scene.layout.VBox;
|
||||
import javafx.stage.FileChooser;
|
||||
import javafx.stage.Stage;
|
||||
@@ -162,6 +160,17 @@ public class MessageSignDialog extends Dialog<ButtonBar.ButtonData> {
|
||||
signature.setStyle("-fx-pref-height: 80px");
|
||||
signature.setWrapText(true);
|
||||
signature.setOnMouseClicked(event -> signature.selectAll());
|
||||
|
||||
ContextMenu signatureMenu = new ContextMenu();
|
||||
MenuItem copyItem = new MenuItem("Copy");
|
||||
copyItem.setOnAction(e -> signature.copy());
|
||||
MenuItem pasteItem = new MenuItem("Paste");
|
||||
pasteItem.setOnAction(e -> signature.paste());
|
||||
MenuItem clearItem = new MenuItem("Clear");
|
||||
clearItem.setOnAction(e -> signature.clear());
|
||||
signatureMenu.getItems().addAll(copyItem, pasteItem, clearItem);
|
||||
signature.setContextMenu(signatureMenu);
|
||||
|
||||
signatureField.getInputs().add(signature);
|
||||
|
||||
Field formatField = new Field();
|
||||
@@ -297,8 +306,8 @@ public class MessageSignDialog extends Dialog<ButtonBar.ButtonData> {
|
||||
}
|
||||
|
||||
private void checkWalletSigning(Wallet wallet) {
|
||||
if(wallet.getKeystores().size() != 1) {
|
||||
throw new IllegalArgumentException("Cannot sign messages using a wallet with multiple keystores - a single key is required");
|
||||
if(wallet.getKeystores().size() != 1 || (wallet.getPolicyType() != PolicyType.SINGLE_HD && wallet.getPolicyType() != PolicyType.SINGLE_SP)) {
|
||||
throw new IllegalArgumentException("Cannot sign messages using this wallet type");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -323,7 +332,7 @@ public class MessageSignDialog extends Dialog<ButtonBar.ButtonData> {
|
||||
private boolean isValidAddress() {
|
||||
try {
|
||||
Address address = getAddress();
|
||||
return address.getScriptType().isAllowed(PolicyType.SINGLE) || address.getScriptType() == ScriptType.P2SH;
|
||||
return address.getScriptType().isAllowed(PolicyType.SINGLE_HD) || address.getScriptType() == ScriptType.P2SH;
|
||||
} catch (InvalidAddressException e) {
|
||||
return false;
|
||||
}
|
||||
@@ -379,18 +388,24 @@ public class MessageSignDialog extends Dialog<ButtonBar.ButtonData> {
|
||||
private void signUnencryptedKeystore(Wallet decryptedWallet) {
|
||||
try {
|
||||
Keystore keystore = decryptedWallet.getKeystores().getFirst();
|
||||
ECKey privKey = keystore.getKey(walletNode);
|
||||
String signatureText;
|
||||
if(isBip322()) {
|
||||
ScriptType scriptType = decryptedWallet.getScriptType();
|
||||
signatureText = Bip322.signMessageBip322(scriptType, message.getText().trim(), privKey);
|
||||
if(decryptedWallet.getPolicyType() == PolicyType.SINGLE_SP) {
|
||||
ECKey spendPrivKey = keystore.getSpendPrivateKey(Collections.emptyMap());
|
||||
signatureText = Bip322.signMessageBip322Sp(walletNode.getAddress(), message.getText().trim(), spendPrivKey, walletNode.getSilentPaymentTweak());
|
||||
spendPrivKey.clear();
|
||||
} else {
|
||||
ScriptType scriptType = isElectrumSignatureFormat() ? ScriptType.P2PKH : decryptedWallet.getScriptType();
|
||||
signatureText = privKey.signMessage(message.getText().trim(), scriptType);
|
||||
ECKey privKey = keystore.getKey(walletNode);
|
||||
if(isBip322()) {
|
||||
ScriptType scriptType = decryptedWallet.getScriptType();
|
||||
signatureText = Bip322.signMessageBip322(scriptType, message.getText().trim(), privKey);
|
||||
} else {
|
||||
ScriptType scriptType = isElectrumSignatureFormat() ? ScriptType.P2PKH : decryptedWallet.getScriptType();
|
||||
signatureText = privKey.signMessage(message.getText().trim(), scriptType);
|
||||
}
|
||||
privKey.clear();
|
||||
}
|
||||
signature.clear();
|
||||
signature.appendText(signatureText);
|
||||
privKey.clear();
|
||||
} catch(Exception e) {
|
||||
log.error("Could not sign message", e);
|
||||
AppServices.showErrorDialog("Could not sign message", e.getMessage());
|
||||
@@ -466,11 +481,11 @@ public class MessageSignDialog extends Dialog<ButtonBar.ButtonData> {
|
||||
if(scriptType == ScriptType.P2SH) {
|
||||
scriptType = ScriptType.P2SH_P2WPKH;
|
||||
}
|
||||
if(!ScriptType.getScriptTypesForPolicyType(PolicyType.SINGLE).contains(scriptType)) {
|
||||
if(!ScriptType.getScriptTypesForPolicyType(PolicyType.SINGLE_HD).contains(scriptType)) {
|
||||
throw new IllegalArgumentException("Only single signature P2PKH, P2SH-P2WPKH or P2WPKH addresses can verify messages.");
|
||||
}
|
||||
|
||||
Address signedMessageAddress = scriptType.getAddress(signedMessageKey);
|
||||
Address signedMessageAddress = scriptType.getAddress(PolicyType.SINGLE_HD, signedMessageKey);
|
||||
return providedAddress.equals(signedMessageAddress);
|
||||
}
|
||||
|
||||
@@ -500,12 +515,9 @@ public class MessageSignDialog extends Dialog<ButtonBar.ButtonData> {
|
||||
|
||||
private void showBip322Qr() {
|
||||
Wallet signingWallet = walletNode.getWallet();
|
||||
ScriptType scriptType = signingWallet.getScriptType();
|
||||
PSBT psbt = buildBip322Psbt(signingWallet);
|
||||
|
||||
PSBT psbt = Bip322.getBip322Psbt(scriptType, walletNode.getAddress(), message.getText().trim());
|
||||
addBip322DerivationInfo(psbt, signingWallet);
|
||||
|
||||
byte[] psbtBytes = psbt.serialize();
|
||||
byte[] psbtBytes = psbt.getForExport().serialize();
|
||||
CryptoPSBT cryptoPSBT = new CryptoPSBT(psbtBytes);
|
||||
BBQR bbqr = new BBQR(BBQRType.PSBT, psbtBytes);
|
||||
QRDisplayDialog qrDisplayDialog = new QRDisplayDialog(cryptoPSBT.toUR(), bbqr, false, true, QREncoding.UR);
|
||||
@@ -516,6 +528,40 @@ public class MessageSignDialog extends Dialog<ButtonBar.ButtonData> {
|
||||
}
|
||||
}
|
||||
|
||||
private PSBT buildBip322Psbt(Wallet signingWallet) {
|
||||
if(signingWallet.getPolicyType() == PolicyType.SINGLE_SP) {
|
||||
Keystore keystore = signingWallet.getKeystores().getFirst();
|
||||
ECKey spendPubKey = keystore.getSilentPaymentScanAddress().getSpendKey();
|
||||
KeyDerivation spendDerivation = new KeyDerivation(keystore.getKeyDerivation().getMasterFingerprint(), KeyDerivation.writePath(KeyDerivation.getBip352SpendDerivation(keystore.getKeyDerivation().getDerivation())));
|
||||
return Bip322.getBip322PsbtSp(walletNode.getAddress(), message.getText().trim(), walletNode.getSilentPaymentTweak(), Map.of(spendPubKey, spendDerivation));
|
||||
}
|
||||
|
||||
PSBT psbt = Bip322.getBip322Psbt(signingWallet.getScriptType(), walletNode.getAddress(), message.getText().trim());
|
||||
addBip322DerivationInfo(psbt, signingWallet);
|
||||
|
||||
return psbt;
|
||||
}
|
||||
|
||||
private String extractBip322Signature(PSBT signedPsbt) {
|
||||
String psbtMessage = signedPsbt.getGenericSignedMessage();
|
||||
if(psbtMessage != null && !psbtMessage.equals(message.getText().trim())) {
|
||||
Optional<ButtonType> response = AppServices.showWarningDialog("Message mismatch",
|
||||
"The message in the signed PSBT does not match the message in this dialog.\n\nPSBT message: " + psbtMessage +
|
||||
"\n\nContinue extracting the signature?", ButtonType.NO, ButtonType.YES);
|
||||
if(response.isEmpty() || response.get() != ButtonType.YES) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
Wallet signingWallet = walletNode.getWallet();
|
||||
if(signingWallet.getPolicyType() == PolicyType.SINGLE_SP) {
|
||||
return Bip322.getBip322SignatureFromPsbtSp(signedPsbt);
|
||||
}
|
||||
|
||||
ECKey pubKey = signingWallet.getKeystores().getFirst().getPubKey(walletNode);
|
||||
return Bip322.getBip322SignatureFromPsbt(signingWallet.getScriptType(), signedPsbt, pubKey);
|
||||
}
|
||||
|
||||
private void addBip322DerivationInfo(PSBT psbt, Wallet signingWallet) {
|
||||
ScriptType scriptType = signingWallet.getScriptType();
|
||||
PSBTInput psbtInput = psbt.getPsbtInputs().get(0);
|
||||
@@ -527,7 +573,7 @@ public class MessageSignDialog extends Dialog<ButtonBar.ButtonData> {
|
||||
psbtInput.setTapInternalKey(pubKey);
|
||||
psbtInput.getTapDerivedPublicKeys().put(ECKey.fromPublicOnly(pubKey.getPubKeyXCoord()), Map.of(fullDerivation, Collections.emptyList()));
|
||||
} else {
|
||||
psbtInput.getDerivedPublicKeys().put(scriptType.getOutputKey(pubKey), fullDerivation);
|
||||
psbtInput.getDerivedPublicKeys().put(scriptType.getOutputKey(signingWallet.getPolicyType(), pubKey), fullDerivation);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -539,11 +585,11 @@ public class MessageSignDialog extends Dialog<ButtonBar.ButtonData> {
|
||||
QRScanDialog.Result result = optionalResult.get();
|
||||
if(result.psbt != null) {
|
||||
try {
|
||||
Wallet signingWallet = walletNode.getWallet();
|
||||
ECKey pubKey = signingWallet.getKeystores().get(0).getPubKey(walletNode);
|
||||
String sig = Bip322.getBip322SignatureFromPsbt(signingWallet.getScriptType(), result.psbt, pubKey);
|
||||
signature.clear();
|
||||
signature.appendText(sig);
|
||||
String sig = extractBip322Signature(result.psbt);
|
||||
if(sig != null) {
|
||||
signature.clear();
|
||||
signature.appendText(sig);
|
||||
}
|
||||
} catch(Exception e) {
|
||||
log.error("Error extracting BIP-322 signature from PSBT", e);
|
||||
AppServices.showErrorDialog("Error extracting signature", e.getMessage());
|
||||
@@ -601,9 +647,7 @@ public class MessageSignDialog extends Dialog<ButtonBar.ButtonData> {
|
||||
|
||||
private void exportBip322File() {
|
||||
Wallet signingWallet = walletNode.getWallet();
|
||||
ScriptType scriptType = signingWallet.getScriptType();
|
||||
PSBT psbt = Bip322.getBip322Psbt(scriptType, walletNode.getAddress(), message.getText().trim());
|
||||
addBip322DerivationInfo(psbt, signingWallet);
|
||||
PSBT psbt = buildBip322Psbt(signingWallet);
|
||||
|
||||
Stage window = new Stage();
|
||||
FileChooser fileChooser = new FileChooser();
|
||||
@@ -613,7 +657,7 @@ public class MessageSignDialog extends Dialog<ButtonBar.ButtonData> {
|
||||
File file = fileChooser.showSaveDialog(window);
|
||||
if(file != null) {
|
||||
try(OutputStream os = new FileOutputStream(file)) {
|
||||
os.write(psbt.serialize());
|
||||
os.write(psbt.getForExport().serialize());
|
||||
} catch(IOException e) {
|
||||
log.error("Error saving BIP-322 PSBT", e);
|
||||
AppServices.showErrorDialog("Error saving PSBT", "Cannot write to " + file.getAbsolutePath());
|
||||
@@ -644,10 +688,11 @@ public class MessageSignDialog extends Dialog<ButtonBar.ButtonData> {
|
||||
try {
|
||||
byte[] psbtBytes = Files.readAllBytes(file.toPath());
|
||||
PSBT signedPsbt = new PSBT(psbtBytes, false);
|
||||
ECKey pubKey = walletNode.getWallet().getKeystores().get(0).getPubKey(walletNode);
|
||||
String sig = Bip322.getBip322SignatureFromPsbt(walletNode.getWallet().getScriptType(), signedPsbt, pubKey);
|
||||
signature.clear();
|
||||
signature.appendText(sig);
|
||||
String sig = extractBip322Signature(signedPsbt);
|
||||
if(sig != null) {
|
||||
signature.clear();
|
||||
signature.appendText(sig);
|
||||
}
|
||||
return;
|
||||
} catch(Exception e) {
|
||||
if(file.getName().toLowerCase(Locale.ROOT).endsWith(".psbt")) {
|
||||
|
||||
@@ -141,7 +141,7 @@ public class MnemonicKeystoreImportPane extends MnemonicKeystorePane {
|
||||
protected void onWordChange(boolean empty, boolean validWords, boolean validChecksum) {
|
||||
if(!empty && validWords) {
|
||||
try {
|
||||
importer.getKeystore(wallet.getScriptType().getDefaultDerivation(), wordEntriesProperty.get(), passphraseProperty.get());
|
||||
importer.getKeystore(wallet.getPolicyType(), wallet.getScriptType().getDefaultDerivation(), wordEntriesProperty.get(), passphraseProperty.get());
|
||||
validChecksum = true;
|
||||
} catch(ImportException e) {
|
||||
if(e.getCause() instanceof MnemonicException.MnemonicTypeException) {
|
||||
@@ -256,7 +256,7 @@ public class MnemonicKeystoreImportPane extends MnemonicKeystorePane {
|
||||
private boolean importKeystore(List<ChildNumber> derivation, boolean dryrun) {
|
||||
importButton.setDisable(true);
|
||||
try {
|
||||
Keystore keystore = importer.getKeystore(derivation, wordEntriesProperty.get(), passphraseProperty.get());
|
||||
Keystore keystore = importer.getKeystore(wallet.getPolicyType(), derivation, wordEntriesProperty.get(), passphraseProperty.get());
|
||||
if(!dryrun) {
|
||||
if(passphraseProperty.get() != null && !passphraseProperty.get().isEmpty()) {
|
||||
KeystorePassphraseDialog keystorePassphraseDialog = new KeystorePassphraseDialog(null, keystore, true);
|
||||
|
||||
+2
-2
@@ -162,7 +162,7 @@ public class MnemonicShareKeystoreImportPane extends MnemonicKeystorePane {
|
||||
existing.add(wordEntriesProperty.get());
|
||||
}
|
||||
|
||||
importer.getKeystore(wallet.getScriptType().getDefaultDerivation(), existing, passphraseProperty.get());
|
||||
importer.getKeystore(wallet.getPolicyType(), wallet.getScriptType().getDefaultDerivation(), existing, passphraseProperty.get());
|
||||
validSet = true;
|
||||
complete = true;
|
||||
} catch(MnemonicException e) {
|
||||
@@ -240,7 +240,7 @@ public class MnemonicShareKeystoreImportPane extends MnemonicKeystorePane {
|
||||
private boolean importKeystore(List<ChildNumber> derivation, boolean dryrun) {
|
||||
importButton.setDisable(true);
|
||||
try {
|
||||
Keystore keystore = importer.getKeystore(derivation, mnemonicShares, passphraseProperty.get());
|
||||
Keystore keystore = importer.getKeystore(wallet.getPolicyType(), derivation, mnemonicShares, passphraseProperty.get());
|
||||
if(!dryrun) {
|
||||
if(passphraseProperty.get() != null && !passphraseProperty.get().isEmpty()) {
|
||||
KeystorePassphraseDialog keystorePassphraseDialog = new KeystorePassphraseDialog(null, keystore, true);
|
||||
|
||||
+36
-22
@@ -81,7 +81,7 @@ public class MnemonicWalletKeystoreImportPane extends MnemonicKeystorePane {
|
||||
protected void onWordChange(boolean empty, boolean validWords, boolean validChecksum) {
|
||||
if(!empty && validWords) {
|
||||
try {
|
||||
importer.getKeystore(ScriptType.P2WPKH.getDefaultDerivation(), wordEntriesProperty.get(), passphraseProperty.get());
|
||||
importer.getKeystore(PolicyType.SINGLE_HD, ScriptType.P2WPKH.getDefaultDerivation(), wordEntriesProperty.get(), passphraseProperty.get());
|
||||
validChecksum = true;
|
||||
} catch(ImportException e) {
|
||||
if(e.getCause() instanceof MnemonicException.MnemonicTypeException) {
|
||||
@@ -108,14 +108,14 @@ public class MnemonicWalletKeystoreImportPane extends MnemonicKeystorePane {
|
||||
discoverButton.setGraphic(progressIndicator);
|
||||
List<Wallet> wallets = new ArrayList<>();
|
||||
|
||||
List<List<ChildNumber>> derivations = ScriptType.getScriptTypesForPolicyType(PolicyType.SINGLE).stream().map(ScriptType::getDefaultDerivation).collect(Collectors.toList());
|
||||
List<List<ChildNumber>> derivations = ScriptType.getScriptTypesForPolicyType(PolicyType.SINGLE_HD).stream().map(ScriptType::getDefaultDerivation).collect(Collectors.toList());
|
||||
derivations.add(List.of(new ChildNumber(0, true)));
|
||||
derivations.add(ScriptType.P2PKH.getDefaultDerivation(1)); //Bisq segwit misderivation
|
||||
|
||||
for(ScriptType scriptType : ScriptType.getScriptTypesForPolicyType(PolicyType.SINGLE)) {
|
||||
for(ScriptType scriptType : ScriptType.getScriptTypesForPolicyType(PolicyType.SINGLE_HD)) {
|
||||
for(List<ChildNumber> derivation : derivations) {
|
||||
try {
|
||||
Wallet wallet = getWallet(scriptType, derivation);
|
||||
Wallet wallet = getWallet(PolicyType.SINGLE_HD, scriptType, derivation);
|
||||
wallets.add(wallet);
|
||||
} catch(ImportException e) {
|
||||
String errorMessage = e.getMessage();
|
||||
@@ -148,7 +148,7 @@ public class MnemonicWalletKeystoreImportPane extends MnemonicKeystorePane {
|
||||
Optional<ButtonType> optButtonType = AppServices.showErrorDialog("No existing wallet found",
|
||||
Config.get().getServerType() == ServerType.BITCOIN_CORE ? "The configured server type is Bitcoin Core, which does not support wallet discovery.\n\n" +
|
||||
"You can however import this wallet and scan the blockchain by supplying a start date. Do you want to import this wallet?" :
|
||||
"Could not find a wallet with existing transactions using this mnemonic. Import this wallet anyway?", ButtonType.NO, ButtonType.YES);
|
||||
"Could not find an HD wallet with existing transactions using this mnemonic. Import this wallet anyway?", ButtonType.NO, ButtonType.YES);
|
||||
if(optButtonType.isPresent() && optButtonType.get() == ButtonType.YES) {
|
||||
setContent(getScriptTypeEntry());
|
||||
setExpanded(true);
|
||||
@@ -163,41 +163,49 @@ public class MnemonicWalletKeystoreImportPane extends MnemonicKeystorePane {
|
||||
walletDiscoveryService.start();
|
||||
}
|
||||
|
||||
private Wallet getWallet(ScriptType scriptType, List<ChildNumber> derivation) throws ImportException {
|
||||
private Wallet getWallet(PolicyType policyType, ScriptType scriptType, List<ChildNumber> derivation) throws ImportException {
|
||||
Wallet wallet = new Wallet("");
|
||||
wallet.setPolicyType(PolicyType.SINGLE);
|
||||
wallet.setPolicyType(policyType);
|
||||
wallet.setScriptType(scriptType);
|
||||
Keystore keystore = importer.getKeystore(derivation, wordEntriesProperty.get(), passphraseProperty.get());
|
||||
Keystore keystore = importer.getKeystore(policyType, derivation, wordEntriesProperty.get(), passphraseProperty.get());
|
||||
wallet.getKeystores().add(keystore);
|
||||
wallet.setDefaultPolicy(Policy.getPolicy(PolicyType.SINGLE, scriptType, wallet.getKeystores(), 1));
|
||||
wallet.setDefaultPolicy(Policy.getPolicy(policyType, scriptType, wallet.getKeystores(), 1));
|
||||
return wallet;
|
||||
}
|
||||
|
||||
private Node getScriptTypeEntry() {
|
||||
Label label = new Label("Script Type:");
|
||||
Label label = new Label("Type:");
|
||||
|
||||
List<PolicyAndScriptType> types = new ArrayList<>();
|
||||
for(PolicyType policyType : List.of(PolicyType.SINGLE_HD, PolicyType.SINGLE_SP)) {
|
||||
for(ScriptType scriptType : ScriptType.getAddressableScriptTypes(policyType)) {
|
||||
types.add(new PolicyAndScriptType(policyType, scriptType));
|
||||
}
|
||||
}
|
||||
|
||||
HBox fieldBox = new HBox(5);
|
||||
fieldBox.setAlignment(Pos.CENTER_RIGHT);
|
||||
ComboBox<ScriptType> scriptTypeComboBox = new ComboBox<>(FXCollections.observableArrayList(ScriptType.getAddressableScriptTypes(PolicyType.SINGLE)));
|
||||
if(scriptTypeComboBox.getItems().contains(ScriptType.P2WPKH)) {
|
||||
scriptTypeComboBox.setValue(ScriptType.P2WPKH);
|
||||
ComboBox<PolicyAndScriptType> comboBox = new ComboBox<>(FXCollections.observableArrayList(types));
|
||||
PolicyAndScriptType defaultType = new PolicyAndScriptType(PolicyType.SINGLE_HD, ScriptType.P2WPKH);
|
||||
if(types.contains(defaultType)) {
|
||||
comboBox.setValue(defaultType);
|
||||
}
|
||||
scriptTypeComboBox.setConverter(new StringConverter<>() {
|
||||
comboBox.setConverter(new StringConverter<>() {
|
||||
@Override
|
||||
public String toString(ScriptType scriptType) {
|
||||
return scriptType == null ? "" : scriptType.getDescription();
|
||||
public String toString(PolicyAndScriptType type) {
|
||||
return type == null ? "" : type.getDescription();
|
||||
}
|
||||
|
||||
@Override
|
||||
public ScriptType fromString(String string) {
|
||||
public PolicyAndScriptType fromString(String string) {
|
||||
return null;
|
||||
}
|
||||
});
|
||||
scriptTypeComboBox.setMaxWidth(170);
|
||||
comboBox.setMaxWidth(220);
|
||||
|
||||
HelpLabel helpLabel = new HelpLabel();
|
||||
helpLabel.setHelpText("Native Segwit is usually the best choice for new wallets.\nTaproot is a new type useful for specific needs.\nNested Segwit and Legacy are useful for recovering older wallets.\nFor existing wallets, be sure to choose the type that matches the wallet you are importing.");
|
||||
fieldBox.getChildren().addAll(scriptTypeComboBox, helpLabel);
|
||||
helpLabel.setHelpText("Native Segwit is usually the best choice for new wallets.\nTaproot is a new type useful for specific needs.\nTaproot Silent Payments creates a silent payment wallet.\nNested Segwit and Legacy are useful for recovering older wallets.\nFor existing wallets, be sure to choose the type that matches the wallet you are importing.");
|
||||
fieldBox.getChildren().addAll(comboBox, helpLabel);
|
||||
|
||||
Region region = new Region();
|
||||
HBox.setHgrow(region, Priority.SOMETIMES);
|
||||
@@ -208,8 +216,8 @@ public class MnemonicWalletKeystoreImportPane extends MnemonicKeystorePane {
|
||||
showHideLink.setVisible(true);
|
||||
setExpanded(false);
|
||||
try {
|
||||
ScriptType scriptType = scriptTypeComboBox.getValue();
|
||||
Wallet wallet = getWallet(scriptType, scriptType.getDefaultDerivation());
|
||||
PolicyAndScriptType type = comboBox.getValue();
|
||||
Wallet wallet = getWallet(type.policyType(), type.scriptType(), type.scriptType().getDefaultDerivation());
|
||||
EventManager.get().post(new WalletImportEvent(wallet));
|
||||
} catch(ImportException e) {
|
||||
log.error("Error importing mnemonic", e);
|
||||
@@ -231,4 +239,10 @@ public class MnemonicWalletKeystoreImportPane extends MnemonicKeystorePane {
|
||||
|
||||
return contentBox;
|
||||
}
|
||||
|
||||
protected record PolicyAndScriptType(PolicyType policyType, ScriptType scriptType) {
|
||||
public String getDescription() {
|
||||
return scriptType.getDescription() + (policyType == PolicyType.SINGLE_SP ? " SP" : " HD");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,6 +14,8 @@ import com.sparrowwallet.drongo.protocol.*;
|
||||
import com.sparrowwallet.drongo.psbt.PSBT;
|
||||
import com.sparrowwallet.drongo.psbt.PSBTInput;
|
||||
import com.sparrowwallet.drongo.psbt.PSBTProofException;
|
||||
import com.sparrowwallet.drongo.silentpayments.*;
|
||||
import com.sparrowwallet.drongo.wallet.Payment;
|
||||
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||
import com.sparrowwallet.drongo.wallet.WalletModel;
|
||||
import com.sparrowwallet.sparrow.AppServices;
|
||||
@@ -66,6 +68,8 @@ public class PrivateKeySweepDialog extends Dialog<Transaction> {
|
||||
private final ComboBox<Wallet> toWallet;
|
||||
private final FeeRangeSlider feeRange;
|
||||
private final CopyableLabel feeRate;
|
||||
private final UnlabeledToggleSwitch ignoreDust;
|
||||
private SilentPaymentAddress silentPaymentAddress;
|
||||
|
||||
public PrivateKeySweepDialog(Wallet wallet) {
|
||||
final DialogPane dialogPane = getDialogPane();
|
||||
@@ -109,7 +113,7 @@ public class PrivateKeySweepDialog extends Dialog<Transaction> {
|
||||
Field keyScriptTypeField = new Field();
|
||||
keyScriptTypeField.setText("Script Type:");
|
||||
keyScriptType = new ComboBox<>();
|
||||
keyScriptType.setItems(FXCollections.observableList(ScriptType.getAddressableScriptTypes(PolicyType.SINGLE)));
|
||||
keyScriptType.setItems(FXCollections.observableList(ScriptType.getAddressableScriptTypes(PolicyType.SINGLE_HD)));
|
||||
keyScriptTypeField.getInputs().add(keyScriptType);
|
||||
|
||||
keyScriptType.setConverter(new StringConverter<ScriptType>() {
|
||||
@@ -167,7 +171,12 @@ public class PrivateKeySweepDialog extends Dialog<Transaction> {
|
||||
feeRange.setFeeRate(AppServices.getDefaultFeeRate());
|
||||
updateFeeRate();
|
||||
|
||||
fieldset.getChildren().addAll(keyField, keyScriptTypeField, addressField, toAddressField, feeRangeField, feeRateField);
|
||||
Field ignoreDustField = new Field();
|
||||
ignoreDustField.setText("Ignore dust:");
|
||||
ignoreDust = new UnlabeledToggleSwitch();
|
||||
ignoreDustField.getInputs().add(ignoreDust);
|
||||
|
||||
fieldset.getChildren().addAll(keyField, keyScriptTypeField, addressField, toAddressField, feeRangeField, feeRateField, ignoreDustField);
|
||||
form.getChildren().add(fieldset);
|
||||
dialogPane.setContent(form);
|
||||
|
||||
@@ -204,18 +213,31 @@ public class PrivateKeySweepDialog extends Dialog<Transaction> {
|
||||
});
|
||||
|
||||
toAddress.textProperty().addListener((observable, oldValue, newValue) -> {
|
||||
try {
|
||||
silentPaymentAddress = SilentPaymentAddress.from(newValue);
|
||||
} catch(Exception e) {
|
||||
silentPaymentAddress = null;
|
||||
}
|
||||
createButton.setDisable(!isValidKey() || !isValidToAddress());
|
||||
});
|
||||
|
||||
toWallet.valueProperty().addListener((observable, oldValue, selectedWallet) -> {
|
||||
if(selectedWallet != null) {
|
||||
toAddress.setText(selectedWallet.getFreshNode(KeyPurpose.RECEIVE).getAddress().toString());
|
||||
if(selectedWallet.getPolicyType() == PolicyType.SINGLE_SP) {
|
||||
toAddress.setText(selectedWallet.getSilentPaymentScanAddress().getSilentPaymentAddress().getAddress());
|
||||
} else {
|
||||
toAddress.setText(selectedWallet.getFreshNode(KeyPurpose.RECEIVE).getAddress().toString());
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
keyScriptType.setValue(ScriptType.P2PKH);
|
||||
if(wallet != null) {
|
||||
toAddress.setText(wallet.getFreshNode(KeyPurpose.RECEIVE).getAddress().toString());
|
||||
if(wallet.getPolicyType() == PolicyType.SINGLE_SP) {
|
||||
toAddress.setText(wallet.getSilentPaymentScanAddress().getSilentPaymentAddress().getAddress());
|
||||
} else {
|
||||
toAddress.setText(wallet.getFreshNode(KeyPurpose.RECEIVE).getAddress().toString());
|
||||
}
|
||||
}
|
||||
|
||||
AppServices.onEscapePressed(dialogPane.getScene(), () -> setResult(null));
|
||||
@@ -272,10 +294,13 @@ public class PrivateKeySweepDialog extends Dialog<Transaction> {
|
||||
}
|
||||
|
||||
private boolean isValidToAddress() {
|
||||
try {
|
||||
Address address = getToAddress();
|
||||
if(silentPaymentAddress != null) {
|
||||
return true;
|
||||
} catch (InvalidAddressException e) {
|
||||
}
|
||||
try {
|
||||
getToAddress();
|
||||
return true;
|
||||
} catch(InvalidAddressException e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -287,14 +312,14 @@ public class PrivateKeySweepDialog extends Dialog<Transaction> {
|
||||
private void setFromAddress() {
|
||||
DumpedPrivateKey privateKey = getPrivateKey();
|
||||
ScriptType scriptType = keyScriptType.getValue();
|
||||
Address address = scriptType.getAddress(privateKey.getKey());
|
||||
Address address = scriptType.getAddress(PolicyType.SINGLE_HD, privateKey.getKey());
|
||||
keyAddress.setText(address.toString());
|
||||
}
|
||||
|
||||
private void setScriptTypes(boolean isValidKey) {
|
||||
boolean compressed = !isValidKey || getPrivateKey().getKey().isCompressed();
|
||||
if(compressed && !keyScriptType.getItems().equals(ScriptType.getAddressableScriptTypes(PolicyType.SINGLE))) {
|
||||
keyScriptType.getItems().addAll(ScriptType.getAddressableScriptTypes(PolicyType.SINGLE).stream().filter(s -> !keyScriptType.getItems().contains(s)).collect(Collectors.toList()));
|
||||
if(compressed && !keyScriptType.getItems().equals(ScriptType.getAddressableScriptTypes(PolicyType.SINGLE_HD))) {
|
||||
keyScriptType.getItems().addAll(ScriptType.getAddressableScriptTypes(PolicyType.SINGLE_HD).stream().filter(s -> !keyScriptType.getItems().contains(s)).collect(Collectors.toList()));
|
||||
} else if(!compressed && !keyScriptType.getItems().equals(List.of(ScriptType.P2PKH))) {
|
||||
keyScriptType.getSelectionModel().select(0);
|
||||
keyScriptType.getItems().removeIf(scriptType -> scriptType != ScriptType.P2PKH);
|
||||
@@ -346,8 +371,9 @@ public class PrivateKeySweepDialog extends Dialog<Transaction> {
|
||||
try {
|
||||
DumpedPrivateKey privateKey = getPrivateKey();
|
||||
ScriptType scriptType = keyScriptType.getValue();
|
||||
Address fromAddress = scriptType.getAddress(privateKey.getKey());
|
||||
Address destAddress = getToAddress();
|
||||
Address fromAddress = scriptType.getAddress(PolicyType.SINGLE_HD, privateKey.getKey());
|
||||
Payment payment = silentPaymentAddress != null ? new SilentPayment(silentPaymentAddress, null, 0, true)
|
||||
: new Payment(getToAddress(), null, 0, true);
|
||||
|
||||
Date since = null;
|
||||
if(Config.get().getServerType() == ServerType.BITCOIN_CORE) {
|
||||
@@ -363,7 +389,16 @@ public class PrivateKeySweepDialog extends Dialog<Transaction> {
|
||||
|
||||
ElectrumServer.AddressUtxosService addressUtxosService = new ElectrumServer.AddressUtxosService(fromAddress, since);
|
||||
addressUtxosService.setOnSucceeded(successEvent -> {
|
||||
createTransaction(privateKey.getKey(), scriptType, addressUtxosService.getValue(), destAddress);
|
||||
List<TransactionOutput> utxos = addressUtxosService.getValue();
|
||||
if(ignoreDust.isSelected()) {
|
||||
utxos = removeDust(utxos);
|
||||
if(utxos.isEmpty()) {
|
||||
AppServices.showErrorDialog("No outputs to sweep", "All of the unspent outputs for this private key have been ignored as dust.");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
createTransaction(privateKey.getKey(), scriptType, utxos, payment);
|
||||
});
|
||||
addressUtxosService.setOnFailed(failedEvent -> {
|
||||
Throwable rootCause = Throwables.getRootCause(failedEvent.getSource().getException());
|
||||
@@ -383,13 +418,19 @@ public class PrivateKeySweepDialog extends Dialog<Transaction> {
|
||||
}
|
||||
}
|
||||
|
||||
private void createTransaction(ECKey privKey, ScriptType scriptType, List<TransactionOutput> txOutputs, Address destAddress) {
|
||||
private List<TransactionOutput> removeDust(List<TransactionOutput> txOutputs) {
|
||||
long dustAttackThreshold = Config.get().getDustAttackThreshold();
|
||||
return txOutputs.stream().filter(txOutput -> txOutput.getValue() > dustAttackThreshold).collect(Collectors.toList());
|
||||
}
|
||||
|
||||
private void createTransaction(ECKey privKey, ScriptType scriptType, List<TransactionOutput> txOutputs, Payment payment) {
|
||||
Address destAddress = payment instanceof SilentPayment silentPayment ? computeSilentPaymentAddress(privKey, scriptType, txOutputs, silentPayment) : payment.getAddress();
|
||||
ECKey pubKey = ECKey.fromPublicOnly(privKey);
|
||||
|
||||
Transaction noFeeTransaction = new Transaction();
|
||||
long total = 0;
|
||||
for(TransactionOutput txOutput : txOutputs) {
|
||||
scriptType.addSpendingInput(noFeeTransaction, txOutput, pubKey, TransactionSignature.dummy(scriptType == P2TR ? TransactionSignature.Type.SCHNORR : TransactionSignature.Type.ECDSA));
|
||||
scriptType.addSpendingInput(PolicyType.SINGLE_HD, noFeeTransaction, txOutput, pubKey, TransactionSignature.dummy(scriptType == P2TR ? TransactionSignature.Type.SCHNORR : TransactionSignature.Type.ECDSA));
|
||||
total += txOutput.getValue();
|
||||
}
|
||||
|
||||
@@ -448,7 +489,7 @@ public class PrivateKeySweepDialog extends Dialog<Transaction> {
|
||||
psbtInput.setWitnessScript(txInput.getWitness().getWitnessScript());
|
||||
}
|
||||
|
||||
if(!psbtInput.sign(scriptType.getOutputKey(privKey))) {
|
||||
if(!psbtInput.sign(scriptType.getOutputKey(PolicyType.SINGLE_HD, privKey))) {
|
||||
AppServices.showErrorDialog("Failed to sign", "Failed to sign for transaction output " + utxoOutput.getHash() + ":" + utxoOutput.getIndex());
|
||||
return;
|
||||
}
|
||||
@@ -456,7 +497,7 @@ public class PrivateKeySweepDialog extends Dialog<Transaction> {
|
||||
TransactionSignature signature = psbtInput.isTaproot() ? psbtInput.getTapKeyPathSignature() : psbtInput.getPartialSignature(pubKey);
|
||||
|
||||
Transaction finalizeTransaction = new Transaction();
|
||||
TransactionInput finalizedTxInput = scriptType.addSpendingInput(finalizeTransaction, utxoOutput, pubKey, signature);
|
||||
TransactionInput finalizedTxInput = scriptType.addSpendingInput(PolicyType.SINGLE_HD, finalizeTransaction, utxoOutput, pubKey, signature);
|
||||
psbtInput.setFinalScriptSig(finalizedTxInput.getScriptSig());
|
||||
psbtInput.setFinalScriptWitness(finalizedTxInput.getWitness());
|
||||
}
|
||||
@@ -468,6 +509,29 @@ public class PrivateKeySweepDialog extends Dialog<Transaction> {
|
||||
}
|
||||
}
|
||||
|
||||
private Address computeSilentPaymentAddress(ECKey privKey, ScriptType scriptType, List<TransactionOutput> txOutputs, SilentPayment silentPayment) {
|
||||
ECKey summedPrivateKey = scriptType.getOutputKey(PolicyType.SINGLE_HD, privKey);
|
||||
if(scriptType == P2TR && summedPrivateKey.hasOddYCoord()) {
|
||||
summedPrivateKey = summedPrivateKey.negatePrivate();
|
||||
}
|
||||
|
||||
Set<HashIndex> outpoints = new LinkedHashSet<>();
|
||||
for(TransactionOutput txOutput : txOutputs) {
|
||||
outpoints.add(new HashIndex(txOutput.getHash(), txOutput.getIndex()));
|
||||
}
|
||||
|
||||
try {
|
||||
SilentPaymentUtils.computeOutputAddresses(List.of(silentPayment), summedPrivateKey, outpoints);
|
||||
if(!silentPayment.isAddressComputed()) {
|
||||
throw new IllegalStateException("Failed to compute silent payment address");
|
||||
}
|
||||
|
||||
return silentPayment.getAddress();
|
||||
} catch(InvalidSilentPaymentException e) {
|
||||
throw new IllegalStateException("Failed to compute silent payment address", e);
|
||||
}
|
||||
}
|
||||
|
||||
public Glyph getGlyph(FontAwesome5.Glyph glyphEnum) {
|
||||
Glyph glyph = new Glyph(FontAwesome5.FONT_NAME, glyphEnum);
|
||||
glyph.setFontSize(12);
|
||||
|
||||
@@ -47,10 +47,11 @@ import javafx.scene.layout.*;
|
||||
import javafx.util.Duration;
|
||||
import javafx.util.StringConverter;
|
||||
import org.controlsfx.tools.Borders;
|
||||
import org.openpnp.capture.CaptureDevice;
|
||||
import io.github.doblon8.openpnp.capture.CaptureDevice;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.math.BigInteger;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.CharBuffer;
|
||||
import java.nio.charset.CharsetDecoder;
|
||||
@@ -582,6 +583,14 @@ public class QRScanDialog extends Dialog<QRScanDialog.Result> {
|
||||
}
|
||||
}
|
||||
|
||||
private ECKey getKey(CryptoHDKey cryptoHDKey) {
|
||||
if(cryptoHDKey.isPrivateKey()) {
|
||||
return ECKey.fromPrivate(new BigInteger(1, cryptoHDKey.getKey()));
|
||||
} else {
|
||||
return ECKey.fromPublicOnly(cryptoHDKey.getKey());
|
||||
}
|
||||
}
|
||||
|
||||
private OutputDescriptor getOutputDescriptor(CryptoOutput cryptoOutput) {
|
||||
ScriptType scriptType = getScriptType(cryptoOutput.getScriptExpressions());
|
||||
|
||||
@@ -679,11 +688,16 @@ public class QRScanDialog extends Dialog<QRScanDialog.Result> {
|
||||
for(int i = 0; i < keys.size(); i++) {
|
||||
RegistryItem key = keys.get(i);
|
||||
if(key instanceof URHDKey urhdKey) {
|
||||
ExtendedKey extendedKey = getExtendedKey(urhdKey);
|
||||
KeyDerivation keyDerivation = getKeyDerivation(urhdKey.getOrigin());
|
||||
source = source.replaceAll("@" + i, OutputDescriptor.writeKey(extendedKey, keyDerivation, null, true, true));
|
||||
if(urhdKey.getName() != null) {
|
||||
mapExtendedPublicKeyLabels.put(extendedKey, urhdKey.getName());
|
||||
if(urhdKey.getChainCode() == null) {
|
||||
ECKey ecKey = getKey(urhdKey);
|
||||
source = source.replaceAll("@" + i, OutputDescriptor.writeKey(ecKey, keyDerivation, true, true));
|
||||
} else {
|
||||
ExtendedKey extendedKey = getExtendedKey(urhdKey);
|
||||
source = source.replaceAll("@" + i, OutputDescriptor.writeKey(extendedKey, keyDerivation, null, true, true));
|
||||
if(urhdKey.getName() != null) {
|
||||
mapExtendedPublicKeyLabels.put(extendedKey, urhdKey.getName());
|
||||
}
|
||||
}
|
||||
} else {
|
||||
throw new IllegalArgumentException("Only extended HD keys are supported in output descriptors");
|
||||
|
||||
@@ -9,13 +9,13 @@ import com.sparrowwallet.drongo.dns.DnsPayment;
|
||||
import com.sparrowwallet.drongo.dns.DnsPaymentCache;
|
||||
import com.sparrowwallet.drongo.dns.DnsPaymentResolver;
|
||||
import com.sparrowwallet.drongo.dns.DnsPaymentValidationException;
|
||||
import com.sparrowwallet.drongo.protocol.Transaction;
|
||||
import com.sparrowwallet.drongo.silentpayments.SilentPayment;
|
||||
import com.sparrowwallet.drongo.silentpayments.SilentPaymentAddress;
|
||||
import com.sparrowwallet.drongo.uri.BitcoinURIParseException;
|
||||
import com.sparrowwallet.drongo.wallet.Payment;
|
||||
import com.sparrowwallet.sparrow.AppServices;
|
||||
import com.sparrowwallet.sparrow.EventManager;
|
||||
import com.sparrowwallet.sparrow.UnitFormat;
|
||||
import com.sparrowwallet.sparrow.event.RequestConnectEvent;
|
||||
import com.sparrowwallet.sparrow.glyphfont.GlyphUtils;
|
||||
import com.sparrowwallet.sparrow.io.Config;
|
||||
@@ -28,6 +28,8 @@ import javafx.event.ActionEvent;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.control.*;
|
||||
import javafx.scene.input.Clipboard;
|
||||
import javafx.scene.input.KeyCode;
|
||||
import javafx.scene.input.KeyEvent;
|
||||
import javafx.scene.layout.StackPane;
|
||||
import javafx.stage.FileChooser;
|
||||
import javafx.util.StringConverter;
|
||||
@@ -35,20 +37,26 @@ import org.controlsfx.control.spreadsheet.*;
|
||||
|
||||
import java.io.*;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.text.DecimalFormat;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.regex.Pattern;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.IntStream;
|
||||
|
||||
public class SendToManyDialog extends Dialog<List<Payment>> {
|
||||
private final BitcoinUnit bitcoinUnit;
|
||||
private final UnitFormat unitFormat;
|
||||
private final UnitFormatDoubleCellType amountCellType;
|
||||
private final SpreadsheetView spreadsheetView;
|
||||
public static final SendToAddressCellType SEND_TO_ADDRESS = new SendToAddressCellType();
|
||||
|
||||
public SendToManyDialog(BitcoinUnit bitcoinUnit, List<Payment> payments) {
|
||||
public SendToManyDialog(BitcoinUnit bitcoinUnit, UnitFormat unitFormat, List<Payment> payments) {
|
||||
this.bitcoinUnit = bitcoinUnit;
|
||||
this.unitFormat = unitFormat == null ? UnitFormat.DOT : unitFormat;
|
||||
this.amountCellType = new UnitFormatDoubleCellType(this.unitFormat);
|
||||
|
||||
final DialogPane dialogPane = new SendToManyDialogPane();
|
||||
setDialogPane(dialogPane);
|
||||
@@ -119,11 +127,9 @@ public class SendToManyDialog extends Dialog<List<Payment>> {
|
||||
addressCell.getStyleClass().add("fixed-width");
|
||||
list.add(addressCell);
|
||||
|
||||
double amount = (double)sendToPayment.payment().getAmount();
|
||||
if(bitcoinUnit == BitcoinUnit.BTC) {
|
||||
amount = amount / Transaction.SATOSHIS_PER_BITCOIN;
|
||||
}
|
||||
SpreadsheetCell amountCell = SpreadsheetCellType.DOUBLE.createCell(row, 1, 1, 1, amount < 0 ? null : amount);
|
||||
long rawAmount = sendToPayment.payment().getAmount();
|
||||
Double amount = rawAmount < 0 ? null : bitcoinUnit.getValue(rawAmount);
|
||||
SpreadsheetCell amountCell = amountCellType.createCell(row, 1, 1, 1, amount);
|
||||
amountCell.setFormat(bitcoinUnit == BitcoinUnit.BTC ? "0.00000000" : "###,###");
|
||||
amountCell.getStyleClass().add("number-value");
|
||||
if(OsType.getCurrent() == OsType.MACOS) {
|
||||
@@ -177,7 +183,7 @@ public class SendToManyDialog extends Dialog<List<Payment>> {
|
||||
for(int row = 0; row < spreadsheetView.getGrid().getRowCount(); row++) {
|
||||
ObservableList<SpreadsheetCell> rowCells = spreadsheetView.getItems().get(row);
|
||||
SendToAddress sendToAddress = (SendToAddress)rowCells.getFirst().getItem();
|
||||
if(sendToAddress.hrn != null && DnsPaymentCache.getDnsPayment(sendToAddress.hrn) == null) {
|
||||
if(sendToAddress != null && sendToAddress.hrn != null && DnsPaymentCache.getDnsPayment(sendToAddress.hrn) == null) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -216,12 +222,15 @@ public class SendToManyDialog extends Dialog<List<Payment>> {
|
||||
}
|
||||
|
||||
try {
|
||||
String rawAmount = csvReader.get(1).trim();
|
||||
String groupingStripped = rawAmount.replaceAll(Pattern.quote(unitFormat.getGroupingSeparator()), "");
|
||||
long amount;
|
||||
if(bitcoinUnit == BitcoinUnit.BTC) {
|
||||
double doubleAmount = Double.parseDouble(csvReader.get(1).replace(",", ""));
|
||||
amount = (long)(doubleAmount * Transaction.SATOSHIS_PER_BITCOIN);
|
||||
String normalised = groupingStripped.replaceAll(Pattern.quote(unitFormat.getDecimalSeparator()), ".");
|
||||
double doubleAmount = Double.parseDouble(normalised);
|
||||
amount = bitcoinUnit.getSatsValue(doubleAmount);
|
||||
} else {
|
||||
amount = Long.parseLong(csvReader.get(1).replace(",", ""));
|
||||
amount = Long.parseLong(groupingStripped);
|
||||
}
|
||||
String label = csvReader.get(2);
|
||||
Optional<String> optDnsPaymentHrn = DnsPayment.getHrn(csvReader.get(0));
|
||||
@@ -359,6 +368,160 @@ public class SendToManyDialog extends Dialog<List<Payment>> {
|
||||
}
|
||||
};
|
||||
|
||||
private static class UnitFormatDoubleCellType extends SpreadsheetCellType<Double> {
|
||||
private final UnitFormat unitFormat;
|
||||
|
||||
UnitFormatDoubleCellType(UnitFormat unitFormat) {
|
||||
super(new UnitFormatDoubleConverter(unitFormat));
|
||||
this.unitFormat = unitFormat;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "double";
|
||||
}
|
||||
|
||||
public SpreadsheetCell createCell(int row, int column, int rowSpan, int columnSpan, Double value) {
|
||||
SpreadsheetCell cell = new SpreadsheetCellBase(row, column, rowSpan, columnSpan, this);
|
||||
cell.setItem(value);
|
||||
return cell;
|
||||
}
|
||||
|
||||
@Override
|
||||
public SpreadsheetCellEditor createEditor(SpreadsheetView view) {
|
||||
return new UnitFormatDoubleEditor(view, unitFormat);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean match(Object value, Object... options) {
|
||||
if(value == null || value instanceof Number) {
|
||||
return true;
|
||||
}
|
||||
try {
|
||||
String s = value.toString();
|
||||
return s == null || s.isEmpty() || converter.fromString(s) != null;
|
||||
} catch(Exception e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Double convertValue(Object value) {
|
||||
if(value instanceof Double d) {
|
||||
return d;
|
||||
}
|
||||
if(value instanceof Number n) {
|
||||
return n.doubleValue();
|
||||
}
|
||||
return converter.fromString(value == null ? null : value.toString());
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString(Double item) {
|
||||
return converter.toString(item);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString(Double item, String format) {
|
||||
return ((StringConverterWithFormat<Double>)converter).toStringFormat(item, format);
|
||||
}
|
||||
}
|
||||
|
||||
private static class UnitFormatDoubleConverter extends StringConverterWithFormat<Double> {
|
||||
private final UnitFormat unitFormat;
|
||||
|
||||
UnitFormatDoubleConverter(UnitFormat unitFormat) {
|
||||
this.unitFormat = unitFormat;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Double fromString(String str) {
|
||||
if(str == null || str.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
String normalised = str.trim()
|
||||
.replaceAll(Pattern.quote(unitFormat.getGroupingSeparator()), "")
|
||||
.replaceAll(Pattern.quote(unitFormat.getDecimalSeparator()), ".");
|
||||
try {
|
||||
return Double.valueOf(normalised);
|
||||
} catch(NumberFormatException e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString(Double item) {
|
||||
return toStringFormat(item, "");
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toStringFormat(Double item, String format) {
|
||||
if(item == null || item.isNaN()) {
|
||||
return "";
|
||||
}
|
||||
if(format == null || format.isEmpty()) {
|
||||
return Double.toString(item);
|
||||
}
|
||||
return new DecimalFormat(format, unitFormat.getDecimalFormatSymbols()).format(item);
|
||||
}
|
||||
}
|
||||
|
||||
private static class UnitFormatDoubleEditor extends SpreadsheetCellEditor {
|
||||
private final UnitFormat unitFormat;
|
||||
private final TextField textField;
|
||||
|
||||
UnitFormatDoubleEditor(SpreadsheetView view, UnitFormat unitFormat) {
|
||||
super(view);
|
||||
this.unitFormat = unitFormat;
|
||||
this.textField = new TextField();
|
||||
this.textField.setTextFormatter(new CoinTextFormatter(unitFormat));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void startEdit(Object item, String format, Object... options) {
|
||||
if(item instanceof Double d && !d.isNaN()) {
|
||||
String text = (format == null || format.isEmpty())
|
||||
? Double.toString(d)
|
||||
: new DecimalFormat(format, unitFormat.getDecimalFormatSymbols()).format(d);
|
||||
textField.setText(text);
|
||||
} else {
|
||||
textField.setText("");
|
||||
}
|
||||
textField.getStyleClass().removeAll("error");
|
||||
textField.setOnKeyPressed(this::onKeyPressed);
|
||||
textField.requestFocus();
|
||||
textField.selectAll();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void end() {
|
||||
textField.setOnKeyPressed(null);
|
||||
textField.setOnKeyReleased(null);
|
||||
textField.getStyleClass().removeAll("error");
|
||||
}
|
||||
|
||||
@Override
|
||||
public TextField getEditor() {
|
||||
return textField;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getControlValue() {
|
||||
String raw = textField.getText();
|
||||
return raw == null ? "" : raw.trim();
|
||||
}
|
||||
|
||||
private void onKeyPressed(KeyEvent event) {
|
||||
if(event.getCode() == KeyCode.ENTER) {
|
||||
endEdit(true);
|
||||
event.consume();
|
||||
} else if(event.getCode() == KeyCode.ESCAPE) {
|
||||
endEdit(false);
|
||||
event.consume();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static class SendToAddress {
|
||||
private final String hrn;
|
||||
private final Address address;
|
||||
@@ -487,11 +650,7 @@ public class SendToManyDialog extends Dialog<List<Payment>> {
|
||||
}
|
||||
|
||||
if(sendToAddress != null && value != null) {
|
||||
if(bitcoinUnit == BitcoinUnit.BTC) {
|
||||
value = value * Transaction.SATOSHIS_PER_BITCOIN;
|
||||
}
|
||||
|
||||
payments.add(sendToAddress.toPayment(label, value.longValue(), false));
|
||||
payments.add(sendToAddress.toPayment(label, bitcoinUnit.getSatsValue(value), false));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ import com.sparrowwallet.drongo.address.Address;
|
||||
import com.sparrowwallet.drongo.bip47.PaymentCode;
|
||||
import com.sparrowwallet.drongo.dns.DnsPayment;
|
||||
import com.sparrowwallet.drongo.dns.DnsPaymentCache;
|
||||
import com.sparrowwallet.drongo.policy.PolicyType;
|
||||
import com.sparrowwallet.drongo.protocol.Sha256Hash;
|
||||
import com.sparrowwallet.drongo.protocol.TransactionOutput;
|
||||
import com.sparrowwallet.drongo.silentpayments.SilentPayment;
|
||||
@@ -228,7 +229,7 @@ public class TransactionDiagram extends GridPane {
|
||||
if(diagram.isExpanded()) {
|
||||
List<Map<BlockTransactionHashIndex, WalletNode>> utxoSets = diagram.getDisplayedUtxoSets();
|
||||
int maxSetSize = utxoSets.stream().mapToInt(Map::size).max().orElse(0);
|
||||
int maxRows = Math.max(maxSetSize * utxoSets.size(), walletTx.getPayments().size() + 2);
|
||||
int maxRows = Math.max(maxSetSize * utxoSets.size(), diagram.getDisplayedOutputs().size() + 1);
|
||||
double diagramHeight = Math.max(DIAGRAM_HEIGHT, Math.min(EXPANDED_DIAGRAM_HEIGHT, maxRows * ROW_HEIGHT));
|
||||
diagram.setMinHeight(diagramHeight);
|
||||
diagram.setMaxHeight(diagramHeight);
|
||||
@@ -256,12 +257,12 @@ public class TransactionDiagram extends GridPane {
|
||||
Pane txPane = getTransactionPane();
|
||||
GridPane.setConstraints(txPane, 3, 0);
|
||||
|
||||
List<Payment> displayedPayments = getDisplayedPayments();
|
||||
List<WalletTransaction.Output> displayedOutputs = getDisplayedOutputs();
|
||||
|
||||
Pane outputsLinesPane = getOutputsLines(displayedPayments);
|
||||
Pane outputsLinesPane = getOutputsLines(displayedOutputs);
|
||||
GridPane.setConstraints(outputsLinesPane, 4, 0);
|
||||
|
||||
Pane outputsPane = getOutputsLabels(displayedPayments);
|
||||
Pane outputsPane = getOutputsLabels(displayedOutputs);
|
||||
GridPane.setConstraints(outputsPane, 5, 0);
|
||||
|
||||
getChildren().clear();
|
||||
@@ -652,33 +653,48 @@ public class TransactionDiagram extends GridPane {
|
||||
return value * (1.0 - scaleFactor) + additional;
|
||||
}
|
||||
|
||||
private List<Payment> getDisplayedPayments() {
|
||||
List<Payment> payments = walletTx.getPayments();
|
||||
|
||||
private List<WalletTransaction.Output> getDisplayedOutputs() {
|
||||
List<WalletTransaction.Output> outputs = walletTx.getOutputs().stream().filter(o -> !(o instanceof WalletTransaction.NonAddressOutput)).toList();
|
||||
int maxPayments = getMaxPayments();
|
||||
if(payments.size() > maxPayments) {
|
||||
List<Payment> displayedPayments = new ArrayList<>();
|
||||
List<Payment> additional = new ArrayList<>();
|
||||
for(Payment payment : payments) {
|
||||
if(displayedPayments.size() < maxPayments - 1) {
|
||||
displayedPayments.add(payment);
|
||||
} else {
|
||||
additional.add(payment);
|
||||
}
|
||||
}
|
||||
long paginableCount = outputs.stream().filter(this::isPaymentAndNotChange).count();
|
||||
|
||||
displayedPayments.add(new AdditionalPayment(additional));
|
||||
return displayedPayments;
|
||||
} else {
|
||||
return payments;
|
||||
if(paginableCount <= maxPayments) {
|
||||
return outputs;
|
||||
}
|
||||
|
||||
List<WalletTransaction.Output> displayedOutputs = new ArrayList<>();
|
||||
List<Payment> additional = new ArrayList<>();
|
||||
int kept = 0;
|
||||
int additionalIdx = 0;
|
||||
for(WalletTransaction.Output output : outputs) {
|
||||
if(isPaymentAndNotChange(output)) {
|
||||
if(kept < maxPayments - 1) {
|
||||
displayedOutputs.add(output);
|
||||
kept++;
|
||||
additionalIdx = displayedOutputs.size();
|
||||
} else {
|
||||
additional.add(output instanceof WalletTransaction.PaymentOutput po ? po.getPayment() : ((WalletTransaction.ConsolidationOutput)output).getWalletNodePayment());
|
||||
}
|
||||
} else {
|
||||
displayedOutputs.add(output);
|
||||
}
|
||||
}
|
||||
Payment additionalPayment = new AdditionalPayment(additional);
|
||||
TransactionOutput additionalOutput = new TransactionOutput(null, additionalPayment.getAmount(), new byte[0]);
|
||||
displayedOutputs.add(additionalIdx, new WalletTransaction.PaymentOutput(additionalOutput, additionalPayment));
|
||||
|
||||
return displayedOutputs;
|
||||
}
|
||||
|
||||
boolean isPaymentAndNotChange(WalletTransaction.Output output) {
|
||||
return (output instanceof WalletTransaction.PaymentOutput && !(output instanceof WalletTransaction.SilentPaymentChangeOutput)) || output instanceof WalletTransaction.ConsolidationOutput;
|
||||
}
|
||||
|
||||
private List<Payment> getUserPayments() {
|
||||
return walletTx.getPayments().stream().filter(payment -> payment.getType() == Payment.Type.DEFAULT || payment.getType() == Payment.Type.ANCHOR).toList();
|
||||
}
|
||||
|
||||
private Pane getOutputsLines(List<Payment> displayedPayments) {
|
||||
private Pane getOutputsLines(List<WalletTransaction.Output> displayedOutputs) {
|
||||
VBox pane = new VBox();
|
||||
Group group = new Group();
|
||||
VBox.setVgrow(group, Priority.ALWAYS);
|
||||
@@ -693,10 +709,9 @@ public class TransactionDiagram extends GridPane {
|
||||
|
||||
double width = 140.0;
|
||||
long sum = walletTx.getTotal();
|
||||
List<Long> values = walletTx.getOutputs().stream().filter(output -> !(output instanceof WalletTransaction.NonAddressOutput))
|
||||
.map(output -> output.getTransactionOutput().getValue()).collect(Collectors.toList());
|
||||
List<Long> values = displayedOutputs.stream().map(o -> o.getTransactionOutput().getValue()).collect(Collectors.toList());
|
||||
values.add(walletTx.getFee());
|
||||
int numOutputs = displayedPayments.size() + walletTx.getChangeMap().size() + 1;
|
||||
int numOutputs = displayedOutputs.size() + 1;
|
||||
for(int i = 1; i <= numOutputs; i++) {
|
||||
CubicCurve curve = new CubicCurve();
|
||||
curve.getStyleClass().add("output-line");
|
||||
@@ -728,121 +743,21 @@ public class TransactionDiagram extends GridPane {
|
||||
return pane;
|
||||
}
|
||||
|
||||
private Pane getOutputsLabels(List<Payment> displayedPayments) {
|
||||
private Pane getOutputsLabels(List<WalletTransaction.Output> displayedOutputs) {
|
||||
VBox outputsBox = new VBox();
|
||||
outputsBox.setPadding(new Insets(0, 20, 0, 10));
|
||||
outputsBox.setAlignment(Pos.BASELINE_LEFT);
|
||||
outputsBox.getChildren().add(createSpacer());
|
||||
|
||||
List<OutputNode> outputNodes = new ArrayList<>();
|
||||
for(Payment payment : displayedPayments) {
|
||||
Glyph outputGlyph = GlyphUtils.getOutputGlyph(walletTx, payment);
|
||||
boolean labelledPayment = outputGlyph.getStyleClass().stream().anyMatch(style -> List.of("premix-icon", "badbank-icon", "whirlpoolfee-icon", "anchor-icon").contains(style)) || payment instanceof AdditionalPayment || payment.getLabel() != null;
|
||||
boolean addressLabel = payment.getLabel() == null || payment.getType() == Payment.Type.FAKE_MIX || payment.getType() == Payment.Type.MIX;
|
||||
Label recipientLabel = new Label(addressLabel ? payment.toString().substring(0, 8) + "..." : payment.getLabel(), outputGlyph);
|
||||
recipientLabel.getStyleClass().add("output-label");
|
||||
recipientLabel.getStyleClass().add(labelledPayment ? "payment-label" : "recipient-label");
|
||||
if(addressLabel) {
|
||||
recipientLabel.setSkin(new AddressLabelSkin(recipientLabel));
|
||||
for(WalletTransaction.Output output : displayedOutputs) {
|
||||
if(output instanceof WalletTransaction.SilentPaymentChangeOutput spChangeOutput) {
|
||||
outputNodes.add(buildSpChangeNode(spChangeOutput));
|
||||
} else if(output instanceof WalletTransaction.ChangeOutput changeOutput) {
|
||||
outputNodes.add(buildHdChangeNode(changeOutput));
|
||||
} else if(output instanceof WalletTransaction.PaymentOutput || output instanceof WalletTransaction.ConsolidationOutput) {
|
||||
outputNodes.add(buildPaymentNode(output));
|
||||
}
|
||||
Wallet toWallet = walletTx.getToWallet(AppServices.get().getOpenWallets().keySet(), payment);
|
||||
WalletNode toNode = payment instanceof WalletNodePayment walletNodePayment ? walletNodePayment.getWalletNode() : null;
|
||||
Wallet toBip47Wallet = getBip47SendWallet(payment);
|
||||
DnsPayment dnsPayment = DnsPaymentCache.getDnsPayment(payment);
|
||||
Tooltip recipientTooltip = new Tooltip((toWallet == null ? (toNode != null ? "Consolidate " : "Pay ") : "Receive ")
|
||||
+ getCoinValue(payment.getAmount()) + " to "
|
||||
+ (payment instanceof AdditionalPayment ? (isExpanded() ? "\n" : "(click to expand)\n") + payment : (toWallet == null ? (dnsPayment == null ? (payment.getLabel() == null ? (toNode != null ? toNode : (toBip47Wallet == null ? "external address" : toBip47Wallet.getDisplayName())) : payment.getLabel()) : dnsPayment.toString()) : toWallet.getFullDisplayName()) + "\n" + payment.getDisplayAddress())
|
||||
+ (walletTx.isDuplicateAddress(payment) ? " (Duplicate)" : ""));
|
||||
recipientTooltip.getStyleClass().add("recipient-label");
|
||||
recipientTooltip.setShowDelay(new Duration(TOOLTIP_SHOW_DELAY));
|
||||
recipientTooltip.setShowDuration(Duration.INDEFINITE);
|
||||
recipientTooltip.setWrapText(true);
|
||||
recipientTooltip.setSkin(new AddressTooltipSkin(recipientTooltip));
|
||||
Window activeWindow = AppServices.getActiveWindow();
|
||||
if(activeWindow != null) {
|
||||
recipientTooltip.setMaxWidth(activeWindow.getWidth());
|
||||
}
|
||||
recipientLabel.setTooltip(recipientTooltip);
|
||||
HBox paymentBox = new HBox();
|
||||
paymentBox.setAlignment(Pos.CENTER_LEFT);
|
||||
paymentBox.getChildren().add(recipientLabel);
|
||||
if(isExpanded()) {
|
||||
recipientLabel.setMinWidth(120);
|
||||
Region region = new Region();
|
||||
region.setMinWidth(20);
|
||||
HBox.setHgrow(region, Priority.ALWAYS);
|
||||
CoinLabel amountLabel = new CoinLabel();
|
||||
amountLabel.setValue(payment.getAmount());
|
||||
amountLabel.setMinWidth(TextUtils.computeTextWidth(amountLabel.getFont(), amountLabel.getText(), 0.0D) + 2);
|
||||
paymentBox.getChildren().addAll(region, amountLabel);
|
||||
}
|
||||
|
||||
if(payment instanceof SilentPayment silentPayment) {
|
||||
outputNodes.add(new OutputNode(paymentBox, silentPayment.isAddressComputed() ? silentPayment.getAddress() : null, payment.getAmount(), null, silentPayment.getSilentPaymentAddress()));
|
||||
} else {
|
||||
Wallet bip47Wallet = toWallet != null && toWallet.isBip47() ? toWallet : (toBip47Wallet != null && toBip47Wallet.isBip47() ? toBip47Wallet : null);
|
||||
PaymentCode paymentCode = bip47Wallet == null ? null : bip47Wallet.getKeystores().getFirst().getExternalPaymentCode();
|
||||
outputNodes.add(new OutputNode(paymentBox, payment.getAddress(), payment.getAmount(), paymentCode, null));
|
||||
}
|
||||
}
|
||||
|
||||
Set<Integer> seenIndexes = new HashSet<>();
|
||||
for(Map.Entry<WalletNode, Long> changeEntry : walletTx.getChangeMap().entrySet()) {
|
||||
WalletNode changeNode = changeEntry.getKey();
|
||||
WalletNode defaultChangeNode = walletTx.getWallet().getFreshNode(KeyPurpose.CHANGE);
|
||||
boolean overGapLimit = (changeNode.getIndex() - defaultChangeNode.getIndex()) > walletTx.getWallet().getGapLimit();
|
||||
|
||||
HBox actionBox = new HBox();
|
||||
actionBox.setAlignment(Pos.CENTER_LEFT);
|
||||
Address changeAddress = walletTx.getChangeAddress(changeNode);
|
||||
String changeDesc = changeAddress.toString().substring(0, 8) + "...";
|
||||
Label changeLabel = new Label(changeDesc, overGapLimit ? getChangeWarningGlyph() : getChangeGlyph());
|
||||
changeLabel.getStyleClass().addAll("output-label", "change-label");
|
||||
changeLabel.setSkin(new AddressLabelSkin(changeLabel));
|
||||
Tooltip changeTooltip = new Tooltip("Change of " + getCoinValue(changeEntry.getValue()) + " to " + changeNode + "\n" + walletTx.getChangeAddress(changeNode).toString() + (overGapLimit ? "\nAddress is beyond the gap limit!" : ""));
|
||||
changeTooltip.getStyleClass().add("change-label");
|
||||
changeTooltip.setShowDelay(new Duration(TOOLTIP_SHOW_DELAY));
|
||||
changeTooltip.setShowDuration(Duration.INDEFINITE);
|
||||
changeTooltip.setSkin(new AddressTooltipSkin(changeTooltip));
|
||||
changeLabel.setTooltip(changeTooltip);
|
||||
actionBox.getChildren().add(changeLabel);
|
||||
|
||||
if(!isFinal()) {
|
||||
Button nextChangeAddressButton = new Button("");
|
||||
nextChangeAddressButton.setGraphic(getChangeReplaceGlyph());
|
||||
nextChangeAddressButton.setOnAction(event -> {
|
||||
EventManager.get().post(new ReplaceChangeAddressEvent(walletTx));
|
||||
});
|
||||
Tooltip replaceChangeTooltip = new Tooltip("Use next change address");
|
||||
nextChangeAddressButton.setTooltip(replaceChangeTooltip);
|
||||
Label replaceChangeLabel = new Label("", nextChangeAddressButton);
|
||||
replaceChangeLabel.getStyleClass().add("replace-change-label");
|
||||
replaceChangeLabel.setVisible(false);
|
||||
actionBox.setOnMouseEntered(event -> replaceChangeLabel.setVisible(true));
|
||||
actionBox.setOnMouseExited(event -> replaceChangeLabel.setVisible(false));
|
||||
actionBox.getChildren().add(replaceChangeLabel);
|
||||
}
|
||||
|
||||
if(isExpanded()) {
|
||||
changeLabel.setMinWidth(120);
|
||||
Region region = new Region();
|
||||
region.setMinWidth(20);
|
||||
HBox.setHgrow(region, Priority.ALWAYS);
|
||||
CoinLabel amountLabel = new CoinLabel();
|
||||
amountLabel.setValue(changeEntry.getValue());
|
||||
amountLabel.setMinWidth(TextUtils.computeTextWidth(amountLabel.getFont(), amountLabel.getText(), 0.0D) + 2);
|
||||
actionBox.getChildren().addAll(region, amountLabel);
|
||||
}
|
||||
|
||||
int changeIndex = outputNodes.size();
|
||||
if(isFinal()) {
|
||||
changeIndex = getOutputIndex(changeAddress, changeEntry.getValue(), seenIndexes);
|
||||
seenIndexes.add(changeIndex);
|
||||
if(changeIndex > outputNodes.size()) {
|
||||
changeIndex = outputNodes.size();
|
||||
}
|
||||
}
|
||||
outputNodes.add(changeIndex, new OutputNode(actionBox, changeAddress, changeEntry.getValue()));
|
||||
}
|
||||
|
||||
for(OutputNode outputNode : outputNodes) {
|
||||
@@ -889,6 +804,143 @@ public class TransactionDiagram extends GridPane {
|
||||
return outputsBox;
|
||||
}
|
||||
|
||||
private OutputNode buildPaymentNode(WalletTransaction.Output output) {
|
||||
Payment payment = output instanceof WalletTransaction.PaymentOutput po ? po.getPayment() : ((WalletTransaction.ConsolidationOutput)output).getWalletNodePayment();
|
||||
boolean spConsolidation = output instanceof WalletTransaction.SilentPaymentConsolidationOutput;
|
||||
|
||||
Glyph outputGlyph = GlyphUtils.getOutputGlyph(walletTx, payment);
|
||||
boolean labelledPayment = outputGlyph.getStyleClass().stream().anyMatch(style -> List.of("premix-icon", "badbank-icon", "whirlpoolfee-icon", "anchor-icon").contains(style)) || payment instanceof AdditionalPayment || payment.getLabel() != null;
|
||||
boolean addressLabel = payment.getLabel() == null || payment.getType() == Payment.Type.MIX;
|
||||
Label recipientLabel = new Label(payment.getType() == Payment.Type.FAKE_MIX ? payment.getType().toDisplayString() : (addressLabel ? payment.toString().substring(0, 8) + "..." : payment.getLabel()), outputGlyph);
|
||||
recipientLabel.getStyleClass().add("output-label");
|
||||
recipientLabel.getStyleClass().add(labelledPayment ? "payment-label" : "recipient-label");
|
||||
if(addressLabel) {
|
||||
recipientLabel.setSkin(new AddressLabelSkin(recipientLabel));
|
||||
}
|
||||
Wallet toWallet = walletTx.getToWallet(AppServices.get().getOpenWallets().keySet(), payment);
|
||||
WalletNode toNode = payment instanceof WalletNodePayment walletNodePayment ? walletNodePayment.getWalletNode() : null;
|
||||
Wallet toBip47Wallet = getBip47SendWallet(payment);
|
||||
DnsPayment dnsPayment = DnsPaymentCache.getDnsPayment(payment);
|
||||
Tooltip recipientTooltip = new Tooltip((toNode != null || spConsolidation ? "Consolidate " : (toWallet == null ? "Pay " : "Receive "))
|
||||
+ getCoinValue(payment.getAmount()) + " to "
|
||||
+ (payment instanceof AdditionalPayment ? (isExpanded() ? "\n" : "(click to expand)\n") + payment : (toNode != null ? toNode : (spConsolidation ? walletTx.getWallet().getFullDisplayName() : (toWallet == null ? (dnsPayment == null ? (payment.getLabel() == null ? (toBip47Wallet == null ? "external address" : toBip47Wallet.getDisplayName()) : payment.getLabel()) : dnsPayment.toString()) : toWallet.getFullDisplayName()))) + "\n" + payment.getDisplayAddress())
|
||||
+ (walletTx.isDuplicateAddress(payment) ? " (Duplicate)" : ""));
|
||||
recipientTooltip.getStyleClass().add("recipient-label");
|
||||
recipientTooltip.setShowDelay(new Duration(TOOLTIP_SHOW_DELAY));
|
||||
recipientTooltip.setShowDuration(Duration.INDEFINITE);
|
||||
recipientTooltip.setWrapText(true);
|
||||
recipientTooltip.setSkin(new AddressTooltipSkin(recipientTooltip));
|
||||
Window activeWindow = AppServices.getActiveWindow();
|
||||
if(activeWindow != null) {
|
||||
recipientTooltip.setMaxWidth(activeWindow.getWidth());
|
||||
}
|
||||
recipientLabel.setTooltip(recipientTooltip);
|
||||
HBox paymentBox = new HBox();
|
||||
paymentBox.setAlignment(Pos.CENTER_LEFT);
|
||||
paymentBox.getChildren().add(recipientLabel);
|
||||
if(isExpanded()) {
|
||||
recipientLabel.setMinWidth(120);
|
||||
Region region = new Region();
|
||||
region.setMinWidth(20);
|
||||
HBox.setHgrow(region, Priority.ALWAYS);
|
||||
CoinLabel amountLabel = new CoinLabel();
|
||||
amountLabel.setValue(payment.getAmount());
|
||||
amountLabel.setMinWidth(TextUtils.computeTextWidth(amountLabel.getFont(), amountLabel.getText(), 0.0D) + 2);
|
||||
paymentBox.getChildren().addAll(region, amountLabel);
|
||||
}
|
||||
|
||||
if(payment instanceof SilentPayment silentPayment) {
|
||||
return new OutputNode(paymentBox, silentPayment.isAddressComputed() ? silentPayment.getAddress() : null, payment.getAmount(), null, silentPayment.getSilentPaymentAddress());
|
||||
}
|
||||
|
||||
Wallet bip47Wallet = toWallet != null && toWallet.isBip47() ? toWallet : (toBip47Wallet != null && toBip47Wallet.isBip47() ? toBip47Wallet : null);
|
||||
PaymentCode paymentCode = bip47Wallet == null ? null : bip47Wallet.getKeystores().getFirst().getExternalPaymentCode();
|
||||
|
||||
return new OutputNode(paymentBox, payment.getAddress(), payment.getAmount(), paymentCode, null);
|
||||
}
|
||||
|
||||
private OutputNode buildHdChangeNode(WalletTransaction.ChangeOutput changeOutput) {
|
||||
WalletNode changeNode = changeOutput.getWalletNode();
|
||||
long value = changeOutput.getValue();
|
||||
boolean overGapLimit = walletTx.getWallet().getPolicyType() != PolicyType.SINGLE_SP &&
|
||||
(changeNode.getIndex() - walletTx.getWallet().getFreshNode(KeyPurpose.CHANGE).getIndex()) > walletTx.getWallet().getGapLimit();
|
||||
|
||||
HBox actionBox = new HBox();
|
||||
actionBox.setAlignment(Pos.CENTER_LEFT);
|
||||
Address changeAddress = walletTx.getChangeAddress(changeNode);
|
||||
String changeDesc = changeAddress.toString().substring(0, 8) + "...";
|
||||
Label changeLabel = new Label(changeDesc, overGapLimit ? getChangeWarningGlyph() : getChangeGlyph());
|
||||
changeLabel.getStyleClass().addAll("output-label", "change-label");
|
||||
changeLabel.setSkin(new AddressLabelSkin(changeLabel));
|
||||
Tooltip changeTooltip = new Tooltip("Change of " + getCoinValue(value) + " to " + changeNode + "\n" + changeAddress.toString() + (overGapLimit ? "\nAddress is beyond the gap limit!" : ""));
|
||||
changeTooltip.getStyleClass().add("change-label");
|
||||
changeTooltip.setShowDelay(new Duration(TOOLTIP_SHOW_DELAY));
|
||||
changeTooltip.setShowDuration(Duration.INDEFINITE);
|
||||
changeTooltip.setSkin(new AddressTooltipSkin(changeTooltip));
|
||||
changeLabel.setTooltip(changeTooltip);
|
||||
actionBox.getChildren().add(changeLabel);
|
||||
|
||||
if(!isFinal()) {
|
||||
Button nextChangeAddressButton = new Button("");
|
||||
nextChangeAddressButton.setGraphic(getChangeReplaceGlyph());
|
||||
nextChangeAddressButton.setOnAction(event -> {
|
||||
EventManager.get().post(new ReplaceChangeAddressEvent(walletTx));
|
||||
});
|
||||
Tooltip replaceChangeTooltip = new Tooltip("Use next change address");
|
||||
nextChangeAddressButton.setTooltip(replaceChangeTooltip);
|
||||
Label replaceChangeLabel = new Label("", nextChangeAddressButton);
|
||||
replaceChangeLabel.getStyleClass().add("replace-change-label");
|
||||
replaceChangeLabel.setVisible(false);
|
||||
actionBox.setOnMouseEntered(event -> replaceChangeLabel.setVisible(true));
|
||||
actionBox.setOnMouseExited(event -> replaceChangeLabel.setVisible(false));
|
||||
actionBox.getChildren().add(replaceChangeLabel);
|
||||
}
|
||||
|
||||
if(isExpanded()) {
|
||||
changeLabel.setMinWidth(120);
|
||||
Region region = new Region();
|
||||
region.setMinWidth(20);
|
||||
HBox.setHgrow(region, Priority.ALWAYS);
|
||||
CoinLabel amountLabel = new CoinLabel();
|
||||
amountLabel.setValue(value);
|
||||
amountLabel.setMinWidth(TextUtils.computeTextWidth(amountLabel.getFont(), amountLabel.getText(), 0.0D) + 2);
|
||||
actionBox.getChildren().addAll(region, amountLabel);
|
||||
}
|
||||
|
||||
return new OutputNode(actionBox, changeAddress, value);
|
||||
}
|
||||
|
||||
private OutputNode buildSpChangeNode(WalletTransaction.SilentPaymentChangeOutput spChangeOutput) {
|
||||
SilentPayment silentPayment = spChangeOutput.getSilentPayment();
|
||||
SilentPaymentAddress spAddress = silentPayment.getSilentPaymentAddress();
|
||||
|
||||
HBox actionBox = new HBox();
|
||||
actionBox.setAlignment(Pos.CENTER_LEFT);
|
||||
Label changeLabel = new Label("Change", getChangeGlyph());
|
||||
changeLabel.getStyleClass().addAll("output-label", "payment-label");
|
||||
changeLabel.setSkin(new AddressLabelSkin(changeLabel));
|
||||
Tooltip changeTooltip = new Tooltip("Change of " + getCoinValue(silentPayment.getAmount()) + "\n" + silentPayment.getDisplayAddress());
|
||||
changeTooltip.getStyleClass().add("change-label");
|
||||
changeTooltip.setShowDelay(new Duration(TOOLTIP_SHOW_DELAY));
|
||||
changeTooltip.setShowDuration(Duration.INDEFINITE);
|
||||
changeTooltip.setSkin(new AddressTooltipSkin(changeTooltip));
|
||||
changeLabel.setTooltip(changeTooltip);
|
||||
actionBox.getChildren().add(changeLabel);
|
||||
|
||||
if(isExpanded()) {
|
||||
changeLabel.setMinWidth(120);
|
||||
Region region = new Region();
|
||||
region.setMinWidth(20);
|
||||
HBox.setHgrow(region, Priority.ALWAYS);
|
||||
CoinLabel amountLabel = new CoinLabel();
|
||||
amountLabel.setValue(silentPayment.getAmount());
|
||||
amountLabel.setMinWidth(TextUtils.computeTextWidth(amountLabel.getFont(), amountLabel.getText(), 0.0D) + 2);
|
||||
actionBox.getChildren().addAll(region, amountLabel);
|
||||
}
|
||||
|
||||
return new OutputNode(actionBox, silentPayment.isAddressComputed() ? silentPayment.getAddress() : null, silentPayment.getAmount(), null, spAddress);
|
||||
}
|
||||
|
||||
private Pane getTransactionPane() {
|
||||
VBox txPane = new VBox();
|
||||
txPane.setPadding(new Insets(0, 5, 0, 5));
|
||||
@@ -970,8 +1022,8 @@ public class TransactionDiagram extends GridPane {
|
||||
}
|
||||
|
||||
private String getDiagramTitle() {
|
||||
if(!isFinal() && walletTx.getPayments().size() > 0 && walletTx.getPayments().get(0).getLabel() != null) {
|
||||
return walletTx.getPayments().get(0).getLabel();
|
||||
if(!isFinal() && !walletTx.getPayments().isEmpty() && walletTx.getPayments().getFirst().getLabel() != null) {
|
||||
return walletTx.getPayments().getFirst().getLabel();
|
||||
} else {
|
||||
return "[" + walletTx.getTransaction().getTxId().toString().substring(0, 6) + "]";
|
||||
}
|
||||
@@ -1023,15 +1075,6 @@ public class TransactionDiagram extends GridPane {
|
||||
return spacer;
|
||||
}
|
||||
|
||||
private int getOutputIndex(Address address, long amount, Collection<Integer> seenIndexes) {
|
||||
List<TransactionOutput> addressOutputs = walletTx.getOutputs().stream().filter(output -> !(output instanceof WalletTransaction.NonAddressOutput))
|
||||
.map(WalletTransaction.Output::getTransactionOutput).collect(Collectors.toList());
|
||||
TransactionOutput output = addressOutputs.stream()
|
||||
.filter(txOutput -> address.equals(txOutput.getScript().getToAddress()) && txOutput.getValue() == amount && !seenIndexes.contains(txOutput.getIndex()))
|
||||
.findFirst().orElseThrow();
|
||||
return addressOutputs.indexOf(output);
|
||||
}
|
||||
|
||||
private Wallet getBip47SendWallet(Payment payment) {
|
||||
if(walletTx.getWallet() != null) {
|
||||
for(Wallet childWallet : walletTx.getWallet().getChildWallets()) {
|
||||
|
||||
@@ -96,22 +96,38 @@ public class TransactionDiagramLabel extends HBox {
|
||||
outputLabels.add(remixOutputLabel);
|
||||
}
|
||||
} else {
|
||||
List<Payment> payments = walletTx.getExternalPayments().stream().filter(payment -> payment.getType() == Payment.Type.DEFAULT).collect(Collectors.toList());
|
||||
List<OutputLabel> paymentLabels = payments.stream().map(payment -> getOutputLabel(transactionDiagram, payment)).collect(Collectors.toList());
|
||||
if(walletTx.getSelectedUtxos().values().stream().allMatch(Objects::isNull)) {
|
||||
paymentLabels.sort(Comparator.comparingInt(paymentLabel -> (paymentLabel.text.startsWith("Receive") ? 0 : 1)));
|
||||
List<OutputLabel> externalLabels = new ArrayList<>();
|
||||
List<OutputLabel> consolidationLabels = new ArrayList<>();
|
||||
List<OutputLabel> mixLabels = new ArrayList<>();
|
||||
for(WalletTransaction.Output output : walletTx.getOutputs()) {
|
||||
if(transactionDiagram.isPaymentAndNotChange(output)) {
|
||||
Payment payment = output instanceof WalletTransaction.PaymentOutput po ? po.getPayment() : ((WalletTransaction.ConsolidationOutput)output).getWalletNodePayment();
|
||||
if(payment.getType() == Payment.Type.MIX || payment.getType() == Payment.Type.FAKE_MIX) {
|
||||
mixLabels.add(getOutputLabel(transactionDiagram, output));
|
||||
} else if(payment.getType() == Payment.Type.DEFAULT || payment.getType() == Payment.Type.ANCHOR) {
|
||||
if(output instanceof WalletTransaction.ConsolidationOutput || output instanceof WalletTransaction.SilentPaymentConsolidationOutput) {
|
||||
consolidationLabels.add(getOutputLabel(transactionDiagram, output));
|
||||
} else {
|
||||
externalLabels.add(getOutputLabel(transactionDiagram, output));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
outputLabels.addAll(paymentLabels);
|
||||
|
||||
List<Payment> consolidations = walletTx.getWalletNodePayments().stream().filter(payment -> payment.getType() == Payment.Type.DEFAULT).collect(Collectors.toList());
|
||||
outputLabels.addAll(consolidations.stream().map(consolidation -> getOutputLabel(transactionDiagram, consolidation)).collect(Collectors.toList()));
|
||||
|
||||
List<Payment> mixes = walletTx.getPayments().stream().filter(payment -> payment.getType() == Payment.Type.MIX || payment.getType() == Payment.Type.FAKE_MIX).collect(Collectors.toList());
|
||||
outputLabels.addAll(mixes.stream().map(payment -> getOutputLabel(transactionDiagram, payment)).collect(Collectors.toList()));
|
||||
if(walletTx.getSelectedUtxos().values().stream().allMatch(Objects::isNull)) {
|
||||
externalLabels.sort(Comparator.comparingInt(paymentLabel -> (paymentLabel.text.startsWith("Receive") ? 0 : 1)));
|
||||
}
|
||||
outputLabels.addAll(externalLabels);
|
||||
outputLabels.addAll(consolidationLabels);
|
||||
outputLabels.addAll(mixLabels);
|
||||
}
|
||||
|
||||
Map<WalletNode, Long> changeMap = walletTx.getChangeMap();
|
||||
outputLabels.addAll(changeMap.entrySet().stream().map(changeEntry -> getOutputLabel(transactionDiagram, changeEntry)).collect(Collectors.toList()));
|
||||
for(WalletTransaction.Output output : walletTx.getOutputs()) {
|
||||
if(output instanceof WalletTransaction.SilentPaymentChangeOutput spChange) {
|
||||
outputLabels.add(getOutputLabel(transactionDiagram, spChange));
|
||||
} else if(output instanceof WalletTransaction.ChangeOutput changeOutput) {
|
||||
outputLabels.add(getOutputLabel(transactionDiagram, changeOutput));
|
||||
}
|
||||
}
|
||||
|
||||
OutputLabel feeOutputLabel = getFeeOutputLabel(transactionDiagram);
|
||||
if(feeOutputLabel != null) {
|
||||
@@ -200,22 +216,31 @@ public class TransactionDiagramLabel extends HBox {
|
||||
return getOutputLabel(glyph, text);
|
||||
}
|
||||
|
||||
private OutputLabel getOutputLabel(TransactionDiagram transactionDiagram, Payment payment) {
|
||||
private OutputLabel getOutputLabel(TransactionDiagram transactionDiagram, WalletTransaction.Output output) {
|
||||
WalletTransaction walletTx = transactionDiagram.getWalletTransaction();
|
||||
Payment payment = output instanceof WalletTransaction.PaymentOutput po ? po.getPayment() : ((WalletTransaction.ConsolidationOutput)output).getWalletNodePayment();
|
||||
boolean spConsolidation = output instanceof WalletTransaction.SilentPaymentConsolidationOutput;
|
||||
Wallet toWallet = walletTx.getToWallet(AppServices.get().getOpenWallets().keySet(), payment);
|
||||
WalletNode toNode = payment instanceof WalletNodePayment walletNodePayment ? walletNodePayment.getWalletNode() : null;
|
||||
|
||||
Glyph glyph = GlyphUtils.getOutputGlyph(transactionDiagram.getWalletTransaction(), payment);
|
||||
String text = (toWallet == null ? (toNode != null ? "Consolidate " : "Pay ") : "Receive ") + transactionDiagram.getCoinValue(payment.getAmount()) + " to " + payment;
|
||||
Glyph glyph = GlyphUtils.getOutputGlyph(walletTx, payment);
|
||||
String text = (toNode != null || spConsolidation ? "Consolidate " : (toWallet == null ? "Pay " : "Receive ")) + transactionDiagram.getCoinValue(payment.getAmount()) + " to " + payment;
|
||||
|
||||
return getOutputLabel(glyph, text);
|
||||
}
|
||||
|
||||
private OutputLabel getOutputLabel(TransactionDiagram transactionDiagram, Map.Entry<WalletNode, Long> changeEntry) {
|
||||
private OutputLabel getOutputLabel(TransactionDiagram transactionDiagram, WalletTransaction.ChangeOutput changeOutput) {
|
||||
WalletTransaction walletTx = transactionDiagram.getWalletTransaction();
|
||||
|
||||
Glyph glyph = GlyphUtils.getChangeGlyph();
|
||||
String text = "Change of " + transactionDiagram.getCoinValue(changeEntry.getValue()) + " to " + walletTx.getChangeAddress(changeEntry.getKey()).toString();
|
||||
String text = "Change of " + transactionDiagram.getCoinValue(changeOutput.getValue()) + " to " + walletTx.getChangeAddress(changeOutput.getWalletNode()).toString();
|
||||
|
||||
return getOutputLabel(glyph, text);
|
||||
}
|
||||
|
||||
private OutputLabel getOutputLabel(TransactionDiagram transactionDiagram, WalletTransaction.SilentPaymentChangeOutput spChangeOutput) {
|
||||
Glyph glyph = GlyphUtils.getChangeGlyph();
|
||||
String text = "Change of " + transactionDiagram.getCoinValue(spChangeOutput.getSilentPayment().getAmount()) + " to " + spChangeOutput.getSilentPayment();
|
||||
|
||||
return getOutputLabel(glyph, text);
|
||||
}
|
||||
|
||||
@@ -44,11 +44,13 @@ public class WalletExportDialog extends Dialog<Wallet> {
|
||||
AnchorPane.setRightAnchor(scrollPane, 0.0);
|
||||
|
||||
List<WalletExport> exporters;
|
||||
if(wallet.getPolicyType() == PolicyType.SINGLE) {
|
||||
if(wallet.getPolicyType() == PolicyType.SINGLE_HD) {
|
||||
exporters = List.of(new Electrum(), new ElectrumPersonalServer(), new Descriptor(), new SpecterDesktop(), new Sparrow(), new WalletLabels(allWalletForms), new WalletTransactions(selectedWalletForm));
|
||||
} else if(wallet.getPolicyType() == PolicyType.MULTI) {
|
||||
} else if(wallet.getPolicyType() == PolicyType.MULTI_HD) {
|
||||
exporters = List.of(new Bip129(), new CaravanMultisig(), new ColdcardMultisig(), new CoboVaultMultisig(), new Electrum(), new ElectrumPersonalServer(), new KeystoneMultisig(),
|
||||
new Descriptor(), new JadeMultisig(), new PassportMultisig(), new SpecterDesktop(), new BlueWalletMultisig(), new SpecterDIY(), new Sparrow(), new WalletLabels(allWalletForms), new WalletTransactions(selectedWalletForm));
|
||||
} else if(wallet.getPolicyType() == PolicyType.SINGLE_SP) {
|
||||
exporters = List.of(new Descriptor(), new Sparrow(), new WalletLabels(allWalletForms), new WalletTransactions(selectedWalletForm));
|
||||
} else {
|
||||
throw new UnsupportedOperationException("Cannot export wallet with policy type " + wallet.getPolicyType());
|
||||
}
|
||||
|
||||
@@ -52,7 +52,7 @@ public class WalletImportDialog extends Dialog<List<Wallet>> {
|
||||
|
||||
importAccordion = new Accordion();
|
||||
List<KeystoreFileImport> keystoreImporters = List.of(new ColdcardSinglesig(), new CoboVaultSinglesig(), new Jade(), new KeystoneSinglesig(), new PassportSinglesig(),
|
||||
new GordianSeedTool(), new SeedSigner(), new SpecterDIY(), new Krux(), new AirGapVault(), new Samourai(), new KeycardShellSinglesig());
|
||||
new GordianSeedTool(), new SeedSigner(), new SpecterDIY(), new Krux(), new AirGapVault(), new Samourai(), new KeycardShellSinglesig(), new EraSinglesig());
|
||||
for(KeystoreFileImport importer : keystoreImporters) {
|
||||
if(!importer.isDeprecated() || Config.get().isShowDeprecatedImportExport()) {
|
||||
FileWalletKeystoreImportPane importPane = new FileWalletKeystoreImportPane(importer);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.sparrowwallet.sparrow.control;
|
||||
|
||||
import com.sparrowwallet.drongo.policy.PolicyType;
|
||||
import com.sparrowwallet.sparrow.AppServices;
|
||||
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
|
||||
import com.sparrowwallet.sparrow.io.Config;
|
||||
@@ -48,9 +49,13 @@ public class WalletNameDialog extends Dialog<WalletNameDialog.NameAndBirthDate>
|
||||
}
|
||||
|
||||
public WalletNameDialog(String initialName, boolean hasExistingTransactions, Date startDate, boolean rename) {
|
||||
this(initialName, hasExistingTransactions, null, startDate, rename);
|
||||
}
|
||||
|
||||
public WalletNameDialog(String initialName, boolean hasExistingTransactions, PolicyType walletPolicyType, Date startDate, boolean rename) {
|
||||
final DialogPane dialogPane = getDialogPane();
|
||||
AppServices.setStageIcon(dialogPane.getScene().getWindow());
|
||||
boolean requestBirthDate = !rename && (Config.get().getServerType() == null || Config.get().getServerType() == ServerType.BITCOIN_CORE);
|
||||
boolean requestBirthDate = !rename && (walletPolicyType == PolicyType.SINGLE_SP || Config.get().getServerType() == null || Config.get().getServerType() == ServerType.BITCOIN_CORE);
|
||||
|
||||
setTitle("Wallet Name");
|
||||
dialogPane.setHeaderText("Enter a name for this wallet:");
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
package com.sparrowwallet.sparrow.control;
|
||||
|
||||
import org.openpnp.capture.CaptureFormat;
|
||||
import io.github.doblon8.openpnp.capture.CaptureFormat;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
@@ -61,7 +61,7 @@ public enum WebcamResolution implements Comparable<WebcamResolution> {
|
||||
|
||||
public static WebcamResolution from(CaptureFormat captureFormat) {
|
||||
for(WebcamResolution resolution : values()) {
|
||||
if(captureFormat.getFormatInfo().width == resolution.width && captureFormat.getFormatInfo().height == resolution.height) {
|
||||
if(captureFormat.formatInfo().width() == resolution.width && captureFormat.formatInfo().height() == resolution.height) {
|
||||
return resolution;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,8 +16,7 @@ import javafx.concurrent.ScheduledService;
|
||||
import javafx.concurrent.Task;
|
||||
import javafx.embed.swing.SwingFXUtils;
|
||||
import javafx.scene.image.Image;
|
||||
import org.openpnp.capture.*;
|
||||
import org.openpnp.capture.library.OpenpnpCaptureLibrary;
|
||||
import io.github.doblon8.openpnp.capture.*;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
@@ -25,6 +24,7 @@ import java.awt.*;
|
||||
import java.awt.geom.RoundRectangle2D;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.awt.image.WritableRaster;
|
||||
import java.io.File;
|
||||
import java.util.*;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.Semaphore;
|
||||
@@ -62,38 +62,46 @@ public class WebcamService extends ScheduledService<Image> {
|
||||
private final Bokmakierie bokmakierie;
|
||||
|
||||
static {
|
||||
if(log.isTraceEnabled()) {
|
||||
OpenpnpCaptureLibrary.INSTANCE.Cap_setLogLevel(8);
|
||||
} else if(log.isDebugEnabled()) {
|
||||
OpenpnpCaptureLibrary.INSTANCE.Cap_setLogLevel(7);
|
||||
} else if(log.isInfoEnabled()) {
|
||||
OpenpnpCaptureLibrary.INSTANCE.Cap_setLogLevel(6);
|
||||
String javaHome = System.getProperty("java.home");
|
||||
if(javaHome != null) {
|
||||
File libFile = new File(new File(javaHome, "lib"), System.mapLibraryName("openpnp-capture"));
|
||||
if(libFile.exists()) {
|
||||
System.load(libFile.getAbsolutePath());
|
||||
}
|
||||
}
|
||||
|
||||
OpenpnpCaptureLibrary.INSTANCE.Cap_installCustomLogFunction((level, ptr) -> {
|
||||
if(log.isTraceEnabled()) {
|
||||
OpenPnpCapture.setLogLevel(LogLevel.VERBOSE);
|
||||
} else if(log.isDebugEnabled()) {
|
||||
OpenPnpCapture.setLogLevel(LogLevel.DEBUG);
|
||||
} else if(log.isInfoEnabled()) {
|
||||
OpenPnpCapture.setLogLevel(LogLevel.INFO);
|
||||
}
|
||||
|
||||
OpenPnpCapture.installCustomLogFunction((level, message) -> {
|
||||
switch(level) {
|
||||
case 0:
|
||||
case 1:
|
||||
case 2:
|
||||
case 3:
|
||||
String err = ptr.getString(0).trim();
|
||||
case LogLevel.EMERGENCY:
|
||||
case LogLevel.ALERT:
|
||||
case LogLevel.CRITICAL:
|
||||
case LogLevel.ERROR:
|
||||
String err = message.trim();
|
||||
if(err.equals("tjDecompressHeader2 failed: No error") || err.matches("getPropertyLimits.*failed on.*")) { //Safe to ignore
|
||||
log.debug(err);
|
||||
} else {
|
||||
log.error(err);
|
||||
}
|
||||
break;
|
||||
case 4:
|
||||
case 5:
|
||||
case 6:
|
||||
log.info(ptr.getString(0).trim());
|
||||
case LogLevel.WARNING:
|
||||
case LogLevel.NOTICE:
|
||||
case LogLevel.INFO:
|
||||
log.info(message.trim());
|
||||
break;
|
||||
case 7:
|
||||
log.debug(ptr.getString(0).trim());
|
||||
case LogLevel.DEBUG:
|
||||
log.debug(message.trim());
|
||||
break;
|
||||
case 8:
|
||||
case LogLevel.VERBOSE:
|
||||
default:
|
||||
log.trace(ptr.getString(0).trim());
|
||||
log.trace(message.trim());
|
||||
break;
|
||||
}
|
||||
});
|
||||
@@ -167,8 +175,8 @@ public class WebcamService extends ScheduledService<Image> {
|
||||
//On macOS and Windows, camera pixel format is largely abstracted away
|
||||
if(OsType.getCurrent() == OsType.UNIX) {
|
||||
deviceFormats.sort((f1, f2) -> {
|
||||
WebcamPixelFormat pf1 = WebcamPixelFormat.fromFourCC(f1.getFormatInfo().fourcc);
|
||||
WebcamPixelFormat pf2 = WebcamPixelFormat.fromFourCC(f2.getFormatInfo().fourcc);
|
||||
WebcamPixelFormat pf1 = WebcamPixelFormat.fromFourCC(f1.formatInfo().fourcc());
|
||||
WebcamPixelFormat pf2 = WebcamPixelFormat.fromFourCC(f2.formatInfo().fourcc());
|
||||
return Integer.compare(WebcamPixelFormat.getPriority(pf1), WebcamPixelFormat.getPriority(pf2));
|
||||
});
|
||||
}
|
||||
@@ -185,17 +193,17 @@ public class WebcamService extends ScheduledService<Image> {
|
||||
format = supportedResolutions.get(resolution);
|
||||
} else {
|
||||
format = device.getFormats().getFirst();
|
||||
log.warn("Could not get standard capture resolution, using " + format.getFormatInfo().width + "x" + format.getFormatInfo().height);
|
||||
log.warn("Could not get standard capture resolution, using " + format.formatInfo().width() + "x" + format.formatInfo().height());
|
||||
}
|
||||
}
|
||||
|
||||
//On Linux, formats not defined in WebcamPixelFormat are unsupported
|
||||
if(OsType.getCurrent() == OsType.UNIX && WebcamPixelFormat.fromFourCC(format.getFormatInfo().fourcc) == null) {
|
||||
log.warn("Unsupported camera pixel format " + WebcamPixelFormat.fourCCToString(format.getFormatInfo().fourcc));
|
||||
if(OsType.getCurrent() == OsType.UNIX && WebcamPixelFormat.fromFourCC(format.formatInfo().fourcc()) == null) {
|
||||
log.warn("Unsupported camera pixel format " + WebcamPixelFormat.fourCCToString(format.formatInfo().fourcc()));
|
||||
}
|
||||
|
||||
if(log.isDebugEnabled()) {
|
||||
log.debug("Opening capture stream on " + device + " with format " + format.getFormatInfo().width + "x" + format.getFormatInfo().height + " (" + WebcamPixelFormat.fourCCToString(format.getFormatInfo().fourcc) + ")");
|
||||
log.debug("Opening capture stream on " + device + " with format " + format.formatInfo().width() + "x" + format.formatInfo().height() + " (" + WebcamPixelFormat.fourCCToString(format.formatInfo().fourcc()) + ")");
|
||||
}
|
||||
|
||||
opening.set(true);
|
||||
@@ -203,7 +211,7 @@ public class WebcamService extends ScheduledService<Image> {
|
||||
opening.set(false);
|
||||
|
||||
try {
|
||||
zoomLimits = stream.getPropertyLimits(CaptureProperty.Zoom);
|
||||
zoomLimits = stream.getPropertyLimits(CaptureProperty.ZOOM);
|
||||
} catch(Throwable e) {
|
||||
log.debug("Error getting zoom limits on " + device + ", assuming no zoom function");
|
||||
}
|
||||
@@ -285,7 +293,7 @@ public class WebcamService extends ScheduledService<Image> {
|
||||
public int getZoom() {
|
||||
if(stream != null && zoomLimits != null) {
|
||||
try {
|
||||
return stream.getProperty(CaptureProperty.Zoom);
|
||||
return stream.getProperty(CaptureProperty.ZOOM);
|
||||
} catch(Exception e) {
|
||||
log.error("Error getting zoom property on " + device, e);
|
||||
}
|
||||
@@ -297,7 +305,7 @@ public class WebcamService extends ScheduledService<Image> {
|
||||
public void setZoom(int value) {
|
||||
if(stream != null && zoomLimits != null) {
|
||||
try {
|
||||
stream.setProperty(CaptureProperty.Zoom, value);
|
||||
stream.setProperty(CaptureProperty.ZOOM, value);
|
||||
} catch(Exception e) {
|
||||
log.error("Error setting zoom property on " + device, e);
|
||||
}
|
||||
|
||||
@@ -51,8 +51,8 @@ public class WebcamView {
|
||||
int currentZoom = service.getZoom();
|
||||
if(currentZoom >= 0) {
|
||||
int newZoom = scrollEvent.getDeltaY() > 0 ? Math.round(currentZoom * 1.1f) : Math.round(currentZoom * 0.9f);
|
||||
newZoom = Math.max(newZoom, service.getZoomLimits().getMin());
|
||||
newZoom = Math.min(newZoom, service.getZoomLimits().getMax());
|
||||
newZoom = Math.max(newZoom, service.getZoomLimits().min());
|
||||
newZoom = Math.min(newZoom, service.getZoomLimits().max());
|
||||
if(newZoom != currentZoom) {
|
||||
service.setZoom(newZoom);
|
||||
}
|
||||
|
||||
@@ -114,7 +114,7 @@ public class XprvKeystoreImportPane extends TitledDescriptionPane {
|
||||
private void importKeystore(List<ChildNumber> derivation) {
|
||||
importButton.setDisable(true);
|
||||
try {
|
||||
Keystore keystore = importer.getKeystore(derivation, xprv);
|
||||
Keystore keystore = importer.getKeystore(wallet.getPolicyType(), derivation, xprv);
|
||||
EventManager.get().post(new KeystoreImportEvent(keystore));
|
||||
} catch (ImportException e) {
|
||||
String errorMessage = e.getMessage();
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.sparrowwallet.sparrow.event;
|
||||
|
||||
import com.sparrowwallet.drongo.psbt.PSBT;
|
||||
import javafx.stage.Window;
|
||||
|
||||
import java.io.File;
|
||||
@@ -10,15 +11,20 @@ import java.io.File;
|
||||
public class RequestTransactionOpenEvent {
|
||||
private final Window window;
|
||||
private final File file;
|
||||
private final PSBT contextPsbt;
|
||||
|
||||
public RequestTransactionOpenEvent(Window window) {
|
||||
this.window = window;
|
||||
this.file = null;
|
||||
this(window, null, null);
|
||||
}
|
||||
|
||||
public RequestTransactionOpenEvent(Window window, File file) {
|
||||
this(window, file, null);
|
||||
}
|
||||
|
||||
public RequestTransactionOpenEvent(Window window, File file, PSBT contextPsbt) {
|
||||
this.window = window;
|
||||
this.file = file;
|
||||
this.contextPsbt = contextPsbt;
|
||||
}
|
||||
|
||||
public Window getWindow() {
|
||||
@@ -28,4 +34,8 @@ public class RequestTransactionOpenEvent {
|
||||
public File getFile() {
|
||||
return file;
|
||||
}
|
||||
|
||||
public PSBT getContextPsbt() {
|
||||
return contextPsbt;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,6 +20,6 @@ public class SettingsChangedEvent {
|
||||
}
|
||||
|
||||
public enum Type {
|
||||
POLICY, SCRIPT_TYPE, MUTLISIG_THRESHOLD, MULTISIG_TOTAL, KEYSTORE_LABEL, KEYSTORE_FINGERPRINT, KEYSTORE_DERIVATION, KEYSTORE_XPUB, GAP_LIMIT, BIRTH_DATE, WATCH_LAST;
|
||||
POLICY, SCRIPT_TYPE, MUTLISIG_THRESHOLD, MULTISIG_TOTAL, KEYSTORE_LABEL, KEYSTORE_FINGERPRINT, KEYSTORE_DERIVATION, KEYSTORE_XPUB, KEYSTORE_SP_SCAN, GAP_LIMIT, BIRTH_DATE, WATCH_LAST;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
package com.sparrowwallet.sparrow.event;
|
||||
|
||||
/**
|
||||
* Posted by SubscriptionService on a live silent-payments delta — i.e. a progress = 1.0 notification
|
||||
* that arrives after the historical scan has already completed. Carries the SP address only;
|
||||
* consumers re-call ElectrumServer.getSilentPaymentHistory(scanAddress, neededStart) to get the
|
||||
* full cache and apply their own wallet.getWalletTransaction(txid) filter for "what's new for me".
|
||||
* <p>
|
||||
* The first historical-scan-complete notification is consumed by the blocking getSilentPaymentHistory
|
||||
* call's latch and does NOT post this event.
|
||||
*/
|
||||
public class SilentPaymentsHistoryUpdatedEvent {
|
||||
private final String spAddress;
|
||||
|
||||
public SilentPaymentsHistoryUpdatedEvent(String spAddress) {
|
||||
this.spAddress = spAddress;
|
||||
}
|
||||
|
||||
public String getSpAddress() {
|
||||
return spAddress;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package com.sparrowwallet.sparrow.event;
|
||||
|
||||
/**
|
||||
* Posted by SubscriptionService on every blockchain.silentpayments.subscribe notification.
|
||||
* Carries the SP address and the scan's progress (0.0–1.0). WalletForm consumers use this for
|
||||
* status UI; the gating decision (whether to display) is per-wallet runtime state, not encoded here.
|
||||
*/
|
||||
public class SilentPaymentsScanProgressEvent {
|
||||
private final String spAddress;
|
||||
private final double progress;
|
||||
|
||||
public SilentPaymentsScanProgressEvent(String spAddress, double progress) {
|
||||
this.spAddress = spAddress;
|
||||
this.progress = progress;
|
||||
}
|
||||
|
||||
public String getSpAddress() {
|
||||
return spAddress;
|
||||
}
|
||||
|
||||
public double getProgress() {
|
||||
return progress;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package com.sparrowwallet.sparrow.event;
|
||||
|
||||
import com.sparrowwallet.drongo.silentpayments.SilentPaymentScanAddress;
|
||||
|
||||
/**
|
||||
* Posted by ElectrumServer.releaseSilentPaymentSubscription when the refcount on a silent-payments
|
||||
* subscription reaches zero. The subscriber starts a background SilentPaymentsUnsubscribeService to
|
||||
* issue the actual unsubscribe RPC, since releaseSilentPaymentSubscription is reached from JFX-thread
|
||||
* paths (wallet close, refresh, history clear) where blocking on a network call is not acceptable.
|
||||
*/
|
||||
public class SilentPaymentsUnsubscribeEvent {
|
||||
private final SilentPaymentScanAddress scanAddress;
|
||||
|
||||
public SilentPaymentsUnsubscribeEvent(SilentPaymentScanAddress scanAddress) {
|
||||
this.scanAddress = scanAddress;
|
||||
}
|
||||
|
||||
public SilentPaymentScanAddress getScanAddress() {
|
||||
return scanAddress;
|
||||
}
|
||||
}
|
||||
@@ -11,18 +11,28 @@ public class ViewPSBTEvent {
|
||||
private final String label;
|
||||
private final File file;
|
||||
private final PSBT psbt;
|
||||
private final PSBT contextPsbt;
|
||||
private final TransactionView initialView;
|
||||
private final Integer initialIndex;
|
||||
|
||||
public ViewPSBTEvent(Window window, String label, File file, PSBT psbt) {
|
||||
this(window, label, file, psbt, TransactionView.HEADERS, null);
|
||||
this(window, label, file, psbt, null, TransactionView.HEADERS, null);
|
||||
}
|
||||
|
||||
public ViewPSBTEvent(Window window, String label, File file, PSBT psbt, PSBT contextPsbt) {
|
||||
this(window, label, file, psbt, contextPsbt, TransactionView.HEADERS, null);
|
||||
}
|
||||
|
||||
public ViewPSBTEvent(Window window, String label, File file, PSBT psbt, TransactionView initialView, Integer initialIndex) {
|
||||
this(window, label, file, psbt, null, initialView, initialIndex);
|
||||
}
|
||||
|
||||
public ViewPSBTEvent(Window window, String label, File file, PSBT psbt, PSBT contextPsbt, TransactionView initialView, Integer initialIndex) {
|
||||
this.window = window;
|
||||
this.label = label;
|
||||
this.file = file;
|
||||
this.psbt = psbt;
|
||||
this.contextPsbt = contextPsbt;
|
||||
this.initialView = initialView;
|
||||
this.initialIndex = initialIndex;
|
||||
}
|
||||
@@ -43,6 +53,10 @@ public class ViewPSBTEvent {
|
||||
return psbt;
|
||||
}
|
||||
|
||||
public PSBT getContextPsbt() {
|
||||
return contextPsbt;
|
||||
}
|
||||
|
||||
public TransactionView getInitialView() {
|
||||
return initialView;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package com.sparrowwallet.sparrow.event;
|
||||
|
||||
import com.sparrowwallet.drongo.protocol.Transaction;
|
||||
import com.sparrowwallet.drongo.psbt.PSBT;
|
||||
import com.sparrowwallet.drongo.wallet.BlockTransaction;
|
||||
import com.sparrowwallet.sparrow.transaction.TransactionView;
|
||||
import com.sparrowwallet.sparrow.wallet.HashIndexEntry;
|
||||
@@ -9,13 +10,19 @@ import javafx.stage.Window;
|
||||
public class ViewTransactionEvent {
|
||||
private final Window window;
|
||||
private final Transaction transaction;
|
||||
private final PSBT contextPsbt;
|
||||
private final BlockTransaction blockTransaction;
|
||||
private final TransactionView initialView;
|
||||
private final Integer initialIndex;
|
||||
|
||||
public ViewTransactionEvent(Window window, Transaction transaction) {
|
||||
this(window, transaction, null);
|
||||
}
|
||||
|
||||
public ViewTransactionEvent(Window window, Transaction transaction, PSBT contextPsbt) {
|
||||
this.window = window;
|
||||
this.transaction = transaction;
|
||||
this.contextPsbt = contextPsbt;
|
||||
this.blockTransaction = null;
|
||||
this.initialView = TransactionView.HEADERS;
|
||||
this.initialIndex = null;
|
||||
@@ -32,6 +39,7 @@ public class ViewTransactionEvent {
|
||||
public ViewTransactionEvent(Window window, BlockTransaction blockTransaction, TransactionView initialView, Integer initialIndex) {
|
||||
this.window = window;
|
||||
this.transaction = blockTransaction.getTransaction();
|
||||
this.contextPsbt = null;
|
||||
this.blockTransaction = blockTransaction;
|
||||
this.initialView = initialView;
|
||||
this.initialIndex = initialIndex;
|
||||
@@ -45,6 +53,10 @@ public class ViewTransactionEvent {
|
||||
return transaction;
|
||||
}
|
||||
|
||||
public PSBT getContextPsbt() {
|
||||
return contextPsbt;
|
||||
}
|
||||
|
||||
public BlockTransaction getBlockTransaction() {
|
||||
return blockTransaction;
|
||||
}
|
||||
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
package com.sparrowwallet.sparrow.event;
|
||||
|
||||
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||
|
||||
public class WalletSilentPaymentAddressesChangedEvent extends WalletChangedEvent {
|
||||
public WalletSilentPaymentAddressesChangedEvent(Wallet wallet) {
|
||||
super(wallet.resolveMasterWallet());
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,6 @@
|
||||
package com.sparrowwallet.sparrow.glyphfont;
|
||||
|
||||
import com.sparrowwallet.drongo.wallet.Payment;
|
||||
import com.sparrowwallet.drongo.wallet.WalletNodePayment;
|
||||
import com.sparrowwallet.drongo.wallet.WalletTransaction;
|
||||
import com.sparrowwallet.sparrow.AppServices;
|
||||
import com.sparrowwallet.sparrow.control.TransactionDiagram;
|
||||
@@ -16,7 +15,7 @@ public class GlyphUtils {
|
||||
return getFakeMixGlyph();
|
||||
} else if(payment.getType().equals(Payment.Type.ANCHOR)) {
|
||||
return getAnchorGlyph();
|
||||
} else if(payment instanceof WalletNodePayment) {
|
||||
} else if(walletTx.isConsolidation(payment)) {
|
||||
return getConsolidationGlyph();
|
||||
} else if(walletTx.isPremixSend(payment)) {
|
||||
return getPremixGlyph();
|
||||
|
||||
@@ -20,6 +20,7 @@ import java.nio.file.Files;
|
||||
import java.nio.file.LinkOption;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
public abstract class Instance {
|
||||
@@ -31,6 +32,8 @@ public abstract class Instance {
|
||||
|
||||
private Selector selector;
|
||||
private ServerSocketChannel serverChannel;
|
||||
private Path acquiredLockFile;
|
||||
private Path acquiredLockFilePointer;
|
||||
|
||||
public Instance(final String applicationId) {
|
||||
this(applicationId, true);
|
||||
@@ -65,6 +68,7 @@ public abstract class Instance {
|
||||
serverChannel.configureBlocking(false);
|
||||
serverChannel.register(selector, SelectionKey.OP_ACCEPT);
|
||||
lockFile.toFile().deleteOnExit();
|
||||
acquiredLockFile = lockFile;
|
||||
} catch(Exception e) {
|
||||
throw new InstanceException("Could not open UNIX socket lock file for instance at " + lockFile.toAbsolutePath(), e);
|
||||
}
|
||||
@@ -145,26 +149,27 @@ public abstract class Instance {
|
||||
|
||||
private Path getLockFile(boolean findExisting) {
|
||||
if(findExisting) {
|
||||
Path pointer = getUserLockFilePointer();
|
||||
try {
|
||||
if(pointer != null && Files.exists(pointer)) {
|
||||
if(Files.isSymbolicLink(pointer)) {
|
||||
return Files.readSymbolicLink(pointer);
|
||||
} else {
|
||||
Path lockFile = Path.of(Files.readString(pointer, StandardCharsets.UTF_8));
|
||||
if(Files.exists(lockFile)) {
|
||||
return lockFile;
|
||||
for(Path pointer : getUserLockFilePointers()) {
|
||||
try {
|
||||
if(Files.exists(pointer)) {
|
||||
if(Files.isSymbolicLink(pointer)) {
|
||||
return Files.readSymbolicLink(pointer);
|
||||
} else {
|
||||
Path lockFile = Path.of(Files.readString(pointer, StandardCharsets.UTF_8));
|
||||
if(Files.exists(lockFile)) {
|
||||
return lockFile;
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch(IOException e) {
|
||||
log.warn("Could not find lock file at " + pointer.toAbsolutePath());
|
||||
} catch(Exception e) {
|
||||
//ignore
|
||||
}
|
||||
} catch(IOException e) {
|
||||
log.warn("Could not find lock file at " + pointer.toAbsolutePath());
|
||||
} catch(Exception e) {
|
||||
//ignore
|
||||
}
|
||||
}
|
||||
|
||||
return Storage.getSparrowDir().toPath().resolve(applicationId + ".lock");
|
||||
return Storage.getStateDir().toPath().resolve(applicationId + ".lock");
|
||||
}
|
||||
|
||||
private void createSymlink(Path lockFile) {
|
||||
@@ -173,6 +178,7 @@ public abstract class Instance {
|
||||
if(pointer != null && !Files.exists(pointer, LinkOption.NOFOLLOW_LINKS)) {
|
||||
Files.createSymbolicLink(pointer, lockFile);
|
||||
pointer.toFile().deleteOnExit();
|
||||
acquiredLockFilePointer = pointer;
|
||||
}
|
||||
} catch(IOException e) {
|
||||
log.debug("Could not create symlink " + pointer.toAbsolutePath() + " to lockFile at " + lockFile.toAbsolutePath() + ", writing as normal file", e);
|
||||
@@ -180,6 +186,7 @@ public abstract class Instance {
|
||||
try {
|
||||
Files.writeString(pointer, lockFile.toAbsolutePath().toString(), StandardCharsets.UTF_8);
|
||||
pointer.toFile().deleteOnExit();
|
||||
acquiredLockFilePointer = pointer;
|
||||
} catch(IOException ex) {
|
||||
log.warn("Could not create pointer " + pointer.toAbsolutePath() + " to lockFile at " + lockFile.toAbsolutePath(), ex);
|
||||
}
|
||||
@@ -188,24 +195,44 @@ public abstract class Instance {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the location this instance writes its pointer to, ignoring any configured home so that instances started with
|
||||
* and without one still find each other.
|
||||
*/
|
||||
private Path getUserLockFilePointer() {
|
||||
if(Boolean.parseBoolean(System.getenv(LINK_ENV_PROPERTY))) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
File sparrowHome = Storage.getSparrowHome(true);
|
||||
if(!sparrowHome.exists()) {
|
||||
Storage.createOwnerOnlyDirectory(sparrowHome);
|
||||
sparrowHome.deleteOnExit(); //Will only delete on exit if empty
|
||||
File stateHome = Storage.getStateHome(true);
|
||||
if(!stateHome.exists()) {
|
||||
Storage.createOwnerOnlyDirectory(stateHome);
|
||||
stateHome.deleteOnExit(); //Will only delete on exit if empty
|
||||
}
|
||||
|
||||
return sparrowHome.toPath().resolve(applicationId + ".default");
|
||||
return stateHome.toPath().resolve(applicationId + ".default");
|
||||
} catch(Exception e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the locations that may hold a pointer to a running instance, without creating any of them.
|
||||
*
|
||||
* An instance started before the state directory was migrated writes its pointer to the default home, so that is searched
|
||||
* as well, ensuring migrating does not hide an instance that is already running.
|
||||
*/
|
||||
private List<Path> getUserLockFilePointers() {
|
||||
Path statePointer = getUserLockFilePointer();
|
||||
if(statePointer == null) {
|
||||
return List.of();
|
||||
}
|
||||
|
||||
Path defaultPointer = Storage.getDefaultHome().toPath().resolve(applicationId + ".default");
|
||||
return statePointer.equals(defaultPointer) ? List.of(statePointer) : List.of(statePointer, defaultPointer);
|
||||
}
|
||||
|
||||
/**
|
||||
* Free the lock if possible. This is only required to be called from the first instance.
|
||||
*
|
||||
@@ -216,10 +243,13 @@ public abstract class Instance {
|
||||
if(serverChannel != null && serverChannel.isOpen()) {
|
||||
serverChannel.close();
|
||||
}
|
||||
if(getUserLockFilePointer() != null) {
|
||||
Files.deleteIfExists(getUserLockFilePointer());
|
||||
//Free the paths this instance acquired, which are not necessarily those that would be resolved now
|
||||
if(acquiredLockFilePointer != null) {
|
||||
Files.deleteIfExists(acquiredLockFilePointer);
|
||||
}
|
||||
if(acquiredLockFile != null) {
|
||||
Files.deleteIfExists(acquiredLockFile);
|
||||
}
|
||||
Files.deleteIfExists(getLockFile(false));
|
||||
} catch(Exception e) {
|
||||
throw new InstanceException(e);
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import com.google.common.io.CharStreams;
|
||||
import com.sparrowwallet.drongo.KeyPurpose;
|
||||
import com.sparrowwallet.drongo.OutputDescriptor;
|
||||
import com.sparrowwallet.drongo.Utils;
|
||||
import com.sparrowwallet.drongo.policy.PolicyType;
|
||||
import com.sparrowwallet.drongo.crypto.Pbkdf2KeyDeriver;
|
||||
import com.sparrowwallet.drongo.protocol.ScriptType;
|
||||
import com.sparrowwallet.drongo.protocol.Sha256Hash;
|
||||
@@ -93,7 +94,7 @@ public class Bip129 implements KeystoreFileExport, KeystoreFileImport, WalletExp
|
||||
}
|
||||
|
||||
@Override
|
||||
public Keystore getKeystore(ScriptType scriptType, InputStream inputStream, String password) throws ImportException {
|
||||
public Keystore getKeystore(PolicyType policyType, ScriptType scriptType, InputStream inputStream, String password) throws ImportException {
|
||||
try {
|
||||
BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8));
|
||||
if(password != null) {
|
||||
@@ -235,6 +236,10 @@ public class Bip129 implements KeystoreFileExport, KeystoreFileImport, WalletExp
|
||||
}
|
||||
|
||||
String header = reader.readLine();
|
||||
if(header == null || !header.startsWith("BSMS")) {
|
||||
throw new ImportException("Not a BSMS file");
|
||||
}
|
||||
|
||||
String descriptor = reader.readLine();
|
||||
String paths = reader.readLine();
|
||||
String address = reader.readLine();
|
||||
|
||||
@@ -2,6 +2,7 @@ package com.sparrowwallet.sparrow.io;
|
||||
|
||||
import com.sparrowwallet.drongo.ExtendedKey;
|
||||
import com.sparrowwallet.drongo.crypto.ChildNumber;
|
||||
import com.sparrowwallet.drongo.policy.PolicyType;
|
||||
import com.sparrowwallet.drongo.wallet.Keystore;
|
||||
import com.sparrowwallet.drongo.wallet.MasterPrivateExtendedKey;
|
||||
import com.sparrowwallet.drongo.wallet.WalletModel;
|
||||
@@ -25,10 +26,10 @@ public class Bip32 implements KeystoreXprvImport {
|
||||
}
|
||||
|
||||
@Override
|
||||
public Keystore getKeystore(List<ChildNumber> derivation, ExtendedKey xprv) throws ImportException {
|
||||
public Keystore getKeystore(PolicyType policyType, List<ChildNumber> derivation, ExtendedKey xprv) throws ImportException {
|
||||
try {
|
||||
MasterPrivateExtendedKey masterPrivateExtendedKey = new MasterPrivateExtendedKey(xprv.getKey().getPrivKeyBytes(), xprv.getKey().getChainCode());
|
||||
return Keystore.fromMasterPrivateExtendedKey(masterPrivateExtendedKey, derivation);
|
||||
return Keystore.fromMasterPrivateExtendedKey(masterPrivateExtendedKey, policyType, derivation);
|
||||
} catch(Exception e) {
|
||||
throw new ImportException(e);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package com.sparrowwallet.sparrow.io;
|
||||
|
||||
import com.sparrowwallet.drongo.crypto.ChildNumber;
|
||||
import com.sparrowwallet.drongo.policy.PolicyType;
|
||||
import com.sparrowwallet.drongo.wallet.*;
|
||||
|
||||
import java.util.List;
|
||||
@@ -22,11 +23,11 @@ public class Bip39 implements KeystoreMnemonicImport {
|
||||
}
|
||||
|
||||
@Override
|
||||
public Keystore getKeystore(List<ChildNumber> derivation, List<String> mnemonicWords, String passphrase) throws ImportException {
|
||||
public Keystore getKeystore(PolicyType policyType, List<ChildNumber> derivation, List<String> mnemonicWords, String passphrase) throws ImportException {
|
||||
try {
|
||||
Bip39MnemonicCode.INSTANCE.check(mnemonicWords);
|
||||
DeterministicSeed seed = new DeterministicSeed(mnemonicWords, passphrase, System.currentTimeMillis(), DeterministicSeed.Type.BIP39);
|
||||
return Keystore.fromSeed(seed, derivation);
|
||||
return Keystore.fromSeed(seed, policyType, derivation);
|
||||
} catch (Exception e) {
|
||||
try {
|
||||
ElectrumMnemonicCode.INSTANCE.check(mnemonicWords);
|
||||
|
||||
@@ -3,6 +3,7 @@ package com.sparrowwallet.sparrow.io;
|
||||
import com.sparrowwallet.drongo.crypto.ChildNumber;
|
||||
import com.sparrowwallet.drongo.crypto.DeterministicKey;
|
||||
import com.sparrowwallet.drongo.crypto.HDKeyDerivation;
|
||||
import com.sparrowwallet.drongo.policy.PolicyType;
|
||||
import com.sparrowwallet.drongo.wallet.Keystore;
|
||||
import com.sparrowwallet.drongo.wallet.MasterPrivateExtendedKey;
|
||||
import com.sparrowwallet.drongo.wallet.MnemonicException;
|
||||
@@ -28,12 +29,12 @@ public class Bip93 implements KeystoreCodexImport {
|
||||
}
|
||||
|
||||
@Override
|
||||
public Keystore getKeystore(List<ChildNumber> derivation, String secretShare) throws ImportException {
|
||||
public Keystore getKeystore(PolicyType policyType, List<ChildNumber> derivation, String secretShare) throws ImportException {
|
||||
try {
|
||||
Codex32.Codex32Data secretData = Codex32.decode(secretShare);
|
||||
DeterministicKey key = HDKeyDerivation.createMasterPrivateKey(secretData.payloadToBip32Secret());
|
||||
MasterPrivateExtendedKey mpek = new MasterPrivateExtendedKey(key);
|
||||
Keystore keystore = Keystore.fromMasterPrivateExtendedKey(mpek, derivation);
|
||||
Keystore keystore = Keystore.fromMasterPrivateExtendedKey(mpek, policyType, derivation);
|
||||
keystore.setLabel("BIP93");
|
||||
return keystore;
|
||||
} catch(MnemonicException e) {
|
||||
|
||||
@@ -49,7 +49,7 @@ public class CaravanMultisig implements WalletImport, WalletExport {
|
||||
|
||||
Wallet wallet = new Wallet();
|
||||
wallet.setName(cf.name);
|
||||
wallet.setPolicyType(PolicyType.MULTI);
|
||||
wallet.setPolicyType(PolicyType.MULTI_HD);
|
||||
ScriptType scriptType = ScriptType.valueOf(cf.addressType.replace('-', '_'));
|
||||
|
||||
for(ExtPublicKey extKey : cf.extendedPublicKeys) {
|
||||
@@ -80,7 +80,7 @@ public class CaravanMultisig implements WalletImport, WalletExport {
|
||||
}
|
||||
|
||||
wallet.setScriptType(scriptType);
|
||||
wallet.setDefaultPolicy(Policy.getPolicy(PolicyType.MULTI, scriptType, wallet.getKeystores(), cf.quorum.requiredSigners));
|
||||
wallet.setDefaultPolicy(Policy.getPolicy(PolicyType.MULTI_HD, scriptType, wallet.getKeystores(), cf.quorum.requiredSigners));
|
||||
|
||||
return wallet;
|
||||
} catch(Exception e) {
|
||||
@@ -99,7 +99,7 @@ public class CaravanMultisig implements WalletImport, WalletExport {
|
||||
throw new ExportException("Cannot export an incomplete wallet");
|
||||
}
|
||||
|
||||
if(!wallet.getPolicyType().equals(PolicyType.MULTI)) {
|
||||
if(!wallet.getPolicyType().equals(PolicyType.MULTI_HD)) {
|
||||
throw new ExportException(getName() + " import requires a multisig wallet");
|
||||
}
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ import com.sparrowwallet.drongo.OsType;
|
||||
import com.sparrowwallet.drongo.address.Address;
|
||||
import com.sparrowwallet.drongo.crypto.ChildNumber;
|
||||
import com.sparrowwallet.drongo.crypto.ECKey;
|
||||
import com.sparrowwallet.drongo.policy.PolicyType;
|
||||
import com.sparrowwallet.drongo.protocol.ScriptType;
|
||||
import com.sparrowwallet.drongo.psbt.PSBT;
|
||||
import com.sparrowwallet.drongo.wallet.Keystore;
|
||||
@@ -107,7 +108,7 @@ public abstract class CardApi {
|
||||
|
||||
public abstract Service<Void> getInitializationService(byte[] entropy, StringProperty messageProperty);
|
||||
|
||||
public abstract Service<Keystore> getImportService(List<ChildNumber> derivation, StringProperty messageProperty);
|
||||
public abstract Service<Keystore> getImportService(PolicyType policyType, List<ChildNumber> derivation, StringProperty messageProperty);
|
||||
|
||||
public abstract Service<PSBT> getSignService(Wallet wallet, PSBT psbt, StringProperty messageProperty);
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.sparrowwallet.sparrow.io;
|
||||
|
||||
import com.sparrowwallet.drongo.policy.PolicyType;
|
||||
import com.sparrowwallet.drongo.protocol.ScriptType;
|
||||
import com.sparrowwallet.drongo.wallet.Keystore;
|
||||
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||
@@ -19,10 +20,9 @@ public class CoboVaultMultisig extends ColdcardMultisig {
|
||||
}
|
||||
|
||||
@Override
|
||||
public Keystore getKeystore(ScriptType scriptType, InputStream inputStream, String password) throws ImportException {
|
||||
Keystore keystore = super.getKeystore(scriptType, inputStream, password);
|
||||
public Keystore getKeystore(PolicyType policyType, ScriptType scriptType, InputStream inputStream, String password) throws ImportException {
|
||||
Keystore keystore = super.getKeystore(policyType, scriptType, inputStream, password);
|
||||
keystore.setLabel("Cobo Vault");
|
||||
keystore.setWalletModel(getWalletModel());
|
||||
|
||||
return keystore;
|
||||
}
|
||||
|
||||
@@ -35,7 +35,11 @@ public class CoboVaultSinglesig implements KeystoreFileImport, WalletImport {
|
||||
}
|
||||
|
||||
@Override
|
||||
public Keystore getKeystore(ScriptType scriptType, InputStream inputStream, String password) throws ImportException {
|
||||
public Keystore getKeystore(PolicyType policyType, ScriptType scriptType, InputStream inputStream, String password) throws ImportException {
|
||||
if(policyType == PolicyType.SINGLE_SP) {
|
||||
throw new ImportException(getName() + " does not support receiving silent payments");
|
||||
}
|
||||
|
||||
try {
|
||||
Gson gson = new Gson();
|
||||
CoboVaultSinglesigKeystore coboKeystore = gson.fromJson(new InputStreamReader(inputStream, StandardCharsets.UTF_8), CoboVaultSinglesigKeystore.class);
|
||||
@@ -47,7 +51,7 @@ public class CoboVaultSinglesig implements KeystoreFileImport, WalletImport {
|
||||
Keystore keystore = new Keystore();
|
||||
keystore.setLabel(getName());
|
||||
keystore.setSource(KeystoreSource.HW_AIRGAPPED);
|
||||
keystore.setWalletModel(WalletModel.COBO_VAULT);
|
||||
keystore.setWalletModel(getWalletModel());
|
||||
keystore.setKeyDerivation(new KeyDerivation(coboKeystore.MasterFingerprint.toLowerCase(Locale.ROOT), "m/" + coboKeystore.AccountKeyPath, true));
|
||||
keystore.setExtendedPublicKey(ExtendedKey.fromDescriptor(coboKeystore.ExtPubKey));
|
||||
|
||||
@@ -70,13 +74,13 @@ public class CoboVaultSinglesig implements KeystoreFileImport, WalletImport {
|
||||
@Override
|
||||
public Wallet importWallet(InputStream inputStream, String password) throws ImportException {
|
||||
//Use default of P2WPKH
|
||||
Keystore keystore = getKeystore(ScriptType.P2WPKH, inputStream, "");
|
||||
Keystore keystore = getKeystore(PolicyType.SINGLE_HD, ScriptType.P2WPKH, inputStream, "");
|
||||
|
||||
Wallet wallet = new Wallet();
|
||||
wallet.setPolicyType(PolicyType.SINGLE);
|
||||
wallet.setPolicyType(PolicyType.SINGLE_HD);
|
||||
wallet.setScriptType(ScriptType.P2WPKH);
|
||||
wallet.getKeystores().add(keystore);
|
||||
wallet.setDefaultPolicy(Policy.getPolicy(PolicyType.SINGLE, ScriptType.P2WPKH, wallet.getKeystores(), null));
|
||||
wallet.setDefaultPolicy(Policy.getPolicy(PolicyType.SINGLE_HD, ScriptType.P2WPKH, wallet.getKeystores(), null));
|
||||
|
||||
try {
|
||||
wallet.checkWallet();
|
||||
|
||||
@@ -32,7 +32,7 @@ public class ColdcardMultisig implements WalletImport, KeystoreFileImport, Walle
|
||||
}
|
||||
|
||||
@Override
|
||||
public Keystore getKeystore(ScriptType scriptType, InputStream inputStream, String password) throws ImportException {
|
||||
public Keystore getKeystore(PolicyType policyType, ScriptType scriptType, InputStream inputStream, String password) throws ImportException {
|
||||
try {
|
||||
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||
inputStream.transferTo(baos);
|
||||
@@ -41,9 +41,9 @@ public class ColdcardMultisig implements WalletImport, KeystoreFileImport, Walle
|
||||
|
||||
Keystore keystore;
|
||||
try {
|
||||
keystore = getKeystoreMultisig(scriptType, firstClone, password);
|
||||
keystore = getKeystoreMultisig(policyType, scriptType, firstClone, password);
|
||||
} catch(Exception e) {
|
||||
keystore = getKeystoreSinglesig(scriptType, secondClone, password);
|
||||
keystore = getKeystoreSinglesig(policyType, scriptType, secondClone, password);
|
||||
}
|
||||
|
||||
return keystore;
|
||||
@@ -52,18 +52,18 @@ public class ColdcardMultisig implements WalletImport, KeystoreFileImport, Walle
|
||||
}
|
||||
}
|
||||
|
||||
private Keystore getKeystoreSinglesig(ScriptType scriptType, InputStream inputStream, String password) throws ImportException {
|
||||
private Keystore getKeystoreSinglesig(PolicyType policyType, ScriptType scriptType, InputStream inputStream, String password) throws ImportException {
|
||||
ColdcardSinglesig coldcardSinglesig = new ColdcardSinglesig();
|
||||
return coldcardSinglesig.getKeystore(scriptType, inputStream, password);
|
||||
return coldcardSinglesig.getKeystore(policyType, scriptType, inputStream, password);
|
||||
}
|
||||
|
||||
public Keystore getKeystoreMultisig(ScriptType scriptType, InputStream inputStream, String password) throws ImportException {
|
||||
public Keystore getKeystoreMultisig(PolicyType policyType, ScriptType scriptType, InputStream inputStream, String password) throws ImportException {
|
||||
InputStreamReader reader = new InputStreamReader(inputStream, StandardCharsets.UTF_8);
|
||||
ColdcardKeystore cck = JsonPersistence.getGson().fromJson(reader, ColdcardKeystore.class);
|
||||
|
||||
Keystore keystore = new Keystore("Coldcard");
|
||||
keystore.setSource(KeystoreSource.HW_AIRGAPPED);
|
||||
keystore.setWalletModel(WalletModel.COLDCARD);
|
||||
keystore.setWalletModel(getWalletModel());
|
||||
|
||||
try {
|
||||
if(cck.xpub != null && cck.path != null) {
|
||||
@@ -119,7 +119,7 @@ public class ColdcardMultisig implements WalletImport, KeystoreFileImport, Walle
|
||||
@Override
|
||||
public Wallet importWallet(InputStream inputStream, String password) throws ImportException {
|
||||
Wallet wallet = new Wallet();
|
||||
wallet.setPolicyType(PolicyType.MULTI);
|
||||
wallet.setPolicyType(PolicyType.MULTI_HD);
|
||||
|
||||
int threshold = 2;
|
||||
ScriptType scriptType = ScriptType.P2SH;
|
||||
@@ -167,7 +167,7 @@ public class ColdcardMultisig implements WalletImport, KeystoreFileImport, Walle
|
||||
}
|
||||
|
||||
|
||||
Policy policy = Policy.getPolicy(PolicyType.MULTI, scriptType, wallet.getKeystores(), threshold);
|
||||
Policy policy = Policy.getPolicy(PolicyType.MULTI_HD, scriptType, wallet.getKeystores(), threshold);
|
||||
wallet.setDefaultPolicy(policy);
|
||||
wallet.setScriptType(scriptType);
|
||||
|
||||
@@ -194,7 +194,7 @@ public class ColdcardMultisig implements WalletImport, KeystoreFileImport, Walle
|
||||
throw new ExportException("Cannot export an incomplete wallet");
|
||||
}
|
||||
|
||||
if(!wallet.getPolicyType().equals(PolicyType.MULTI)) {
|
||||
if(!wallet.getPolicyType().equals(PolicyType.MULTI_HD)) {
|
||||
throw new ExportException(getName() + " import requires a multisig wallet");
|
||||
}
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ import com.sparrowwallet.drongo.KeyDerivation;
|
||||
import com.sparrowwallet.drongo.policy.Policy;
|
||||
import com.sparrowwallet.drongo.policy.PolicyType;
|
||||
import com.sparrowwallet.drongo.protocol.ScriptType;
|
||||
import com.sparrowwallet.drongo.silentpayments.SilentPaymentScanAddress;
|
||||
import com.sparrowwallet.drongo.wallet.*;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
@@ -54,21 +55,37 @@ public class ColdcardSinglesig implements KeystoreFileImport, WalletImport {
|
||||
}
|
||||
|
||||
@Override
|
||||
public Keystore getKeystore(ScriptType scriptType, InputStream inputStream, String password) throws ImportException {
|
||||
public Keystore getKeystore(PolicyType policyType, ScriptType scriptType, InputStream inputStream, String password) throws ImportException {
|
||||
try {
|
||||
Gson gson = new Gson();
|
||||
Type stringStringMap = new TypeToken<Map<String, JsonElement>>() {
|
||||
}.getType();
|
||||
Map<String, JsonElement> map = gson.fromJson(new InputStreamReader(inputStream, StandardCharsets.UTF_8), stringStringMap);
|
||||
|
||||
if (map.get("xfp") == null) {
|
||||
throw new ImportException("File was not a valid " + getName() + " wallet export");
|
||||
if(map.get("xfp") == null) {
|
||||
throw new ImportException("Export was not a valid " + getName() + " wallet export");
|
||||
}
|
||||
|
||||
String masterFingerprint = map.get("xfp").getAsString();
|
||||
|
||||
for (String key : map.keySet()) {
|
||||
if (key.startsWith("bip")) {
|
||||
if(policyType == PolicyType.SINGLE_SP) {
|
||||
JsonElement bip352Element = map.get("bip352");
|
||||
if(bip352Element == null) {
|
||||
throw new ImportException("Export does not contain the spscan value for silent payments");
|
||||
}
|
||||
|
||||
ColdcardKeystore ck = gson.fromJson(bip352Element, ColdcardKeystore.class);
|
||||
Keystore keystore = new Keystore();
|
||||
keystore.setLabel(getName());
|
||||
keystore.setSource(KeystoreSource.HW_AIRGAPPED);
|
||||
keystore.setWalletModel(getWalletModel());
|
||||
keystore.setKeyDerivation(new KeyDerivation(masterFingerprint, ck.deriv, true));
|
||||
keystore.setSilentPaymentScanAddress(SilentPaymentScanAddress.fromKeyString(ck.spscan));
|
||||
return keystore;
|
||||
}
|
||||
|
||||
for(String key : map.keySet()) {
|
||||
if(key.startsWith("bip")) {
|
||||
ColdcardKeystore ck = gson.fromJson(map.get(key), ColdcardKeystore.class);
|
||||
|
||||
if(ck.name != null) {
|
||||
@@ -77,7 +94,7 @@ public class ColdcardSinglesig implements KeystoreFileImport, WalletImport {
|
||||
Keystore keystore = new Keystore();
|
||||
keystore.setLabel(getName());
|
||||
keystore.setSource(KeystoreSource.HW_AIRGAPPED);
|
||||
keystore.setWalletModel(WalletModel.COLDCARD);
|
||||
keystore.setWalletModel(getWalletModel());
|
||||
keystore.setKeyDerivation(new KeyDerivation(masterFingerprint, ck.deriv, true));
|
||||
keystore.setExtendedPublicKey(ExtendedKey.fromDescriptor(ck.xpub));
|
||||
|
||||
@@ -86,7 +103,7 @@ public class ColdcardSinglesig implements KeystoreFileImport, WalletImport {
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
} catch(Exception e) {
|
||||
throw new ImportException("Error getting " + getName() + " keystore", e);
|
||||
}
|
||||
|
||||
@@ -101,13 +118,13 @@ public class ColdcardSinglesig implements KeystoreFileImport, WalletImport {
|
||||
@Override
|
||||
public Wallet importWallet(InputStream inputStream, String password) throws ImportException {
|
||||
//Use default of P2WPKH
|
||||
Keystore keystore = getKeystore(ScriptType.P2WPKH, inputStream, "");
|
||||
Keystore keystore = getKeystore(PolicyType.SINGLE_HD, ScriptType.P2WPKH, inputStream, "");
|
||||
|
||||
Wallet wallet = new Wallet();
|
||||
wallet.setPolicyType(PolicyType.SINGLE);
|
||||
wallet.setPolicyType(PolicyType.SINGLE_HD);
|
||||
wallet.setScriptType(ScriptType.P2WPKH);
|
||||
wallet.getKeystores().add(keystore);
|
||||
wallet.setDefaultPolicy(Policy.getPolicy(PolicyType.SINGLE, ScriptType.P2WPKH, wallet.getKeystores(), null));
|
||||
wallet.setDefaultPolicy(Policy.getPolicy(PolicyType.SINGLE_HD, ScriptType.P2WPKH, wallet.getKeystores(), null));
|
||||
|
||||
try {
|
||||
wallet.checkWallet();
|
||||
@@ -122,6 +139,7 @@ public class ColdcardSinglesig implements KeystoreFileImport, WalletImport {
|
||||
public String deriv;
|
||||
public String name;
|
||||
public String xpub;
|
||||
public String spscan;
|
||||
public String xfp;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,12 +18,12 @@ import org.slf4j.LoggerFactory;
|
||||
import java.io.*;
|
||||
import java.lang.reflect.Type;
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static com.sparrowwallet.sparrow.AppServices.ENUMERATE_HW_PERIOD_SECS;
|
||||
import static com.sparrowwallet.sparrow.net.PagedBatchRequestBuilder.DEFAULT_PAGE_SIZE;
|
||||
import static com.sparrowwallet.sparrow.net.TcpTransport.DEFAULT_MAX_TIMEOUT;
|
||||
import static com.sparrowwallet.sparrow.wallet.WalletUtxosEntry.DUST_ATTACK_THRESHOLD_SATS;
|
||||
import static com.sparrowwallet.sparrow.wallet.WalletUtxosEntry.DUST_ATTACK_THRESHOLD_SP_SATS;
|
||||
|
||||
public class Config {
|
||||
private static final Logger log = LoggerFactory.getLogger(Config.class);
|
||||
@@ -64,6 +64,7 @@ public class Config {
|
||||
private List<File> recentWalletFiles;
|
||||
private Integer keyDerivationPeriod;
|
||||
private long dustAttackThreshold = DUST_ATTACK_THRESHOLD_SATS;
|
||||
private long dustAttackThresholdSp = DUST_ATTACK_THRESHOLD_SP_SATS;
|
||||
private int enumerateHwPeriod = ENUMERATE_HW_PERIOD_SECS;
|
||||
private QRDensity qrDensity;
|
||||
private QREncoding qrEncoding;
|
||||
@@ -107,8 +108,8 @@ public class Config {
|
||||
}
|
||||
|
||||
private static File getConfigFile() {
|
||||
File sparrowDir = Storage.getSparrowDir();
|
||||
return new File(sparrowDir, CONFIG_FILENAME);
|
||||
File configDir = Storage.getConfigDir();
|
||||
return new File(configDir, CONFIG_FILENAME);
|
||||
}
|
||||
|
||||
private static Config load() {
|
||||
@@ -448,6 +449,10 @@ public class Config {
|
||||
return dustAttackThreshold;
|
||||
}
|
||||
|
||||
public long getDustAttackThresholdSp() {
|
||||
return dustAttackThresholdSp;
|
||||
}
|
||||
|
||||
public int getEnumerateHwPeriod() {
|
||||
return enumerateHwPeriod;
|
||||
}
|
||||
@@ -556,13 +561,6 @@ public class Config {
|
||||
flush();
|
||||
}
|
||||
|
||||
public void changePublicServer() {
|
||||
List<Server> otherServers = PublicElectrumServer.getServers().stream().map(PublicElectrumServer::getServer).filter(server -> !server.equals(getPublicElectrumServer())).collect(Collectors.toList());
|
||||
if(!otherServers.isEmpty()) {
|
||||
setPublicElectrumServer(otherServers.get(new Random().nextInt(otherServers.size())));
|
||||
}
|
||||
}
|
||||
|
||||
public Server getCoreServer() {
|
||||
return coreServer;
|
||||
}
|
||||
|
||||
@@ -3,10 +3,10 @@ package com.sparrowwallet.sparrow.io;
|
||||
import com.sparrowwallet.drongo.KeyDerivation;
|
||||
import com.sparrowwallet.drongo.KeyPurpose;
|
||||
import com.sparrowwallet.drongo.OutputDescriptor;
|
||||
import com.sparrowwallet.drongo.policy.PolicyType;
|
||||
import com.sparrowwallet.drongo.wallet.Keystore;
|
||||
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||
import com.sparrowwallet.drongo.wallet.WalletModel;
|
||||
import com.sparrowwallet.sparrow.wallet.KeystoreController;
|
||||
|
||||
import java.io.*;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
@@ -29,26 +29,43 @@ public class Descriptor implements WalletImport, WalletExport {
|
||||
public void exportWallet(Wallet wallet, OutputStream outputStream, String password) throws ExportException {
|
||||
try {
|
||||
BufferedWriter bufferedWriter = new BufferedWriter(new OutputStreamWriter(outputStream));
|
||||
bufferedWriter.write("# Receive and change descriptor (BIP389):");
|
||||
bufferedWriter.newLine();
|
||||
if(wallet.getPolicyType() == PolicyType.SINGLE_SP) {
|
||||
OutputDescriptor outputDescriptor = OutputDescriptor.getOutputDescriptor(wallet);
|
||||
|
||||
OutputDescriptor outputDescriptor = OutputDescriptor.getOutputDescriptor(wallet, KeyPurpose.DEFAULT_PURPOSES, null);
|
||||
bufferedWriter.write(outputDescriptor.toString(true));
|
||||
bufferedWriter.newLine();
|
||||
bufferedWriter.newLine();
|
||||
bufferedWriter.newLine();
|
||||
bufferedWriter.write("# Single argument descriptor:");
|
||||
bufferedWriter.newLine();
|
||||
|
||||
bufferedWriter.write("# Receive descriptor (Bitcoin Core):");
|
||||
bufferedWriter.newLine();
|
||||
OutputDescriptor receiveDescriptor = OutputDescriptor.getOutputDescriptor(wallet, KeyPurpose.RECEIVE, null);
|
||||
bufferedWriter.write(receiveDescriptor.toString(true));
|
||||
bufferedWriter.newLine();
|
||||
bufferedWriter.newLine();
|
||||
bufferedWriter.write("# Change descriptor (Bitcoin Core):");
|
||||
bufferedWriter.newLine();
|
||||
OutputDescriptor changeDescriptor = OutputDescriptor.getOutputDescriptor(wallet, KeyPurpose.CHANGE, null);
|
||||
bufferedWriter.write(changeDescriptor.toString(true));
|
||||
bufferedWriter.newLine();
|
||||
bufferedWriter.write(outputDescriptor.toString(true));
|
||||
bufferedWriter.newLine();
|
||||
bufferedWriter.newLine();
|
||||
|
||||
bufferedWriter.write("# Two argument descriptor:");
|
||||
bufferedWriter.newLine();
|
||||
|
||||
bufferedWriter.write(outputDescriptor.toString(true, true, true, true));
|
||||
bufferedWriter.newLine();
|
||||
} else {
|
||||
bufferedWriter.write("# Receive and change descriptor:");
|
||||
bufferedWriter.newLine();
|
||||
|
||||
OutputDescriptor outputDescriptor = OutputDescriptor.getOutputDescriptor(wallet, KeyPurpose.DEFAULT_PURPOSES, null);
|
||||
bufferedWriter.write(outputDescriptor.toString(true));
|
||||
bufferedWriter.newLine();
|
||||
bufferedWriter.newLine();
|
||||
bufferedWriter.newLine();
|
||||
|
||||
bufferedWriter.write("# Receive descriptor:");
|
||||
bufferedWriter.newLine();
|
||||
OutputDescriptor receiveDescriptor = OutputDescriptor.getOutputDescriptor(wallet, KeyPurpose.RECEIVE, null);
|
||||
bufferedWriter.write(receiveDescriptor.toString(true));
|
||||
bufferedWriter.newLine();
|
||||
bufferedWriter.newLine();
|
||||
bufferedWriter.write("# Change descriptor:");
|
||||
bufferedWriter.newLine();
|
||||
OutputDescriptor changeDescriptor = OutputDescriptor.getOutputDescriptor(wallet, KeyPurpose.CHANGE, null);
|
||||
bufferedWriter.write(changeDescriptor.toString(true));
|
||||
bufferedWriter.newLine();
|
||||
}
|
||||
|
||||
bufferedWriter.flush();
|
||||
} catch(Exception e) {
|
||||
|
||||
@@ -40,10 +40,10 @@ public class Electrum implements KeystoreFileImport, WalletImport, WalletExport
|
||||
}
|
||||
|
||||
@Override
|
||||
public Keystore getKeystore(ScriptType scriptType, InputStream inputStream, String password) throws ImportException {
|
||||
public Keystore getKeystore(PolicyType policyType, ScriptType scriptType, InputStream inputStream, String password) throws ImportException {
|
||||
Wallet wallet = importWallet(inputStream, password);
|
||||
|
||||
if(!wallet.getPolicyType().equals(PolicyType.SINGLE) || wallet.getKeystores().size() != 1) {
|
||||
if(wallet.getPolicyType().equals(PolicyType.MULTI_HD) || wallet.getKeystores().size() != 1) {
|
||||
throw new ImportException("Multisig wallet detected - import it using File > Import Wallet");
|
||||
}
|
||||
|
||||
@@ -203,13 +203,13 @@ public class Electrum implements KeystoreFileImport, WalletImport, WalletExport
|
||||
wallet.setScriptType(scriptType);
|
||||
|
||||
if(ew.wallet_type.equals("standard")) {
|
||||
wallet.setPolicyType(PolicyType.SINGLE);
|
||||
wallet.setDefaultPolicy(Policy.getPolicy(PolicyType.SINGLE, scriptType, wallet.getKeystores(), 1));
|
||||
wallet.setPolicyType(PolicyType.SINGLE_HD);
|
||||
wallet.setDefaultPolicy(Policy.getPolicy(PolicyType.SINGLE_HD, scriptType, wallet.getKeystores(), 1));
|
||||
} else if(ew.wallet_type.contains("of")) {
|
||||
wallet.setPolicyType(PolicyType.MULTI);
|
||||
wallet.setPolicyType(PolicyType.MULTI_HD);
|
||||
String[] mOfn = ew.wallet_type.split("of");
|
||||
int threshold = Integer.parseInt(mOfn[0]);
|
||||
wallet.setDefaultPolicy(Policy.getPolicy(PolicyType.MULTI, scriptType, wallet.getKeystores(), threshold));
|
||||
wallet.setDefaultPolicy(Policy.getPolicy(PolicyType.MULTI_HD, scriptType, wallet.getKeystores(), threshold));
|
||||
} else {
|
||||
throw new ImportException("Unknown Electrum wallet type of " + ew.wallet_type);
|
||||
}
|
||||
@@ -308,9 +308,9 @@ public class Electrum implements KeystoreFileImport, WalletImport, WalletExport
|
||||
public void exportWallet(Wallet wallet, OutputStream outputStream, String password) throws ExportException {
|
||||
try {
|
||||
ElectrumJsonWallet ew = new ElectrumJsonWallet();
|
||||
if(wallet.getPolicyType().equals(PolicyType.SINGLE)) {
|
||||
if(wallet.getPolicyType().equals(PolicyType.SINGLE_HD)) {
|
||||
ew.wallet_type = "standard";
|
||||
} else if(wallet.getPolicyType().equals(PolicyType.MULTI)) {
|
||||
} else if(wallet.getPolicyType().equals(PolicyType.MULTI_HD)) {
|
||||
ew.wallet_type = wallet.getDefaultPolicy().getNumSignaturesRequired() + "of" + wallet.getKeystores().size();
|
||||
} else {
|
||||
throw new ExportException("Could not export a wallet with a " + wallet.getPolicyType() + " policy");
|
||||
@@ -367,9 +367,9 @@ public class Electrum implements KeystoreFileImport, WalletImport, WalletExport
|
||||
throw new ExportException("Cannot export a keystore of source " + keystore.getSource());
|
||||
}
|
||||
|
||||
if(wallet.getPolicyType().equals(PolicyType.SINGLE)) {
|
||||
if(wallet.getPolicyType().equals(PolicyType.SINGLE_HD)) {
|
||||
ew.keystores.put("keystore", ek);
|
||||
} else if(wallet.getPolicyType().equals(PolicyType.MULTI)) {
|
||||
} else if(wallet.getPolicyType().equals(PolicyType.MULTI_HD)) {
|
||||
ew.keystores.put("x" + index + "/", ek);
|
||||
}
|
||||
|
||||
|
||||
@@ -30,6 +30,10 @@ public class ElectrumPersonalServer implements WalletExport {
|
||||
|
||||
@Override
|
||||
public void exportWallet(Wallet wallet, OutputStream outputStream, String password) throws ExportException {
|
||||
if(wallet.getPolicyType() == PolicyType.SINGLE_SP) {
|
||||
throw new ExportException(getName() + " does not support silent payments wallets.");
|
||||
}
|
||||
|
||||
if(wallet.getScriptType() == ScriptType.P2TR) {
|
||||
throw new ExportException(getName() + " does not support Taproot wallets.");
|
||||
}
|
||||
@@ -61,7 +65,7 @@ public class ElectrumPersonalServer implements WalletExport {
|
||||
writer.write(wallet.getFullName().replace(' ', '_') + " = ");
|
||||
|
||||
ExtendedKey.Header xpubHeader = ExtendedKey.Header.fromScriptType(wallet.getScriptType(), false);
|
||||
if(wallet.getPolicyType() == PolicyType.MULTI) {
|
||||
if(wallet.getPolicyType() == PolicyType.MULTI_HD) {
|
||||
writer.write(wallet.getDefaultPolicy().getNumSignaturesRequired() + " ");
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
package com.sparrowwallet.sparrow.io;
|
||||
|
||||
import com.sparrowwallet.drongo.policy.PolicyType;
|
||||
import com.sparrowwallet.drongo.protocol.ScriptType;
|
||||
import com.sparrowwallet.drongo.wallet.Keystore;
|
||||
import com.sparrowwallet.drongo.wallet.WalletModel;
|
||||
|
||||
import java.io.InputStream;
|
||||
|
||||
public class EraSinglesig extends KeystoneSinglesig {
|
||||
@Override
|
||||
public String getName() {
|
||||
return "ERA Wallet";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getKeystoreImportDescription(int account) {
|
||||
return "Import QR created on your ERA Wallet by opening your wallet and selecting Link > Sparrow from the wallet menu.";
|
||||
}
|
||||
|
||||
@Override
|
||||
public WalletModel getWalletModel() {
|
||||
return WalletModel.ERA_WALLET;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Keystore getKeystore(PolicyType policyType, ScriptType scriptType, InputStream inputStream, String password) throws ImportException {
|
||||
Keystore keystore = super.getKeystore(policyType, scriptType, inputStream, password);
|
||||
keystore.setLabel("ERA Wallet");
|
||||
|
||||
return keystore;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isFileFormatAvailable() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isWalletImportFileFormatAvailable() {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
package com.sparrowwallet.sparrow.io;
|
||||
|
||||
import com.sparrowwallet.drongo.Network;
|
||||
import com.sparrowwallet.drongo.policy.PolicyType;
|
||||
import com.sparrowwallet.drongo.protocol.ScriptType;
|
||||
import com.sparrowwallet.drongo.wallet.Keystore;
|
||||
import com.sparrowwallet.drongo.wallet.WalletModel;
|
||||
@@ -25,7 +26,7 @@ public class GordianSeedTool implements KeystoreFileImport {
|
||||
}
|
||||
|
||||
@Override
|
||||
public Keystore getKeystore(ScriptType scriptType, InputStream inputStream, String password) throws ImportException {
|
||||
public Keystore getKeystore(PolicyType policyType, ScriptType scriptType, InputStream inputStream, String password) throws ImportException {
|
||||
throw new ImportException("Only QR imports are supported.");
|
||||
}
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ 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.silentpayments.SilentPaymentScanAddress;
|
||||
import com.sparrowwallet.drongo.wallet.StandardAccount;
|
||||
import com.sparrowwallet.drongo.wallet.WalletModel;
|
||||
import com.sparrowwallet.lark.DeviceException;
|
||||
@@ -145,7 +146,7 @@ public class Hwi {
|
||||
}
|
||||
}
|
||||
|
||||
public Map<WalletType, String> getXpubs(Device device, String passphrase, Map<WalletType, String> accountDerivationPaths, Map<WalletType, String> accountXpubs) throws ImportException {
|
||||
public Map<WalletType, ExtendedKey> getXpubs(Device device, String passphrase, Map<WalletType, String> accountDerivationPaths, Map<WalletType, ExtendedKey> accountXpubs) throws ImportException {
|
||||
for(Map.Entry<WalletType, String> entry : accountDerivationPaths.entrySet()) {
|
||||
accountXpubs.put(entry.getKey(), getXpub(device, passphrase, entry.getValue()));
|
||||
}
|
||||
@@ -153,12 +154,12 @@ public class Hwi {
|
||||
return accountXpubs;
|
||||
}
|
||||
|
||||
public String getXpub(Device device, String passphrase, String derivationPath) throws ImportException {
|
||||
public ExtendedKey getXpub(Device device, String passphrase, String derivationPath) throws ImportException {
|
||||
try {
|
||||
Lark lark = getLark(passphrase);
|
||||
ExtendedKey xpub = lark.getPubKeyAtPath(device.getType(), device.getPath(), derivationPath);
|
||||
isPromptActive = false;
|
||||
return xpub.toString();
|
||||
return xpub;
|
||||
} catch(DeviceException e) {
|
||||
throw new ImportException(e.getMessage(), e);
|
||||
} catch(RuntimeException e) {
|
||||
@@ -167,6 +168,20 @@ public class Hwi {
|
||||
}
|
||||
}
|
||||
|
||||
public SilentPaymentScanAddress getSpscan(Device device, String passphrase, String derivationPath) throws ImportException {
|
||||
try {
|
||||
Lark lark = getLark(passphrase);
|
||||
SilentPaymentScanAddress spscan = lark.getSpscanAtPath(device.getType(), device.getPath(), derivationPath);
|
||||
isPromptActive = false;
|
||||
return spscan;
|
||||
} catch(DeviceException e) {
|
||||
throw new ImportException(e.getMessage(), e);
|
||||
} catch(RuntimeException e) {
|
||||
log.error("Error retrieving spscan", e);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
public String displayAddress(Device device, String passphrase, ScriptType scriptType, OutputDescriptor addressDescriptor,
|
||||
OutputDescriptor walletDescriptor, String walletName, byte[] walletRegistration) throws DisplayAddressException {
|
||||
try {
|
||||
@@ -254,7 +269,7 @@ public class Hwi {
|
||||
private static void deleteHwiDir() {
|
||||
try {
|
||||
if(OsType.getCurrent() == OsType.MACOS || OsType.getCurrent() == OsType.WINDOWS) {
|
||||
File hwiHomeDir = new File(Storage.getSparrowDir(), HWI_HOME_DIR);
|
||||
File hwiHomeDir = new File(Storage.getCacheDir(), HWI_HOME_DIR);
|
||||
if(hwiHomeDir.exists()) {
|
||||
IOUtils.deleteDirectory(hwiHomeDir);
|
||||
}
|
||||
@@ -421,7 +436,7 @@ public class Hwi {
|
||||
}
|
||||
}
|
||||
|
||||
public static class GetXpubService extends Service<String> {
|
||||
public static class GetXpubService extends Service<ExtendedKey> {
|
||||
private final Device device;
|
||||
private final String passphrase;
|
||||
private final String derivationPath;
|
||||
@@ -433,9 +448,9 @@ public class Hwi {
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Task<String> createTask() {
|
||||
protected Task<ExtendedKey> createTask() {
|
||||
return new Task<>() {
|
||||
protected String call() throws ImportException {
|
||||
protected ExtendedKey call() throws ImportException {
|
||||
Hwi hwi = new Hwi();
|
||||
return hwi.getXpub(device, passphrase, derivationPath);
|
||||
}
|
||||
@@ -443,7 +458,29 @@ public class Hwi {
|
||||
}
|
||||
}
|
||||
|
||||
public static class GetXpubsService extends Service<Map<WalletType, String>> {
|
||||
public static class GetSpscanService extends Service<SilentPaymentScanAddress> {
|
||||
private final Device device;
|
||||
private final String passphrase;
|
||||
private final String derivationPath;
|
||||
|
||||
public GetSpscanService(Device device, String passphrase, String derivationPath) {
|
||||
this.device = device;
|
||||
this.passphrase = passphrase;
|
||||
this.derivationPath = derivationPath;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Task<SilentPaymentScanAddress> createTask() {
|
||||
return new Task<>() {
|
||||
protected SilentPaymentScanAddress call() throws ImportException {
|
||||
Hwi hwi = new Hwi();
|
||||
return hwi.getSpscan(device, passphrase, derivationPath);
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
public static class GetXpubsService extends Service<Map<WalletType, ExtendedKey>> {
|
||||
private final Device device;
|
||||
private final String passphrase;
|
||||
private final Map<WalletType, String> accountDerivationPaths;
|
||||
@@ -455,13 +492,13 @@ public class Hwi {
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Task<Map<WalletType, String>> createTask() {
|
||||
protected Task<Map<WalletType, ExtendedKey>> createTask() {
|
||||
return new Task<>() {
|
||||
protected Map<WalletType, String> call() throws ImportException {
|
||||
protected Map<WalletType, ExtendedKey> call() throws ImportException {
|
||||
Hwi hwi = new Hwi();
|
||||
updateProgress(0, accountDerivationPaths.size());
|
||||
ObservableMap<WalletType, String> accountXpubs = FXCollections.observableMap(new LinkedHashMap<>());
|
||||
accountXpubs.addListener((MapChangeListener<? super WalletType, ? super String>) _ -> updateProgress(accountXpubs.size(), accountDerivationPaths.size()));
|
||||
ObservableMap<WalletType, ExtendedKey> accountXpubs = FXCollections.observableMap(new LinkedHashMap<>());
|
||||
accountXpubs.addListener((MapChangeListener<? super WalletType, ? super ExtendedKey>) _ -> updateProgress(accountXpubs.size(), accountDerivationPaths.size()));
|
||||
return hwi.getXpubs(device, passphrase, accountDerivationPaths, accountXpubs);
|
||||
}
|
||||
};
|
||||
@@ -507,7 +544,7 @@ public class Hwi {
|
||||
private BitBoxPairingDialog pairingDialog;
|
||||
|
||||
public BitBoxFxNoiseConfig() {
|
||||
super(Path.of(Storage.getSparrowHome().getAbsolutePath(), LARK_HOME_DIR, BITBOX_FILENAME).toFile());
|
||||
super(Path.of(Storage.getDataHome().getAbsolutePath(), LARK_HOME_DIR, BITBOX_FILENAME).toFile());
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -557,7 +594,7 @@ public class Hwi {
|
||||
private String deviceInfo;
|
||||
|
||||
public TrezorFxNoiseConfig() {
|
||||
super(Path.of(Storage.getSparrowHome().getAbsolutePath(), LARK_HOME_DIR, TREZOR_FILENAME).toFile());
|
||||
super(Path.of(Storage.getDataHome().getAbsolutePath(), LARK_HOME_DIR, TREZOR_FILENAME).toFile());
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.sparrowwallet.sparrow.io;
|
||||
|
||||
import com.sparrowwallet.drongo.policy.PolicyType;
|
||||
import com.sparrowwallet.drongo.protocol.ScriptType;
|
||||
import com.sparrowwallet.drongo.wallet.Keystore;
|
||||
import com.sparrowwallet.drongo.wallet.WalletModel;
|
||||
@@ -24,7 +25,7 @@ public class Jade implements KeystoreFileImport {
|
||||
}
|
||||
|
||||
@Override
|
||||
public Keystore getKeystore(ScriptType scriptType, InputStream inputStream, String password) throws ImportException {
|
||||
public Keystore getKeystore(PolicyType policyType, ScriptType scriptType, InputStream inputStream, String password) throws ImportException {
|
||||
throw new ImportException("Failed to detect a valid " + scriptType.getDescription() + " keystore.");
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.sparrowwallet.sparrow.io;
|
||||
|
||||
import com.sparrowwallet.drongo.policy.PolicyType;
|
||||
import com.sparrowwallet.drongo.protocol.ScriptType;
|
||||
import com.sparrowwallet.drongo.wallet.Keystore;
|
||||
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||
@@ -63,14 +64,13 @@ public class JadeMultisig extends ColdcardMultisig {
|
||||
Wallet wallet = super.importWallet(inputStream, password);
|
||||
for(Keystore keystore : wallet.getKeystores()) {
|
||||
keystore.setLabel(keystore.getLabel().replace("Coldcard", "Jade"));
|
||||
keystore.setWalletModel(WalletModel.JADE);
|
||||
}
|
||||
|
||||
return wallet;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Keystore getKeystore(ScriptType scriptType, InputStream inputStream, String password) throws ImportException {
|
||||
public Keystore getKeystore(PolicyType policyType, ScriptType scriptType, InputStream inputStream, String password) throws ImportException {
|
||||
throw new ImportException("Failed to detect a valid " + scriptType.getDescription() + " keystore.");
|
||||
}
|
||||
|
||||
|
||||
@@ -4,17 +4,32 @@ import com.google.gson.*;
|
||||
import com.sparrowwallet.drongo.ExtendedKey;
|
||||
import com.sparrowwallet.drongo.FileType;
|
||||
import com.sparrowwallet.drongo.IOUtils;
|
||||
import com.sparrowwallet.drongo.KeyDerivation;
|
||||
import com.sparrowwallet.drongo.Utils;
|
||||
import com.sparrowwallet.drongo.address.Address;
|
||||
import com.sparrowwallet.drongo.address.InvalidAddressException;
|
||||
import com.sparrowwallet.drongo.bip47.PaymentCode;
|
||||
import com.sparrowwallet.drongo.crypto.Argon2KeyDeriver;
|
||||
import com.sparrowwallet.drongo.crypto.AsymmetricKeyDeriver;
|
||||
import com.sparrowwallet.drongo.crypto.ECKey;
|
||||
import com.sparrowwallet.drongo.crypto.EncryptedData;
|
||||
import com.sparrowwallet.drongo.crypto.EncryptionType;
|
||||
import com.sparrowwallet.drongo.protocol.Sha256Hash;
|
||||
import com.sparrowwallet.drongo.protocol.Transaction;
|
||||
import com.sparrowwallet.drongo.silentpayments.SilentPaymentAddress;
|
||||
import com.sparrowwallet.drongo.silentpayments.SilentPaymentScanAddress;
|
||||
import com.sparrowwallet.drongo.policy.Miniscript;
|
||||
import com.sparrowwallet.drongo.policy.Policy;
|
||||
import com.sparrowwallet.drongo.policy.PolicyType;
|
||||
import com.sparrowwallet.drongo.wallet.BlockTransaction;
|
||||
import com.sparrowwallet.drongo.wallet.BlockTransactionHashIndex;
|
||||
import com.sparrowwallet.drongo.wallet.DeterministicSeed;
|
||||
import com.sparrowwallet.drongo.wallet.Keystore;
|
||||
import com.sparrowwallet.drongo.wallet.MasterPrivateExtendedKey;
|
||||
import com.sparrowwallet.drongo.wallet.UtxoMixData;
|
||||
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||
import com.sparrowwallet.drongo.wallet.WalletNode;
|
||||
import com.sparrowwallet.drongo.wallet.WalletTable;
|
||||
|
||||
import java.io.*;
|
||||
import java.lang.reflect.Type;
|
||||
@@ -337,6 +352,36 @@ public class JsonPersistence implements Persistence {
|
||||
gsonBuilder.registerTypeAdapter(Transaction.class, new TransactionDeserializer());
|
||||
gsonBuilder.registerTypeAdapter(Address.class, new AddressSerializer());
|
||||
gsonBuilder.registerTypeAdapter(Address.class, new AddressDeserializer());
|
||||
gsonBuilder.registerTypeAdapter(PolicyType.class, new PolicyTypeSerializer());
|
||||
gsonBuilder.registerTypeAdapter(PolicyType.class, new PolicyTypeDeserializer());
|
||||
gsonBuilder.registerTypeAdapter(File.class, new FileSerializer());
|
||||
gsonBuilder.registerTypeAdapter(File.class, new FileDeserializer());
|
||||
gsonBuilder.registerTypeAdapter(SilentPaymentAddress.class, new SilentPaymentAddressSerializer());
|
||||
gsonBuilder.registerTypeAdapter(SilentPaymentAddress.class, new SilentPaymentAddressDeserializer());
|
||||
gsonBuilder.registerTypeAdapter(SilentPaymentScanAddress.class, new SilentPaymentScanAddressSerializer());
|
||||
gsonBuilder.registerTypeAdapter(SilentPaymentScanAddress.class, new SilentPaymentScanAddressDeserializer());
|
||||
|
||||
//Reflection on any class outside this project fails at Gson adapter construction when running on the module path, unless its module opens the package to Gson
|
||||
//Blocking these classes here ensures classpath-based tests fail in the same way production does, ensuring custom serializers are registered above as necessary
|
||||
gsonBuilder.addReflectionAccessFilter(rawClass -> rawClass.getName().startsWith("com.sparrowwallet.") ? ReflectionAccessFilter.FilterResult.INDECISIVE : ReflectionAccessFilter.FilterResult.BLOCK_ALL);
|
||||
|
||||
//Instance creators are required for classes without no-args constructors, since Gson's fallback of sun.misc.Unsafe is unavailable on the module path (jdk.unsupported is not resolved)
|
||||
//Unsafe is also explicitly disabled below so that classpath-based tests fail in the same way production does if a class is missed here
|
||||
gsonBuilder.disableJdkUnsafe();
|
||||
gsonBuilder.registerTypeAdapter(Policy.class, (InstanceCreator<Policy>) type -> new Policy(null));
|
||||
gsonBuilder.registerTypeAdapter(Miniscript.class, (InstanceCreator<Miniscript>) type -> new Miniscript(null));
|
||||
gsonBuilder.registerTypeAdapter(KeyDerivation.class, (InstanceCreator<KeyDerivation>) type -> new KeyDerivation(null, (String)null));
|
||||
gsonBuilder.registerTypeAdapter(WalletNode.class, (InstanceCreator<WalletNode>) type -> new WalletNode("m/0"));
|
||||
gsonBuilder.registerTypeAdapter(BlockTransaction.class, (InstanceCreator<BlockTransaction>) type -> new BlockTransaction(null, 0, null, null, null));
|
||||
gsonBuilder.registerTypeAdapter(BlockTransactionHashIndex.class, (InstanceCreator<BlockTransactionHashIndex>) type -> new BlockTransactionHashIndex(null, 0, null, null, 0, 0));
|
||||
gsonBuilder.registerTypeAdapter(DeterministicSeed.class, (InstanceCreator<DeterministicSeed>) type -> new DeterministicSeed((EncryptedData)null, false, 0, null));
|
||||
gsonBuilder.registerTypeAdapter(MasterPrivateExtendedKey.class, (InstanceCreator<MasterPrivateExtendedKey>) type -> new MasterPrivateExtendedKey((EncryptedData)null));
|
||||
gsonBuilder.registerTypeAdapter(EncryptedData.class, (InstanceCreator<EncryptedData>) type -> new EncryptedData(new byte[0], new byte[0], null, (EncryptionType)null));
|
||||
gsonBuilder.registerTypeAdapter(EncryptionType.class, (InstanceCreator<EncryptionType>) type -> new EncryptionType(null, null));
|
||||
gsonBuilder.registerTypeAdapter(PaymentCode.class, (InstanceCreator<PaymentCode>) type -> new PaymentCode(new byte[33], new byte[32]));
|
||||
gsonBuilder.registerTypeAdapter(UtxoMixData.class, (InstanceCreator<UtxoMixData>) type -> new UtxoMixData(0, null));
|
||||
gsonBuilder.registerTypeAdapter(WalletTable.class, (InstanceCreator<WalletTable>) type -> new WalletTable(null, null, 0, null));
|
||||
|
||||
if(includeWalletSerializers) {
|
||||
gsonBuilder.registerTypeAdapter(Keystore.class, new KeystoreSerializer());
|
||||
gsonBuilder.registerTypeAdapter(WalletNode.class, new NodeSerializer());
|
||||
@@ -400,6 +445,51 @@ public class JsonPersistence implements Persistence {
|
||||
}
|
||||
}
|
||||
|
||||
//Maintains the same format as the previous reflective serialization of java.io.File, which requires java.base/java.io to be opened to Gson
|
||||
private static class FileSerializer implements JsonSerializer<File> {
|
||||
@Override
|
||||
public JsonElement serialize(File src, Type typeOfSrc, JsonSerializationContext context) {
|
||||
JsonObject jsonObject = new JsonObject();
|
||||
jsonObject.addProperty("path", src.getPath());
|
||||
return jsonObject;
|
||||
}
|
||||
}
|
||||
|
||||
private static class FileDeserializer implements JsonDeserializer<File> {
|
||||
@Override
|
||||
public File deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
|
||||
return new File(json.isJsonObject() ? json.getAsJsonObject().get("path").getAsString() : json.getAsJsonPrimitive().getAsString());
|
||||
}
|
||||
}
|
||||
|
||||
private static class SilentPaymentAddressSerializer implements JsonSerializer<SilentPaymentAddress> {
|
||||
@Override
|
||||
public JsonElement serialize(SilentPaymentAddress src, Type typeOfSrc, JsonSerializationContext context) {
|
||||
return new JsonPrimitive(src.getAddress());
|
||||
}
|
||||
}
|
||||
|
||||
private static class SilentPaymentAddressDeserializer implements JsonDeserializer<SilentPaymentAddress> {
|
||||
@Override
|
||||
public SilentPaymentAddress deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
|
||||
return SilentPaymentAddress.from(json.getAsJsonPrimitive().getAsString());
|
||||
}
|
||||
}
|
||||
|
||||
private static class SilentPaymentScanAddressSerializer implements JsonSerializer<SilentPaymentScanAddress> {
|
||||
@Override
|
||||
public JsonElement serialize(SilentPaymentScanAddress src, Type typeOfSrc, JsonSerializationContext context) {
|
||||
return new JsonPrimitive(src.toKeyString());
|
||||
}
|
||||
}
|
||||
|
||||
private static class SilentPaymentScanAddressDeserializer implements JsonDeserializer<SilentPaymentScanAddress> {
|
||||
@Override
|
||||
public SilentPaymentScanAddress deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
|
||||
return SilentPaymentScanAddress.fromKeyString(json.getAsJsonPrimitive().getAsString());
|
||||
}
|
||||
}
|
||||
|
||||
private static class DateSerializer implements JsonSerializer<Date> {
|
||||
@Override
|
||||
public JsonElement serialize(Date src, Type typeOfSrc, JsonSerializationContext context) {
|
||||
@@ -453,6 +543,30 @@ public class JsonPersistence implements Persistence {
|
||||
}
|
||||
}
|
||||
|
||||
private static class PolicyTypeSerializer implements JsonSerializer<PolicyType> {
|
||||
@Override
|
||||
public JsonElement serialize(PolicyType src, Type typeOfSrc, JsonSerializationContext context) {
|
||||
return switch(src) {
|
||||
case SINGLE_HD -> new JsonPrimitive("SINGLE");
|
||||
case MULTI_HD -> new JsonPrimitive("MULTI");
|
||||
default -> new JsonPrimitive(src.name());
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
private static class PolicyTypeDeserializer implements JsonDeserializer<PolicyType> {
|
||||
@Override
|
||||
public PolicyType deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
|
||||
String value = json.getAsJsonPrimitive().getAsString();
|
||||
|
||||
return switch(value) {
|
||||
case "SINGLE" -> PolicyType.SINGLE_HD;
|
||||
case "MULTI" -> PolicyType.MULTI_HD;
|
||||
default -> PolicyType.valueOf(value);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
private static class KeystoreSerializer implements JsonSerializer<Keystore> {
|
||||
@Override
|
||||
public JsonElement serialize(Keystore keystore, Type typeOfSrc, JsonSerializationContext context) {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.sparrowwallet.sparrow.io;
|
||||
|
||||
import com.sparrowwallet.drongo.policy.PolicyType;
|
||||
import com.sparrowwallet.drongo.protocol.ScriptType;
|
||||
import com.sparrowwallet.drongo.wallet.Keystore;
|
||||
import com.sparrowwallet.drongo.wallet.WalletModel;
|
||||
@@ -23,10 +24,9 @@ public class KeycardShellMultisig extends ColdcardMultisig {
|
||||
}
|
||||
|
||||
@Override
|
||||
public Keystore getKeystore(ScriptType scriptType, InputStream inputStream, String password) throws ImportException {
|
||||
Keystore keystore = super.getKeystore(scriptType, inputStream, password);
|
||||
public Keystore getKeystore(PolicyType policyType, ScriptType scriptType, InputStream inputStream, String password) throws ImportException {
|
||||
Keystore keystore = super.getKeystore(policyType, scriptType, inputStream, password);
|
||||
keystore.setLabel("Keycard Shell");
|
||||
keystore.setWalletModel(getWalletModel());
|
||||
|
||||
return keystore;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package com.sparrowwallet.sparrow.io;
|
||||
|
||||
import com.sparrowwallet.drongo.Network;
|
||||
import com.sparrowwallet.drongo.policy.PolicyType;
|
||||
import com.sparrowwallet.drongo.protocol.ScriptType;
|
||||
import com.sparrowwallet.drongo.wallet.Keystore;
|
||||
import com.sparrowwallet.drongo.wallet.WalletModel;
|
||||
@@ -24,10 +25,9 @@ public class KeycardShellSinglesig extends KeystoneSinglesig {
|
||||
}
|
||||
|
||||
@Override
|
||||
public Keystore getKeystore(ScriptType scriptType, InputStream inputStream, String password) throws ImportException {
|
||||
Keystore keystore = super.getKeystore(scriptType, inputStream, password);
|
||||
public Keystore getKeystore(PolicyType policyType, ScriptType scriptType, InputStream inputStream, String password) throws ImportException {
|
||||
Keystore keystore = super.getKeystore(policyType, scriptType, inputStream, password);
|
||||
keystore.setLabel("Keycard Shell");
|
||||
keystore.setWalletModel(getWalletModel());
|
||||
|
||||
return keystore;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.sparrowwallet.sparrow.io;
|
||||
|
||||
import com.sparrowwallet.drongo.policy.PolicyType;
|
||||
import com.sparrowwallet.drongo.protocol.ScriptType;
|
||||
import com.sparrowwallet.drongo.wallet.Keystore;
|
||||
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||
@@ -19,10 +20,9 @@ public class KeystoneMultisig extends ColdcardMultisig {
|
||||
}
|
||||
|
||||
@Override
|
||||
public Keystore getKeystore(ScriptType scriptType, InputStream inputStream, String password) throws ImportException {
|
||||
Keystore keystore = super.getKeystore(scriptType, inputStream, password);
|
||||
public Keystore getKeystore(PolicyType policyType, ScriptType scriptType, InputStream inputStream, String password) throws ImportException {
|
||||
Keystore keystore = super.getKeystore(policyType, scriptType, inputStream, password);
|
||||
keystore.setLabel("Keystone");
|
||||
keystore.setWalletModel(getWalletModel());
|
||||
|
||||
return keystore;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
package com.sparrowwallet.sparrow.io;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
import com.sparrowwallet.drongo.ExtendedKey;
|
||||
import com.sparrowwallet.drongo.KeyDerivation;
|
||||
import com.sparrowwallet.drongo.OutputDescriptor;
|
||||
@@ -37,7 +36,7 @@ public class KeystoneSinglesig implements KeystoreFileImport, WalletImport {
|
||||
}
|
||||
|
||||
@Override
|
||||
public Keystore getKeystore(ScriptType scriptType, InputStream inputStream, String password) throws ImportException {
|
||||
public Keystore getKeystore(PolicyType policyType, ScriptType scriptType, InputStream inputStream, String password) throws ImportException {
|
||||
try {
|
||||
String outputDescriptor = new BufferedReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8)).lines().collect(Collectors.joining("\n"));
|
||||
OutputDescriptor descriptor = OutputDescriptor.getOutputDescriptor(outputDescriptor);
|
||||
@@ -46,24 +45,22 @@ public class KeystoneSinglesig implements KeystoreFileImport, WalletImport {
|
||||
throw new IllegalArgumentException("Output descriptor describes a multisig wallet");
|
||||
}
|
||||
|
||||
if(policyType == PolicyType.SINGLE_SP && !descriptor.isSilentPayments()) {
|
||||
throw new IllegalArgumentException("Export does not contain the spscan value for silent payments");
|
||||
}
|
||||
|
||||
if(descriptor.getScriptType() != scriptType) {
|
||||
throw new IllegalArgumentException("Output descriptor describes a " + descriptor.getScriptType().getDescription() + " wallet");
|
||||
}
|
||||
|
||||
ExtendedKey xpub = descriptor.getSingletonExtendedPublicKey();
|
||||
KeyDerivation keyDerivation = descriptor.getKeyDerivation(xpub);
|
||||
|
||||
Keystore keystore = new Keystore();
|
||||
Wallet wallet = descriptor.toWallet();
|
||||
Keystore keystore = wallet.getKeystores().getFirst();
|
||||
keystore.setLabel(getName());
|
||||
keystore.setSource(KeystoreSource.HW_AIRGAPPED);
|
||||
keystore.setWalletModel(WalletModel.KEYSTONE);
|
||||
keystore.setKeyDerivation(keyDerivation);
|
||||
keystore.setExtendedPublicKey(xpub);
|
||||
keystore.setWalletModel(getWalletModel());
|
||||
|
||||
return keystore;
|
||||
} catch (IllegalArgumentException e) {
|
||||
throw new ImportException("Error getting " + getName() + " keystore - not an output descriptor", e);
|
||||
} catch (Exception e) {
|
||||
} catch(Exception e) {
|
||||
throw new ImportException("Error getting " + getName() + " keystore", e);
|
||||
}
|
||||
}
|
||||
@@ -76,13 +73,13 @@ public class KeystoneSinglesig implements KeystoreFileImport, WalletImport {
|
||||
@Override
|
||||
public Wallet importWallet(InputStream inputStream, String password) throws ImportException {
|
||||
//Use default of P2WPKH
|
||||
Keystore keystore = getKeystore(ScriptType.P2WPKH, inputStream, "");
|
||||
Keystore keystore = getKeystore(PolicyType.SINGLE_HD, ScriptType.P2WPKH, inputStream, "");
|
||||
|
||||
Wallet wallet = new Wallet();
|
||||
wallet.setPolicyType(PolicyType.SINGLE);
|
||||
wallet.setPolicyType(PolicyType.SINGLE_HD);
|
||||
wallet.setScriptType(ScriptType.P2WPKH);
|
||||
wallet.getKeystores().add(keystore);
|
||||
wallet.setDefaultPolicy(Policy.getPolicy(PolicyType.SINGLE, ScriptType.P2WPKH, wallet.getKeystores(), null));
|
||||
wallet.setDefaultPolicy(Policy.getPolicy(PolicyType.SINGLE_HD, ScriptType.P2WPKH, wallet.getKeystores(), null));
|
||||
|
||||
try {
|
||||
wallet.checkWallet();
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
package com.sparrowwallet.sparrow.io;
|
||||
|
||||
import com.sparrowwallet.drongo.crypto.ChildNumber;
|
||||
import com.sparrowwallet.drongo.policy.PolicyType;
|
||||
import com.sparrowwallet.drongo.wallet.Keystore;
|
||||
import javafx.beans.property.StringProperty;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public interface KeystoreCardImport extends CardImport {
|
||||
Keystore getKeystore(String pin, List<ChildNumber> derivation, StringProperty messageProperty) throws ImportException;
|
||||
Keystore getKeystore(PolicyType policyType, String pin, List<ChildNumber> derivation, StringProperty messageProperty) throws ImportException;
|
||||
String getKeystoreImportDescription(int account);
|
||||
}
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
package com.sparrowwallet.sparrow.io;
|
||||
|
||||
import com.sparrowwallet.drongo.crypto.ChildNumber;
|
||||
import com.sparrowwallet.drongo.policy.PolicyType;
|
||||
import com.sparrowwallet.drongo.wallet.Keystore;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public interface KeystoreCodexImport extends KeystoreImport {
|
||||
Keystore getKeystore(List<ChildNumber> derivation, String secretShare) throws ImportException;
|
||||
Keystore getKeystore(PolicyType policyType, List<ChildNumber> derivation, String secretShare) throws ImportException;
|
||||
}
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
package com.sparrowwallet.sparrow.io;
|
||||
|
||||
import com.sparrowwallet.drongo.policy.PolicyType;
|
||||
import com.sparrowwallet.drongo.protocol.ScriptType;
|
||||
import com.sparrowwallet.drongo.wallet.Keystore;
|
||||
|
||||
import java.io.InputStream;
|
||||
|
||||
public interface KeystoreFileImport extends KeystoreImport, FileImport {
|
||||
Keystore getKeystore(ScriptType scriptType, InputStream inputStream, String password) throws ImportException;
|
||||
Keystore getKeystore(PolicyType policyType, ScriptType scriptType, InputStream inputStream, String password) throws ImportException;
|
||||
String getKeystoreImportDescription(int account);
|
||||
default String getKeystoreImportDescription() {
|
||||
return getKeystoreImportDescription(0);
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
package com.sparrowwallet.sparrow.io;
|
||||
|
||||
import com.sparrowwallet.drongo.crypto.ChildNumber;
|
||||
import com.sparrowwallet.drongo.policy.PolicyType;
|
||||
import com.sparrowwallet.drongo.wallet.Keystore;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public interface KeystoreMnemonicImport extends KeystoreImport {
|
||||
Keystore getKeystore(List<ChildNumber> derivation, List<String> mnemonicWords, String passphrase) throws ImportException;
|
||||
Keystore getKeystore(PolicyType policyType, List<ChildNumber> derivation, List<String> mnemonicWords, String passphrase) throws ImportException;
|
||||
}
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
package com.sparrowwallet.sparrow.io;
|
||||
|
||||
import com.sparrowwallet.drongo.crypto.ChildNumber;
|
||||
import com.sparrowwallet.drongo.policy.PolicyType;
|
||||
import com.sparrowwallet.drongo.wallet.Keystore;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public interface KeystoreMnemonicShareImport extends KeystoreImport {
|
||||
Keystore getKeystore(List<ChildNumber> derivation, List<List<String>> mnemonicShares, String passphrase) throws ImportException;
|
||||
Keystore getKeystore(PolicyType policyType, List<ChildNumber> derivation, List<List<String>> mnemonicShares, String passphrase) throws ImportException;
|
||||
}
|
||||
|
||||
@@ -2,10 +2,11 @@ package com.sparrowwallet.sparrow.io;
|
||||
|
||||
import com.sparrowwallet.drongo.ExtendedKey;
|
||||
import com.sparrowwallet.drongo.crypto.ChildNumber;
|
||||
import com.sparrowwallet.drongo.policy.PolicyType;
|
||||
import com.sparrowwallet.drongo.wallet.Keystore;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public interface KeystoreXprvImport extends KeystoreImport {
|
||||
Keystore getKeystore(List<ChildNumber> derivation, ExtendedKey xprv) throws ImportException;
|
||||
Keystore getKeystore(PolicyType policyType, List<ChildNumber> derivation, ExtendedKey xprv) throws ImportException;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.sparrowwallet.sparrow.io;
|
||||
|
||||
import com.sparrowwallet.drongo.policy.PolicyType;
|
||||
import com.sparrowwallet.drongo.protocol.ScriptType;
|
||||
import com.sparrowwallet.drongo.wallet.Keystore;
|
||||
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||
@@ -19,10 +20,9 @@ public class PassportMultisig extends ColdcardMultisig {
|
||||
}
|
||||
|
||||
@Override
|
||||
public Keystore getKeystore(ScriptType scriptType, InputStream inputStream, String password) throws ImportException {
|
||||
Keystore keystore = super.getKeystore(scriptType, inputStream, password);
|
||||
public Keystore getKeystore(PolicyType policyType, ScriptType scriptType, InputStream inputStream, String password) throws ImportException {
|
||||
Keystore keystore = super.getKeystore(policyType, scriptType, inputStream, password);
|
||||
keystore.setLabel("Passport");
|
||||
keystore.setWalletModel(getWalletModel());
|
||||
|
||||
return keystore;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.sparrowwallet.sparrow.io;
|
||||
|
||||
import com.sparrowwallet.drongo.policy.PolicyType;
|
||||
import com.sparrowwallet.drongo.protocol.ScriptType;
|
||||
import com.sparrowwallet.drongo.wallet.Keystore;
|
||||
import com.sparrowwallet.drongo.wallet.WalletModel;
|
||||
@@ -18,10 +19,9 @@ public class PassportSinglesig extends ColdcardSinglesig {
|
||||
}
|
||||
|
||||
@Override
|
||||
public Keystore getKeystore(ScriptType scriptType, InputStream inputStream, String password) throws ImportException {
|
||||
Keystore keystore = super.getKeystore(scriptType, inputStream, password);
|
||||
public Keystore getKeystore(PolicyType policyType, ScriptType scriptType, InputStream inputStream, String password) throws ImportException {
|
||||
Keystore keystore = super.getKeystore(policyType, scriptType, inputStream, password);
|
||||
keystore.setLabel("Passport");
|
||||
keystore.setWalletModel(getWalletModel());
|
||||
|
||||
return keystore;
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import com.google.gson.JsonParseException;
|
||||
import com.google.gson.reflect.TypeToken;
|
||||
import com.sparrowwallet.drongo.Utils;
|
||||
import com.sparrowwallet.drongo.crypto.SamouraiUtil;
|
||||
import com.sparrowwallet.drongo.policy.PolicyType;
|
||||
import com.sparrowwallet.drongo.protocol.ScriptType;
|
||||
import com.sparrowwallet.drongo.wallet.*;
|
||||
|
||||
@@ -25,7 +26,11 @@ public class Samourai implements KeystoreFileImport {
|
||||
}
|
||||
|
||||
@Override
|
||||
public Keystore getKeystore(ScriptType scriptType, InputStream inputStream, String password) throws ImportException {
|
||||
public Keystore getKeystore(PolicyType policyType, ScriptType scriptType, InputStream inputStream, String password) throws ImportException {
|
||||
if(policyType == PolicyType.SINGLE_SP) {
|
||||
throw new ImportException(getName() + " does not support receiving silent payments");
|
||||
}
|
||||
|
||||
try {
|
||||
String input = CharStreams.toString(new InputStreamReader(inputStream, StandardCharsets.UTF_8));
|
||||
|
||||
@@ -52,7 +57,7 @@ public class Samourai implements KeystoreFileImport {
|
||||
|
||||
SamouraiBackup backup = new Gson().fromJson(decrypted, SamouraiBackup.class);
|
||||
DeterministicSeed seed = new DeterministicSeed(Utils.hexToBytes(backup.wallet.seed), password, 0);
|
||||
Keystore keystore = Keystore.fromSeed(seed, scriptType.getDefaultDerivation());
|
||||
Keystore keystore = Keystore.fromSeed(seed, PolicyType.SINGLE_HD, scriptType.getDefaultDerivation());
|
||||
keystore.setLabel(getWalletModel().toDisplayString());
|
||||
return keystore;
|
||||
} catch(JsonParseException e) {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package com.sparrowwallet.sparrow.io;
|
||||
|
||||
import com.sparrowwallet.drongo.crypto.ChildNumber;
|
||||
import com.sparrowwallet.drongo.policy.PolicyType;
|
||||
import com.sparrowwallet.drongo.wallet.*;
|
||||
import com.sparrowwallet.drongo.wallet.slip39.RecoveryState;
|
||||
import com.sparrowwallet.drongo.wallet.slip39.Share;
|
||||
@@ -25,7 +26,7 @@ public class Slip39 implements KeystoreMnemonicShareImport {
|
||||
}
|
||||
|
||||
@Override
|
||||
public Keystore getKeystore(List<ChildNumber> derivation, List<List<String>> mnemonicShares, String passphrase) throws ImportException {
|
||||
public Keystore getKeystore(PolicyType policyType, List<ChildNumber> derivation, List<List<String>> mnemonicShares, String passphrase) throws ImportException {
|
||||
try {
|
||||
RecoveryState recoveryState = new RecoveryState();
|
||||
for(List<String> mnemonicWords : mnemonicShares) {
|
||||
@@ -36,7 +37,7 @@ public class Slip39 implements KeystoreMnemonicShareImport {
|
||||
if(recoveryState.isComplete()) {
|
||||
byte[] secret = recoveryState.recover(passphrase.getBytes(StandardCharsets.UTF_8));
|
||||
DeterministicSeed seed = new DeterministicSeed(secret, passphrase, System.currentTimeMillis(), DeterministicSeed.Type.SLIP39);
|
||||
return Keystore.fromSeed(seed, derivation);
|
||||
return Keystore.fromSeed(seed, policyType, derivation);
|
||||
} else {
|
||||
throw new Slip39ProgressException(recoveryState.getShortStatus(), recoveryState.getStatus());
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
package com.sparrowwallet.sparrow.io;
|
||||
|
||||
import com.google.common.io.Files;
|
||||
import com.sparrowwallet.drongo.IOUtils;
|
||||
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||
import com.sparrowwallet.drongo.wallet.WalletModel;
|
||||
import com.sparrowwallet.sparrow.AppServices;
|
||||
@@ -11,6 +11,7 @@ import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.StandardCopyOption;
|
||||
import java.util.Map;
|
||||
|
||||
@@ -29,13 +30,14 @@ public class Sparrow implements WalletImport, WalletExport {
|
||||
|
||||
@Override
|
||||
public void exportWallet(Wallet wallet, OutputStream outputStream, String password) throws ExportException {
|
||||
File tempDir = null;
|
||||
try {
|
||||
Wallet exportedWallet = !wallet.isMasterWallet() ? wallet.getMasterWallet() : wallet;
|
||||
PersistenceType persistenceType = PersistenceType.DB;
|
||||
Persistence persistence = persistenceType.getInstance();
|
||||
Storage storage = AppServices.get().getOpenWallets().get(exportedWallet);
|
||||
File tempFile = File.createTempFile(exportedWallet.getName() + "tmp", "." + persistenceType.getExtension());
|
||||
tempFile.delete();
|
||||
tempDir = Files.createTempDirectory("sparrow").toFile();
|
||||
File tempFile = new File(tempDir, exportedWallet.getName() + "." + persistenceType.getExtension());
|
||||
Storage tempStorage = new Storage(persistence, tempFile);
|
||||
tempStorage.setKeyDeriver(storage.getKeyDeriver());
|
||||
tempStorage.setEncryptionPubKey(storage.getEncryptionPubKey());
|
||||
@@ -46,12 +48,13 @@ public class Sparrow implements WalletImport, WalletExport {
|
||||
tempStorage.saveWallet(childWallet);
|
||||
}
|
||||
persistence.close();
|
||||
Files.copy(tempStorage.getWalletFile(), outputStream);
|
||||
Files.copy(tempStorage.getWalletFile().toPath(), outputStream);
|
||||
outputStream.flush();
|
||||
tempStorage.getWalletFile().delete();
|
||||
} catch(Exception e) {
|
||||
log.error("Error exporting Sparrow wallet file", e);
|
||||
throw new ExportException("Error exporting Sparrow wallet file", e);
|
||||
} finally {
|
||||
deleteTempDirectory(tempDir);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -88,11 +91,11 @@ public class Sparrow implements WalletImport, WalletExport {
|
||||
@Override
|
||||
public Wallet importWallet(InputStream inputStream, String password) throws ImportException {
|
||||
Storage storage = null;
|
||||
Wallet wallet = null;
|
||||
File tempFile = null;
|
||||
File tempDir = null;
|
||||
try {
|
||||
tempFile = File.createTempFile("sparrow", null);
|
||||
java.nio.file.Files.copy(inputStream, tempFile.toPath(), StandardCopyOption.REPLACE_EXISTING);
|
||||
tempDir = Files.createTempDirectory("sparrow").toFile();
|
||||
File tempFile = new File(tempDir, "sparrow");
|
||||
Files.copy(inputStream, tempFile.toPath(), StandardCopyOption.REPLACE_EXISTING);
|
||||
PersistenceType persistenceType = Storage.detectPersistenceType(tempFile);
|
||||
persistenceType = (persistenceType == null ? PersistenceType.JSON : persistenceType);
|
||||
if(persistenceType != PersistenceType.JSON || !isEncrypted(tempFile)) {
|
||||
@@ -102,6 +105,7 @@ public class Sparrow implements WalletImport, WalletExport {
|
||||
}
|
||||
|
||||
storage = new Storage(persistenceType, tempFile);
|
||||
Wallet wallet;
|
||||
if(!isEncrypted(tempFile)) {
|
||||
wallet = storage.loadUnencryptedWallet().getWallet();
|
||||
} else {
|
||||
@@ -118,19 +122,23 @@ public class Sparrow implements WalletImport, WalletExport {
|
||||
throw new ImportException("Error importing Sparrow wallet", e);
|
||||
} finally {
|
||||
if(storage != null) {
|
||||
storage.close();
|
||||
storage.closeAndWait();
|
||||
}
|
||||
|
||||
if(tempFile != null) {
|
||||
if(wallet != null) {
|
||||
File migratedWalletFile = Storage.getExistingWallet(tempFile.getParentFile(), wallet.getName());
|
||||
if(migratedWalletFile != null) {
|
||||
migratedWalletFile.delete();
|
||||
}
|
||||
deleteTempDirectory(tempDir);
|
||||
}
|
||||
}
|
||||
|
||||
private void deleteTempDirectory(File tempDir) {
|
||||
if(tempDir != null) {
|
||||
File[] tempFiles = tempDir.listFiles();
|
||||
if(tempFiles != null) {
|
||||
for(File file : tempFiles) {
|
||||
IOUtils.secureDelete(file);
|
||||
}
|
||||
|
||||
tempFile.delete();
|
||||
}
|
||||
|
||||
tempDir.delete();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ package com.sparrowwallet.sparrow.io;
|
||||
|
||||
import com.google.common.io.CharStreams;
|
||||
import com.sparrowwallet.drongo.OutputDescriptor;
|
||||
import com.sparrowwallet.drongo.policy.PolicyType;
|
||||
import com.sparrowwallet.drongo.protocol.ScriptType;
|
||||
import com.sparrowwallet.drongo.wallet.Keystore;
|
||||
import com.sparrowwallet.drongo.wallet.KeystoreSource;
|
||||
@@ -17,24 +18,24 @@ public class SpecterDIY implements KeystoreFileImport, WalletExport {
|
||||
private static final Logger log = LoggerFactory.getLogger(SpecterDIY.class);
|
||||
|
||||
@Override
|
||||
public Keystore getKeystore(ScriptType scriptType, InputStream inputStream, String password) throws ImportException {
|
||||
public Keystore getKeystore(PolicyType policyType, ScriptType scriptType, InputStream inputStream, String password) throws ImportException {
|
||||
try {
|
||||
String text = CharStreams.toString(new InputStreamReader(inputStream, StandardCharsets.UTF_8));
|
||||
String outputDesc = "sh(" + text + ")";
|
||||
String outputDesc = policyType == PolicyType.SINGLE_SP ? "sp(" + text + ")" : "sh(" + text + ")";
|
||||
OutputDescriptor outputDescriptor = OutputDescriptor.getOutputDescriptor(outputDesc);
|
||||
Wallet wallet = outputDescriptor.toWallet();
|
||||
|
||||
if(wallet.getKeystores().size() != 1) {
|
||||
throw new ImportException("Could not determine keystore from import");
|
||||
throw new IllegalArgumentException("Could not determine keystore from import");
|
||||
}
|
||||
|
||||
Keystore keystore = wallet.getKeystores().get(0);
|
||||
Keystore keystore = wallet.getKeystores().getFirst();
|
||||
keystore.setLabel(getName());
|
||||
keystore.setWalletModel(getWalletModel());
|
||||
keystore.setSource(KeystoreSource.HW_AIRGAPPED);
|
||||
|
||||
return keystore;
|
||||
} catch(IOException e) {
|
||||
} catch(Exception e) {
|
||||
throw new ImportException("Error getting " + getName() + " keystore", e);
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user