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 | ||
|
|
67e1573329 | ||
|
|
23a9db5fc6 | ||
|
|
e9108b85cd | ||
|
|
3766585474 | ||
|
|
0e7aa34d07 | ||
|
|
537c2ffef0 | ||
|
|
8b4f388eb7 | ||
|
|
6a07adfea5 | ||
|
|
eeade6a173 | ||
|
|
4c2b8055ec | ||
|
|
04d210ed4b | ||
|
|
5b737be76d | ||
|
|
908fb80d2a | ||
|
|
eda7087bdd | ||
|
|
a5d61f1a9f | ||
|
|
2621bb6504 | ||
|
|
7dfc432bd2 | ||
|
|
df5b5146ba | ||
|
|
452584d87d | ||
|
|
5c823533d3 | ||
|
|
6561971d4f | ||
|
|
8ebf8dc041 | ||
|
|
419e8e1374 | ||
|
|
684965cf89 | ||
|
|
3498b5bab1 | ||
|
|
74635b1d1f | ||
|
|
229c6430ed | ||
|
|
c74abfeeb7 | ||
|
|
c78bc7b1cf | ||
|
|
87cd96a627 | ||
|
|
7ae04a2564 | ||
|
|
d4c0df0df9 | ||
|
|
dc88dc4ede | ||
|
|
a478edfad7 | ||
|
|
46d444615c | ||
|
|
78fe55787b | ||
|
|
cd0be365de | ||
|
|
b9d62c0b7b | ||
|
|
53d1f196e4 | ||
|
|
64b8da14a6 | ||
|
|
529cc3d6e7 | ||
|
|
e13fe897e1 | ||
|
|
dac1a61601 | ||
|
|
227c7ae637 | ||
|
|
e11675b9a8 | ||
|
|
146e6cbdc4 | ||
|
|
e4fa0c9356 | ||
|
|
30719c5a7f | ||
|
|
a120d08ea1 | ||
|
|
b51bd90261 | ||
|
|
a765105d34 | ||
|
|
2cd7d0bdf3 | ||
|
|
c6c24028d9 | ||
|
|
efb81555bd | ||
|
|
b11afdf2bb | ||
|
|
779a55c494 | ||
|
|
b06d858794 | ||
|
|
306c025cbf | ||
|
|
b145bddf07 | ||
|
|
edee72d97d | ||
|
|
b87650a740 | ||
|
|
0c9bbc2550 | ||
|
|
088f8e302c | ||
|
|
4309216ad7 | ||
|
|
dffaf806cd | ||
|
|
448067db54 | ||
|
|
6b498c28c2 | ||
|
|
4724dc7700 | ||
|
|
883558fd9c | ||
|
|
9d576bc45d | ||
|
|
49d807f31b | ||
|
|
0c2ee6690f | ||
|
|
85c81adc09 | ||
|
|
04de83706e | ||
|
|
ab99f1d392 | ||
|
|
53857389b7 | ||
|
|
98576d40c6 | ||
|
|
96f7d4bdb3 | ||
|
|
0c67962730 | ||
|
|
0515d8056b | ||
|
|
cf15760d34 | ||
|
|
f28f15df66 | ||
|
|
34900d2909 | ||
|
|
430f2ee320 | ||
|
|
74160ba35f | ||
|
|
59d85cdd05 | ||
|
|
0406421918 | ||
|
|
4ddd09fe22 | ||
|
|
a49edb7c45 | ||
|
|
5275cab436 | ||
|
|
4dcd463d92 | ||
|
|
f900f6dcb2 | ||
|
|
b0ab6c9d42 | ||
|
|
b8a703add2 | ||
|
|
4185a8dced | ||
|
|
a608197038 | ||
|
|
b4630043eb | ||
|
|
6452bf8184 | ||
|
|
71cb5852b3 | ||
|
|
8efee4eaae | ||
|
|
ac044c6f71 | ||
|
|
d2d45e5432 | ||
|
|
c16997eacc | ||
|
|
21543de007 | ||
|
|
6f772cb193 | ||
|
|
a4634f6a3d | ||
|
|
a794dafdf9 | ||
|
|
9fb19ebe8b | ||
|
|
46a39225bd | ||
|
|
a0f7e2e65d | ||
|
|
39cb946e08 | ||
|
|
0894407b0b | ||
|
|
c76acb8d82 | ||
|
|
16e73ebc32 | ||
|
|
3a5fa69fb6 | ||
|
|
4774830ce4 | ||
|
|
2f62a9e9c8 | ||
|
|
75bcfe2253 | ||
|
|
bedf1399ea | ||
|
|
58575793ea | ||
|
|
6c9b580d4f | ||
|
|
31909b7a15 | ||
|
|
092267339a | ||
|
|
0974918cff | ||
|
|
0f4c36b3c2 | ||
|
|
e1fe35fb74 | ||
|
|
d37fd00c4b | ||
|
|
5f54f86df7 | ||
|
|
e2fa3df08d | ||
|
|
6d6ede9abe | ||
|
|
cca9ab1056 | ||
|
|
9e33861110 | ||
|
|
c3d3fd1fda | ||
|
|
ca8553ecb8 | ||
|
|
d23ee8c086 | ||
|
|
e776a17ad4 | ||
|
|
480ce1e476 | ||
|
|
656cd90b08 | ||
|
|
8df0777959 | ||
|
|
84566b92e6 | ||
|
|
7802510e58 | ||
|
|
efb1eb1051 | ||
|
|
6240667478 | ||
|
|
2c27112dad | ||
|
|
6d53e1ed1d | ||
|
|
e8c5660897 | ||
|
|
bef6c750bd | ||
|
|
4ec3603789 | ||
|
|
90c9f9733f | ||
|
|
64efcf67d3 | ||
|
|
385d173948 | ||
|
|
d81b868049 | ||
|
|
2ff7a15d1e | ||
|
|
f48fa7e23c | ||
|
|
4632850e1e | ||
|
|
5f62523710 | ||
|
|
9dcf210762 | ||
|
|
c7e9a0a161 | ||
|
|
fa10714844 | ||
|
|
80105aee62 | ||
|
|
3c5fa58a16 | ||
|
|
2a2be2617c | ||
|
|
6c9a0d14cd | ||
|
|
f82fcb58bb | ||
|
|
5ec3bff6a4 | ||
|
|
134dc826ba | ||
|
|
cd2a6623a4 | ||
|
|
49ab9e40e3 | ||
|
|
cec7eac9ac | ||
|
|
33e043fd9a | ||
|
|
3aae26b196 |
@@ -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,53 +5,70 @@ on: workflow_dispatch
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
env:
|
||||
JDK_JAVAC_OPTIONS: '-Xlint:-module'
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
matrix:
|
||||
os: [windows-2022, ubuntu-22.04, ubuntu-22.04-arm, macos-13, macos-14]
|
||||
os: [windows-2022, ubuntu-22.04, ubuntu-22.04-arm, macos-15-intel, macos-14]
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
with:
|
||||
submodules: true
|
||||
- name: Set up JDK 22.0.2
|
||||
uses: actions/setup-java@v4
|
||||
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@0f481fcb613427c0f801b606911222b5b6f3083a # v5.5.0
|
||||
with:
|
||||
distribution: 'temurin'
|
||||
java-version: '22.0.2'
|
||||
java-version: '25.0.2'
|
||||
- name: Show Build Versions
|
||||
run: ./gradlew -v
|
||||
- name: Build with Gradle
|
||||
run: ./gradlew jpackage
|
||||
- name: Package zip distribution
|
||||
if: ${{ runner.os == 'Windows' || runner.os == 'macOS' }}
|
||||
- name: Codesign, package and notarize macOS distribution
|
||||
if: ${{ runner.os == 'macOS' }}
|
||||
uses: sparrowwallet/github-actions/codesign-macos@6eeb7bf9b882cf89ff96b77f99bcf487b9d33992 # v1
|
||||
with:
|
||||
app-name: Sparrow
|
||||
certificate: ${{ secrets.MACOS_CERTIFICATE }}
|
||||
certificate-password: ${{ secrets.MACOS_CERTIFICATE_PASSWORD }}
|
||||
apple-id: ${{ secrets.MACOS_NOTARIZATION_APPLE_ID }}
|
||||
team-id: ${{ secrets.MACOS_NOTARIZATION_TEAM_ID }}
|
||||
notarization-password: ${{ secrets.MACOS_NOTARIZATION_PASSWORD }}
|
||||
- name: Package Windows zip distribution
|
||||
if: ${{ runner.os == 'Windows' }}
|
||||
run: ./gradlew packageZipDistribution
|
||||
- name: Package tar distribution
|
||||
- name: Package Linux tar distribution
|
||||
if: ${{ runner.os == 'Linux' }}
|
||||
run: ./gradlew packageTarDistribution
|
||||
- name: Repackage deb distribution
|
||||
- name: Repackage Linux deb distribution
|
||||
if: ${{ runner.os == 'Linux' }}
|
||||
run: ./repackage.sh
|
||||
- name: Upload Artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: Sparrow Build - ${{ runner.os }} ${{ runner.arch }}
|
||||
path: |
|
||||
build/jpackage/*
|
||||
!build/jpackage/Sparrow/
|
||||
!build/jpackage/Sparrow.app/
|
||||
- name: Headless build with Gradle
|
||||
if: ${{ runner.os == 'Linux' }}
|
||||
run: ./gradlew -Djava.awt.headless=true clean jpackage
|
||||
- name: Package headless tar distribution
|
||||
- name: Package Linux headless tar distribution
|
||||
if: ${{ runner.os == 'Linux' }}
|
||||
run: ./gradlew -Djava.awt.headless=true packageTarDistribution
|
||||
- name: Repackage headless deb distribution
|
||||
- name: Repackage Linux headless deb distribution
|
||||
if: ${{ runner.os == 'Linux' }}
|
||||
run: ./repackage.sh
|
||||
- name: Upload Headless Artifact
|
||||
if: ${{ runner.os == 'Linux' }}
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: Sparrow Build - ${{ runner.os }} ${{ runner.arch }} Headless
|
||||
path: |
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
# Enable auto-env through the sdkman_auto_env config
|
||||
# Add key=value pairs of SDKs to use below
|
||||
java=25.0.2-tem
|
||||
@@ -16,14 +16,14 @@ or for those without SSH credentials:
|
||||
|
||||
`git clone --recursive https://github.com/sparrowwallet/sparrow.git`
|
||||
|
||||
In order to build, Sparrow requires Java 22 or higher to be installed.
|
||||
The release binaries are built with [Eclipse Temurin 22.0.2+9](https://github.com/adoptium/temurin22-binaries/releases/tag/jdk-22.0.2%2B9).
|
||||
In order to build, Sparrow requires Java 25 or higher to be installed.
|
||||
The release binaries are built with [Eclipse Temurin 25.0.2+10](https://github.com/adoptium/temurin25-binaries/releases/tag/jdk-25.0.2%2B10).
|
||||
If you are using [SDKMAN](https://sdkman.io/), you can use `sdk env install` to ensure you have the correct version.
|
||||
|
||||
Other packages may also be necessary to build depending on the platform. On Debian/Ubuntu systems:
|
||||
|
||||
`sudo apt install -y rpm fakeroot binutils`
|
||||
|
||||
|
||||
The Sparrow binaries can be built from source using
|
||||
|
||||
`./gradlew jpackage`
|
||||
@@ -44,7 +44,7 @@ If you prefer to run Sparrow directly from source, it can be launched from withi
|
||||
|
||||
`./sparrow`
|
||||
|
||||
Java 22 or higher must be installed.
|
||||
Java 25 or higher must be installed.
|
||||
|
||||
## Configuration
|
||||
|
||||
@@ -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.
|
||||
|
||||
+187
-116
@@ -1,9 +1,9 @@
|
||||
plugins {
|
||||
id 'application'
|
||||
id 'org-openjfx-javafxplugin'
|
||||
id 'org.beryx.jlink' version '3.1.1'
|
||||
id 'org.gradlex.extra-java-module-info' version '1.9'
|
||||
id 'io.matthewnelson.kmp.tor.resource-filterjar' version '408.16.3'
|
||||
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'
|
||||
}
|
||||
|
||||
def os = org.gradle.internal.os.OperatingSystem.current()
|
||||
@@ -19,21 +19,20 @@ if(System.getProperty("os.arch") == "aarch64") {
|
||||
}
|
||||
def headless = "true".equals(System.getProperty("java.awt.headless"))
|
||||
|
||||
group 'com.sparrowwallet'
|
||||
version '2.2.3'
|
||||
group = 'com.sparrowwallet'
|
||||
version = '2.5.4'
|
||||
|
||||
repositories {
|
||||
mavenCentral()
|
||||
maven { url 'https://code.sparrowwallet.com/api/packages/sparrowwallet/maven' }
|
||||
maven { url = uri('https://code.sparrowwallet.com/api/packages/sparrowwallet/maven') }
|
||||
}
|
||||
|
||||
tasks.withType(AbstractArchiveTask) {
|
||||
preserveFileTimestamps = false
|
||||
reproducibleFileOrder = true
|
||||
tasks.withType(AbstractArchiveTask).configureEach {
|
||||
useFileSystemPermissions()
|
||||
}
|
||||
|
||||
javafx {
|
||||
version = headless ? "18" : "23.0.2"
|
||||
version = "26"
|
||||
modules = [ 'javafx.controls', 'javafx.fxml', 'javafx.swing', 'javafx.graphics' ]
|
||||
}
|
||||
|
||||
@@ -45,25 +44,25 @@ dependencies {
|
||||
//Any changes to the dependencies must be reflected in the module definitions below!
|
||||
implementation(project(':drongo'))
|
||||
implementation(project(':lark'))
|
||||
implementation('com.google.guava:guava:33.0.0-jre')
|
||||
implementation('com.google.code.gson:gson:2.9.1')
|
||||
implementation('com.google.guava:guava:33.5.0-jre')
|
||||
implementation('com.google.code.gson:gson:2.13.2')
|
||||
implementation('com.h2database:h2:2.1.214')
|
||||
implementation('com.zaxxer:HikariCP:4.0.3') {
|
||||
implementation('com.zaxxer:HikariCP:7.0.2') {
|
||||
exclude group: 'org.slf4j'
|
||||
}
|
||||
implementation('org.jdbi:jdbi3-core:3.20.0') {
|
||||
implementation('org.jdbi:jdbi3-core:3.51.0') {
|
||||
exclude group: 'org.slf4j'
|
||||
}
|
||||
implementation('org.jdbi:jdbi3-sqlobject:3.20.0') {
|
||||
implementation('org.jdbi:jdbi3-sqlobject:3.51.0') {
|
||||
exclude group: 'org.slf4j'
|
||||
}
|
||||
implementation('org.flywaydb:flyway-core:9.22.3')
|
||||
implementation('org.fxmisc.richtext:richtextfx:0.10.4')
|
||||
implementation('org.fxmisc.richtext:richtextfx:0.11.7')
|
||||
implementation('no.tornado:tornadofx-controls:1.0.4')
|
||||
implementation('com.google.zxing:javase:3.4.0') {
|
||||
implementation('com.google.zxing:javase:3.5.4') {
|
||||
exclude group: 'com.beust', module: 'jcommander'
|
||||
}
|
||||
implementation('org.jcommander:jcommander:2.0')
|
||||
implementation('org.jcommander:jcommander:3.0')
|
||||
implementation('com.github.arteam:simple-json-rpc-core:1.3')
|
||||
implementation('com.github.arteam:simple-json-rpc-client:1.3') {
|
||||
exclude group: 'com.github.arteam', module: 'simple-json-rpc-core'
|
||||
@@ -71,17 +70,19 @@ dependencies {
|
||||
implementation('com.github.arteam:simple-json-rpc-server:1.3') {
|
||||
exclude group: 'org.slf4j'
|
||||
}
|
||||
implementation('com.fasterxml.jackson.core:jackson-databind:2.17.2')
|
||||
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.28-6')
|
||||
implementation("io.matthewnelson.kmp-tor:runtime:2.2.1")
|
||||
implementation("io.matthewnelson.kmp-tor:resource-exec-tor-gpl:408.16.3")
|
||||
implementation('org.jetbrains.kotlinx:kotlinx-coroutines-javafx:1.10.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') {
|
||||
exclude group: 'org.jetbrains.kotlin', module: 'kotlin-stdlib-common'
|
||||
}
|
||||
implementation('de.codecentric.centerdevice:centerdevice-nsmenufx:2.1.7')
|
||||
implementation('org.controlsfx:controlsfx:11.1.0' ) {
|
||||
implementation('de.jangassen:nsmenufx:3.1.0') {
|
||||
exclude group: 'net.java.dev.jna', module: 'jna'
|
||||
}
|
||||
implementation('org.controlsfx:controlsfx:11.2.3' ) {
|
||||
exclude group: 'org.openjfx', module: 'javafx-base'
|
||||
exclude group: 'org.openjfx', module: 'javafx-graphics'
|
||||
exclude group: 'org.openjfx', module: 'javafx-controls'
|
||||
@@ -92,25 +93,25 @@ dependencies {
|
||||
}
|
||||
implementation('dev.bwt:bwt-jni:0.1.8')
|
||||
implementation('net.sourceforge.javacsv:javacsv:2.0')
|
||||
implementation ('org.slf4j:slf4j-api:2.0.12')
|
||||
implementation('org.slf4j:jul-to-slf4j:2.0.12') {
|
||||
implementation ('org.slf4j:slf4j-api:2.0.17')
|
||||
implementation('org.slf4j:jul-to-slf4j:2.0.17') {
|
||||
exclude group: 'org.slf4j'
|
||||
}
|
||||
implementation('com.sparrowwallet.bokmakierie:bokmakierie:1.0')
|
||||
implementation('com.sparrowwallet:tern:1.0.6')
|
||||
implementation('io.reactivex.rxjava2:rxjava:2.2.15')
|
||||
implementation('io.reactivex.rxjava2:rxjava:2.2.21')
|
||||
implementation('io.reactivex.rxjava2:rxjavafx:2.2.2')
|
||||
implementation('org.apache.commons:commons-lang3:3.7')
|
||||
implementation('org.apache.commons:commons-compress:1.27.1')
|
||||
implementation('net.sourceforge.streamsupport:streamsupport:1.7.0')
|
||||
implementation('com.github.librepdf:openpdf:1.3.30')
|
||||
implementation('com.googlecode.lanterna:lanterna:3.1.3')
|
||||
implementation('net.coobird:thumbnailator:0.4.18')
|
||||
implementation('org.apache.commons:commons-lang3:3.20.0')
|
||||
implementation('org.apache.commons:commons-compress:1.28.0')
|
||||
implementation('com.github.librepdf:openpdf:1.3.43')
|
||||
implementation('com.googlecode.lanterna:lanterna:3.1.5')
|
||||
implementation('net.coobird:thumbnailator:0.4.21')
|
||||
implementation('com.github.hervegirod:fxsvgimage:1.1')
|
||||
implementation('com.sparrowwallet:toucan:0.9.0')
|
||||
implementation('com.jcraft:jzlib:1.1.3')
|
||||
testImplementation('org.junit.jupiter:junit-jupiter-api:5.10.0')
|
||||
testRuntimeOnly('org.junit.jupiter:junit-jupiter-engine:5.10.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')
|
||||
}
|
||||
|
||||
@@ -123,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", "--add-opens=java.base/java.io=com.google.gson", "--add-reads=org.flywaydb.core=java.desktop"]
|
||||
jvmArgs = ["--enable-native-access=ALL-UNNAMED"]
|
||||
}
|
||||
|
||||
application {
|
||||
@@ -141,6 +134,13 @@ application {
|
||||
mainClass = 'com.sparrowwallet.sparrow.SparrowWallet'
|
||||
|
||||
applicationDefaultJvmArgs = ["-XX:+HeapDumpOnOutOfMemoryError",
|
||||
"--enable-native-access=com.sparrowwallet.drongo",
|
||||
"--enable-native-access=com.sun.jna",
|
||||
"--enable-native-access=javafx.graphics",
|
||||
"--enable-native-access=com.fazecast.jSerialComm",
|
||||
"--enable-native-access=org.usb4java",
|
||||
"--enable-native-access=io.github.doblon8.jzbar",
|
||||
"--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",
|
||||
@@ -150,29 +150,27 @@ application {
|
||||
"--add-opens=javafx.controls/javafx.scene.control.cell=com.sparrowwallet.sparrow",
|
||||
"--add-opens=org.controlsfx.controls/impl.org.controlsfx.skin=com.sparrowwallet.sparrow",
|
||||
"--add-opens=org.controlsfx.controls/impl.org.controlsfx.skin=javafx.fxml",
|
||||
"--add-opens=javafx.graphics/com.sun.javafx.tk=centerdevice.nsmenufx",
|
||||
"--add-opens=javafx.graphics/com.sun.javafx.tk.quantum=centerdevice.nsmenufx",
|
||||
"--add-opens=javafx.graphics/com.sun.glass.ui=centerdevice.nsmenufx",
|
||||
"--add-opens=javafx.controls/com.sun.javafx.scene.control=centerdevice.nsmenufx",
|
||||
"--add-opens=javafx.graphics/com.sun.javafx.menu=centerdevice.nsmenufx",
|
||||
"--add-opens=javafx.graphics/com.sun.glass.ui=com.sparrowwallet.sparrow",
|
||||
"--add-opens=javafx.graphics/com.sun.javafx.application=com.sparrowwallet.sparrow",
|
||||
"--add-opens=javafx.graphics/javafx.scene.input=com.sparrowwallet.sparrow",
|
||||
"--add-opens=java.base/java.net=com.sparrowwallet.sparrow",
|
||||
"--add-opens=java.base/java.io=com.google.gson",
|
||||
"--add-opens=java.smartcardio/sun.security.smartcardio=com.sparrowwallet.sparrow",
|
||||
"--add-reads=kotlin.stdlib=kotlinx.coroutines.core",
|
||||
"--add-reads=org.flywaydb.core=java.desktop"]
|
||||
|
||||
if(os.macOsX) {
|
||||
applicationDefaultJvmArgs += ["-Dprism.lcdtext=false", "-Xdock:name=Sparrow",
|
||||
"--add-opens=javafx.graphics/com.sun.glass.ui.mac=centerdevice.nsmenufx"]
|
||||
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'
|
||||
@@ -183,13 +181,48 @@ jlink {
|
||||
requires 'jdk.crypto.cryptoki'
|
||||
requires 'java.management'
|
||||
requires 'io.leangen.geantyref'
|
||||
requires 'static jdk.jfr'
|
||||
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 = ["--add-opens=javafx.graphics/com.sun.javafx.css=org.controlsfx.controls",
|
||||
jvmArgs = ["--enable-native-access=com.sparrowwallet.drongo",
|
||||
"--enable-native-access=com.sun.jna",
|
||||
"--enable-native-access=javafx.graphics",
|
||||
"--enable-native-access=com.sparrowwallet.merged.module",
|
||||
"--enable-native-access=com.fazecast.jSerialComm",
|
||||
"--enable-native-access=org.usb4java",
|
||||
"--enable-native-access=io.github.doblon8.jzbar",
|
||||
"--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",
|
||||
"--add-opens=javafx.controls/com.sun.javafx.scene.control.inputmap=org.controlsfx.controls",
|
||||
@@ -198,16 +231,10 @@ jlink {
|
||||
"--add-opens=javafx.controls/javafx.scene.control.cell=com.sparrowwallet.sparrow",
|
||||
"--add-opens=org.controlsfx.controls/impl.org.controlsfx.skin=com.sparrowwallet.sparrow",
|
||||
"--add-opens=org.controlsfx.controls/impl.org.controlsfx.skin=javafx.fxml",
|
||||
"--add-opens=javafx.graphics/com.sun.javafx.tk=centerdevice.nsmenufx",
|
||||
"--add-opens=javafx.graphics/com.sun.javafx.tk.quantum=centerdevice.nsmenufx",
|
||||
"--add-opens=javafx.graphics/com.sun.glass.ui=centerdevice.nsmenufx",
|
||||
"--add-opens=javafx.controls/com.sun.javafx.scene.control=centerdevice.nsmenufx",
|
||||
"--add-opens=javafx.graphics/com.sun.javafx.menu=centerdevice.nsmenufx",
|
||||
"--add-opens=javafx.graphics/com.sun.glass.ui=com.sparrowwallet.sparrow",
|
||||
"--add-opens=javafx.graphics/javafx.scene.input=com.sparrowwallet.sparrow",
|
||||
"--add-opens=javafx.graphics/com.sun.javafx.application=com.sparrowwallet.sparrow",
|
||||
"--add-opens=java.base/java.net=com.sparrowwallet.sparrow",
|
||||
"--add-opens=java.base/java.io=com.google.gson",
|
||||
"--add-opens=java.smartcardio/sun.security.smartcardio=com.sparrowwallet.sparrow",
|
||||
"--add-reads=com.sparrowwallet.merged.module=java.desktop",
|
||||
"--add-reads=com.sparrowwallet.merged.module=java.sql",
|
||||
@@ -221,17 +248,18 @@ jlink {
|
||||
"--add-reads=com.sparrowwallet.merged.module=org.bouncycastle.pg",
|
||||
"--add-reads=com.sparrowwallet.merged.module=org.bouncycastle.provider",
|
||||
"--add-reads=com.sparrowwallet.merged.module=kotlin.stdlib",
|
||||
"--add-reads=com.sparrowwallet.merged.module=org.reactfx.reactfx",
|
||||
"--add-reads=kotlin.stdlib=kotlinx.coroutines.core",
|
||||
"--add-reads=org.flywaydb.core=java.desktop"]
|
||||
|
||||
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", "--add-opens=javafx.graphics/com.sun.glass.ui.mac=centerdevice.nsmenufx"]
|
||||
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")
|
||||
@@ -241,10 +269,10 @@ jlink {
|
||||
appVersion = "${version}"
|
||||
skipInstaller = os.macOsX || properties.skipInstallers
|
||||
imageOptions = []
|
||||
installerOptions = ['--file-associations', 'src/main/deploy/psbt.properties', '--file-associations', 'src/main/deploy/txn.properties', '--file-associations', 'src/main/deploy/asc.properties', '--file-associations', 'src/main/deploy/bitcoin.properties', '--file-associations', 'src/main/deploy/auth47.properties', '--file-associations', 'src/main/deploy/lightning.properties', '--license-file', 'LICENSE']
|
||||
installerOptions = ['--file-associations', 'src/main/deploy/psbt.properties', '--file-associations', 'src/main/deploy/txn.properties', '--file-associations', 'src/main/deploy/asc.properties', '--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) {
|
||||
@@ -253,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/osx/sparrow.icns', '--resource-dir', 'src/main/deploy/package/osx/']
|
||||
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"
|
||||
}
|
||||
}
|
||||
@@ -272,15 +300,19 @@ 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) {
|
||||
if(os.windows) {
|
||||
commandLine 'icacls', "$buildDir\\image\\legal", '/grant', 'Users:(OI)(CI)F', '/T'
|
||||
def usersGroup = '*S-1-5-32-545' // Windows "Users" group SID (language-independent)
|
||||
commandLine 'icacls', "$buildDir\\image\\legal", '/grant', "${usersGroup}:(OI)(CI)F", '/T'
|
||||
} else {
|
||||
commandLine 'chmod', '-R', 'u+w', "$buildDir/image/legal"
|
||||
}
|
||||
@@ -308,6 +340,13 @@ tasks.register('prepareResourceDir', Copy) {
|
||||
}
|
||||
}
|
||||
|
||||
tasks.register('copyMimeInfo', Copy) {
|
||||
mustRunAfter tasks.jpackageImage
|
||||
from('src/main/deploy/package/linux')
|
||||
into(layout.buildDirectory.dir('jpackage/Sparrow/lib'))
|
||||
include('sparrowwallet-Sparrow-MimeInfo.xml')
|
||||
}
|
||||
|
||||
static def getDirectorySize(File directory) {
|
||||
long size = 0
|
||||
if(directory.isFile()) {
|
||||
@@ -346,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')
|
||||
@@ -374,39 +482,12 @@ 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('de.codecentric.centerdevice:centerdevice-nsmenufx', 'centerdevice.nsmenufx') {
|
||||
exports('de.codecentric.centerdevice')
|
||||
requires('javafx.base')
|
||||
requires('javafx.controls')
|
||||
requires('javafx.graphics')
|
||||
}
|
||||
module('net.sourceforge.javacsv:javacsv', 'net.sourceforge.javacsv') {
|
||||
exports('com.csvreader')
|
||||
}
|
||||
module('com.google.guava:listenablefuture|empty-to-avoid-conflict-with-guava', 'com.google.guava.listenablefuture')
|
||||
module('com.google.code.findbugs:jsr305', 'com.google.code.findbugs.jsr305')
|
||||
module('j2objc-annotations-2.8.jar', 'com.google.j2objc.j2objc.annotations', '2.8')
|
||||
module('org.jdbi:jdbi3-core', 'org.jdbi.v3.core') {
|
||||
exports('org.jdbi.v3.core')
|
||||
exports('org.jdbi.v3.core.mapper')
|
||||
exports('org.jdbi.v3.core.statement')
|
||||
exports('org.jdbi.v3.core.result')
|
||||
exports('org.jdbi.v3.core.h2')
|
||||
exports('org.jdbi.v3.core.spi')
|
||||
requires('io.leangen.geantyref')
|
||||
requires('java.sql')
|
||||
requires('org.slf4j')
|
||||
requires('com.github.benmanes.caffeine')
|
||||
}
|
||||
module('io.leangen.geantyref:geantyref', 'io.leangen.geantyref') {
|
||||
exports('io.leangen.geantyref')
|
||||
}
|
||||
module('org.fxmisc.richtext:richtextfx', 'org.fxmisc.richtext') {
|
||||
exports('org.fxmisc.richtext')
|
||||
exports('org.fxmisc.richtext.event')
|
||||
@@ -416,10 +497,10 @@ extraJavaModuleInfo {
|
||||
requires('javafx.graphics')
|
||||
requires('org.fxmisc.flowless')
|
||||
requires('org.reactfx.reactfx')
|
||||
requires('org.fxmisc.undo.undofx')
|
||||
requires('org.fxmisc.undo')
|
||||
requires('org.fxmisc.wellbehaved')
|
||||
}
|
||||
module('org.fxmisc.undo:undofx', 'org.fxmisc.undo.undofx') {
|
||||
module('org.fxmisc.undo:undofx', 'org.fxmisc.undo') {
|
||||
requires('javafx.base')
|
||||
requires('javafx.controls')
|
||||
requires('javafx.graphics')
|
||||
@@ -466,16 +547,6 @@ extraJavaModuleInfo {
|
||||
exports('co.nstant.in.cbor.model')
|
||||
exports('co.nstant.in.cbor.builder')
|
||||
}
|
||||
module('net.sourceforge.streamsupport:streamsupport', 'net.sourceforge.streamsupport') {
|
||||
requires('jdk.unsupported')
|
||||
exports('java8.util')
|
||||
exports('java8.util.function')
|
||||
exports('java8.util.stream')
|
||||
}
|
||||
module('net.coobird:thumbnailator', 'net.coobird.thumbnailator') {
|
||||
exports('net.coobird.thumbnailator')
|
||||
requires('java.desktop')
|
||||
}
|
||||
module('org.jcommander:jcommander', 'org.jcommander') {
|
||||
exports('com.beust.jcommander')
|
||||
}
|
||||
|
||||
@@ -1,24 +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'
|
||||
implementation 'org.javamodularity:moduleplugin:1.8.14'
|
||||
}
|
||||
|
||||
repositories {
|
||||
mavenCentral()
|
||||
maven {
|
||||
url "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,49 +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.javamodularity.moduleplugin.ModuleSystemPlugin;
|
||||
import org.openjfx.gradle.tasks.ExecTask;
|
||||
|
||||
public class JavaFXPlugin implements Plugin<Project> {
|
||||
|
||||
@Override
|
||||
public void apply(Project project) {
|
||||
project.getPlugins().apply(OsDetectorPlugin.class);
|
||||
project.getPlugins().apply(ModuleSystemPlugin.class);
|
||||
|
||||
project.getExtensions().create("javafx", JavaFXOptions.class, project);
|
||||
|
||||
project.getTasks().create("configJavafxRun", ExecTask.class, project);
|
||||
}
|
||||
}
|
||||
@@ -1,124 +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.logging.Logger;
|
||||
import org.gradle.api.logging.Logging;
|
||||
import org.gradle.api.plugins.ApplicationPlugin;
|
||||
import org.gradle.api.tasks.JavaExec;
|
||||
import org.gradle.api.tasks.TaskAction;
|
||||
import org.javamodularity.moduleplugin.extensions.RunModuleOptions;
|
||||
import org.openjfx.gradle.JavaFXModule;
|
||||
import org.openjfx.gradle.JavaFXOptions;
|
||||
import org.openjfx.gradle.JavaFXPlatform;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import java.io.File;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.TreeSet;
|
||||
|
||||
public class ExecTask extends DefaultTask {
|
||||
|
||||
private static final Logger LOGGER = Logging.getLogger(ExecTask.class);
|
||||
|
||||
private final Project project;
|
||||
private JavaExec execTask;
|
||||
|
||||
@Inject
|
||||
public ExecTask(Project project) {
|
||||
this.project = project;
|
||||
project.getPluginManager().withPlugin(ApplicationPlugin.APPLICATION_PLUGIN_NAME, e -> {
|
||||
execTask = (JavaExec) project.getTasks().findByName(ApplicationPlugin.TASK_RUN_NAME);
|
||||
if (execTask != null) {
|
||||
execTask.dependsOn(this);
|
||||
} else {
|
||||
throw new GradleException("Run task not found.");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@TaskAction
|
||||
public void action() {
|
||||
if (execTask != null) {
|
||||
JavaFXOptions javaFXOptions = project.getExtensions().getByType(JavaFXOptions.class);
|
||||
JavaFXModule.validateModules(javaFXOptions.getModules());
|
||||
|
||||
var definedJavaFXModuleNames = new TreeSet<>(javaFXOptions.getModules());
|
||||
if (!definedJavaFXModuleNames.isEmpty()) {
|
||||
RunModuleOptions moduleOptions = execTask.getExtensions().findByType(RunModuleOptions.class);
|
||||
|
||||
final FileCollection classpathWithoutJavaFXJars = execTask.getClasspath().filter(
|
||||
jar -> Arrays.stream(JavaFXModule.values()).noneMatch(javaFXModule -> jar.getName().contains(javaFXModule.getArtifactName()))
|
||||
);
|
||||
final FileCollection javaFXPlatformJars = execTask.getClasspath().filter(jar -> isJavaFXJar(jar, javaFXOptions.getPlatform()));
|
||||
|
||||
if (moduleOptions != null) {
|
||||
LOGGER.info("Modular JavaFX application found");
|
||||
// Remove empty JavaFX jars from classpath
|
||||
execTask.setClasspath(classpathWithoutJavaFXJars.plus(javaFXPlatformJars));
|
||||
definedJavaFXModuleNames.forEach(javaFXModule -> moduleOptions.getAddModules().add(javaFXModule));
|
||||
} else {
|
||||
LOGGER.info("Non-modular JavaFX application found");
|
||||
// Remove all JavaFX jars from classpath
|
||||
execTask.setClasspath(classpathWithoutJavaFXJars);
|
||||
|
||||
var javaFXModuleJvmArgs = List.of("--module-path", javaFXPlatformJars.getAsPath());
|
||||
|
||||
var jvmArgs = new ArrayList<String>();
|
||||
jvmArgs.add("--add-modules");
|
||||
jvmArgs.add(String.join(",", definedJavaFXModuleNames));
|
||||
|
||||
List<String> execJvmArgs = execTask.getJvmArgs();
|
||||
if (execJvmArgs != null) {
|
||||
jvmArgs.addAll(execJvmArgs);
|
||||
}
|
||||
jvmArgs.addAll(javaFXModuleJvmArgs);
|
||||
|
||||
execTask.setJvmArgs(jvmArgs);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
throw new GradleException("Run task not found. Please, make sure the Application plugin is applied");
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean isJavaFXJar(File jar, JavaFXPlatform platform) {
|
||||
return jar.isFile() &&
|
||||
Arrays.stream(JavaFXModule.values()).anyMatch(javaFXModule ->
|
||||
javaFXModule.compareJarFileName(platform, jar.getName()) ||
|
||||
javaFXModule.getModuleJarFileName().equals(jar.getName()));
|
||||
}
|
||||
}
|
||||
+20
-47
@@ -12,63 +12,36 @@ Work on resolving both of these issues is ongoing.
|
||||
### Install Java
|
||||
|
||||
Because Sparrow bundles a Java runtime in the release binaries, it is essential to have the same version of Java installed when creating the release.
|
||||
For v1.6.6 to v1.9.1, this was Eclipse Temurin 18.0.1+10. For v2.0.0 and later, Eclipse Temurin 22.0.2+9 is used.
|
||||
For v1.6.6 to v1.9.1, this was Eclipse Temurin 18.0.1+10. For v2.0.0 to v2.3.1, this was Eclipse Temurin 22.0.2+9. For v2.4.0 and later, Eclipse Temurin 25.0.2+10 is used.
|
||||
|
||||
Note: Do not install Java using a system package manager (e.g. apt, dnf, rpm).
|
||||
Linux packages replace the JDK's bundled `cacerts` file with a symlink to the system CA certificates, which differ from those in the release tarballs and will produce a non-reproducible build.
|
||||
|
||||
#### Java from Adoptium github repo
|
||||
|
||||
It is available for all supported platforms from [Eclipse Temurin 22.0.2+9](https://github.com/adoptium/temurin22-binaries/releases/tag/jdk-22.0.2%2B9).
|
||||
It is available for all supported platforms from [Eclipse Temurin 25.0.2+10](https://github.com/adoptium/temurin25-binaries/releases/tag/jdk-25.0.2%2B10).
|
||||
|
||||
For reference, the downloads are as follows:
|
||||
- [Linux x64](https://github.com/adoptium/temurin22-binaries/releases/download/jdk-22.0.2%2B9/OpenJDK22U-jdk_x64_linux_hotspot_22.0.2_9.tar.gz)
|
||||
- [Linux aarch64](https://github.com/adoptium/temurin22-binaries/releases/download/jdk-22.0.2%2B9/OpenJDK22U-jdk_aarch64_linux_hotspot_22.0.2_9.tar.gz)
|
||||
- [MacOS x64](https://github.com/adoptium/temurin22-binaries/releases/download/jdk-22.0.2%2B9/OpenJDK22U-jdk_x64_mac_hotspot_22.0.2_9.tar.gz)
|
||||
- [MacOS aarch64](https://github.com/adoptium/temurin22-binaries/releases/download/jdk-22.0.2%2B9/OpenJDK22U-jdk_aarch64_mac_hotspot_22.0.2_9.tar.gz)
|
||||
- [Windows x64](https://github.com/adoptium/temurin22-binaries/releases/download/jdk-22.0.2%2B9/OpenJDK22U-jdk_x64_windows_hotspot_22.0.2_9.zip)
|
||||
- [Linux x64](https://github.com/adoptium/temurin25-binaries/releases/download/jdk-25.0.2%2B10/OpenJDK25U-jdk_x64_linux_hotspot_25.0.2_10.tar.gz)
|
||||
- [Linux aarch64](https://github.com/adoptium/temurin25-binaries/releases/download/jdk-25.0.2%2B10/OpenJDK25U-jdk_aarch64_linux_hotspot_25.0.2_10.tar.gz)
|
||||
- [MacOS x64](https://github.com/adoptium/temurin25-binaries/releases/download/jdk-25.0.2%2B10/OpenJDK25U-jdk_x64_mac_hotspot_25.0.2_10.tar.gz)
|
||||
- [MacOS aarch64](https://github.com/adoptium/temurin25-binaries/releases/download/jdk-25.0.2%2B10/OpenJDK25U-jdk_aarch64_mac_hotspot_25.0.2_10.tar.gz)
|
||||
- [Windows x64](https://github.com/adoptium/temurin25-binaries/releases/download/jdk-25.0.2%2B10/OpenJDK25U-jdk_x64_windows_hotspot_25.0.2_10.zip)
|
||||
|
||||
#### Java from Adoptium deb repo
|
||||
|
||||
It is also possible to install via a package manager on *nix systems. For example, on Debian/Ubuntu systems:
|
||||
|
||||
- Install dependencies:
|
||||
```sh
|
||||
sudo apt-get install -y wget curl apt-transport-https gnupg
|
||||
On Linux, extract the tarball and set `JAVA_HOME` to use it for the build:
|
||||
```shell
|
||||
tar -xzf OpenJDK25U-jdk_x64_linux_hotspot_25.0.2_10.tar.gz
|
||||
export JAVA_HOME=$PWD/jdk-25.0.2+10
|
||||
export PATH=$JAVA_HOME/bin:$PATH
|
||||
```
|
||||
|
||||
Download Adoptium public PGP key:
|
||||
```sh
|
||||
curl --tlsv1.2 --proto =https --location -o adoptium.asc https://packages.adoptium.net/artifactory/api/gpg/key/public
|
||||
```
|
||||
#### Java from SDKMAN
|
||||
|
||||
Check if key fingerprint matches: `3B04D753C9050D9A5D343F39843C48A565F8F04B`:
|
||||
```
|
||||
gpg --import --import-options show-only adoptium.asc
|
||||
```
|
||||
If key doesn't match, do not proceed.
|
||||
|
||||
Add Adoptium PGP key to a the keyring shared folder:
|
||||
```sh
|
||||
sudo cp adoptium.asc /usr/share/keyrings/
|
||||
```
|
||||
|
||||
Add Adoptium debian repository:
|
||||
```sh
|
||||
echo "deb [signed-by=/usr/share/keyrings/adoptium.asc] https://packages.adoptium.net/artifactory/deb $(awk -F= '/^VERSION_CODENAME/{print$2}' /etc/os-release) main" | sudo tee /etc/apt/sources.list.d/adoptium.list
|
||||
```
|
||||
|
||||
Update cache, install the desired temurin version and configure java to be linked to this same version:
|
||||
```
|
||||
sudo apt update -y
|
||||
sudo apt-get install -y temurin-22-jdk=22.0.2+9
|
||||
sudo update-alternatives --config java
|
||||
```
|
||||
|
||||
#### Java from SDK
|
||||
|
||||
A alternative option for all platforms is to use the [sdkman.io](https://sdkman.io/) package manager ([Git Bash for Windows](https://git-scm.com/download/win) is a good choice on that platform).
|
||||
An alternative option for all platforms is to use the [sdkman.io](https://sdkman.io/) package manager ([Git Bash for Windows](https://git-scm.com/download/win) is a good choice on that platform).
|
||||
See the installation [instructions here](https://sdkman.io/install).
|
||||
Once installed, run
|
||||
```shell
|
||||
sdk install java 22.0.2-tem
|
||||
sdk install java 25.0.2-tem
|
||||
```
|
||||
|
||||
### Other requirements
|
||||
@@ -83,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.2.2"
|
||||
GIT_TAG="2.5.3"
|
||||
```
|
||||
|
||||
The project can then be initially cloned as follows:
|
||||
@@ -101,7 +74,7 @@ git checkout "${GIT_TAG}"
|
||||
```
|
||||
|
||||
Note - there is an additional step if you updated rather than initially cloned your repo at `GIT_TAG`.
|
||||
This is due to the [drongo submodule](https://github.com/sparrowwallet/drongo/tree/master) which needs to be checked out to the commit state it had at the time of the release.
|
||||
This is due to the Git submodules which need to be checked out to the commit state they had at the time of the release.
|
||||
Only then your build will be comparable to the provided one in the release section of Github.
|
||||
To checkout the submodule to the correct commit for `GIT_TAG`, additionally run:
|
||||
|
||||
|
||||
+1
-1
Submodule drongo updated: 13e1fafbe8...fa1bb4eec2
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
Vendored
BIN
Binary file not shown.
+1
-1
@@ -1,6 +1,6 @@
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-bin.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-9.1.0-bin.zip
|
||||
networkTimeout=10000
|
||||
validateDistributionUrl=true
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
#!/bin/sh
|
||||
|
||||
#
|
||||
# Copyright © 2015-2021 the original authors.
|
||||
# Copyright © 2015 the original authors.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
@@ -15,6 +15,8 @@
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
#
|
||||
|
||||
##############################################################################
|
||||
#
|
||||
@@ -55,7 +57,7 @@
|
||||
# Darwin, MinGW, and NonStop.
|
||||
#
|
||||
# (3) This script is generated from the Groovy template
|
||||
# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
|
||||
# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
|
||||
# within the Gradle project.
|
||||
#
|
||||
# You can find Gradle at https://github.com/gradle/gradle/.
|
||||
@@ -84,7 +86,7 @@ done
|
||||
# shellcheck disable=SC2034
|
||||
APP_BASE_NAME=${0##*/}
|
||||
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
|
||||
APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit
|
||||
APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit
|
||||
|
||||
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
||||
MAX_FD=maximum
|
||||
@@ -112,7 +114,6 @@ case "$( uname )" in #(
|
||||
NONSTOP* ) nonstop=true ;;
|
||||
esac
|
||||
|
||||
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
|
||||
|
||||
|
||||
# Determine the Java command to use to start the JVM.
|
||||
@@ -170,7 +171,6 @@ fi
|
||||
# For Cygwin or MSYS, switch paths to Windows format before running java
|
||||
if "$cygwin" || "$msys" ; then
|
||||
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
|
||||
CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
|
||||
|
||||
JAVACMD=$( cygpath --unix "$JAVACMD" )
|
||||
|
||||
@@ -203,15 +203,14 @@ fi
|
||||
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
|
||||
|
||||
# Collect all arguments for the java command:
|
||||
# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
|
||||
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
|
||||
# and any embedded shellness will be escaped.
|
||||
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
|
||||
# treated as '${Hostname}' itself on the command line.
|
||||
|
||||
set -- \
|
||||
"-Dorg.gradle.appname=$APP_BASE_NAME" \
|
||||
-classpath "$CLASSPATH" \
|
||||
org.gradle.wrapper.GradleWrapperMain \
|
||||
-jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \
|
||||
"$@"
|
||||
|
||||
# Stop when "xargs" is not available.
|
||||
|
||||
Vendored
+13
-12
@@ -13,6 +13,8 @@
|
||||
@rem See the License for the specific language governing permissions and
|
||||
@rem limitations under the License.
|
||||
@rem
|
||||
@rem SPDX-License-Identifier: Apache-2.0
|
||||
@rem
|
||||
|
||||
@if "%DEBUG%"=="" @echo off
|
||||
@rem ##########################################################################
|
||||
@@ -43,11 +45,11 @@ set JAVA_EXE=java.exe
|
||||
%JAVA_EXE% -version >NUL 2>&1
|
||||
if %ERRORLEVEL% equ 0 goto execute
|
||||
|
||||
echo.
|
||||
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||
echo.
|
||||
echo Please set the JAVA_HOME variable in your environment to match the
|
||||
echo location of your Java installation.
|
||||
echo. 1>&2
|
||||
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
|
||||
echo. 1>&2
|
||||
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
|
||||
echo location of your Java installation. 1>&2
|
||||
|
||||
goto fail
|
||||
|
||||
@@ -57,22 +59,21 @@ set JAVA_EXE=%JAVA_HOME%/bin/java.exe
|
||||
|
||||
if exist "%JAVA_EXE%" goto execute
|
||||
|
||||
echo.
|
||||
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
|
||||
echo.
|
||||
echo Please set the JAVA_HOME variable in your environment to match the
|
||||
echo location of your Java installation.
|
||||
echo. 1>&2
|
||||
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
|
||||
echo. 1>&2
|
||||
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
|
||||
echo location of your Java installation. 1>&2
|
||||
|
||||
goto fail
|
||||
|
||||
:execute
|
||||
@rem Setup the command line
|
||||
|
||||
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
|
||||
|
||||
|
||||
@rem Execute Gradle
|
||||
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
|
||||
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %*
|
||||
|
||||
:end
|
||||
@rem End local scope for the variables with windows NT shell
|
||||
|
||||
+1
-1
Submodule lark updated: 5facb25ede...00fdebc715
@@ -1,2 +0,0 @@
|
||||
mime-type=x-scheme-handler/auth47
|
||||
description=Auth47 Authentication URI
|
||||
@@ -1,2 +0,0 @@
|
||||
mime-type=x-scheme-handler/bitcoin
|
||||
description=Bitcoin Scheme URI
|
||||
@@ -1,2 +0,0 @@
|
||||
mime-type=x-scheme-handler/lightning
|
||||
description=LNURL URI
|
||||
@@ -26,7 +26,7 @@ case "$1" in
|
||||
xdg-mime install /opt/sparrowwallet/lib/sparrowwallet-Sparrow-MimeInfo.xml
|
||||
install -D -m 644 /opt/sparrowwallet/lib/runtime/conf/udev/*.rules /etc/udev/rules.d
|
||||
if ! getent group plugdev > /dev/null; then
|
||||
groupadd plugdev
|
||||
groupadd -r plugdev
|
||||
fi
|
||||
if ! groups "${SUDO_USER:-$(whoami)}" | grep -q plugdev; then
|
||||
usermod -aG plugdev "${SUDO_USER:-$(whoami)}"
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<mime-info xmlns="http://www.freedesktop.org/standards/shared-mime-info">
|
||||
<mime-type type="application/psbt">
|
||||
<comment>Partially Signed Bitcoin Transaction</comment>
|
||||
<glob pattern="*.psbt"/>
|
||||
</mime-type>
|
||||
<mime-type type="application/bitcoin-transaction">
|
||||
<comment>Bitcoin Transaction</comment>
|
||||
<glob pattern="*.txn"/>
|
||||
</mime-type>
|
||||
<mime-type type="application/pgp-signature">
|
||||
<comment>ASCII Armored File</comment>
|
||||
<glob pattern="*.asc"/>
|
||||
</mime-type>
|
||||
<mime-type type="x-scheme-handler/bitcoin">
|
||||
<comment>Bitcoin Scheme URI</comment>
|
||||
</mime-type>
|
||||
<mime-type type="x-scheme-handler/auth47">
|
||||
<comment>Auth47 Authentication URI</comment>
|
||||
</mime-type>
|
||||
<mime-type type="x-scheme-handler/lightning">
|
||||
<comment>LNURL URI</comment>
|
||||
</mime-type>
|
||||
</mime-info>
|
||||
@@ -82,7 +82,7 @@ xdg-desktop-menu install /opt/sparrowwallet/lib/sparrowwallet-Sparrow.desktop
|
||||
xdg-mime install /opt/sparrowwallet/lib/sparrowwallet-Sparrow-MimeInfo.xml
|
||||
install -D -m 644 /opt/sparrowwallet/lib/runtime/conf/udev/*.rules /etc/udev/rules.d
|
||||
if ! getent group plugdev > /dev/null; then
|
||||
groupadd plugdev
|
||||
groupadd -r plugdev
|
||||
fi
|
||||
if ! groups "${SUDO_USER:-$(whoami)}" | grep -q plugdev; then
|
||||
usermod -aG plugdev "${SUDO_USER:-$(whoami)}"
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>2.2.3</string>
|
||||
<string>2.5.4</string>
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
<!-- See https://developer.apple.com/app-store/categories/ for list of AppStore categories -->
|
||||
@@ -33,6 +33,8 @@
|
||||
<string>Copyright (C) 2021</string>
|
||||
<key>NSHighResolutionCapable</key>
|
||||
<string>true</string>
|
||||
<key>NSCameraUseContinuityCameraDeviceType</key>
|
||||
<true/>
|
||||
<key>NSCameraUsageDescription</key>
|
||||
<string>Sparrow requires access to the camera in order to scan QR codes</string>
|
||||
<key>NSLocalNetworkUsageDescription</key>
|
||||
|
Before Width: | Height: | Size: 99 KiB After Width: | Height: | Size: 99 KiB |
@@ -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>
|
||||
|
||||
@@ -3,13 +3,14 @@ package com.sparrowwallet.sparrow;
|
||||
import com.beust.jcommander.JCommander;
|
||||
import com.google.common.eventbus.Subscribe;
|
||||
import com.sparrowwallet.drongo.*;
|
||||
import com.sparrowwallet.drongo.address.Address;
|
||||
import com.sparrowwallet.drongo.crypto.*;
|
||||
import com.sparrowwallet.drongo.dns.DnsPayment;
|
||||
import com.sparrowwallet.drongo.dns.DnsPaymentCache;
|
||||
import com.sparrowwallet.drongo.policy.PolicyType;
|
||||
import com.sparrowwallet.drongo.protocol.*;
|
||||
import com.sparrowwallet.drongo.psbt.PSBT;
|
||||
import com.sparrowwallet.drongo.psbt.PSBTInput;
|
||||
import com.sparrowwallet.drongo.psbt.PSBTParseException;
|
||||
import com.sparrowwallet.drongo.psbt.PSBTSignatureException;
|
||||
import com.sparrowwallet.drongo.psbt.*;
|
||||
import com.sparrowwallet.drongo.silentpayments.SilentPaymentAddress;
|
||||
import com.sparrowwallet.drongo.wallet.*;
|
||||
import com.sparrowwallet.hummingbird.UR;
|
||||
import com.sparrowwallet.hummingbird.registry.CryptoPSBT;
|
||||
@@ -30,7 +31,7 @@ import com.sparrowwallet.sparrow.transaction.TransactionView;
|
||||
import com.sparrowwallet.sparrow.wallet.Entry;
|
||||
import com.sparrowwallet.sparrow.wallet.WalletController;
|
||||
import com.sparrowwallet.sparrow.wallet.WalletForm;
|
||||
import de.codecentric.centerdevice.MenuToolkit;
|
||||
import de.jangassen.MenuToolkit;
|
||||
import javafx.animation.*;
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.binding.Bindings;
|
||||
@@ -49,12 +50,14 @@ import javafx.geometry.Side;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.Scene;
|
||||
import javafx.scene.control.*;
|
||||
import javafx.scene.image.Image;
|
||||
import javafx.scene.image.ImageView;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.control.Menu;
|
||||
import javafx.scene.control.MenuItem;
|
||||
import javafx.scene.input.*;
|
||||
import javafx.scene.layout.*;
|
||||
import javafx.scene.paint.Color;
|
||||
import javafx.stage.*;
|
||||
import javafx.stage.Window;
|
||||
import javafx.util.Duration;
|
||||
import org.controlsfx.control.Notifications;
|
||||
import org.controlsfx.control.StatusBar;
|
||||
@@ -69,6 +72,7 @@ import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.text.ParseException;
|
||||
import java.util.*;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static com.sparrowwallet.sparrow.AppServices.*;
|
||||
@@ -84,7 +88,9 @@ 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;
|
||||
|
||||
@FXML
|
||||
private MenuItem saveTransaction;
|
||||
@@ -134,6 +140,13 @@ public class AppController implements Initializable {
|
||||
@FXML
|
||||
private ToggleGroup unitFormat;
|
||||
|
||||
@FXML
|
||||
private CheckMenuItem chunkAddresses;
|
||||
|
||||
@FXML
|
||||
private CheckMenuItem hideEmptyUsedAddresses;
|
||||
private static final BooleanProperty hideEmptyUsedAddressesProperty = new SimpleBooleanProperty();
|
||||
|
||||
@FXML
|
||||
private ToggleGroup theme;
|
||||
|
||||
@@ -142,8 +155,7 @@ public class AppController implements Initializable {
|
||||
private static final BooleanProperty openWalletsInNewWindowsProperty = new SimpleBooleanProperty();
|
||||
|
||||
@FXML
|
||||
private CheckMenuItem hideEmptyUsedAddresses;
|
||||
private static final BooleanProperty hideEmptyUsedAddressesProperty = new SimpleBooleanProperty();
|
||||
private CheckMenuItem hideAmounts;
|
||||
|
||||
@FXML
|
||||
private CheckMenuItem useHdCameraResolution;
|
||||
@@ -366,6 +378,13 @@ public class AppController implements Initializable {
|
||||
Optional<Toggle> selectedFormatToggle = unitFormat.getToggles().stream().filter(toggle -> selectedFormat.equals(toggle.getUserData())).findFirst();
|
||||
selectedFormatToggle.ifPresent(toggle -> unitFormat.selectToggle(toggle));
|
||||
|
||||
chunkAddresses.setSelected(Config.get().isChunkAddresses());
|
||||
if(Config.get().isChunkAddresses()) {
|
||||
rootBox.getStyleClass().add("chunk-addresses");
|
||||
}
|
||||
hideEmptyUsedAddressesProperty.set(Config.get().isHideEmptyUsedAddresses());
|
||||
hideEmptyUsedAddresses.selectedProperty().bindBidirectional(hideEmptyUsedAddressesProperty);
|
||||
|
||||
Theme configTheme = Config.get().getTheme();
|
||||
if(configTheme == null) {
|
||||
configTheme = Theme.LIGHT;
|
||||
@@ -378,8 +397,7 @@ public class AppController implements Initializable {
|
||||
|
||||
openWalletsInNewWindowsProperty.set(Config.get().isOpenWalletsInNewWindows());
|
||||
openWalletsInNewWindows.selectedProperty().bindBidirectional(openWalletsInNewWindowsProperty);
|
||||
hideEmptyUsedAddressesProperty.set(Config.get().isHideEmptyUsedAddresses());
|
||||
hideEmptyUsedAddresses.selectedProperty().bindBidirectional(hideEmptyUsedAddressesProperty);
|
||||
hideAmounts.setSelected(Config.get().isHideAmounts());
|
||||
useHdCameraResolutionProperty.set(Config.get().getWebcamResolution() == null || Config.get().getWebcamResolution().isWidescreenAspect());
|
||||
useHdCameraResolution.selectedProperty().bindBidirectional(useHdCameraResolutionProperty);
|
||||
mirrorCameraImageProperty.set(Config.get().isMirrorCapture());
|
||||
@@ -401,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()));
|
||||
@@ -453,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);
|
||||
@@ -506,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 {
|
||||
@@ -575,10 +593,10 @@ public class AppController implements Initializable {
|
||||
sudo install -m 644 /opt/sparrowwallet/lib/runtime/conf/udev/*.rules /etc/udev/rules.d
|
||||
sudo udevadm control --reload
|
||||
sudo udevadm trigger
|
||||
sudo groupadd -f plugdev
|
||||
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);
|
||||
@@ -596,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();
|
||||
@@ -610,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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -633,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());
|
||||
@@ -657,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) {
|
||||
@@ -763,7 +787,8 @@ public class AppController implements Initializable {
|
||||
byte[] txBytes = transaction.bitcoinSerialize();
|
||||
UR ur = UR.fromBytes(txBytes);
|
||||
BBQR bbqr = new BBQR(BBQRType.TXN, txBytes);
|
||||
QRDisplayDialog qrDisplayDialog = new QRDisplayDialog(ur, bbqr, false, false, false);
|
||||
String raw = Utils.bytesToHex(txBytes);
|
||||
QRDisplayDialog qrDisplayDialog = new QRDisplayDialog(ur, bbqr, raw, false, false, QREncoding.UR);
|
||||
qrDisplayDialog.initOwner(rootStack.getScene().getWindow());
|
||||
qrDisplayDialog.showAndWait();
|
||||
} catch(Exception e) {
|
||||
@@ -822,10 +847,10 @@ public class AppController implements Initializable {
|
||||
try(FileOutputStream outputStream = new FileOutputStream(file)) {
|
||||
if(asText) {
|
||||
PrintWriter writer = new PrintWriter(new OutputStreamWriter(outputStream, StandardCharsets.UTF_8));
|
||||
writer.print(transactionTabData.getPsbt().toBase64String(includeXpubs));
|
||||
writer.print(transactionTabData.getPsbt().getForExport().toBase64String(includeXpubs));
|
||||
writer.flush();
|
||||
} else {
|
||||
outputStream.write(transactionTabData.getPsbt().serialize(includeXpubs, true));
|
||||
outputStream.write(transactionTabData.getPsbt().getForExport().serialize(includeXpubs, true));
|
||||
}
|
||||
} catch(IOException e) {
|
||||
log.error("Error saving PSBT", e);
|
||||
@@ -848,7 +873,7 @@ public class AppController implements Initializable {
|
||||
TabData tabData = (TabData)selectedTab.getUserData();
|
||||
if(tabData.getType() == TabData.TabType.TRANSACTION) {
|
||||
TransactionTabData transactionTabData = (TransactionTabData)tabData;
|
||||
String data = asBase64 ? transactionTabData.getPsbt().toBase64String() : transactionTabData.getPsbt().toString();
|
||||
String data = asBase64 ? transactionTabData.getPsbt().getForExport().toBase64String() : transactionTabData.getPsbt().getForExport().toString();
|
||||
|
||||
ClipboardContent content = new ClipboardContent();
|
||||
content.putString(data);
|
||||
@@ -862,10 +887,10 @@ public class AppController implements Initializable {
|
||||
if(tabData.getType() == TabData.TabType.TRANSACTION) {
|
||||
TransactionTabData transactionTabData = (TransactionTabData)tabData;
|
||||
|
||||
byte[] psbtBytes = transactionTabData.getPsbt().serialize();
|
||||
byte[] psbtBytes = transactionTabData.getPsbt().getForExport().serialize();
|
||||
CryptoPSBT cryptoPSBT = new CryptoPSBT(psbtBytes);
|
||||
BBQR bbqr = new BBQR(BBQRType.PSBT, psbtBytes);
|
||||
QRDisplayDialog qrDisplayDialog = new QRDisplayDialog(cryptoPSBT.toUR(), bbqr, false, true, false);
|
||||
QRDisplayDialog qrDisplayDialog = new QRDisplayDialog(cryptoPSBT.toUR(), bbqr, false, true, QREncoding.UR);
|
||||
qrDisplayDialog.initOwner(rootStack.getScene().getWindow());
|
||||
qrDisplayDialog.show();
|
||||
}
|
||||
@@ -937,12 +962,28 @@ public class AppController implements Initializable {
|
||||
EventManager.get().post(new OpenWalletsNewWindowsStatusEvent(item.isSelected()));
|
||||
}
|
||||
|
||||
public void chunkAddresses(ActionEvent event) {
|
||||
CheckMenuItem item = (CheckMenuItem)event.getSource();
|
||||
Config.get().setChunkAddresses(item.isSelected());
|
||||
if(item.isSelected() && !rootBox.getStyleClass().contains("chunk-addresses")) {
|
||||
rootBox.getStyleClass().add("chunk-addresses");
|
||||
} else {
|
||||
rootBox.getStyleClass().remove("chunk-addresses");
|
||||
}
|
||||
}
|
||||
|
||||
public void hideEmptyUsedAddresses(ActionEvent event) {
|
||||
CheckMenuItem item = (CheckMenuItem)event.getSource();
|
||||
Config.get().setHideEmptyUsedAddresses(item.isSelected());
|
||||
EventManager.get().post(new HideEmptyUsedAddressesStatusEvent(item.isSelected()));
|
||||
}
|
||||
|
||||
public void hideAmounts(ActionEvent event) {
|
||||
CheckMenuItem item = (CheckMenuItem)event.getSource();
|
||||
Config.get().setHideAmounts(item.isSelected());
|
||||
EventManager.get().post(new HideAmountsStatusEvent(item.isSelected()));
|
||||
}
|
||||
|
||||
public void useHdCameraResolution(ActionEvent event) {
|
||||
CheckMenuItem item = (CheckMenuItem)event.getSource();
|
||||
if(Config.get().getWebcamResolution().isStandardAspect() && item.isSelected()) {
|
||||
@@ -999,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) {
|
||||
@@ -1009,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();
|
||||
@@ -1031,9 +1072,13 @@ 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) {
|
||||
Map<String, String> env = builder.environment();
|
||||
env.remove("LD_LIBRARY_PATH");
|
||||
}
|
||||
builder.start();
|
||||
quit(event);
|
||||
} catch(Exception e) {
|
||||
@@ -1052,7 +1097,7 @@ public class AppController implements Initializable {
|
||||
verifyOpened = true;
|
||||
}
|
||||
} else {
|
||||
openTransactionFile(file);
|
||||
openTransactionFile(file, null);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1082,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);
|
||||
}
|
||||
}
|
||||
@@ -1100,12 +1145,37 @@ public class AppController implements Initializable {
|
||||
AppServices.moveToActiveWindowScreen(window, 800, 450);
|
||||
List<File> files = fileChooser.showOpenMultipleDialog(window);
|
||||
if(files != null) {
|
||||
configureWalletsDir(files);
|
||||
for(File file : files) {
|
||||
openWalletFile(file, forceSameWindow);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void configureWalletsDir(List<File> files) {
|
||||
List<File> parentDirs = files.stream().map(File::getParentFile).distinct().collect(Collectors.toList());
|
||||
if(parentDirs.size() == 1 && !Boolean.FALSE.equals(Config.get().getSuggestChangeWalletsDir())) {
|
||||
File selectedDir = parentDirs.getFirst();
|
||||
boolean sameDir;
|
||||
try {
|
||||
sameDir = Files.isSameFile(selectedDir.toPath(), Storage.getWalletsDir().toPath());
|
||||
} catch(IOException e) {
|
||||
sameDir = selectedDir.toPath().normalize().equals(Storage.getWalletsDir().toPath().normalize());
|
||||
}
|
||||
if(!sameDir) {
|
||||
ConfirmationAlert alert = new ConfirmationAlert("Change wallets directory?",
|
||||
"Do you want to configure Sparrow to use " + selectedDir + " as the default wallets directory?", ButtonType.NO, ButtonType.YES);
|
||||
Optional<ButtonType> optType = alert.showAndWait();
|
||||
if(optType.isPresent() && optType.get() == ButtonType.YES) {
|
||||
Config.get().setWalletsDir(selectedDir);
|
||||
Config.get().setSuggestChangeWalletsDir(null);
|
||||
} else if(alert.isDontAskAgain()) {
|
||||
Config.get().setSuggestChangeWalletsDir(Boolean.FALSE);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void openWalletFile(File file, boolean forceSameWindow) {
|
||||
try {
|
||||
Storage storage = new Storage(file);
|
||||
@@ -1198,28 +1268,31 @@ public class AppController implements Initializable {
|
||||
List<WalletForm> selectedWalletForms = getSelectedWalletForms();
|
||||
WalletImportDialog dlg = new WalletImportDialog(selectedWalletForms);
|
||||
dlg.initOwner(rootStack.getScene().getWindow());
|
||||
Optional<Wallet> optionalWallet = dlg.showAndWait();
|
||||
if(optionalWallet.isPresent()) {
|
||||
Wallet wallet = optionalWallet.get();
|
||||
Optional<List<Wallet>> optionalWallets = dlg.showAndWait();
|
||||
if(optionalWallets.isPresent()) {
|
||||
List<Wallet> wallets = optionalWallets.get();
|
||||
|
||||
List<WalletTabData> walletTabData = getOpenWalletTabData();
|
||||
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 &&
|
||||
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()) {
|
||||
EventManager.get().post(new ExistingWalletImportedEvent(optNewWalletForm.get().getWalletId(), wallet));
|
||||
selectTab(optNewWalletForm.get().getWallet());
|
||||
} else if(selectedWalletForms.isEmpty() || wallet != selectedWalletForms.get(0).getWallet()) {
|
||||
addImportedWallet(wallet);
|
||||
for(Wallet wallet : wallets) {
|
||||
List<WalletTabData> walletTabData = getOpenWalletTabData();
|
||||
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_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()) {
|
||||
EventManager.get().post(new ExistingWalletImportedEvent(optNewWalletForm.get().getWalletId(), wallet));
|
||||
selectTab(optNewWalletForm.get().getWallet());
|
||||
} else if(selectedWalletForms.isEmpty() || wallet != selectedWalletForms.get(0).getWallet()) {
|
||||
addImportedWallet(wallet);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private boolean attemptImportWallet(File file, SecureString password) {
|
||||
List<WalletImport> walletImporters = List.of(new ColdcardSinglesig(), new ColdcardMultisig(),
|
||||
new Bip129(),
|
||||
new Electrum(),
|
||||
new SpecterDesktop(),
|
||||
new Descriptor(),
|
||||
@@ -1261,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;
|
||||
}
|
||||
@@ -1406,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);
|
||||
}
|
||||
@@ -1441,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;
|
||||
@@ -1861,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());
|
||||
}
|
||||
@@ -1912,6 +1992,39 @@ public class AppController implements Initializable {
|
||||
}
|
||||
}
|
||||
|
||||
//Add DNS payment information if not already cached
|
||||
for(PSBTOutput psbtOutput : psbt.getPsbtOutputs()) {
|
||||
if(psbtOutput.getDnssecProof() != null && !psbtOutput.getDnssecProof().isEmpty()) {
|
||||
Address address = psbtOutput.getScript() != null ? psbtOutput.getScript().getToAddress() : null;
|
||||
if(address != null && DnsPaymentCache.getDnsPayment(address) == null) {
|
||||
try {
|
||||
Optional<DnsPayment> optDnsPayment = psbtOutput.getDnsPayment();
|
||||
if(optDnsPayment.isPresent() && address.equals(optDnsPayment.get().bitcoinURI().getAddress())) {
|
||||
DnsPaymentCache.putDnsPayment(address, optDnsPayment.get());
|
||||
}
|
||||
} catch(Exception e) {
|
||||
log.debug("Error resolving DNS payment", e);
|
||||
}
|
||||
}
|
||||
|
||||
SilentPaymentAddress silentPaymentAddress = psbtOutput.getSilentPaymentAddress();
|
||||
if(address != null && silentPaymentAddress == null) {
|
||||
silentPaymentAddress = AppServices.get().getOpenWallets().keySet().stream()
|
||||
.map(wallet -> wallet.getSilentPaymentAddress(address)).filter(Objects::nonNull).findFirst().orElse(null);
|
||||
}
|
||||
if(silentPaymentAddress != null && DnsPaymentCache.getDnsPayment(silentPaymentAddress) == null) {
|
||||
try {
|
||||
Optional<DnsPayment> optDnsPayment = psbtOutput.getDnsPayment();
|
||||
if(optDnsPayment.isPresent() && silentPaymentAddress.equals(optDnsPayment.get().bitcoinURI().getSilentPaymentAddress())) {
|
||||
DnsPaymentCache.putDnsPayment(silentPaymentAddress, optDnsPayment.get());
|
||||
}
|
||||
} catch(Exception e) {
|
||||
log.debug("Error resolving DNS payment", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Window psbtWalletWindow = AppServices.get().getWindowForPSBT(psbt);
|
||||
if(psbtWalletWindow != null && !tabs.getScene().getWindow().equals(psbtWalletWindow)) {
|
||||
EventManager.get().post(new ViewPSBTEvent(psbtWalletWindow, name, file, psbt));
|
||||
@@ -1935,39 +2048,13 @@ public class AppController implements Initializable {
|
||||
private void addTransactionTab(String name, File file, Transaction transaction, PSBT psbt, BlockTransaction blockTransaction, TransactionView initialView, Integer initialIndex) {
|
||||
for(Tab tab : tabs.getTabs()) {
|
||||
TabData tabData = (TabData)tab.getUserData();
|
||||
if(tabData instanceof TransactionTabData) {
|
||||
TransactionTabData transactionTabData = (TransactionTabData)tabData;
|
||||
if(tabData instanceof TransactionTabData transactionTabData) {
|
||||
if(isExistingTransaction(transactionTabData, transaction, psbt, getTabName(tab))) {
|
||||
handleTransactionMerge(transactionTabData, psbt, name, tab);
|
||||
return;
|
||||
}
|
||||
|
||||
//If an exact match bytewise of an existing tab, return that tab
|
||||
if(Arrays.equals(transactionTabData.getTransaction().bitcoinSerialize(), transaction.bitcoinSerialize())) {
|
||||
if(transactionTabData.getPsbt() != null && psbt != null && !transactionTabData.getPsbt().isFinalized()) {
|
||||
if(!psbt.isFinalized()) {
|
||||
//As per BIP174, combine PSBTs with matching transactions so long as they are not yet finalized
|
||||
transactionTabData.getPsbt().combine(psbt);
|
||||
if(name != null && !name.isEmpty()) {
|
||||
((Label)tab.getGraphic()).setText(name);
|
||||
}
|
||||
|
||||
EventManager.get().post(new PSBTCombinedEvent(transactionTabData.getPsbt()));
|
||||
} else {
|
||||
//If the new PSBT is finalized, copy the finalized fields to the existing unfinalized PSBT
|
||||
for(int i = 0; i < transactionTabData.getPsbt().getPsbtInputs().size(); i++) {
|
||||
PSBTInput existingInput = transactionTabData.getPsbt().getPsbtInputs().get(i);
|
||||
PSBTInput finalizedInput = psbt.getPsbtInputs().get(i);
|
||||
existingInput.setFinalScriptSig(finalizedInput.getFinalScriptSig());
|
||||
existingInput.setFinalScriptWitness(finalizedInput.getFinalScriptWitness());
|
||||
existingInput.clearNonFinalFields();
|
||||
}
|
||||
|
||||
if(name != null && !name.isEmpty()) {
|
||||
((Label)tab.getGraphic()).setText(name);
|
||||
}
|
||||
|
||||
EventManager.get().post(new PSBTFinalizedEvent(transactionTabData.getPsbt()));
|
||||
}
|
||||
}
|
||||
|
||||
tabs.getSelectionModel().select(tab);
|
||||
if(transactionTabData.getPsbt() != null && transactionTabData.getPsbt().possibleUnverifiableSilentPaymentsTransaction(transaction) && !openUnverifiableTransaction(getTabName(tab))) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -1981,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 {
|
||||
@@ -2034,6 +2149,117 @@ public class AppController implements Initializable {
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isExistingTransaction(TransactionTabData transactionTabData, Transaction transaction, PSBT psbt, String tabName) {
|
||||
PSBT currentPsbt = transactionTabData.getPsbt();
|
||||
Transaction currentTransaction = transactionTabData.getTransaction();
|
||||
|
||||
if(currentPsbt != null && psbt != null && currentPsbt.matches(psbt)) {
|
||||
return true;
|
||||
} else if(currentTransaction.getTxId().equals(transaction.getTxId())) {
|
||||
if(currentTransaction.getWTxId().equals(transaction.getWTxId())) {
|
||||
return true;
|
||||
} else if(currentPsbt == null) {
|
||||
AppServices.showWarningDialog("Suspicious Transaction",
|
||||
"This transaction has the same txid as the transaction in tab " + tabName + ", but contains different witnesses. It will be opened in a separate tab.");
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private void handleTransactionMerge(TransactionTabData transactionTabData, PSBT psbt, String name, Tab tab) {
|
||||
PSBT currentPsbt = transactionTabData.getPsbt();
|
||||
|
||||
if(currentPsbt != null && psbt != null && !currentPsbt.isFinalized()) {
|
||||
if(!psbt.isFinalized()) {
|
||||
//As per BIP174, combine PSBTs with matching transactions so long as they are not yet finalized
|
||||
try {
|
||||
currentPsbt.verifyCombinedSignatures(psbt);
|
||||
currentPsbt.combine(psbt);
|
||||
setTabName(tab, name);
|
||||
EventManager.get().post(new PSBTCombinedEvent(currentPsbt));
|
||||
} catch(PSBTSignatureException e) {
|
||||
AppServices.showErrorDialog("Invalid PSBT", e.getMessage());
|
||||
}
|
||||
} else {
|
||||
//If the new PSBT is finalized, copy the finalized fields to the existing unfinalized PSBT
|
||||
currentPsbt.copyFinalizedFields(psbt);
|
||||
setTabName(tab, name);
|
||||
EventManager.get().post(new PSBTFinalizedEvent(currentPsbt));
|
||||
}
|
||||
}
|
||||
|
||||
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",
|
||||
"This transaction contains an unverifiable silent payments output.\n\n" +
|
||||
"The tab " + tabName + " contains a similar transaction spending to a silent payments address, " +
|
||||
"but this transaction does not contain enough information to determine if the recipient address is correct.\n\n" +
|
||||
"Open the transaction in another tab?", ButtonType.YES, ButtonType.NO);
|
||||
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();
|
||||
}
|
||||
|
||||
private void setTabName(Tab tab, String name){
|
||||
if(name != null && !name.isEmpty()) {
|
||||
((Label)tab.getGraphic()).setText(name);
|
||||
}
|
||||
}
|
||||
|
||||
private ContextMenu getTabContextMenu(Tab tab) {
|
||||
ContextMenu contextMenu = new ContextMenu();
|
||||
|
||||
@@ -2050,23 +2276,33 @@ public class AppController implements Initializable {
|
||||
}
|
||||
|
||||
MenuItem moveRight = new MenuItem("Move Right");
|
||||
moveRight.setAccelerator(new KeyCodeCombination(KeyCode.RIGHT, KeyCombination.CONTROL_DOWN, KeyCombination.SHIFT_DOWN));
|
||||
moveRight.setOnAction(event -> {
|
||||
int index = tabs.getTabs().indexOf(tab);
|
||||
int currentIndex = tabs.getSelectionModel().getSelectedIndex();
|
||||
if(currentIndex + 1 >= tabs.getTabs().size()) {
|
||||
return;
|
||||
}
|
||||
Tab selectedTab = tabs.getSelectionModel().getSelectedItem();
|
||||
tabs.getTabs().removeListener(tabsChangeListener);
|
||||
tabs.getTabs().remove(tab);
|
||||
tabs.getTabs().add(index + 1, tab);
|
||||
tabs.getTabs().remove(selectedTab);
|
||||
tabs.getTabs().add(currentIndex + 1, selectedTab);
|
||||
tabs.getTabs().addListener(tabsChangeListener);
|
||||
tabs.getSelectionModel().select(tab);
|
||||
tabs.getSelectionModel().select(selectedTab);
|
||||
EventManager.get().post(new RequestOpenWalletsEvent()); //Rearrange recent files list
|
||||
});
|
||||
MenuItem moveLeft = new MenuItem("Move Left");
|
||||
moveLeft.setAccelerator(new KeyCodeCombination(KeyCode.LEFT, KeyCombination.CONTROL_DOWN, KeyCombination.SHIFT_DOWN));
|
||||
moveLeft.setOnAction(event -> {
|
||||
int index = tabs.getTabs().indexOf(tab);
|
||||
int currentIndex = tabs.getSelectionModel().getSelectedIndex();
|
||||
if(currentIndex == 0) {
|
||||
return;
|
||||
}
|
||||
Tab selectedTab = tabs.getSelectionModel().getSelectedItem();
|
||||
tabs.getTabs().removeListener(tabsChangeListener);
|
||||
tabs.getTabs().remove(tab);
|
||||
tabs.getTabs().add(index - 1, tab);
|
||||
tabs.getTabs().remove(selectedTab);
|
||||
tabs.getTabs().add(currentIndex - 1, selectedTab);
|
||||
tabs.getTabs().addListener(tabsChangeListener);
|
||||
tabs.getSelectionModel().select(tab);
|
||||
tabs.getSelectionModel().select(selectedTab);
|
||||
EventManager.get().post(new RequestOpenWalletsEvent()); //Rearrange recent files list
|
||||
});
|
||||
contextMenu.getItems().addAll(moveRight, moveLeft);
|
||||
@@ -2499,7 +2735,7 @@ public class AppController implements Initializable {
|
||||
if(event instanceof TransactionTabSelectedEvent) {
|
||||
TransactionTabSelectedEvent txTabEvent = (TransactionTabSelectedEvent)event;
|
||||
TransactionTabData transactionTabData = txTabEvent.getTransactionTabData();
|
||||
if(transactionTabData.getPsbt() == null || transactionTabData.getPsbt().getTransaction() != transactionTabData.getTransaction()) {
|
||||
if(transactionTabData.getPsbt() == null || !transactionTabData.getPsbt().getTransaction().getTxId().equals(transactionTabData.getTransaction().getTxId())) {
|
||||
saveTransaction.setVisible(true);
|
||||
saveTransaction.setDisable(false);
|
||||
} else {
|
||||
@@ -3025,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());
|
||||
}
|
||||
}
|
||||
@@ -3034,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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3068,6 +3306,11 @@ public class AppController implements Initializable {
|
||||
hideEmptyUsedAddresses.setSelected(event.isHideEmptyUsedAddresses());
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void hideAmountsStatusChanged(HideAmountsStatusEvent event) {
|
||||
hideAmounts.setSelected(event.isHideAmounts());
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void requestOpenWallets(RequestOpenWalletsEvent event) {
|
||||
EventManager.get().post(new OpenWalletsEvent(tabs.getScene().getWindow(), getOpenWalletTabData()));
|
||||
@@ -3090,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 {
|
||||
@@ -91,8 +93,7 @@ public class AppServices {
|
||||
private static final String TOR_DEFAULT_PROXY_CIRCUIT_ID = "default";
|
||||
|
||||
public static final List<Integer> TARGET_BLOCKS_RANGE = List.of(1, 2, 3, 4, 5, 10, 25, 50);
|
||||
public static final List<Long> LONG_FEE_RATES_RANGE = List.of(1L, 2L, 4L, 8L, 16L, 32L, 64L, 128L, 256L, 512L, 1024L, 2048L, 4096L, 8192L);
|
||||
public static final List<Long> FEE_RATES_RANGE = LONG_FEE_RATES_RANGE.subList(0, LONG_FEE_RATES_RANGE.size() - 3);
|
||||
private static final List<Double> LONG_FEE_RATES_RANGE = List.of(1d, 2d, 4d, 8d, 16d, 32d, 64d, 128d, 256d, 512d, 1024d, 2048d, 4096d, 8192d);
|
||||
public static final double FALLBACK_FEE_RATE = 20000d / 1000;
|
||||
public static final double TESTNET_FALLBACK_FEE_RATE = 1000d / 1000;
|
||||
|
||||
@@ -142,6 +143,8 @@ public class AppServices {
|
||||
|
||||
private static Double minimumRelayFeeRate;
|
||||
|
||||
private static Double serverMinimumRelayFeeRate;
|
||||
|
||||
private static CurrencyRate fiatCurrencyExchangeRate;
|
||||
|
||||
private static List<Device> devices;
|
||||
@@ -211,6 +214,7 @@ public class AppServices {
|
||||
preventSleepService = createPreventSleepService();
|
||||
|
||||
onlineProperty.addListener(onlineServicesListener);
|
||||
minimumRelayFeeRate = getConfiguredMinimumRelayFeeRate(config);
|
||||
|
||||
if(config.getMode() == Mode.ONLINE) {
|
||||
if(config.requiresInternalTor()) {
|
||||
@@ -326,6 +330,9 @@ public class AppServices {
|
||||
"\n\nChange the configured server certificate if you would like to proceed.");
|
||||
} else {
|
||||
crtFile = Storage.getCertificateFile(tlsServerException.getServer().getHost());
|
||||
if(crtFile == null) {
|
||||
crtFile = Storage.getCaCertificateFile(tlsServerException.getServer().getHost());
|
||||
}
|
||||
if(crtFile != null) {
|
||||
Optional<ButtonType> optButton = AppServices.showErrorDialog("SSL Handshake Failed", "The certificate provided by the server at " + tlsServerException.getServer().getHost() + " appears to have changed." +
|
||||
"\n\nThis may be simply due to a certificate renewal, or it may indicate a man-in-the-middle attack." +
|
||||
@@ -362,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;
|
||||
@@ -750,6 +760,26 @@ public class AppServices {
|
||||
return Math.max(minRate, Transaction.DUST_RELAY_TX_FEE);
|
||||
}
|
||||
|
||||
public static List<Double> getLongFeeRatesRange() {
|
||||
if(minimumRelayFeeRate == null || minimumRelayFeeRate >= Transaction.DEFAULT_MIN_RELAY_FEE) {
|
||||
return LONG_FEE_RATES_RANGE;
|
||||
} else {
|
||||
List<Double> longFeeRatesRange = new ArrayList<>();
|
||||
longFeeRatesRange.add(minimumRelayFeeRate);
|
||||
longFeeRatesRange.addAll(LONG_FEE_RATES_RANGE);
|
||||
return longFeeRatesRange;
|
||||
}
|
||||
}
|
||||
|
||||
public static List<Double> getFeeRatesRange() {
|
||||
if(minimumRelayFeeRate == null || minimumRelayFeeRate >= Transaction.DEFAULT_MIN_RELAY_FEE) {
|
||||
return LONG_FEE_RATES_RANGE.subList(0, LONG_FEE_RATES_RANGE.size() - 3);
|
||||
} else {
|
||||
List<Double> longFeeRatesRange = getLongFeeRatesRange();
|
||||
return longFeeRatesRange.subList(0, longFeeRatesRange.size() - 4);
|
||||
}
|
||||
}
|
||||
|
||||
public static Double getNextBlockMedianFeeRate() {
|
||||
return nextBlockMedianFeeRate == null ? getDefaultFeeRate() : nextBlockMedianFeeRate;
|
||||
}
|
||||
@@ -788,10 +818,18 @@ public class AppServices {
|
||||
});
|
||||
}
|
||||
|
||||
public static Double getConfiguredMinimumRelayFeeRate(Config config) {
|
||||
return config.getMinRelayFeeRate() >= 0d && config.getMinRelayFeeRate() < Transaction.DEFAULT_MIN_RELAY_FEE ? config.getMinRelayFeeRate() : null;
|
||||
}
|
||||
|
||||
public static Double getMinimumRelayFeeRate() {
|
||||
return minimumRelayFeeRate == null ? Transaction.DEFAULT_MIN_RELAY_FEE : minimumRelayFeeRate;
|
||||
}
|
||||
|
||||
public static Double getServerMinimumRelayFeeRate() {
|
||||
return serverMinimumRelayFeeRate;
|
||||
}
|
||||
|
||||
public static CurrencyRate getFiatCurrencyExchangeRate() {
|
||||
return fiatCurrencyExchangeRate;
|
||||
}
|
||||
@@ -805,8 +843,8 @@ public class AppServices {
|
||||
}
|
||||
|
||||
public static void addPayjoinURI(BitcoinURI bitcoinURI) {
|
||||
if(bitcoinURI.getPayjoinUrl() == null) {
|
||||
throw new IllegalArgumentException("Not a payjoin URI");
|
||||
if(bitcoinURI.getPayjoinUrl() == null || bitcoinURI.getAddress() == null) {
|
||||
throw new IllegalArgumentException("Not a valid payjoin URI");
|
||||
}
|
||||
payjoinURIs.put(bitcoinURI.getAddress(), bitcoinURI);
|
||||
}
|
||||
@@ -818,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);
|
||||
@@ -829,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);
|
||||
}
|
||||
@@ -857,8 +915,13 @@ public class AppServices {
|
||||
Stage stage = (Stage)window;
|
||||
stage.getIcons().add(getWindowIcon());
|
||||
|
||||
if(stage.getScene() != null && Config.get().getTheme() == Theme.DARK) {
|
||||
stage.getScene().getStylesheets().add(AppServices.class.getResource("darktheme.css").toExternalForm());
|
||||
if(stage.getScene() != null) {
|
||||
if(Config.get().getTheme() == Theme.DARK) {
|
||||
stage.getScene().getStylesheets().add(AppServices.class.getResource("darktheme.css").toExternalForm());
|
||||
}
|
||||
if(Config.get().isChunkAddresses()) {
|
||||
stage.getScene().getRoot().getStyleClass().add("chunk-addresses");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1039,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 {
|
||||
@@ -1059,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()) {
|
||||
@@ -1174,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()
|
||||
@@ -1184,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
|
||||
@@ -1203,7 +1268,7 @@ public class AppServices {
|
||||
}
|
||||
|
||||
public static Font getMonospaceFont() {
|
||||
return Font.font("Roboto Mono", 13);
|
||||
return Font.font("Fragment Mono Regular", 13);
|
||||
}
|
||||
|
||||
public static boolean isOnWayland() {
|
||||
@@ -1219,7 +1284,10 @@ public class AppServices {
|
||||
public void newConnection(ConnectionEvent event) {
|
||||
currentBlockHeight = event.getBlockHeight();
|
||||
System.setProperty(Network.BLOCK_HEIGHT_PROPERTY, Integer.toString(currentBlockHeight));
|
||||
minimumRelayFeeRate = Math.max(event.getMinimumRelayFeeRate(), Transaction.DEFAULT_MIN_RELAY_FEE);
|
||||
if(getConfiguredMinimumRelayFeeRate(Config.get()) == null) {
|
||||
minimumRelayFeeRate = event.getMinimumRelayFeeRate() == null ? Transaction.DEFAULT_MIN_RELAY_FEE : event.getMinimumRelayFeeRate();
|
||||
}
|
||||
serverMinimumRelayFeeRate = event.getMinimumRelayFeeRate();
|
||||
latestBlockHeader = event.getBlockHeader();
|
||||
Config.get().addRecentServer();
|
||||
|
||||
@@ -1416,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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.sparrowwallet.sparrow;
|
||||
|
||||
import com.sparrowwallet.drongo.KeyDerivation;
|
||||
import com.sparrowwallet.drongo.policy.PolicyType;
|
||||
import com.sparrowwallet.drongo.protocol.ScriptChunk;
|
||||
import com.sparrowwallet.drongo.wallet.Keystore;
|
||||
@@ -54,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();
|
||||
@@ -72,10 +73,13 @@ public abstract class BaseController {
|
||||
StringBuilder builder = new StringBuilder();
|
||||
builder.append("[");
|
||||
builder.append(keystore.getKeyDerivation().getMasterFingerprint());
|
||||
builder.append("/");
|
||||
builder.append(keystore.getKeyDerivation().getDerivationPath().replaceFirst("^m?/", ""));
|
||||
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;
|
||||
|
||||
@@ -113,8 +113,8 @@ public class SparrowDesktop extends Application {
|
||||
private void initializeFonts() {
|
||||
GlyphFontRegistry.register(new FontAwesome5());
|
||||
GlyphFontRegistry.register(new FontAwesome5Brands());
|
||||
Font.loadFont(AppServices.class.getResourceAsStream("/font/RobotoMono-Regular.ttf"), 13);
|
||||
Font.loadFont(AppServices.class.getResourceAsStream("/font/RobotoMono-Italic.ttf"), 11);
|
||||
Font.loadFont(AppServices.class.getResourceAsStream("/font/FragmentMono-Regular.ttf"), 13);
|
||||
Font.loadFont(AppServices.class.getResourceAsStream("/font/FragmentMono-Italic.ttf"), 11);
|
||||
if(OsType.getCurrent() == OsType.MACOS) {
|
||||
Font.loadFont(AppServices.class.getResourceAsStream("/font/LiberationSans-Regular.ttf"), 13);
|
||||
}
|
||||
|
||||
@@ -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.2.3";
|
||||
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());
|
||||
|
||||
@@ -22,6 +22,7 @@ public class AddressLabel extends IdLabel {
|
||||
|
||||
public AddressLabel(String text) {
|
||||
super(text);
|
||||
setSkin(new AddressTextFieldSkin(this));
|
||||
addressProperty().addListener((observable, oldValue, newValue) -> {
|
||||
setAddressAsText(newValue);
|
||||
contextMenu.copyHex.setText("Copy " + newValue.getOutputScriptDataType());
|
||||
|
||||
@@ -0,0 +1,148 @@
|
||||
package com.sparrowwallet.sparrow.control;
|
||||
|
||||
import com.sparrowwallet.drongo.Network;
|
||||
import javafx.beans.value.ChangeListener;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.control.ContentDisplay;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.control.skin.LabelSkin;
|
||||
import javafx.scene.text.Font;
|
||||
import javafx.scene.text.Text;
|
||||
import javafx.scene.text.TextFlow;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
public class AddressLabelSkin extends LabelSkin {
|
||||
public static final int CHUNK_SIZE = 4;
|
||||
public static final Pattern CHUNK_PATTERN = Pattern.compile("(?<=\\G.{" + CHUNK_SIZE + "})");
|
||||
|
||||
private final TextFlow displayFlow;
|
||||
private final ChangeListener<String> textListener;
|
||||
private final ChangeListener<Font> fontListener;
|
||||
|
||||
public AddressLabelSkin(Label control) {
|
||||
super(control);
|
||||
|
||||
displayFlow = new TextFlow();
|
||||
displayFlow.setManaged(false);
|
||||
displayFlow.setMouseTransparent(true);
|
||||
|
||||
getChildren().addFirst(displayFlow);
|
||||
|
||||
textListener = (_, _, newText) -> updateDisplay(newText);
|
||||
fontListener = (_, _, _) -> updateDisplay(control.getText());
|
||||
control.textProperty().addListener(textListener);
|
||||
control.fontProperty().addListener(fontListener);
|
||||
updateDisplay(control.getText());
|
||||
|
||||
control.setStyle("-fx-text-fill: transparent;");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void dispose() {
|
||||
getSkinnable().textProperty().removeListener(textListener);
|
||||
getSkinnable().fontProperty().removeListener(fontListener);
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
private void updateDisplay(String text) {
|
||||
displayFlow.getChildren().clear();
|
||||
if(text == null || text.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
List<AddressSpan> addresses = findAddresses(text);
|
||||
|
||||
int pos = 0;
|
||||
for(AddressSpan span : addresses) {
|
||||
if(span.start > pos) {
|
||||
Text normalText = createText(text.substring(pos, span.start), false);
|
||||
displayFlow.getChildren().add(normalText);
|
||||
}
|
||||
|
||||
addChunkedAddress(text.substring(span.start, span.end));
|
||||
pos = span.end;
|
||||
}
|
||||
|
||||
if(pos < text.length()) {
|
||||
Text normalText = createText(text.substring(pos), false);
|
||||
displayFlow.getChildren().add(normalText);
|
||||
}
|
||||
}
|
||||
|
||||
private void addChunkedAddress(String address) {
|
||||
String[] chunks = CHUNK_PATTERN.split(address);
|
||||
for(int i = 0; i < chunks.length; i++) {
|
||||
Text chunk = createText(chunks[i], i % 2 != 0);
|
||||
displayFlow.getChildren().add(chunk);
|
||||
}
|
||||
}
|
||||
|
||||
private Text createText(String content, boolean alternate) {
|
||||
Text text = new Text(content);
|
||||
text.setFont(getSkinnable().getFont());
|
||||
text.getStyleClass().add("address-chunk");
|
||||
if(alternate) {
|
||||
text.getStyleClass().add("alternate");
|
||||
}
|
||||
return text;
|
||||
}
|
||||
|
||||
private List<AddressSpan> findAddresses(String text) {
|
||||
List<AddressSpan> spans = new ArrayList<>();
|
||||
|
||||
Pattern wordBoundary = Pattern.compile("\\S+");
|
||||
Matcher matcher = wordBoundary.matcher(text);
|
||||
|
||||
while(matcher.find()) {
|
||||
String candidate = matcher.group();
|
||||
if(isValidAddress(candidate)) {
|
||||
spans.add(new AddressSpan(matcher.start(), matcher.end()));
|
||||
}
|
||||
}
|
||||
|
||||
return spans;
|
||||
}
|
||||
|
||||
private boolean isValidAddress(String candidate) {
|
||||
Network network = Network.get();
|
||||
return network.hasP2PKHAddressPrefix(candidate) || network.hasP2SHAddressPrefix(candidate) ||
|
||||
candidate.startsWith(network.getBech32AddressHRP()) || candidate.startsWith(network.getSilentPaymentsAddressHrp());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void updateChildren() {
|
||||
super.updateChildren();
|
||||
if(displayFlow != null && !getChildren().contains(displayFlow)) {
|
||||
getChildren().addFirst(displayFlow);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void layoutChildren(double x, double y, double w, double h) {
|
||||
super.layoutChildren(x, y, w, h);
|
||||
|
||||
// Position TextFlow to align with the label's text area
|
||||
Label label = getSkinnable();
|
||||
Insets padding = label.getPadding();
|
||||
|
||||
Node graphic = label.getGraphic();
|
||||
double graphicOffset = 0;
|
||||
if(graphic != null && label.getContentDisplay() == ContentDisplay.LEFT) {
|
||||
graphicOffset = graphic.getLayoutBounds().getWidth() + label.getGraphicTextGap();
|
||||
}
|
||||
|
||||
displayFlow.resizeRelocate(
|
||||
x + padding.getLeft() + graphicOffset,
|
||||
y + padding.getTop(),
|
||||
w - padding.getLeft() - padding.getRight() - graphicOffset,
|
||||
h - padding.getTop() - padding.getBottom()
|
||||
);
|
||||
}
|
||||
|
||||
private record AddressSpan(int start, int end) {}
|
||||
}
|
||||
@@ -0,0 +1,274 @@
|
||||
package com.sparrowwallet.sparrow.control;
|
||||
|
||||
import com.sparrowwallet.drongo.Network;
|
||||
import com.sparrowwallet.drongo.protocol.Base58;
|
||||
import com.sparrowwallet.drongo.protocol.Bech32;
|
||||
import impl.org.controlsfx.skin.CustomTextFieldSkin;
|
||||
import javafx.beans.property.ObjectProperty;
|
||||
import javafx.beans.value.ChangeListener;
|
||||
import javafx.beans.property.SimpleObjectProperty;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.control.TextField;
|
||||
import javafx.scene.layout.Region;
|
||||
import javafx.scene.shape.Path;
|
||||
import javafx.scene.shape.Rectangle;
|
||||
import javafx.scene.text.Font;
|
||||
import javafx.scene.text.Text;
|
||||
import javafx.scene.text.TextFlow;
|
||||
import org.controlsfx.control.textfield.CustomTextField;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
public class AddressTextFieldSkin extends CustomTextFieldSkin {
|
||||
private static final boolean[] BASE58_OK = buildOkTable(new String(Base58.ALPHABET));
|
||||
private static final boolean[] BECH32_DATA_OK = buildOkTable(Bech32.CHARSET);
|
||||
|
||||
private final TextFlow displayFlow;
|
||||
private final Rectangle clip;
|
||||
private final ChangeListener<String> textListener;
|
||||
private final ChangeListener<Font> fontListener;
|
||||
|
||||
public AddressTextFieldSkin(TextField control) {
|
||||
super(control);
|
||||
|
||||
displayFlow = new TextFlow();
|
||||
displayFlow.setManaged(false);
|
||||
displayFlow.setMouseTransparent(true);
|
||||
|
||||
clip = new Rectangle();
|
||||
displayFlow.setClip(clip);
|
||||
|
||||
getChildren().addFirst(displayFlow);
|
||||
|
||||
textListener = (_, _, newText) -> updateDisplay(newText);
|
||||
fontListener = (_, _, _) -> updateDisplay(control.getText());
|
||||
control.textProperty().addListener(textListener);
|
||||
control.fontProperty().addListener(fontListener);
|
||||
updateDisplay(control.getText());
|
||||
|
||||
control.setStyle("-fx-text-fill: transparent;");
|
||||
|
||||
// Unbind caret color since it's normally bound to textFill
|
||||
unbindCaretColor(getChildren());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void dispose() {
|
||||
getSkinnable().textProperty().removeListener(textListener);
|
||||
getSkinnable().fontProperty().removeListener(fontListener);
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
private void unbindCaretColor(javafx.collections.ObservableList<Node> children) {
|
||||
for(Node node : children) {
|
||||
if(node instanceof Path path && path.getStroke() != null) {
|
||||
path.fillProperty().unbind();
|
||||
path.strokeProperty().unbind();
|
||||
path.getStyleClass().add("address-field-caret");
|
||||
} else if(node instanceof javafx.scene.Parent parent) {
|
||||
unbindCaretColor(parent.getChildrenUnmodifiable());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public ObjectProperty<Node> leftProperty() {
|
||||
if(getSkinnable() instanceof CustomTextField customTextField) {
|
||||
return customTextField.leftProperty();
|
||||
}
|
||||
|
||||
return new SimpleObjectProperty<>();
|
||||
}
|
||||
|
||||
@Override
|
||||
public ObjectProperty<Node> rightProperty() {
|
||||
if(getSkinnable() instanceof CustomTextField customTextField) {
|
||||
return customTextField.rightProperty();
|
||||
}
|
||||
|
||||
return new SimpleObjectProperty<>();
|
||||
}
|
||||
|
||||
private void updateDisplay(String text) {
|
||||
displayFlow.getChildren().clear();
|
||||
if(text == null || text.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
List<AddressSpan> addresses = findAddresses(text);
|
||||
|
||||
int pos = 0;
|
||||
for(AddressSpan span : addresses) {
|
||||
if(span.start > pos) {
|
||||
Text normalText = createText(text.substring(pos, span.start), false);
|
||||
displayFlow.getChildren().add(normalText);
|
||||
}
|
||||
|
||||
addChunkedAddress(text.substring(span.start, span.end));
|
||||
pos = span.end;
|
||||
}
|
||||
|
||||
if(pos < text.length()) {
|
||||
Text normalText = createText(text.substring(pos), false);
|
||||
displayFlow.getChildren().add(normalText);
|
||||
}
|
||||
}
|
||||
|
||||
private void addChunkedAddress(String address) {
|
||||
String[] chunks = AddressLabelSkin.CHUNK_PATTERN.split(address);
|
||||
for(int i = 0; i < chunks.length; i++) {
|
||||
Text chunk = createText(chunks[i], i % 2 != 0);
|
||||
displayFlow.getChildren().add(chunk);
|
||||
}
|
||||
}
|
||||
|
||||
private Text createText(String content, boolean alternate) {
|
||||
Text text = new Text(content);
|
||||
text.setFont(getSkinnable().getFont());
|
||||
text.getStyleClass().add("address-chunk");
|
||||
if(alternate) {
|
||||
text.getStyleClass().add("alternate");
|
||||
}
|
||||
return text;
|
||||
}
|
||||
|
||||
private List<AddressSpan> findAddresses(String text) {
|
||||
List<AddressSpan> spans = new ArrayList<>();
|
||||
|
||||
Pattern wordBoundary = Pattern.compile("\\S+");
|
||||
Matcher matcher = wordBoundary.matcher(text);
|
||||
|
||||
while(matcher.find()) {
|
||||
String candidate = matcher.group();
|
||||
if(isValidAddress(candidate)) {
|
||||
spans.add(new AddressSpan(matcher.start(), matcher.end()));
|
||||
}
|
||||
}
|
||||
|
||||
return spans;
|
||||
}
|
||||
|
||||
private boolean isValidAddress(String candidate) {
|
||||
if(candidate == null || candidate.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
Network network = Network.get();
|
||||
|
||||
// Base58 (legacy) partial: must start with a legacy prefix and contain only base58 chars.
|
||||
if(network.hasP2PKHAddressPrefix(candidate) || network.hasP2SHAddressPrefix(candidate)) {
|
||||
return containsOnlyAscii(candidate, BASE58_OK);
|
||||
}
|
||||
|
||||
String lower = candidate.toLowerCase(Locale.ROOT);
|
||||
|
||||
// Bech32 (segwit v0/v1) partial: starts with HRP, then optional '1', then bech32 data charset.
|
||||
if(lower.startsWith(network.getBech32AddressHRP())) {
|
||||
return isBech32LikePartial(lower);
|
||||
}
|
||||
|
||||
// Silent payments partial (bech32-like): starts with its HRP, then optional '1', then bech32 data charset.
|
||||
if(lower.startsWith(network.getSilentPaymentsAddressHrp())) {
|
||||
return isBech32LikePartial(lower);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static boolean isBech32LikePartial(String lower) {
|
||||
int sep = lower.indexOf(Bech32.BECH32_SEPARATOR);
|
||||
|
||||
if(sep < 0) {
|
||||
return containsOnlyHrpChars(lower);
|
||||
}
|
||||
|
||||
String hrp = lower.substring(0, sep);
|
||||
String dataPart = lower.substring(sep + 1);
|
||||
|
||||
if(hrp.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return containsOnlyHrpChars(hrp) && containsOnlyAscii(dataPart, BECH32_DATA_OK);
|
||||
}
|
||||
|
||||
private static boolean containsOnlyHrpChars(String s) {
|
||||
for(int i = 0; i < s.length(); i++) {
|
||||
char c = s.charAt(i);
|
||||
boolean ok = (c >= 'a' && c <= 'z') || (c >= '0' && c <= '9');
|
||||
if(!ok) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private static boolean[] buildOkTable(String allowed) {
|
||||
boolean[] ok = new boolean[128];
|
||||
for(int i = 0; i < allowed.length(); i++) {
|
||||
char c = allowed.charAt(i);
|
||||
if(c < ok.length) {
|
||||
ok[c] = true;
|
||||
} else {
|
||||
throw new IllegalArgumentException("Non-ASCII allowed char: " + c);
|
||||
}
|
||||
}
|
||||
return ok;
|
||||
}
|
||||
|
||||
private static boolean containsOnlyAscii(String s, boolean[] ok) {
|
||||
for(int i = 0; i < s.length(); i++) {
|
||||
char c = s.charAt(i);
|
||||
if(c >= ok.length || !ok[c]) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void layoutChildren(double x, double y, double w, double h) {
|
||||
super.layoutChildren(x, y, w, h);
|
||||
|
||||
Insets padding = getSkinnable().getPadding();
|
||||
|
||||
double leftWidth = 0;
|
||||
double rightWidth = 0;
|
||||
if(getSkinnable() instanceof CustomTextField customTextField) {
|
||||
Node left = customTextField.getLeft();
|
||||
Node right = customTextField.getRight();
|
||||
if(left != null) {
|
||||
leftWidth = left.getLayoutBounds().getWidth();
|
||||
if(left instanceof Region leftRegion) {
|
||||
leftWidth += leftRegion.getPadding().getLeft() + leftRegion.getPadding().getRight() + 1;
|
||||
}
|
||||
}
|
||||
if(right != null) {
|
||||
rightWidth = right.getLayoutBounds().getWidth();
|
||||
if(right instanceof Region rightRegion) {
|
||||
rightWidth += rightRegion.getPadding().getLeft() + rightRegion.getPadding().getRight();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
double availableWidth = w - padding.getLeft() - padding.getRight() - leftWidth - rightWidth;
|
||||
clip.setWidth(availableWidth);
|
||||
clip.setHeight(h);
|
||||
|
||||
double topOffset = getSkinnable().getBaselineOffset() - displayFlow.getBaselineOffset();
|
||||
|
||||
displayFlow.resizeRelocate(
|
||||
padding.getLeft() + leftWidth,
|
||||
topOffset,
|
||||
displayFlow.prefWidth(-1),
|
||||
h - padding.getTop() - padding.getBottom()
|
||||
);
|
||||
}
|
||||
|
||||
private record AddressSpan(int start, int end) {}
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
package com.sparrowwallet.sparrow.control;
|
||||
|
||||
import com.sparrowwallet.drongo.address.Address;
|
||||
import com.sparrowwallet.drongo.address.InvalidAddressException;
|
||||
import javafx.beans.value.ChangeListener;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.control.*;
|
||||
import javafx.scene.text.Text;
|
||||
import javafx.scene.text.TextFlow;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
public class AddressTooltipSkin implements Skin<Tooltip> {
|
||||
private final Tooltip tooltip;
|
||||
private final TextFlow textFlow;
|
||||
private final ChangeListener<String> textListener;
|
||||
|
||||
public AddressTooltipSkin(Tooltip tooltip) {
|
||||
this.tooltip = tooltip;
|
||||
|
||||
textFlow = new TextFlow();
|
||||
textFlow.getStyleClass().addAll(tooltip.getStyleClass());
|
||||
|
||||
textListener = (_, _, newText) -> updateDisplay(newText);
|
||||
tooltip.textProperty().addListener(textListener);
|
||||
updateDisplay(tooltip.getText());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Tooltip getSkinnable() {
|
||||
return tooltip;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Node getNode() {
|
||||
return textFlow;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void dispose() {
|
||||
tooltip.textProperty().removeListener(textListener);
|
||||
}
|
||||
|
||||
private void updateDisplay(String text) {
|
||||
textFlow.getChildren().clear();
|
||||
if(text == null || text.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
List<AddressSpan> addresses = findAddresses(text);
|
||||
|
||||
int pos = 0;
|
||||
for(AddressSpan span : addresses) {
|
||||
if(span.start > pos) {
|
||||
textFlow.getChildren().add(createText(text.substring(pos, span.start), false));
|
||||
}
|
||||
addChunkedAddress(text.substring(span.start, span.end));
|
||||
pos = span.end;
|
||||
}
|
||||
|
||||
if(pos < text.length()) {
|
||||
textFlow.getChildren().add(createText(text.substring(pos), false));
|
||||
}
|
||||
}
|
||||
|
||||
private void addChunkedAddress(String address) {
|
||||
String[] chunks = AddressLabelSkin.CHUNK_PATTERN.split(address);
|
||||
for(int i = 0; i < chunks.length; i++) {
|
||||
textFlow.getChildren().add(createText(chunks[i], i % 2 != 0));
|
||||
}
|
||||
}
|
||||
|
||||
private Text createText(String content, boolean alternate) {
|
||||
Text text = new Text(content);
|
||||
text.getStyleClass().add("address-chunk");
|
||||
if(alternate) {
|
||||
text.getStyleClass().add("alternate");
|
||||
}
|
||||
return text;
|
||||
}
|
||||
|
||||
private List<AddressSpan> findAddresses(String text) {
|
||||
List<AddressSpan> spans = new ArrayList<>();
|
||||
|
||||
Pattern wordBoundary = Pattern.compile("\\S+");
|
||||
Matcher matcher = wordBoundary.matcher(text);
|
||||
|
||||
while(matcher.find()) {
|
||||
String candidate = matcher.group();
|
||||
if(isValidAddress(candidate)) {
|
||||
spans.add(new AddressSpan(matcher.start(), matcher.end()));
|
||||
}
|
||||
}
|
||||
|
||||
return spans;
|
||||
}
|
||||
|
||||
private boolean isValidAddress(String candidate) {
|
||||
try {
|
||||
Address.fromString(candidate);
|
||||
return true;
|
||||
} catch(InvalidAddressException e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private record AddressSpan(int start, int end) {}
|
||||
}
|
||||
@@ -30,7 +30,11 @@ public class AddressTreeTable extends CoinTreeTable {
|
||||
addressCol.setCellValueFactory((TreeTableColumn.CellDataFeatures<Entry, Entry> param) -> {
|
||||
return new ReadOnlyObjectWrapper<>(param.getValue().getValue());
|
||||
});
|
||||
addressCol.setCellFactory(p -> new EntryCell());
|
||||
addressCol.setCellFactory(p -> {
|
||||
EntryCell entryCell = new EntryCell();
|
||||
entryCell.setSkin(new AddressTreeTableCellSkin<>(entryCell));
|
||||
return entryCell;
|
||||
});
|
||||
addressCol.setSortable(false);
|
||||
getColumns().add(addressCol);
|
||||
|
||||
@@ -76,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();
|
||||
|
||||
|
||||
@@ -0,0 +1,128 @@
|
||||
package com.sparrowwallet.sparrow.control;
|
||||
|
||||
import javafx.beans.value.ChangeListener;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.scene.control.TreeTableCell;
|
||||
import javafx.scene.control.skin.TreeTableCellSkin;
|
||||
import javafx.scene.layout.Region;
|
||||
import javafx.scene.text.Text;
|
||||
import javafx.scene.text.TextFlow;
|
||||
|
||||
public class AddressTreeTableCellSkin<S, T> extends TreeTableCellSkin<S, T> {
|
||||
private final TextFlow displayFlow;
|
||||
private final ChangeListener<String> textListener;
|
||||
private final Text ellipsisText;
|
||||
private String currentDisplayedText;
|
||||
|
||||
public AddressTreeTableCellSkin(TreeTableCell<S, T> cell) {
|
||||
super(cell);
|
||||
|
||||
displayFlow = new TextFlow();
|
||||
displayFlow.setManaged(false);
|
||||
displayFlow.setMouseTransparent(true);
|
||||
displayFlow.setMinWidth(Region.USE_PREF_SIZE);
|
||||
getChildren().add(displayFlow);
|
||||
|
||||
ellipsisText = new Text("...");
|
||||
ellipsisText.fontProperty().bind(cell.fontProperty());
|
||||
ellipsisText.getStyleClass().add("address-chunk");
|
||||
|
||||
textListener = (_, _, newText) -> updateDisplay(newText);
|
||||
cell.textProperty().addListener(textListener);
|
||||
updateDisplay(cell.getText());
|
||||
|
||||
cell.setStyle("-fx-text-fill: transparent;");
|
||||
}
|
||||
|
||||
private void updateDisplay(String text) {
|
||||
currentDisplayedText = text;
|
||||
buildDisplay(text, false);
|
||||
}
|
||||
|
||||
private void buildDisplay(String text, boolean truncated) {
|
||||
displayFlow.getChildren().clear();
|
||||
|
||||
if(text == null || text.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if(getSkinnable().getStyleClass().contains("address-cell")) {
|
||||
String[] chunks = AddressLabelSkin.CHUNK_PATTERN.split(text);
|
||||
for(int i = 0; i < chunks.length; i++) {
|
||||
displayFlow.getChildren().add(createText(chunks[i], i % 2 != 0));
|
||||
}
|
||||
} else {
|
||||
Text normalText = createText(text, false);
|
||||
displayFlow.getChildren().add(normalText);
|
||||
}
|
||||
|
||||
if(truncated) {
|
||||
displayFlow.getChildren().add(ellipsisText);
|
||||
}
|
||||
}
|
||||
|
||||
private Text createText(String content, boolean alternate) {
|
||||
Text text = new Text(content);
|
||||
text.fontProperty().bind(getSkinnable().fontProperty());
|
||||
text.getStyleClass().add("address-chunk");
|
||||
if(alternate) {
|
||||
text.getStyleClass().add("alternate");
|
||||
}
|
||||
return text;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void layoutChildren(double x, double y, double w, double h) {
|
||||
super.layoutChildren(x, y, w, h);
|
||||
|
||||
TreeTableCell<S, T> cell = getSkinnable();
|
||||
Insets padding = cell.getPadding();
|
||||
|
||||
double leftOffset = 0;
|
||||
double topOffset = y + padding.getTop();
|
||||
|
||||
Text labeledText = (Text)getChildren().stream().filter(n -> n instanceof Text).findFirst().orElse(null);
|
||||
if(labeledText != null) {
|
||||
leftOffset = labeledText.getLayoutX();
|
||||
topOffset = labeledText.getLayoutY() - labeledText.getBaselineOffset();
|
||||
|
||||
String fullText = cell.getText();
|
||||
String displayedText = labeledText.getText();
|
||||
|
||||
if(fullText != null && displayedText != null && !fullText.equals(displayedText)) {
|
||||
String ellipsis = cell.getEllipsisString();
|
||||
if(displayedText.endsWith(ellipsis)) {
|
||||
String truncatedText = displayedText.substring(0, displayedText.length() - ellipsis.length());
|
||||
if(!truncatedText.equals(currentDisplayedText)) {
|
||||
currentDisplayedText = truncatedText;
|
||||
buildDisplay(truncatedText, true);
|
||||
}
|
||||
}
|
||||
} else if(fullText != null && !fullText.equals(currentDisplayedText)) {
|
||||
currentDisplayedText = fullText;
|
||||
buildDisplay(fullText, false);
|
||||
}
|
||||
}
|
||||
|
||||
displayFlow.resizeRelocate(
|
||||
leftOffset,
|
||||
topOffset,
|
||||
w - padding.getLeft() - padding.getRight(),
|
||||
h - padding.getTop() - padding.getBottom()
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void updateChildren() {
|
||||
super.updateChildren();
|
||||
if(displayFlow != null && !getChildren().contains(displayFlow)) {
|
||||
getChildren().add(displayFlow);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void dispose() {
|
||||
getSkinnable().textProperty().removeListener(textListener);
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
@@ -128,4 +128,15 @@ public class BalanceChart extends LineChart<Number, Number> {
|
||||
NumberAxis yaxis = (NumberAxis)getYAxis();
|
||||
yaxis.setTickLabelFormatter(new CoinAxisFormatter(yaxis, format, unit));
|
||||
}
|
||||
|
||||
public void refreshAxisLabels() {
|
||||
NumberAxis yaxis = (NumberAxis)getYAxis();
|
||||
// Force the axis to redraw by invalidating the upper and lower bounds
|
||||
yaxis.setAutoRanging(false);
|
||||
double lower = yaxis.getLowerBound();
|
||||
double upper = yaxis.getUpperBound();
|
||||
yaxis.setLowerBound(lower);
|
||||
yaxis.setUpperBound(upper);
|
||||
yaxis.setAutoRanging(true);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,8 @@ package com.sparrowwallet.sparrow.control;
|
||||
import com.google.common.base.Throwables;
|
||||
import com.sparrowwallet.drongo.KeyDerivation;
|
||||
import com.sparrowwallet.drongo.crypto.ChildNumber;
|
||||
import com.sparrowwallet.drongo.policy.PolicyType;
|
||||
import com.sparrowwallet.drongo.protocol.ScriptType;
|
||||
import com.sparrowwallet.drongo.protocol.Sha256Hash;
|
||||
import com.sparrowwallet.drongo.wallet.*;
|
||||
import com.sparrowwallet.sparrow.AppServices;
|
||||
@@ -43,14 +45,24 @@ public class CardImportPane extends TitledDescriptionPane {
|
||||
private static final Logger log = LoggerFactory.getLogger(CardImportPane.class);
|
||||
|
||||
private final KeystoreCardImport importer;
|
||||
private final PolicyType policyType;
|
||||
private List<ChildNumber> derivation;
|
||||
protected Button importButton;
|
||||
private final SimpleStringProperty pin = new SimpleStringProperty("");
|
||||
|
||||
public CardImportPane(Wallet wallet, KeystoreCardImport importer, KeyDerivation requiredDerivation) {
|
||||
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.derivation = requiredDerivation == null ? wallet.getScriptType().getDefaultDerivation() : requiredDerivation.getDerivation();
|
||||
this.policyType = wallet.getPolicyType();
|
||||
this.derivation = requiredDerivation == null ? getDefaultDerivation(wallet, defaultDerivation) : requiredDerivation.getDerivation();
|
||||
}
|
||||
|
||||
private static List<ChildNumber> getDefaultDerivation(Wallet wallet, KeyDerivation defaultDerivation) {
|
||||
if(defaultDerivation != null && !defaultDerivation.getDerivation().isEmpty()) {
|
||||
return defaultDerivation.getDerivation();
|
||||
}
|
||||
|
||||
return wallet.getScriptType() == null ? ScriptType.P2WPKH.getDefaultDerivation() : wallet.getScriptType().getDefaultDerivation();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -102,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()));
|
||||
});
|
||||
@@ -343,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;
|
||||
@@ -359,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);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -0,0 +1,251 @@
|
||||
package com.sparrowwallet.sparrow.control;
|
||||
|
||||
import com.sparrowwallet.drongo.KeyDerivation;
|
||||
import com.sparrowwallet.drongo.crypto.ChildNumber;
|
||||
import com.sparrowwallet.drongo.protocol.ScriptType;
|
||||
import com.sparrowwallet.drongo.wallet.Keystore;
|
||||
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||
import com.sparrowwallet.sparrow.EventManager;
|
||||
import com.sparrowwallet.sparrow.event.KeystoreImportEvent;
|
||||
import com.sparrowwallet.sparrow.glyphfont.GlyphUtils;
|
||||
import com.sparrowwallet.sparrow.io.ImportException;
|
||||
import com.sparrowwallet.sparrow.io.KeystoreCodexImport;
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.property.SimpleStringProperty;
|
||||
import javafx.beans.value.ChangeListener;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.control.Button;
|
||||
import javafx.scene.control.ContentDisplay;
|
||||
import javafx.scene.control.Control;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.control.MenuItem;
|
||||
import javafx.scene.control.SplitMenuButton;
|
||||
import javafx.scene.control.TextField;
|
||||
import javafx.scene.control.Tooltip;
|
||||
import javafx.scene.layout.AnchorPane;
|
||||
import javafx.scene.layout.HBox;
|
||||
import javafx.scene.layout.Priority;
|
||||
import javafx.scene.layout.VBox;
|
||||
import org.controlsfx.validation.ValidationResult;
|
||||
import org.controlsfx.validation.ValidationSupport;
|
||||
import org.controlsfx.validation.Validator;
|
||||
import org.controlsfx.validation.decoration.StyleClassValidationDecoration;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class CodexKeystoreImportPane extends TitledDescriptionPane {
|
||||
protected final Wallet wallet;
|
||||
private final KeystoreCodexImport importer;
|
||||
private final KeyDerivation defaultDerivation;
|
||||
|
||||
private SplitMenuButton importButton;
|
||||
|
||||
private Button enterCodexButton;
|
||||
private Button calculateButton;
|
||||
|
||||
protected Label validLabel;
|
||||
protected Label invalidLabel;
|
||||
|
||||
protected final SimpleStringProperty secretShareProperty = new SimpleStringProperty("");
|
||||
|
||||
public CodexKeystoreImportPane(Wallet wallet, KeystoreCodexImport importer, KeyDerivation defaultDerivation) {
|
||||
super(importer.getName(), "Enter secret share", importer.getKeystoreImportDescription(), importer.getWalletModel());
|
||||
this.wallet = wallet;
|
||||
this.importer = importer;
|
||||
this.defaultDerivation = defaultDerivation;
|
||||
|
||||
createImportButton();
|
||||
buttonBox.getChildren().add(importButton);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Control createButton() {
|
||||
enterCodexButton = new Button("Enter Secret Share");
|
||||
enterCodexButton.managedProperty().bind(enterCodexButton.visibleProperty());
|
||||
enterCodexButton.setOnAction(event -> {
|
||||
enterCodex();
|
||||
});
|
||||
|
||||
return enterCodexButton;
|
||||
}
|
||||
|
||||
private void enterCodex() {
|
||||
setDescription("Enter secret share");
|
||||
showHideLink.setVisible(false);
|
||||
setContent(getSecretShareEntry());
|
||||
setExpanded(true);
|
||||
}
|
||||
|
||||
private void importKeystore(List<ChildNumber> derivation) {
|
||||
importButton.setDisable(true);
|
||||
try {
|
||||
Keystore keystore = importer.getKeystore(wallet.getPolicyType(), derivation, secretShareProperty.get());
|
||||
EventManager.get().post(new KeystoreImportEvent(keystore));
|
||||
} catch(ImportException e) {
|
||||
String errorMessage = e.getMessage();
|
||||
if(e.getCause() != null && e.getCause().getMessage() != null && !e.getCause().getMessage().isEmpty()) {
|
||||
errorMessage = e.getCause().getMessage();
|
||||
}
|
||||
setError("Import Error", errorMessage);
|
||||
importButton.setDisable(false);
|
||||
}
|
||||
}
|
||||
|
||||
private void createImportButton() {
|
||||
importButton = new SplitMenuButton();
|
||||
importButton.setAlignment(Pos.CENTER_RIGHT);
|
||||
importButton.setText("Import Keystore");
|
||||
setDefaultButton(importButton);
|
||||
importButton.setOnAction(event -> {
|
||||
importButton.setDisable(true);
|
||||
importKeystore(getDefaultDerivation());
|
||||
});
|
||||
String[] accounts = new String[]{"Import Default Account #0", "Import Account #1", "Import Account #2", "Import Account #3", "Import Account #4", "Import Account #5", "Import Account #6", "Import Account #7", "Import Account #8", "Import Account #9"};
|
||||
int scriptAccountsLength = ScriptType.P2SH.equals(wallet.getScriptType()) ? 1 : accounts.length;
|
||||
for(int i = 0; i < scriptAccountsLength; i++) {
|
||||
MenuItem item = new MenuItem(accounts[i]);
|
||||
final List<ChildNumber> derivation = wallet.getScriptType().getDefaultDerivation(i);
|
||||
item.setOnAction(event -> {
|
||||
importButton.setDisable(true);
|
||||
importKeystore(derivation);
|
||||
});
|
||||
importButton.getItems().add(item);
|
||||
}
|
||||
|
||||
importButton.managedProperty().bind(importButton.visibleProperty());
|
||||
importButton.setVisible(false);
|
||||
}
|
||||
|
||||
private List<ChildNumber> getDefaultDerivation() {
|
||||
return defaultDerivation == null || defaultDerivation.getDerivation().isEmpty() ? wallet.getScriptType().getDefaultDerivation() : defaultDerivation.getDerivation();
|
||||
}
|
||||
|
||||
private void onInputChange(boolean empty, boolean validChecksum) {
|
||||
if(!empty) {
|
||||
try {
|
||||
importer.getKeystore(wallet.getPolicyType(), ScriptType.P2WPKH.getDefaultDerivation(), secretShareProperty.get());
|
||||
validChecksum = true;
|
||||
} catch(ImportException e) {
|
||||
invalidLabel.setText("Invalid checksum");
|
||||
invalidLabel.setTooltip(null);
|
||||
}
|
||||
}
|
||||
|
||||
calculateButton.setDisable(!validChecksum);
|
||||
validLabel.setVisible(validChecksum);
|
||||
invalidLabel.setVisible(!validChecksum && !empty);
|
||||
}
|
||||
|
||||
private Node getSecretShareEntry() {
|
||||
VBox vBox = new VBox(20);
|
||||
vBox.setPadding(new Insets(10, 30, 10, 30));
|
||||
|
||||
HBox shareEntry = new HBox(10);
|
||||
shareEntry.setAlignment(Pos.CENTER_LEFT);
|
||||
Label shareLabel = new Label("Secret:");
|
||||
TextField shareField = new TextField();
|
||||
HBox.setHgrow(shareField, Priority.ALWAYS);
|
||||
shareField.setPromptText("ms...");
|
||||
shareField.textProperty().addListener((observable, oldValue, newValue) -> {
|
||||
secretShareProperty.set(newValue);
|
||||
});
|
||||
shareEntry.getChildren().addAll(shareLabel, shareField);
|
||||
vBox.getChildren().add(shareEntry);
|
||||
|
||||
AnchorPane buttonPane = new AnchorPane();
|
||||
|
||||
validLabel = new Label("Valid checksum", GlyphUtils.getSuccessGlyph());
|
||||
validLabel.setContentDisplay(ContentDisplay.LEFT);
|
||||
validLabel.setGraphicTextGap(5.0);
|
||||
validLabel.managedProperty().bind(validLabel.visibleProperty());
|
||||
validLabel.setVisible(false);
|
||||
buttonPane.getChildren().add(validLabel);
|
||||
AnchorPane.setTopAnchor(validLabel, 5.0);
|
||||
AnchorPane.setLeftAnchor(validLabel, 0.0);
|
||||
|
||||
invalidLabel = new Label("Invalid checksum", GlyphUtils.getInvalidGlyph());
|
||||
invalidLabel.setContentDisplay(ContentDisplay.LEFT);
|
||||
invalidLabel.setGraphicTextGap(5.0);
|
||||
invalidLabel.managedProperty().bind(invalidLabel.visibleProperty());
|
||||
invalidLabel.setVisible(false);
|
||||
buttonPane.getChildren().add(invalidLabel);
|
||||
AnchorPane.setTopAnchor(invalidLabel, 5.0);
|
||||
AnchorPane.setLeftAnchor(invalidLabel, 0.0);
|
||||
|
||||
secretShareProperty.addListener((ChangeListener<String>) (c, oldval, newval) -> {
|
||||
boolean empty = secretShareProperty.isEmpty().get();
|
||||
boolean validChecksum = false;
|
||||
onInputChange(empty, validChecksum);
|
||||
});
|
||||
|
||||
HBox rightBox = new HBox();
|
||||
rightBox.setSpacing(10);
|
||||
|
||||
calculateButton = new Button("Create Keystore");
|
||||
calculateButton.setDisable(true);
|
||||
calculateButton.setDefaultButton(true);
|
||||
calculateButton.managedProperty().bind(calculateButton.visibleProperty());
|
||||
calculateButton.setTooltip(new Tooltip("Create the keystore from the provided secret share"));
|
||||
calculateButton.setOnAction(event -> {
|
||||
setExpanded(true);
|
||||
enterCodexButton.setVisible(false);
|
||||
importButton.setVisible(true);
|
||||
importButton.setDisable(false);
|
||||
setDescription("Ready to import");
|
||||
showHideLink.setText("Show Derivation...");
|
||||
showHideLink.setVisible(false);
|
||||
setContent(getDerivationEntry(getDefaultDerivation()));
|
||||
});
|
||||
|
||||
rightBox.getChildren().add(calculateButton);
|
||||
|
||||
buttonPane.getChildren().add(rightBox);
|
||||
AnchorPane.setRightAnchor(rightBox, 0.0);
|
||||
|
||||
vBox.getChildren().add(buttonPane);
|
||||
|
||||
Platform.runLater(shareField::requestFocus);
|
||||
|
||||
return vBox;
|
||||
}
|
||||
|
||||
private Node getDerivationEntry(List<ChildNumber> derivation) {
|
||||
TextField derivationField = new TextField();
|
||||
derivationField.setPromptText("Derivation path");
|
||||
derivationField.setText(KeyDerivation.writePath(derivation));
|
||||
HBox.setHgrow(derivationField, Priority.ALWAYS);
|
||||
|
||||
ValidationSupport validationSupport = new ValidationSupport();
|
||||
validationSupport.setValidationDecorator(new StyleClassValidationDecoration());
|
||||
validationSupport.registerValidator(derivationField, Validator.combine(
|
||||
Validator.createEmptyValidator("Derivation is required"),
|
||||
(Control c, String newValue) -> ValidationResult.fromErrorIf(c, "Invalid derivation", !KeyDerivation.isValid(newValue))
|
||||
));
|
||||
|
||||
Button importDerivationButton = new Button("Import Custom Derivation Keystore");
|
||||
importDerivationButton.setDisable(true);
|
||||
importDerivationButton.setOnAction(event -> {
|
||||
showHideLink.setVisible(true);
|
||||
setExpanded(false);
|
||||
List<ChildNumber> importDerivation = KeyDerivation.parsePath(derivationField.getText());
|
||||
importKeystore(importDerivation);
|
||||
});
|
||||
|
||||
derivationField.textProperty().addListener((observable, oldValue, newValue) -> {
|
||||
importButton.setDisable(newValue.isEmpty() || !KeyDerivation.isValid(newValue) || !KeyDerivation.parsePath(newValue).equals(derivation));
|
||||
importDerivationButton.setDisable(newValue.isEmpty() || !KeyDerivation.isValid(newValue) || KeyDerivation.parsePath(newValue).equals(derivation));
|
||||
});
|
||||
|
||||
HBox contentBox = new HBox();
|
||||
contentBox.setAlignment(Pos.TOP_RIGHT);
|
||||
contentBox.setSpacing(20);
|
||||
contentBox.getChildren().add(derivationField);
|
||||
contentBox.getChildren().add(importDerivationButton);
|
||||
contentBox.setPadding(new Insets(10, 30, 10, 30));
|
||||
contentBox.setPrefHeight(60);
|
||||
|
||||
return contentBox;
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@ package com.sparrowwallet.sparrow.control;
|
||||
|
||||
import com.sparrowwallet.drongo.BitcoinUnit;
|
||||
import com.sparrowwallet.sparrow.UnitFormat;
|
||||
import com.sparrowwallet.sparrow.io.Config;
|
||||
import javafx.scene.chart.NumberAxis;
|
||||
import javafx.util.StringConverter;
|
||||
|
||||
@@ -18,6 +19,10 @@ final class CoinAxisFormatter extends StringConverter<Number> {
|
||||
|
||||
@Override
|
||||
public String toString(Number object) {
|
||||
if(Config.get().isHideAmounts()) {
|
||||
return "";
|
||||
}
|
||||
|
||||
Double value = bitcoinUnit.getValue(object.longValue());
|
||||
return new CoinTextFormatter(unitFormat).getCoinFormat().format(value);
|
||||
}
|
||||
|
||||
@@ -58,16 +58,22 @@ class CoinCell extends TreeTableCell<Entry, Number> implements ConfirmationsList
|
||||
DecimalFormat decimalFormat = (amount.longValue() == 0L ? format.getBtcFormat() : format.getTableBtcFormat());
|
||||
final String btcValue = decimalFormat.format(amount.doubleValue() / Transaction.SATOSHIS_PER_BITCOIN);
|
||||
|
||||
if(unit.equals(BitcoinUnit.BTC)) {
|
||||
tooltip.setValue(satsValue + " " + BitcoinUnit.SATOSHIS.getLabel());
|
||||
setText(btcValue);
|
||||
if(Config.get().isHideAmounts()) {
|
||||
setText(CoinLabel.HIDDEN_AMOUNT_TEXT);
|
||||
setTooltip(null);
|
||||
setContextMenu(null);
|
||||
} else {
|
||||
tooltip.setValue(btcValue + " " + BitcoinUnit.BTC.getLabel());
|
||||
setText(satsValue);
|
||||
if(unit.equals(BitcoinUnit.BTC)) {
|
||||
tooltip.setValue(satsValue + " " + BitcoinUnit.SATOSHIS.getLabel());
|
||||
setText(btcValue);
|
||||
} else {
|
||||
tooltip.setValue(btcValue + " " + BitcoinUnit.BTC.getLabel());
|
||||
setText(satsValue);
|
||||
}
|
||||
setTooltip(tooltip);
|
||||
contextMenu.updateAmount(amount);
|
||||
setContextMenu(contextMenu);
|
||||
}
|
||||
setTooltip(tooltip);
|
||||
contextMenu.updateAmount(amount);
|
||||
setContextMenu(contextMenu);
|
||||
|
||||
if(entry instanceof TransactionEntry transactionEntry) {
|
||||
tooltip.showConfirmations(transactionEntry.confirmationsProperty(), transactionEntry.isCoinbase());
|
||||
@@ -86,14 +92,16 @@ class CoinCell extends TreeTableCell<Entry, Number> implements ConfirmationsList
|
||||
}
|
||||
} else if(entry instanceof UtxoEntry) {
|
||||
setGraphic(null);
|
||||
} else if(entry instanceof HashIndexEntry) {
|
||||
} else if(entry instanceof HashIndexEntry hashIndexEntry) {
|
||||
tooltip.hideConfirmations();
|
||||
|
||||
Region node = new Region();
|
||||
node.setPrefWidth(10);
|
||||
setGraphic(node);
|
||||
setContentDisplay(ContentDisplay.RIGHT);
|
||||
|
||||
if(((HashIndexEntry) entry).getType() == HashIndexEntry.Type.INPUT) {
|
||||
satsValue = "-" + satsValue;
|
||||
if(hashIndexEntry.getType() == HashIndexEntry.Type.INPUT && !Config.get().isHideAmounts()) {
|
||||
setText("-" + getText());
|
||||
}
|
||||
} else {
|
||||
setGraphic(null);
|
||||
@@ -148,6 +156,14 @@ class CoinCell extends TreeTableCell<Entry, Number> implements ConfirmationsList
|
||||
setTooltipText();
|
||||
}
|
||||
|
||||
public void hideConfirmations() {
|
||||
showConfirmations = false;
|
||||
isCoinbase = false;
|
||||
confirmationsProperty.unbind();
|
||||
|
||||
setTooltipText();
|
||||
}
|
||||
|
||||
private void setTooltipText() {
|
||||
setText(value + (showConfirmations ? " (" + getConfirmationsDescription() + ")" : ""));
|
||||
}
|
||||
|
||||
@@ -13,6 +13,8 @@ import javafx.scene.input.Clipboard;
|
||||
import javafx.scene.input.ClipboardContent;
|
||||
|
||||
public class CoinLabel extends Label {
|
||||
public static final String HIDDEN_AMOUNT_TEXT = "\u2022\u2022\u2022\u2022\u2022";
|
||||
|
||||
private final LongProperty valueProperty = new SimpleLongProperty(-1);
|
||||
private final Tooltip tooltip;
|
||||
private final CoinContextMenu contextMenu;
|
||||
@@ -49,6 +51,13 @@ public class CoinLabel extends Label {
|
||||
}
|
||||
|
||||
private void setValueAsText(Long value, BitcoinUnit bitcoinUnit) {
|
||||
if(Config.get().isHideAmounts()) {
|
||||
setText(HIDDEN_AMOUNT_TEXT);
|
||||
setTooltip(null);
|
||||
setContextMenu(null);
|
||||
return;
|
||||
}
|
||||
|
||||
setTooltip(tooltip);
|
||||
setContextMenu(contextMenu);
|
||||
|
||||
|
||||
@@ -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());
|
||||
|
||||
|
||||
@@ -6,10 +6,16 @@ import javafx.beans.property.SimpleObjectProperty;
|
||||
import javafx.scene.Cursor;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.control.ComboBox;
|
||||
import javafx.scene.control.ContextMenu;
|
||||
import javafx.scene.control.MenuItem;
|
||||
import javafx.scene.control.SeparatorMenuItem;
|
||||
import javafx.scene.input.Clipboard;
|
||||
import javafx.scene.layout.Region;
|
||||
import javafx.scene.layout.StackPane;
|
||||
import org.controlsfx.control.textfield.CustomTextField;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class ComboBoxTextField extends CustomTextField {
|
||||
private final ObjectProperty<ComboBox<?>> comboProperty = new SimpleObjectProperty<>();
|
||||
|
||||
@@ -68,4 +74,53 @@ public class ComboBoxTextField extends CustomTextField {
|
||||
public void setComboProperty(ComboBox<?> comboProperty) {
|
||||
this.comboProperty.set(comboProperty);
|
||||
}
|
||||
|
||||
public ContextMenu getCustomContextMenu(List<MenuItem> customItems) {
|
||||
return new CustomContextMenu(customItems);
|
||||
}
|
||||
|
||||
public class CustomContextMenu extends ContextMenu {
|
||||
public CustomContextMenu(List<MenuItem> customItems) {
|
||||
super();
|
||||
setFont(null);
|
||||
|
||||
MenuItem undo = new MenuItem("Undo");
|
||||
undo.setOnAction(_ -> undo());
|
||||
|
||||
MenuItem redo = new MenuItem("Redo");
|
||||
redo.setOnAction(_ -> redo());
|
||||
|
||||
MenuItem cut = new MenuItem("Cut");
|
||||
cut.setOnAction(_ -> cut());
|
||||
|
||||
MenuItem copy = new MenuItem("Copy");
|
||||
copy.setOnAction(_ -> copy());
|
||||
|
||||
MenuItem paste = new MenuItem("Paste");
|
||||
paste.setOnAction(_ -> paste());
|
||||
|
||||
MenuItem delete = new MenuItem("Delete");
|
||||
delete.setOnAction(_ -> deleteText(getSelection()));
|
||||
|
||||
MenuItem selectAll = new MenuItem("Select All");
|
||||
selectAll.setOnAction(_ -> selectAll());
|
||||
|
||||
getItems().addAll(undo, redo, new SeparatorMenuItem(), cut, copy, paste, delete, new SeparatorMenuItem(), selectAll);
|
||||
getItems().addAll(customItems);
|
||||
|
||||
setOnShowing(_ -> {
|
||||
boolean hasSelection = getSelection().getLength() > 0;
|
||||
boolean hasText = getText() != null && !getText().isEmpty();
|
||||
boolean clipboardHasContent = Clipboard.getSystemClipboard().hasString();
|
||||
|
||||
undo.setDisable(!isUndoable());
|
||||
redo.setDisable(!isRedoable());
|
||||
cut.setDisable(!isEditable() || !hasSelection);
|
||||
copy.setDisable(!hasSelection);
|
||||
paste.setDisable(!isEditable() || !clipboardHasContent);
|
||||
delete.setDisable(!hasSelection);
|
||||
selectAll.setDisable(!hasText);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,8 +10,7 @@ import javafx.scene.control.MenuItem;
|
||||
import javafx.scene.control.Tooltip;
|
||||
import javafx.scene.input.Clipboard;
|
||||
import javafx.scene.input.ClipboardContent;
|
||||
import javafx.event.EventHandler;
|
||||
import javafx.scene.input.MouseEvent;
|
||||
import javafx.scene.input.MouseButton;
|
||||
|
||||
public class CopyableCoinLabel extends CopyableLabel {
|
||||
private final LongProperty valueProperty = new SimpleLongProperty(-1);
|
||||
@@ -29,6 +28,10 @@ public class CopyableCoinLabel extends CopyableLabel {
|
||||
valueProperty().addListener((observable, oldValue, newValue) -> setValueAsText((Long)newValue, Config.get().getUnitFormat(), Config.get().getBitcoinUnit()));
|
||||
|
||||
setOnMouseClicked(event -> {
|
||||
if(!event.getButton().equals(MouseButton.PRIMARY)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if(bitcoinUnit == null) {
|
||||
bitcoinUnit = Config.get().getBitcoinUnit();
|
||||
}
|
||||
@@ -67,6 +70,13 @@ public class CopyableCoinLabel extends CopyableLabel {
|
||||
}
|
||||
|
||||
private void setValueAsText(Long value, UnitFormat unitFormat, BitcoinUnit bitcoinUnit) {
|
||||
if(Config.get().isHideAmounts()) {
|
||||
setText(CoinLabel.HIDDEN_AMOUNT_TEXT);
|
||||
setTooltip(null);
|
||||
setContextMenu(null);
|
||||
return;
|
||||
}
|
||||
|
||||
setTooltip(tooltip);
|
||||
setContextMenu(contextMenu);
|
||||
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -9,8 +9,8 @@ import javafx.scene.control.*;
|
||||
import javafx.scene.control.Button;
|
||||
|
||||
public class DescriptorQRDisplayDialog extends QRDisplayDialog {
|
||||
public DescriptorQRDisplayDialog(String walletName, String outputDescriptor, UR ur, BBQR bbqr, boolean selectBbqrButton) {
|
||||
super(ur, bbqr, false, false, selectBbqrButton);
|
||||
public DescriptorQRDisplayDialog(String walletName, String outputDescriptor, UR ur, BBQR bbqr, QREncoding encoding) {
|
||||
super(ur, bbqr, false, false, encoding);
|
||||
|
||||
DialogPane dialogPane = getDialogPane();
|
||||
final ButtonType pdfButtonType = new javafx.scene.control.ButtonType("Save PDF...", ButtonBar.ButtonData.HELP_2);
|
||||
@@ -20,8 +20,13 @@ public class DescriptorQRDisplayDialog extends QRDisplayDialog {
|
||||
pdfButton.setGraphicTextGap(5);
|
||||
pdfButton.setGraphic(getGlyph(FontAwesome5.Glyph.FILE_PDF));
|
||||
pdfButton.addEventFilter(ActionEvent.ACTION, event -> {
|
||||
PdfUtils.saveOutputDescriptor(walletName, outputDescriptor, ur, isUseBbqrEncoding() ? bbqr : null);
|
||||
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;
|
||||
@@ -19,6 +20,7 @@ import com.sparrowwallet.sparrow.event.*;
|
||||
import com.sparrowwallet.sparrow.io.*;
|
||||
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
|
||||
import com.sparrowwallet.sparrow.net.ElectrumServer;
|
||||
import com.sparrowwallet.sparrow.net.ServerType;
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.property.SimpleStringProperty;
|
||||
import javafx.beans.property.StringProperty;
|
||||
@@ -51,7 +53,8 @@ public class DevicePane extends TitledDescriptionPane {
|
||||
private final Wallet wallet;
|
||||
private final PSBT psbt;
|
||||
private final OutputDescriptor outputDescriptor;
|
||||
private final KeyDerivation keyDerivation;
|
||||
private final KeyDerivation defaultDerivation;
|
||||
private final KeyDerivation requiredDerivation;
|
||||
private final String message;
|
||||
private final List<StandardAccount> availableAccounts;
|
||||
private final Device device;
|
||||
@@ -74,13 +77,14 @@ public class DevicePane extends TitledDescriptionPane {
|
||||
|
||||
private boolean defaultDevice;
|
||||
|
||||
public DevicePane(Wallet wallet, Device device, boolean defaultDevice, KeyDerivation requiredDerivation) {
|
||||
public DevicePane(Wallet wallet, Device device, boolean defaultDevice, KeyDerivation defaultDerivation, KeyDerivation requiredDerivation) {
|
||||
super(device.getModel().toDisplayString(), "", "", device.getModel());
|
||||
this.deviceOperation = DeviceOperation.IMPORT;
|
||||
this.wallet = wallet;
|
||||
this.psbt = null;
|
||||
this.outputDescriptor = null;
|
||||
this.keyDerivation = requiredDerivation;
|
||||
this.defaultDerivation = defaultDerivation;
|
||||
this.requiredDerivation = requiredDerivation;
|
||||
this.message = null;
|
||||
this.availableAccounts = null;
|
||||
this.device = device;
|
||||
@@ -107,7 +111,8 @@ public class DevicePane extends TitledDescriptionPane {
|
||||
this.wallet = wallet;
|
||||
this.psbt = psbt;
|
||||
this.outputDescriptor = null;
|
||||
this.keyDerivation = null;
|
||||
this.defaultDerivation = null;
|
||||
this.requiredDerivation = null;
|
||||
this.message = null;
|
||||
this.availableAccounts = null;
|
||||
this.device = device;
|
||||
@@ -134,7 +139,8 @@ public class DevicePane extends TitledDescriptionPane {
|
||||
this.wallet = wallet;
|
||||
this.psbt = null;
|
||||
this.outputDescriptor = outputDescriptor;
|
||||
this.keyDerivation = null;
|
||||
this.defaultDerivation = null;
|
||||
this.requiredDerivation = null;
|
||||
this.message = null;
|
||||
this.availableAccounts = null;
|
||||
this.device = device;
|
||||
@@ -151,13 +157,14 @@ public class DevicePane extends TitledDescriptionPane {
|
||||
buttonBox.getChildren().addAll(setPassphraseButton, displayAddressButton);
|
||||
}
|
||||
|
||||
public DevicePane(Wallet wallet, String message, KeyDerivation keyDerivation, Device device, boolean defaultDevice) {
|
||||
public DevicePane(Wallet wallet, String message, KeyDerivation requiredDerivation, Device device, boolean defaultDevice) {
|
||||
super(device.getModel().toDisplayString(), "", "", device.getModel());
|
||||
this.deviceOperation = DeviceOperation.SIGN_MESSAGE;
|
||||
this.wallet = wallet;
|
||||
this.psbt = null;
|
||||
this.outputDescriptor = null;
|
||||
this.keyDerivation = keyDerivation;
|
||||
this.defaultDerivation = requiredDerivation;
|
||||
this.requiredDerivation = requiredDerivation;
|
||||
this.message = message;
|
||||
this.availableAccounts = null;
|
||||
this.device = device;
|
||||
@@ -184,7 +191,8 @@ public class DevicePane extends TitledDescriptionPane {
|
||||
this.wallet = wallet;
|
||||
this.psbt = null;
|
||||
this.outputDescriptor = null;
|
||||
this.keyDerivation = null;
|
||||
this.defaultDerivation = null;
|
||||
this.requiredDerivation = null;
|
||||
this.message = null;
|
||||
this.device = device;
|
||||
this.defaultDevice = defaultDevice;
|
||||
@@ -207,7 +215,8 @@ public class DevicePane extends TitledDescriptionPane {
|
||||
this.wallet = null;
|
||||
this.psbt = null;
|
||||
this.outputDescriptor = null;
|
||||
this.keyDerivation = null;
|
||||
this.defaultDerivation = null;
|
||||
this.requiredDerivation = null;
|
||||
this.message = null;
|
||||
this.device = device;
|
||||
this.defaultDevice = defaultDevice;
|
||||
@@ -286,27 +295,50 @@ public class DevicePane extends TitledDescriptionPane {
|
||||
}
|
||||
|
||||
private void createImportButton() {
|
||||
importButton = keyDerivation == null ? new SplitMenuButton() : new Button();
|
||||
importButton = requiredDerivation == null ? new SplitMenuButton() : new Button();
|
||||
importButton.setAlignment(Pos.CENTER_RIGHT);
|
||||
importButton.setText("Import Keystore");
|
||||
importButton.setOnAction(event -> {
|
||||
importButton.setDisable(true);
|
||||
List<ChildNumber> defaultDerivation = wallet.getScriptType() == null ? ScriptType.P2WPKH.getDefaultDerivation() : wallet.getScriptType().getDefaultDerivation();
|
||||
importKeystore(keyDerivation == null ? defaultDerivation : keyDerivation.getDerivation());
|
||||
importKeystore(requiredDerivation == null ? getDefaultDerivation() : requiredDerivation.getDerivation());
|
||||
});
|
||||
|
||||
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...");
|
||||
discoverItem.setDisable(!AppServices.isConnected());
|
||||
discoverItem.setOnAction(_ -> discoverWallet());
|
||||
importMenuButton.getItems().add(discoverItem);
|
||||
} else {
|
||||
String[] accounts = new String[] {"Default Account #0", "Account #1", "Account #2", "Account #3", "Account #4", "Account #5", "Account #6", "Account #7", "Account #8", "Account #9"};
|
||||
int scriptAccountsLength = ScriptType.P2SH.equals(wallet.getScriptType()) ? 1 : accounts.length;
|
||||
@@ -363,7 +395,7 @@ public class DevicePane extends TitledDescriptionPane {
|
||||
signMessageButton.managedProperty().bind(signMessageButton.visibleProperty());
|
||||
signMessageButton.setVisible(false);
|
||||
|
||||
if(device.getFingerprint() != null && !device.getFingerprint().equals(keyDerivation.getMasterFingerprint())) {
|
||||
if(device.getFingerprint() != null && !device.getFingerprint().equals(requiredDerivation.getMasterFingerprint())) {
|
||||
signMessageButton.setDisable(true);
|
||||
}
|
||||
}
|
||||
@@ -372,7 +404,6 @@ public class DevicePane extends TitledDescriptionPane {
|
||||
discoverKeystoresButton = new Button("Discover");
|
||||
discoverKeystoresButton.setAlignment(Pos.CENTER_RIGHT);
|
||||
discoverKeystoresButton.setOnAction(event -> {
|
||||
discoverKeystoresButton.setDisable(true);
|
||||
discoverKeystores();
|
||||
});
|
||||
discoverKeystoresButton.managedProperty().bind(discoverKeystoresButton.visibleProperty());
|
||||
@@ -433,6 +464,14 @@ public class DevicePane extends TitledDescriptionPane {
|
||||
getAddressButton.setVisible(false);
|
||||
}
|
||||
|
||||
private List<ChildNumber> getDefaultDerivation() {
|
||||
if(defaultDerivation != null && !defaultDerivation.getDerivation().isEmpty()) {
|
||||
return defaultDerivation.getDerivation();
|
||||
}
|
||||
|
||||
return wallet == null || wallet.getScriptType() == null ? ScriptType.P2WPKH.getDefaultDerivation() : wallet.getScriptType().getDefaultDerivation();
|
||||
}
|
||||
|
||||
private void unlock(Device device) {
|
||||
if(device.getModel().requiresPinPrompt()) {
|
||||
promptPin();
|
||||
@@ -453,20 +492,26 @@ public class DevicePane extends TitledDescriptionPane {
|
||||
});
|
||||
vBox.getChildren().addAll(pinField, enterPinButton);
|
||||
|
||||
TilePane tilePane = new TilePane();
|
||||
tilePane.setPrefColumns(3);
|
||||
tilePane.setHgap(10);
|
||||
tilePane.setVgap(10);
|
||||
tilePane.setMaxWidth(150);
|
||||
tilePane.setMaxHeight(120);
|
||||
GridPane gridPane = new GridPane();
|
||||
gridPane.setHgap(10);
|
||||
gridPane.setVgap(10);
|
||||
gridPane.setMaxWidth(150);
|
||||
gridPane.setMaxHeight(device.getModel().hasZeroInPin() ? 160 : 120);
|
||||
|
||||
int[] digits = new int[] {7, 8, 9, 4, 5, 6, 1, 2, 3};
|
||||
int[] digits = device.getModel().hasZeroInPin() ? new int[] {7, 8, 9, 4, 5, 6, 1, 2, 3, 0} : new int[] {7, 8, 9, 4, 5, 6, 1, 2, 3};
|
||||
for(int i = 0; i < digits.length; i++) {
|
||||
Button pinButton = new Button();
|
||||
Glyph circle = new Glyph(FontAwesome5.FONT_NAME, "CIRCLE");
|
||||
pinButton.setGraphic(circle);
|
||||
pinButton.setUserData(digits[i]);
|
||||
tilePane.getChildren().add(pinButton);
|
||||
GridPane.setRowIndex(pinButton, i / 3);
|
||||
GridPane.setColumnIndex(pinButton, i % 3);
|
||||
if((i / 3) == 3) {
|
||||
GridPane.setHgrow(pinButton, Priority.ALWAYS);
|
||||
GridPane.setColumnSpan(pinButton, 3);
|
||||
pinButton.setMaxWidth(Double.MAX_VALUE);
|
||||
}
|
||||
gridPane.getChildren().add(pinButton);
|
||||
pinButton.setOnAction(event -> {
|
||||
pinField.setText(pinField.getText() + pinButton.getUserData());
|
||||
});
|
||||
@@ -474,7 +519,7 @@ public class DevicePane extends TitledDescriptionPane {
|
||||
|
||||
HBox contentBox = new HBox();
|
||||
contentBox.setSpacing(50);
|
||||
contentBox.getChildren().add(tilePane);
|
||||
contentBox.getChildren().add(gridPane);
|
||||
contentBox.getChildren().add(vBox);
|
||||
contentBox.setPadding(new Insets(10, 0, 10, 0));
|
||||
contentBox.setAlignment(Pos.TOP_CENTER);
|
||||
@@ -494,7 +539,7 @@ public class DevicePane extends TitledDescriptionPane {
|
||||
|
||||
SplitMenuButton sendPassphraseButton = new SplitMenuButton();
|
||||
sendPassphraseButton.setText("Send Passphrase");
|
||||
sendPassphraseButton.getStyleClass().add("default-button");
|
||||
setDefaultButton(sendPassphraseButton);
|
||||
sendPassphraseButton.setOnAction(event -> {
|
||||
setExpanded(false);
|
||||
setDescription("Confirm passphrase on device...");
|
||||
@@ -686,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());
|
||||
});
|
||||
@@ -705,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);
|
||||
}
|
||||
@@ -722,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();
|
||||
@@ -730,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) {
|
||||
@@ -746,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 {
|
||||
@@ -858,7 +941,7 @@ public class DevicePane extends TitledDescriptionPane {
|
||||
if(device.isCard()) {
|
||||
try {
|
||||
CardApi cardApi = CardApi.getCardApi(device.getModel(), pin.get());
|
||||
Service<String> signMessageService = cardApi.getSignMessageService(message, wallet.getScriptType(), keyDerivation.getDerivation(), messageProperty);
|
||||
Service<String> signMessageService = cardApi.getSignMessageService(message, wallet.getScriptType(), requiredDerivation.getDerivation(), messageProperty);
|
||||
handleCardOperation(signMessageService, signMessageButton, "Signing", true, event -> {
|
||||
String signature = signMessageService.getValue();
|
||||
EventManager.get().post(new MessageSignedEvent(wallet, signature));
|
||||
@@ -869,7 +952,7 @@ public class DevicePane extends TitledDescriptionPane {
|
||||
signButton.setDisable(false);
|
||||
}
|
||||
} else {
|
||||
Hwi.SignMessageService signMessageService = new Hwi.SignMessageService(device, passphrase.get(), message, keyDerivation.getDerivationPath());
|
||||
Hwi.SignMessageService signMessageService = new Hwi.SignMessageService(device, passphrase.get(), message, requiredDerivation.getDerivationPath());
|
||||
signMessageService.setOnSucceeded(successEvent -> {
|
||||
String signature = signMessageService.getValue();
|
||||
EventManager.get().post(new MessageSignedEvent(wallet, signature));
|
||||
@@ -883,37 +966,141 @@ public class DevicePane extends TitledDescriptionPane {
|
||||
}
|
||||
}
|
||||
|
||||
private void discoverWallet() {
|
||||
importButton.setDisable(true);
|
||||
importButton.setMaxHeight(importButton.getHeight());
|
||||
ProgressIndicator progressIndicator = new ProgressIndicator(0);
|
||||
progressIndicator.getStyleClass().add("button-progress");
|
||||
importButton.setGraphic(progressIndicator);
|
||||
List<Wallet> wallets = new ArrayList<>();
|
||||
|
||||
RangeInputDialog rangeInputDialog = new RangeInputDialog(StandardAccount.ACCOUNT_0.getAccountNumber(), StandardAccount.ACCOUNT_30.getAccountNumber(), StandardAccount.ACCOUNT_10.getAccountNumber());
|
||||
rangeInputDialog.setTitle("Choose number of accounts");
|
||||
rangeInputDialog.setHeaderText("Enter the number of additional accounts to scan for existing funds.\n\nThis may take a few minutes depending on how many accounts are selected.");
|
||||
Optional<Integer> optRange = rangeInputDialog.showAndWait();
|
||||
if(optRange.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
List<StandardAccount> discoveryAccounts = new ArrayList<>(Arrays.asList(StandardAccount.values()).subList(0, optRange.get() + 1));
|
||||
Map<Hwi.WalletType, String> derivationPaths = new LinkedHashMap<>();
|
||||
List<ScriptType> scriptTypes = new ArrayList<>(ScriptType.getAddressableScriptTypes(PolicyType.SINGLE_HD));
|
||||
if(device.getModel() == WalletModel.BITBOX_02) {
|
||||
scriptTypes.remove(ScriptType.P2PKH);
|
||||
}
|
||||
for(ScriptType scriptType : scriptTypes) {
|
||||
for(StandardAccount discoveryAccount : discoveryAccounts) {
|
||||
derivationPaths.put(new Hwi.WalletType(scriptType, discoveryAccount), KeyDerivation.writePath(scriptType.getDefaultDerivation(discoveryAccount.getAccountNumber())));
|
||||
}
|
||||
}
|
||||
|
||||
Hwi.GetXpubsService getXpubsService = new Hwi.GetXpubsService(device, passphrase.get(), derivationPaths);
|
||||
getXpubsService.setOnSucceeded(_ -> {
|
||||
Map<Hwi.WalletType, ExtendedKey> accountXpubs = getXpubsService.getValue();
|
||||
|
||||
for(Map.Entry<Hwi.WalletType, ExtendedKey> entry : accountXpubs.entrySet()) {
|
||||
try {
|
||||
Wallet wallet = new Wallet(device.getModel().toDisplayString());
|
||||
wallet.setPolicyType(PolicyType.SINGLE_HD);
|
||||
wallet.setScriptType(entry.getKey().scriptType());
|
||||
Keystore keystore = new Keystore();
|
||||
keystore.setLabel(device.getModel().toDisplayString());
|
||||
keystore.setSource(KeystoreSource.HW_USB);
|
||||
keystore.setWalletModel(device.getModel());
|
||||
keystore.setKeyDerivation(new KeyDerivation(device.getFingerprint(), derivationPaths.get(entry.getKey())));
|
||||
keystore.setExtendedPublicKey(entry.getValue());
|
||||
wallet.getKeystores().add(keystore);
|
||||
wallet.setDefaultPolicy(Policy.getPolicy(PolicyType.SINGLE_HD, entry.getKey().scriptType(), wallet.getKeystores(), 1));
|
||||
if(entry.getKey().standardAccount().equals(StandardAccount.ACCOUNT_0)) {
|
||||
wallets.add(wallet);
|
||||
} else {
|
||||
Wallet masterWallet = wallets.getLast();
|
||||
wallet.setName(entry.getKey().standardAccount().getName());
|
||||
wallet.setMasterWallet(masterWallet);
|
||||
masterWallet.getChildWallets().add(wallet);
|
||||
}
|
||||
} catch(Exception e) {
|
||||
setError("Could not retrieve xpub", e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
ElectrumServer.WalletDiscoveryService walletDiscoveryService = new ElectrumServer.WalletDiscoveryService(wallets);
|
||||
walletDiscoveryService.setOnSucceeded(_ -> {
|
||||
importButton.setGraphic(null);
|
||||
Optional<List<Wallet>> optWallets = walletDiscoveryService.getValue();
|
||||
if(optWallets.isPresent()) {
|
||||
List<Wallet> discoveredWallets = optWallets.get();
|
||||
if(discoveredWallets.size() > 1) {
|
||||
for(Wallet wallet : discoveredWallets) {
|
||||
wallet.setName(wallet.getName() + " " + wallet.getScriptType().getDescription());
|
||||
}
|
||||
}
|
||||
EventManager.get().post(new WalletImportEvent(discoveredWallets));
|
||||
} else {
|
||||
AppServices.showErrorDialog("No existing wallet found",
|
||||
Config.get().getServerType() == ServerType.BITCOIN_CORE ? "The configured server type is Bitcoin Core, which does not support wallet discovery.\n\n" +
|
||||
"You can however import the " + device.getModel().toDisplayString() + " and scan the blockchain by supplying a start date." :
|
||||
"Could not find an HD wallet with existing transactions using the " + device.getModel().toDisplayString() + ".");
|
||||
setDefaultStatus();
|
||||
importButton.setDisable(false);
|
||||
}
|
||||
});
|
||||
walletDiscoveryService.setOnFailed(failedEvent -> {
|
||||
log.error("Failed to discover wallets", failedEvent.getSource().getException());
|
||||
setError("Failed to discover wallets", failedEvent.getSource().getException().getMessage());
|
||||
importButton.setGraphic(null);
|
||||
importButton.setDisable(false);
|
||||
});
|
||||
walletDiscoveryService.start();
|
||||
});
|
||||
getXpubsService.setOnFailed(_ -> {
|
||||
setError("Could not retrieve xpub", getXpubsService.getException().getMessage());
|
||||
importButton.setGraphic(null);
|
||||
importButton.setDisable(false);
|
||||
});
|
||||
progressIndicator.progressProperty().bind(getXpubsService.progressProperty());
|
||||
getXpubsService.progressProperty().addListener((_, _, newValue) -> setDescription("Discovering... (" + Math.round(newValue.doubleValue() * 100) + "%)"));
|
||||
showHideLink.setVisible(false);
|
||||
getXpubsService.start();
|
||||
}
|
||||
|
||||
private void discoverKeystores() {
|
||||
if(wallet.getKeystores().size() != 1) {
|
||||
setError("Could not discover keystores", "Only single signature wallets are supported for keystore discovery");
|
||||
return;
|
||||
}
|
||||
|
||||
discoverKeystoresButton.setDisable(true);
|
||||
discoverKeystoresButton.setMaxHeight(discoverKeystoresButton.getHeight());
|
||||
ProgressIndicator progressIndicator = new ProgressIndicator(0);
|
||||
progressIndicator.getStyleClass().add("button-progress");
|
||||
discoverKeystoresButton.setGraphic(progressIndicator);
|
||||
|
||||
String masterFingerprint = wallet.getKeystores().get(0).getKeyDerivation().getMasterFingerprint();
|
||||
|
||||
Wallet copyWallet = wallet.copy();
|
||||
Map<StandardAccount, String> accountDerivationPaths = new LinkedHashMap<>();
|
||||
Map<Hwi.WalletType, String> accountDerivationPaths = new LinkedHashMap<>();
|
||||
for(StandardAccount availableAccount : availableAccounts) {
|
||||
Wallet availableWallet = copyWallet.addChildWallet(availableAccount);
|
||||
Keystore availableKeystore = availableWallet.getKeystores().get(0);
|
||||
String derivationPath = availableKeystore.getKeyDerivation().getDerivationPath();
|
||||
accountDerivationPaths.put(availableAccount, derivationPath);
|
||||
accountDerivationPaths.put(new Hwi.WalletType(wallet.getScriptType(), availableAccount), derivationPath);
|
||||
}
|
||||
|
||||
Map<StandardAccount, Keystore> importedKeystores = new LinkedHashMap<>();
|
||||
Hwi.GetXpubsService getXpubsService = new Hwi.GetXpubsService(device, passphrase.get(), accountDerivationPaths);
|
||||
getXpubsService.setOnSucceeded(workerStateEvent -> {
|
||||
Map<StandardAccount, String> accountXpubs = getXpubsService.getValue();
|
||||
Map<Hwi.WalletType, ExtendedKey> accountXpubs = getXpubsService.getValue();
|
||||
|
||||
for(Map.Entry<StandardAccount, 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()));
|
||||
importedKeystores.put(entry.getKey(), keystore);
|
||||
keystore.setExtendedPublicKey(entry.getValue());
|
||||
importedKeystores.put(entry.getKey().standardAccount(), keystore);
|
||||
} catch(Exception e) {
|
||||
setError("Could not retrieve xpub", e.getMessage());
|
||||
}
|
||||
@@ -927,15 +1114,18 @@ public class DevicePane extends TitledDescriptionPane {
|
||||
accountDiscoveryService.setOnFailed(event -> {
|
||||
log.error("Failed to discover accounts", event.getSource().getException());
|
||||
setError("Failed to discover accounts", event.getSource().getException().getMessage());
|
||||
discoverKeystoresButton.setGraphic(null);
|
||||
discoverKeystoresButton.setDisable(false);
|
||||
});
|
||||
accountDiscoveryService.start();
|
||||
});
|
||||
getXpubsService.setOnFailed(workerStateEvent -> {
|
||||
setError("Could not retrieve xpub", getXpubsService.getException().getMessage());
|
||||
discoverKeystoresButton.setGraphic(null);
|
||||
discoverKeystoresButton.setDisable(false);
|
||||
});
|
||||
setDescription("Discovering...");
|
||||
progressIndicator.progressProperty().bind(getXpubsService.progressProperty());
|
||||
getXpubsService.progressProperty().addListener((_, _, newValue) -> setDescription("Discovering... (" + Math.round(newValue.doubleValue() * 100) + "%)"));
|
||||
showHideLink.setVisible(false);
|
||||
getXpubsService.start();
|
||||
}
|
||||
@@ -992,13 +1182,12 @@ public class DevicePane extends TitledDescriptionPane {
|
||||
private void showOperationButton() {
|
||||
if(deviceOperation.equals(DeviceOperation.IMPORT)) {
|
||||
if(defaultDevice) {
|
||||
importButton.getStyleClass().add("default-button");
|
||||
setDefaultButton(importButton);
|
||||
}
|
||||
importButton.setVisible(true);
|
||||
showHideLink.setText("Show derivation...");
|
||||
showHideLink.setVisible(!device.isCard());
|
||||
List<ChildNumber> defaultDerivation = wallet.getScriptType() == null ? ScriptType.P2WPKH.getDefaultDerivation() : wallet.getScriptType().getDefaultDerivation();
|
||||
setContent(getDerivationEntry(keyDerivation == null ? defaultDerivation : keyDerivation.getDerivation()));
|
||||
setContent(getDerivationEntry(requiredDerivation == null ? getDefaultDerivation() : requiredDerivation.getDerivation()));
|
||||
} else if(deviceOperation.equals(DeviceOperation.SIGN)) {
|
||||
signButton.setDefaultButton(defaultDevice);
|
||||
signButton.setVisible(true);
|
||||
@@ -1017,7 +1206,7 @@ public class DevicePane extends TitledDescriptionPane {
|
||||
showHideLink.setVisible(false);
|
||||
} else if(deviceOperation.equals(DeviceOperation.GET_PRIVATE_KEY)) {
|
||||
if(defaultDevice) {
|
||||
getPrivateKeyButton.getStyleClass().add("default-button");
|
||||
setDefaultButton(getPrivateKeyButton);
|
||||
}
|
||||
getPrivateKeyButton.setVisible(true);
|
||||
showHideLink.setVisible(false);
|
||||
@@ -1032,7 +1221,7 @@ public class DevicePane extends TitledDescriptionPane {
|
||||
TextField derivationField = new TextField();
|
||||
derivationField.setPromptText("Derivation path");
|
||||
derivationField.setText(KeyDerivation.writePath(derivation));
|
||||
derivationField.setDisable(device.isCard() || keyDerivation != null);
|
||||
derivationField.setDisable(device.isCard() || requiredDerivation != null);
|
||||
HBox.setHgrow(derivationField, Priority.ALWAYS);
|
||||
|
||||
ValidationSupport validationSupport = new ValidationSupport();
|
||||
@@ -1048,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) -> {
|
||||
@@ -1224,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);
|
||||
|
||||
@@ -62,7 +62,7 @@ public class DownloadVerifierDialog extends Dialog<ButtonBar.ButtonData> {
|
||||
private static final List<String> ARCHIVE_EXTENSIONS = List.of("zip", "tar.gz", "tar.bz2", "tar.xz", "rar", "7z");
|
||||
|
||||
private static final String SPARROW_RELEASE_PREFIX = "sparrow-";
|
||||
private static final String SPARROW_RELEASE_ALT_PREFIX = "sparrow_";
|
||||
private static final String[] SPARROW_RELEASE_ALT_PREFIXES = { "sparrowwallet-", "sparrowwallet_", "sparrowserver-", "sparrowserver_" };
|
||||
private static final String SPARROW_MANIFEST_SUFFIX = "-manifest.txt";
|
||||
private static final String SPARROW_SIGNATURE_SUFFIX = SPARROW_MANIFEST_SUFFIX + ".asc";
|
||||
private static final Pattern SPARROW_RELEASE_VERSION = Pattern.compile("[0-9]+(\\.[0-9]+)*");
|
||||
@@ -465,7 +465,7 @@ public class DownloadVerifierDialog extends Dialog<ButtonBar.ButtonData> {
|
||||
}
|
||||
|
||||
String providedName = providedFile.getName().toLowerCase(Locale.ROOT);
|
||||
if(providedName.startsWith(SPARROW_RELEASE_PREFIX) || providedName.startsWith(SPARROW_RELEASE_ALT_PREFIX)) {
|
||||
if(providedName.startsWith(SPARROW_RELEASE_PREFIX) || Arrays.stream(SPARROW_RELEASE_ALT_PREFIXES).anyMatch(providedName::startsWith)) {
|
||||
Matcher matcher = SPARROW_RELEASE_VERSION.matcher(providedFile.getName());
|
||||
if(matcher.find()) {
|
||||
String version = matcher.group();
|
||||
@@ -591,7 +591,8 @@ public class DownloadVerifierDialog extends Dialog<ButtonBar.ButtonData> {
|
||||
}
|
||||
}
|
||||
|
||||
if((name.startsWith(SPARROW_RELEASE_PREFIX) || name.startsWith(SPARROW_RELEASE_ALT_PREFIX)) && file.length() >= MIN_VALID_SPARROW_RELEASE_SIZE) {
|
||||
if((name.startsWith(SPARROW_RELEASE_PREFIX) || Arrays.stream(SPARROW_RELEASE_ALT_PREFIXES).anyMatch(name::startsWith))
|
||||
&& file.length() >= MIN_VALID_SPARROW_RELEASE_SIZE) {
|
||||
Matcher matcher = SPARROW_RELEASE_VERSION.matcher(name);
|
||||
return matcher.find();
|
||||
}
|
||||
|
||||
@@ -4,7 +4,10 @@ import com.sparrowwallet.drongo.KeyPurpose;
|
||||
import com.sparrowwallet.drongo.OsType;
|
||||
import com.sparrowwallet.drongo.Utils;
|
||||
import com.sparrowwallet.drongo.address.Address;
|
||||
import com.sparrowwallet.drongo.policy.PolicyType;
|
||||
import com.sparrowwallet.drongo.protocol.*;
|
||||
import com.sparrowwallet.drongo.silentpayments.SilentPayment;
|
||||
import com.sparrowwallet.drongo.silentpayments.SilentPaymentAddress;
|
||||
import com.sparrowwallet.drongo.wallet.*;
|
||||
import com.sparrowwallet.sparrow.AppServices;
|
||||
import com.sparrowwallet.sparrow.EventManager;
|
||||
@@ -55,7 +58,7 @@ public class EntryCell extends TreeTableCell<Entry, Entry> implements Confirmati
|
||||
super.updateItem(entry, empty);
|
||||
|
||||
//Return immediately to avoid CPU usage when updating the same invisible cell to determine tableview size (see https://bugs.openjdk.org/browse/JDK-8280442)
|
||||
if(this == lastCell && !getTableRow().isVisible()) {
|
||||
if(this == lastCell && !getTableRow().isVisible() && isTableSizeRecalculation()) {
|
||||
return;
|
||||
}
|
||||
lastCell = this;
|
||||
@@ -66,8 +69,7 @@ public class EntryCell extends TreeTableCell<Entry, Entry> implements Confirmati
|
||||
setText(null);
|
||||
setGraphic(null);
|
||||
} else {
|
||||
if(entry instanceof TransactionEntry) {
|
||||
TransactionEntry transactionEntry = (TransactionEntry)entry;
|
||||
if(entry instanceof TransactionEntry transactionEntry) {
|
||||
if(transactionEntry.getBlockTransaction().getHeight() == -1) {
|
||||
setText("Unconfirmed Parent");
|
||||
setContextMenu(new UnconfirmedTransactionContextMenu(transactionEntry));
|
||||
@@ -101,7 +103,7 @@ public class EntryCell extends TreeTableCell<Entry, Entry> implements Confirmati
|
||||
actionBox.getChildren().add(viewTransactionButton);
|
||||
|
||||
BlockTransaction blockTransaction = transactionEntry.getBlockTransaction();
|
||||
if(blockTransaction.getHeight() <= 0 && canRBF(blockTransaction) &&
|
||||
if(blockTransaction.getHeight() <= 0 && canRBF(blockTransaction, transactionEntry.getWallet()) &&
|
||||
Config.get().isIncludeMempoolOutputs() && transactionEntry.getWallet().allInputsFromWallet(blockTransaction.getHash())) {
|
||||
Button increaseFeeButton = new Button("");
|
||||
increaseFeeButton.setGraphic(getIncreaseFeeRBFGlyph());
|
||||
@@ -121,21 +123,20 @@ public class EntryCell extends TreeTableCell<Entry, Entry> implements Confirmati
|
||||
}
|
||||
|
||||
setGraphic(actionBox);
|
||||
} else if(entry instanceof NodeEntry) {
|
||||
NodeEntry nodeEntry = (NodeEntry)entry;
|
||||
} else if(entry instanceof NodeEntry nodeEntry) {
|
||||
Address address = nodeEntry.getAddress();
|
||||
getStyleClass().add("address-cell");
|
||||
setText(address.toString());
|
||||
setContextMenu(new AddressContextMenu(address, nodeEntry.getOutputDescriptor(), nodeEntry, true, getTreeTableView()));
|
||||
Tooltip tooltip = new Tooltip();
|
||||
tooltip.setShowDelay(Duration.millis(250));
|
||||
tooltip.setText(nodeEntry.getNode().toString());
|
||||
setTooltip(tooltip);
|
||||
getStyleClass().add("address-cell");
|
||||
|
||||
HBox actionBox = new HBox();
|
||||
actionBox.getStyleClass().add("cell-actions");
|
||||
|
||||
if(!nodeEntry.getNode().getWallet().isBip47()) {
|
||||
if(!nodeEntry.getNode().getWallet().isBip47() && nodeEntry.getNode().getWallet().getPolicyType() != PolicyType.SINGLE_SP) {
|
||||
Button receiveButton = new Button("");
|
||||
receiveButton.setGraphic(getReceiveGlyph());
|
||||
receiveButton.setOnAction(event -> {
|
||||
@@ -163,8 +164,7 @@ public class EntryCell extends TreeTableCell<Entry, Entry> implements Confirmati
|
||||
setContextMenu(null);
|
||||
setGraphic(new HBox());
|
||||
}
|
||||
} else if(entry instanceof HashIndexEntry) {
|
||||
HashIndexEntry hashIndexEntry = (HashIndexEntry)entry;
|
||||
} else if(entry instanceof HashIndexEntry hashIndexEntry) {
|
||||
setText(hashIndexEntry.getDescription());
|
||||
setContextMenu(getTreeTableView().getStyleClass().contains("bip47") ? null : new HashIndexEntryContextMenu(getTreeTableView(), hashIndexEntry));
|
||||
Tooltip tooltip = new Tooltip();
|
||||
@@ -212,13 +212,14 @@ public class EntryCell extends TreeTableCell<Entry, Entry> implements Confirmati
|
||||
|
||||
private static void increaseFee(TransactionEntry transactionEntry, boolean cancelTransaction) {
|
||||
BlockTransaction blockTransaction = transactionEntry.getBlockTransaction();
|
||||
boolean silentPaymentTransaction = transactionEntry.getWallet().isSilentPaymentsTransaction(blockTransaction);
|
||||
Map<BlockTransactionHashIndex, WalletNode> walletTxos = transactionEntry.getWallet().getWalletTxos();
|
||||
List<BlockTransactionHashIndex> utxos = transactionEntry.getChildren().stream()
|
||||
.filter(e -> e instanceof HashIndexEntry)
|
||||
.map(e -> (HashIndexEntry)e)
|
||||
.filter(e -> e.getType().equals(HashIndexEntry.Type.INPUT) && e.isSpendable())
|
||||
.map(e -> blockTransaction.getTransaction().getInputs().get((int)e.getHashIndex().getIndex()))
|
||||
.filter(i -> Config.get().isMempoolFullRbf() || i.isReplaceByFeeEnabled())
|
||||
.filter(i -> Config.get().isMempoolFullRbf() || i.isReplaceByFeeEnabled() || silentPaymentTransaction)
|
||||
.map(txInput -> walletTxos.keySet().stream().filter(txo -> txo.getHash().equals(txInput.getOutpoint().getHash()) && txo.getIndex() == txInput.getOutpoint().getIndex()).findFirst().get())
|
||||
.collect(Collectors.toList());
|
||||
|
||||
@@ -243,12 +244,13 @@ public class EntryCell extends TreeTableCell<Entry, Entry> implements Confirmati
|
||||
.collect(Collectors.toList());
|
||||
|
||||
boolean consolidationTransaction = consolidationOutputs.size() == blockTransaction.getTransaction().getOutputs().size() && consolidationOutputs.size() == 1;
|
||||
boolean safeToAddInputsOrOutputs = transactionEntry.getWallet().isSafeToAddInputsOrOutputs(blockTransaction);
|
||||
long changeTotal = ourOutputs.stream().mapToLong(TransactionOutput::getValue).sum() - consolidationOutputs.stream().mapToLong(TransactionOutput::getValue).sum();
|
||||
Transaction tx = blockTransaction.getTransaction();
|
||||
double vSize = tx.getVirtualSize();
|
||||
if(changeTotal == 0) {
|
||||
//Add change output length to vSize if change was not present on the original transaction
|
||||
TransactionOutput changeOutput = new TransactionOutput(new Transaction(), 1L, transactionEntry.getWallet().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);
|
||||
@@ -257,7 +259,7 @@ public class EntryCell extends TreeTableCell<Entry, Entry> implements Confirmati
|
||||
List<OutputGroup> outputGroups = transactionEntry.getWallet().getGroupedUtxos(txoFilters, feeRate, AppServices.getMinimumRelayFeeRate(), Config.get().isGroupByAddress())
|
||||
.stream().filter(outputGroup -> outputGroup.getEffectiveValue() >= 0).collect(Collectors.toList());
|
||||
Collections.shuffle(outputGroups);
|
||||
while((double)changeTotal / vSize < getMaxFeeRate() && !outputGroups.isEmpty() && !cancelTransaction && !consolidationTransaction) {
|
||||
while((double)changeTotal / vSize < getMaxFeeRate() && !outputGroups.isEmpty() && !cancelTransaction && !consolidationTransaction && safeToAddInputsOrOutputs) {
|
||||
//If there is insufficient change output, include another random output group so the fee can be increased
|
||||
OutputGroup outputGroup = outputGroups.remove(0);
|
||||
for(BlockTransactionHashIndex utxo : outputGroup.getUtxos()) {
|
||||
@@ -298,9 +300,13 @@ public class EntryCell extends TreeTableCell<Entry, Entry> implements Confirmati
|
||||
label += " (Replaced By Fee)";
|
||||
}
|
||||
|
||||
if(txOutput.getScript().getToAddress() != null) {
|
||||
Address address = txOutput.getScript().getToAddress();
|
||||
if(address != null) {
|
||||
long value = txOutput.getValue();
|
||||
//Disable change creation by enabling max payment when there is only one output and no additional UTXOs included
|
||||
return new Payment(txOutput.getScript().getToAddress(), label, txOutput.getValue(), blockTransaction.getTransaction().getOutputs().size() == 1 && rbfChange == 0);
|
||||
boolean sendMax = blockTransaction.getTransaction().getOutputs().size() == 1 && rbfChange == 0;
|
||||
SilentPaymentAddress silentPaymentAddress = transactionEntry.getWallet().getSilentPaymentAddress(address);
|
||||
return silentPaymentAddress == null ? new Payment(address, label, value, sendMax) : new SilentPayment(silentPaymentAddress, label, value, sendMax);
|
||||
}
|
||||
|
||||
return null;
|
||||
@@ -329,15 +335,17 @@ 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();
|
||||
}
|
||||
|
||||
EventManager.get().post(new SendActionEvent(transactionEntry.getWallet(), utxos));
|
||||
Platform.runLater(() -> EventManager.get().post(new SpendUtxoEvent(transactionEntry.getWallet(), utxos, payments, opReturns.isEmpty() ? null : opReturns, rbfFee, true, blockTransaction)));
|
||||
Platform.runLater(() -> EventManager.get().post(new SpendUtxoEvent(transactionEntry.getWallet(), utxos, payments, opReturns.isEmpty() ? null : opReturns, rbfFee, true, blockTransaction, safeToAddInputsOrOutputs)));
|
||||
}
|
||||
|
||||
private static Double getMaxFeeRate() {
|
||||
@@ -364,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()));
|
||||
@@ -391,19 +399,23 @@ 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)));
|
||||
Platform.runLater(() -> EventManager.get().post(new SpendUtxoEvent(transactionEntry.getWallet(), utxos, List.of(payment), null, blockTransaction.getFee(), true, null, true)));
|
||||
}
|
||||
|
||||
private static boolean canRBF(BlockTransaction blockTransaction) {
|
||||
return Config.get().isMempoolFullRbf() || blockTransaction.getTransaction().isReplaceByFee();
|
||||
private static boolean canRBF(BlockTransaction blockTransaction, Wallet wallet) {
|
||||
return Config.get().isMempoolFullRbf() || blockTransaction.getTransaction().isReplaceByFee() || wallet.isSilentPaymentsTransaction(blockTransaction);
|
||||
}
|
||||
|
||||
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) {
|
||||
@@ -476,7 +488,7 @@ public class EntryCell extends TreeTableCell<Entry, Entry> implements Confirmati
|
||||
tooltip += "\nFee rate: " + String.format("%.2f", feeRate) + " sats/vB";
|
||||
}
|
||||
|
||||
tooltip += "\nRBF: " + (canRBF(transactionEntry.getBlockTransaction()) ? "Enabled" : "Disabled");
|
||||
tooltip += "\nRBF: " + (canRBF(transactionEntry.getBlockTransaction(), transactionEntry.getWallet()) ? "Enabled" : "Disabled");
|
||||
}
|
||||
|
||||
return tooltip;
|
||||
@@ -544,6 +556,7 @@ public class EntryCell extends TreeTableCell<Entry, Entry> implements Confirmati
|
||||
|
||||
private static class UnconfirmedTransactionContextMenu extends ContextMenu {
|
||||
public UnconfirmedTransactionContextMenu(TransactionEntry transactionEntry) {
|
||||
Wallet wallet = transactionEntry.getWallet();
|
||||
BlockTransaction blockTransaction = transactionEntry.getBlockTransaction();
|
||||
MenuItem viewTransaction = new MenuItem("View Transaction");
|
||||
viewTransaction.setGraphic(getViewTransactionGlyph());
|
||||
@@ -553,7 +566,7 @@ public class EntryCell extends TreeTableCell<Entry, Entry> implements Confirmati
|
||||
});
|
||||
getItems().add(viewTransaction);
|
||||
|
||||
if(canRBF(blockTransaction) && Config.get().isIncludeMempoolOutputs() && transactionEntry.getWallet().allInputsFromWallet(blockTransaction.getHash())) {
|
||||
if(canRBF(blockTransaction, wallet) && Config.get().isIncludeMempoolOutputs() && transactionEntry.getWallet().allInputsFromWallet(blockTransaction.getHash())) {
|
||||
MenuItem increaseFee = new MenuItem("Increase Fee (RBF)");
|
||||
increaseFee.setGraphic(getIncreaseFeeRBFGlyph());
|
||||
increaseFee.setOnAction(AE -> {
|
||||
@@ -564,7 +577,7 @@ public class EntryCell extends TreeTableCell<Entry, Entry> implements Confirmati
|
||||
getItems().add(increaseFee);
|
||||
}
|
||||
|
||||
if(canRBF(blockTransaction) && Config.get().isIncludeMempoolOutputs() && transactionEntry.getWallet().allInputsFromWallet(blockTransaction.getHash())) {
|
||||
if(canRBF(blockTransaction, wallet) && Config.get().isIncludeMempoolOutputs() && transactionEntry.getWallet().allInputsFromWallet(blockTransaction.getHash())) {
|
||||
MenuItem cancelTx = new MenuItem("Cancel Transaction (RBF)");
|
||||
cancelTx.setGraphic(getCancelTransactionRBFGlyph());
|
||||
cancelTx.setOnAction(AE -> {
|
||||
@@ -657,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 -> {
|
||||
@@ -850,4 +863,11 @@ public class EntryCell extends TreeTableCell<Entry, Entry> implements Confirmati
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isTableSizeRecalculation() {
|
||||
//As per https://bugs.openjdk.org/browse/JDK-8265669 we check for cell visibility to avoid unnecessary recalculation, but this can result in false positives
|
||||
//The method releaseCell in VirtualFlow is responsible for setting accumCell visibility to false after use, so check this method is calling updateItem
|
||||
return StackWalker.getInstance().walk(frames -> frames.anyMatch(frame -> frame.getClassName().equals("javafx.scene.control.skin.VirtualFlow")
|
||||
&& frame.getMethodName().equals("releaseCell")));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.sparrowwallet.sparrow.control;
|
||||
|
||||
import com.sparrowwallet.drongo.protocol.Transaction;
|
||||
import com.sparrowwallet.sparrow.AppServices;
|
||||
import com.sparrowwallet.sparrow.net.FeeRatesSource;
|
||||
import javafx.application.Platform;
|
||||
@@ -7,6 +8,7 @@ import javafx.scene.Node;
|
||||
import javafx.scene.control.Slider;
|
||||
import javafx.util.StringConverter;
|
||||
|
||||
import java.text.DecimalFormat;
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@@ -14,9 +16,11 @@ import static com.sparrowwallet.sparrow.AppServices.*;
|
||||
|
||||
public class FeeRangeSlider extends Slider {
|
||||
private static final double FEE_RATE_SCROLL_INCREMENT = 0.01;
|
||||
private static final DecimalFormat INTEGER_FEE_RATE_FORMAT = new DecimalFormat("0");
|
||||
private static final DecimalFormat FRACTIONAL_FEE_RATE_FORMAT = new DecimalFormat("0.###");
|
||||
|
||||
public FeeRangeSlider() {
|
||||
super(0, FEE_RATES_RANGE.size() - 1, 0);
|
||||
super(0, AppServices.getFeeRatesRange().size() - 1, 0);
|
||||
setMajorTickUnit(1);
|
||||
setMinorTickCount(0);
|
||||
setSnapToTicks(false);
|
||||
@@ -27,11 +31,11 @@ public class FeeRangeSlider extends Slider {
|
||||
setLabelFormatter(new StringConverter<>() {
|
||||
@Override
|
||||
public String toString(Double object) {
|
||||
Long feeRate = LONG_FEE_RATES_RANGE.get(object.intValue());
|
||||
Double feeRate = AppServices.getLongFeeRatesRange().get(object.intValue());
|
||||
if(isLongFeeRange() && feeRate >= 1000) {
|
||||
return feeRate / 1000 + "k";
|
||||
return INTEGER_FEE_RATE_FORMAT.format(feeRate / 1000) + "k";
|
||||
}
|
||||
return Long.toString(feeRate);
|
||||
return feeRate > 0d && feeRate < Transaction.DEFAULT_MIN_RELAY_FEE ? FRACTIONAL_FEE_RATE_FORMAT.format(feeRate) : INTEGER_FEE_RATE_FORMAT.format(feeRate);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -51,10 +55,10 @@ public class FeeRangeSlider extends Slider {
|
||||
setOnScroll(event -> {
|
||||
if(event.getDeltaY() != 0) {
|
||||
double newFeeRate = getFeeRate() + (event.getDeltaY() > 0 ? FEE_RATE_SCROLL_INCREMENT : -FEE_RATE_SCROLL_INCREMENT);
|
||||
if(newFeeRate < LONG_FEE_RATES_RANGE.get(0)) {
|
||||
newFeeRate = LONG_FEE_RATES_RANGE.get(0);
|
||||
} else if(newFeeRate > LONG_FEE_RATES_RANGE.get(LONG_FEE_RATES_RANGE.size() - 1)) {
|
||||
newFeeRate = LONG_FEE_RATES_RANGE.get(LONG_FEE_RATES_RANGE.size() - 1);
|
||||
if(newFeeRate < AppServices.getLongFeeRatesRange().getFirst()) {
|
||||
newFeeRate = AppServices.getLongFeeRatesRange().getFirst();
|
||||
} else if(newFeeRate > AppServices.getLongFeeRatesRange().getLast()) {
|
||||
newFeeRate = AppServices.getLongFeeRatesRange().getLast();
|
||||
}
|
||||
setFeeRate(newFeeRate);
|
||||
}
|
||||
@@ -62,27 +66,79 @@ public class FeeRangeSlider extends Slider {
|
||||
}
|
||||
|
||||
public double getFeeRate() {
|
||||
return Math.pow(2.0, getValue());
|
||||
return getFeeRate(AppServices.getMinimumRelayFeeRate());
|
||||
}
|
||||
|
||||
public double getFeeRate(Double minRelayFeeRate) {
|
||||
if(minRelayFeeRate >= Transaction.DEFAULT_MIN_RELAY_FEE) {
|
||||
return Math.pow(2.0, getValue());
|
||||
}
|
||||
|
||||
if(getValue() < 1.0d) {
|
||||
if(minRelayFeeRate == 0.0d) {
|
||||
return getValue();
|
||||
}
|
||||
return Math.pow(minRelayFeeRate, 1.0d - getValue());
|
||||
}
|
||||
|
||||
return Math.pow(2.0, getValue() - 1.0d);
|
||||
}
|
||||
|
||||
public void setFeeRate(double feeRate) {
|
||||
double value = Math.log(feeRate) / Math.log(2);
|
||||
setFeeRate(feeRate, AppServices.getMinimumRelayFeeRate());
|
||||
}
|
||||
|
||||
public void setFeeRate(double feeRate, Double minRelayFeeRate) {
|
||||
double value = getValue(feeRate, minRelayFeeRate);
|
||||
updateMaxFeeRange(value);
|
||||
setValue(value);
|
||||
}
|
||||
|
||||
private double getValue(double feeRate, Double minRelayFeeRate) {
|
||||
double value;
|
||||
|
||||
if(minRelayFeeRate >= Transaction.DEFAULT_MIN_RELAY_FEE) {
|
||||
value = Math.log(feeRate) / Math.log(2);
|
||||
} else {
|
||||
if(feeRate < Transaction.DEFAULT_MIN_RELAY_FEE) {
|
||||
if(minRelayFeeRate == 0.0d) {
|
||||
return feeRate;
|
||||
}
|
||||
value = 1.0d - (Math.log(feeRate) / Math.log(minRelayFeeRate));
|
||||
} else {
|
||||
value = (Math.log(feeRate) / Math.log(2.0)) + 1.0d;
|
||||
}
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
public void updateFeeRange(Double minRelayFeeRate, Double previousMinRelayFeeRate) {
|
||||
if(minRelayFeeRate != null && previousMinRelayFeeRate != null) {
|
||||
setFeeRate(getFeeRate(previousMinRelayFeeRate), minRelayFeeRate);
|
||||
}
|
||||
setMinorTickCount(1);
|
||||
setMinorTickCount(0);
|
||||
}
|
||||
|
||||
private void updateMaxFeeRange(double value) {
|
||||
if(value >= getMax() && !isLongFeeRange()) {
|
||||
setMax(LONG_FEE_RATES_RANGE.size() - 1);
|
||||
if(AppServices.getMinimumRelayFeeRate() < Transaction.DEFAULT_MIN_RELAY_FEE) {
|
||||
setMin(1.0d);
|
||||
}
|
||||
setMax(AppServices.getLongFeeRatesRange().size() - 1);
|
||||
updateTrackHighlight();
|
||||
} else if(value == getMin() && isLongFeeRange()) {
|
||||
setMax(FEE_RATES_RANGE.size() - 1);
|
||||
if(AppServices.getMinimumRelayFeeRate() < Transaction.DEFAULT_MIN_RELAY_FEE) {
|
||||
setMin(0.0d);
|
||||
}
|
||||
setMax(AppServices.getFeeRatesRange().size() - 1);
|
||||
updateTrackHighlight();
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isLongFeeRange() {
|
||||
return getMax() > FEE_RATES_RANGE.size() - 1;
|
||||
public boolean isLongFeeRange() {
|
||||
return getMax() > AppServices.getFeeRatesRange().size() - 1;
|
||||
}
|
||||
|
||||
public void updateTrackHighlight() {
|
||||
@@ -137,9 +193,9 @@ public class FeeRangeSlider extends Slider {
|
||||
}
|
||||
|
||||
private int getPercentageOfFeeRange(Double feeRate) {
|
||||
double index = Math.log(feeRate) / Math.log(2);
|
||||
double index = getValue(feeRate, AppServices.getMinimumRelayFeeRate());
|
||||
if(isLongFeeRange()) {
|
||||
index *= ((double)FEE_RATES_RANGE.size() / (LONG_FEE_RATES_RANGE.size())) * 0.99;
|
||||
index *= ((double)AppServices.getFeeRatesRange().size() / (AppServices.getLongFeeRatesRange().size())) * 0.99;
|
||||
}
|
||||
return (int)Math.round(index * 10.0);
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import com.sparrowwallet.drongo.OsType;
|
||||
import com.sparrowwallet.drongo.protocol.Transaction;
|
||||
import com.sparrowwallet.sparrow.CurrencyRate;
|
||||
import com.sparrowwallet.sparrow.UnitFormat;
|
||||
import com.sparrowwallet.sparrow.io.Config;
|
||||
import com.sparrowwallet.sparrow.wallet.Entry;
|
||||
import javafx.scene.control.ContextMenu;
|
||||
import javafx.scene.control.MenuItem;
|
||||
@@ -47,20 +48,27 @@ public class FiatCell extends TreeTableCell<Entry, Number> {
|
||||
CurrencyRate currencyRate = coinTreeTable.getCurrencyRate();
|
||||
|
||||
if(currencyRate != null && currencyRate.isAvailable()) {
|
||||
Currency currency = currencyRate.getCurrency();
|
||||
double btcRate = currencyRate.getBtcRate();
|
||||
if(Config.get().isHideAmounts()) {
|
||||
setText(CoinLabel.HIDDEN_AMOUNT_TEXT);
|
||||
setGraphic(null);
|
||||
setTooltip(null);
|
||||
setContextMenu(null);
|
||||
} else {
|
||||
Currency currency = currencyRate.getCurrency();
|
||||
double btcRate = currencyRate.getBtcRate();
|
||||
|
||||
BigDecimal satsBalance = BigDecimal.valueOf(amount.longValue());
|
||||
BigDecimal btcBalance = satsBalance.divide(BigDecimal.valueOf(Transaction.SATOSHIS_PER_BITCOIN));
|
||||
BigDecimal fiatBalance = btcBalance.multiply(BigDecimal.valueOf(btcRate));
|
||||
BigDecimal satsBalance = BigDecimal.valueOf(amount.longValue());
|
||||
BigDecimal btcBalance = satsBalance.divide(BigDecimal.valueOf(Transaction.SATOSHIS_PER_BITCOIN));
|
||||
BigDecimal fiatBalance = btcBalance.multiply(BigDecimal.valueOf(btcRate));
|
||||
|
||||
String label = format.formatCurrencyValue(fiatBalance.doubleValue());
|
||||
tooltip.setText("1 BTC = " + currency.getSymbol() + " " + format.formatCurrencyValue(btcRate));
|
||||
String label = format.formatCurrencyValue(fiatBalance.doubleValue());
|
||||
tooltip.setText("1 BTC = " + currency.getSymbol() + " " + format.formatCurrencyValue(btcRate));
|
||||
|
||||
setText(label);
|
||||
setGraphic(null);
|
||||
setTooltip(tooltip);
|
||||
setContextMenu(contextMenu);
|
||||
setText(label);
|
||||
setGraphic(null);
|
||||
setTooltip(tooltip);
|
||||
setContextMenu(contextMenu);
|
||||
}
|
||||
} else {
|
||||
setText(null);
|
||||
setGraphic(null);
|
||||
|
||||
@@ -90,6 +90,13 @@ public class FiatLabel extends CopyableLabel {
|
||||
|
||||
private void setValueAsText(long balance, UnitFormat unitFormat) {
|
||||
if(getCurrency() != null && getBtcRate() > 0.0) {
|
||||
if(Config.get().isHideAmounts()) {
|
||||
setText(CoinLabel.HIDDEN_AMOUNT_TEXT);
|
||||
setTooltip(null);
|
||||
setContextMenu(null);
|
||||
return;
|
||||
}
|
||||
|
||||
BigDecimal satsBalance = BigDecimal.valueOf(balance);
|
||||
BigDecimal btcBalance = satsBalance.divide(BigDecimal.valueOf(Transaction.SATOSHIS_PER_BITCOIN));
|
||||
BigDecimal fiatBalance = btcBalance.multiply(BigDecimal.valueOf(getBtcRate()));
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -157,7 +157,7 @@ public class FileKeystoreExportPane extends TitledDescriptionPane {
|
||||
if(exporter instanceof Bip129) {
|
||||
UR ur = UR.fromBytes(baos.toByteArray());
|
||||
BBQR bbqr = new BBQR(BBQRType.UNICODE, baos.toByteArray());
|
||||
qrDisplayDialog = new QRDisplayDialog(ur, bbqr, false, true, false);
|
||||
qrDisplayDialog = new QRDisplayDialog(ur, bbqr, false, true, QREncoding.UR);
|
||||
} else {
|
||||
qrDisplayDialog = new QRDisplayDialog(baos.toString(StandardCharsets.UTF_8));
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
@@ -171,18 +171,18 @@ public class FileWalletExportPane extends TitledDescriptionPane {
|
||||
} else if(exporter instanceof Bip129 || exporter instanceof WalletLabels) {
|
||||
UR ur = UR.fromBytes(outputStream.toByteArray());
|
||||
BBQR bbqr = new BBQR(BBQRType.UNICODE, outputStream.toByteArray());
|
||||
qrDisplayDialog = new QRDisplayDialog(ur, bbqr, false, false, false);
|
||||
qrDisplayDialog = new QRDisplayDialog(ur, bbqr, false, false, QREncoding.UR);
|
||||
} else if(exporter instanceof Descriptor) {
|
||||
boolean addBbqrOption = exportWallet.getKeystores().stream().anyMatch(keystore -> keystore.getWalletModel().showBbqr());
|
||||
boolean selectBbqrOption = exportWallet.getKeystores().stream().allMatch(keystore -> keystore.getWalletModel().selectBbqr());
|
||||
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, selectBbqrOption);
|
||||
qrDisplayDialog = new DescriptorQRDisplayDialog(exportWallet.getFullDisplayName(), outputDescriptor.toString(true), registryItem.toUR(), bbqr, encoding);
|
||||
} else if(exporter.getClass().equals(ColdcardMultisig.class)) {
|
||||
UR ur = UR.fromBytes(outputStream.toByteArray());
|
||||
BBQR bbqr = new BBQR(BBQRType.UNICODE, outputStream.toByteArray());
|
||||
qrDisplayDialog = new QRDisplayDialog(ur, bbqr, false, false, true);
|
||||
qrDisplayDialog = new QRDisplayDialog(ur, bbqr, false, false, QREncoding.BBQR);
|
||||
} else {
|
||||
qrDisplayDialog = new QRDisplayDialog(outputStream.toString(StandardCharsets.UTF_8));
|
||||
}
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,7 +10,12 @@ import com.sparrowwallet.drongo.crypto.Bip322;
|
||||
import com.sparrowwallet.drongo.crypto.ECKey;
|
||||
import com.sparrowwallet.drongo.policy.PolicyType;
|
||||
import com.sparrowwallet.drongo.protocol.ScriptType;
|
||||
import com.sparrowwallet.drongo.psbt.PSBT;
|
||||
import com.sparrowwallet.drongo.psbt.PSBTInput;
|
||||
import com.sparrowwallet.drongo.wallet.*;
|
||||
import com.sparrowwallet.hummingbird.registry.CryptoPSBT;
|
||||
import com.sparrowwallet.sparrow.io.bbqr.BBQR;
|
||||
import com.sparrowwallet.sparrow.io.bbqr.BBQRType;
|
||||
import com.sparrowwallet.sparrow.AppServices;
|
||||
import com.sparrowwallet.sparrow.EventManager;
|
||||
import com.sparrowwallet.sparrow.event.*;
|
||||
@@ -20,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;
|
||||
@@ -134,6 +137,7 @@ public class MessageSignDialog extends Dialog<ButtonBar.ButtonData> {
|
||||
address.getStyleClass().add("id");
|
||||
address.setEditable(walletNode == null);
|
||||
address.setTooltip(new Tooltip("Only singlesig addresses can sign"));
|
||||
address.setSkin(new AddressTextFieldSkin(address));
|
||||
addressField.getInputs().add(address);
|
||||
|
||||
if(walletNode != null) {
|
||||
@@ -156,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();
|
||||
@@ -241,7 +256,7 @@ public class MessageSignDialog extends Dialog<ButtonBar.ButtonData> {
|
||||
if(wallet != null) {
|
||||
setWalletNodeFromAddress(wallet, address);
|
||||
if(walletNode != null) {
|
||||
setFormatFromScriptType(getSigningScriptType(walletNode));
|
||||
setFormatFromScriptType(walletNode.getWallet().getScriptType());
|
||||
}
|
||||
}
|
||||
} catch(InvalidAddressException e) {
|
||||
@@ -249,6 +264,13 @@ public class MessageSignDialog extends Dialog<ButtonBar.ButtonData> {
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
formatGroup.selectedToggleProperty().addListener((_, _, newVal) -> {
|
||||
if(wallet != null) {
|
||||
boolean canSignSelectedFormat = canSignAllFormats(wallet) || newVal == formatElectrum;
|
||||
signButton.setDisable(!isValidAddress() || !canSign || !canSignSelectedFormat);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
EventManager.get().register(this);
|
||||
@@ -276,7 +298,7 @@ public class MessageSignDialog extends Dialog<ButtonBar.ButtonData> {
|
||||
}
|
||||
|
||||
if(wallet != null && walletNode != null) {
|
||||
setFormatFromScriptType(getSigningScriptType(walletNode));
|
||||
setFormatFromScriptType(walletNode.getWallet().getScriptType());
|
||||
} else {
|
||||
formatGroup.selectToggle(formatElectrum);
|
||||
}
|
||||
@@ -284,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");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -295,7 +317,7 @@ public class MessageSignDialog extends Dialog<ButtonBar.ButtonData> {
|
||||
|| wallet.getKeystores().getFirst().getWalletModel().isCard();
|
||||
}
|
||||
|
||||
private boolean canSignBip322(Wallet wallet) {
|
||||
private boolean canSignAllFormats(Wallet wallet) {
|
||||
return wallet.getKeystores().getFirst().hasPrivateKey();
|
||||
}
|
||||
|
||||
@@ -310,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;
|
||||
}
|
||||
@@ -320,11 +342,6 @@ public class MessageSignDialog extends Dialog<ButtonBar.ButtonData> {
|
||||
walletNode = wallet.getWalletAddresses().get(address);
|
||||
}
|
||||
|
||||
private ScriptType getSigningScriptType(WalletNode walletNode) {
|
||||
ScriptType scriptType = walletNode.getWallet().getScriptType();
|
||||
return canSign(walletNode.getWallet()) && !canSignBip322(walletNode.getWallet()) ? ScriptType.P2PKH : scriptType;
|
||||
}
|
||||
|
||||
private void setFormatFromScriptType(ScriptType scriptType) {
|
||||
formatElectrum.setDisable(scriptType == ScriptType.P2TR);
|
||||
formatTrezor.setDisable(scriptType == ScriptType.P2TR || scriptType == ScriptType.P2PKH);
|
||||
@@ -371,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());
|
||||
@@ -458,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);
|
||||
}
|
||||
|
||||
@@ -472,6 +495,11 @@ public class MessageSignDialog extends Dialog<ButtonBar.ButtonData> {
|
||||
return;
|
||||
}
|
||||
|
||||
if(isBip322()) {
|
||||
showBip322Qr();
|
||||
return;
|
||||
}
|
||||
|
||||
//Note we can expect a single keystore due to the check in the constructor
|
||||
KeyDerivation firstDerivation = walletNode.getWallet().getKeystores().get(0).getKeyDerivation();
|
||||
String derivationPath = KeyDerivation.writePath(firstDerivation.extend(walletNode.getDerivation()).getDerivation(), false);
|
||||
@@ -485,13 +513,88 @@ public class MessageSignDialog extends Dialog<ButtonBar.ButtonData> {
|
||||
}
|
||||
}
|
||||
|
||||
private void showBip322Qr() {
|
||||
Wallet signingWallet = walletNode.getWallet();
|
||||
PSBT psbt = buildBip322Psbt(signingWallet);
|
||||
|
||||
byte[] psbtBytes = psbt.getForExport().serialize();
|
||||
CryptoPSBT cryptoPSBT = new CryptoPSBT(psbtBytes);
|
||||
BBQR bbqr = new BBQR(BBQRType.PSBT, psbtBytes);
|
||||
QRDisplayDialog qrDisplayDialog = new QRDisplayDialog(cryptoPSBT.toUR(), bbqr, false, true, QREncoding.UR);
|
||||
qrDisplayDialog.initOwner(getDialogPane().getScene().getWindow());
|
||||
Optional<ButtonType> optButtonType = qrDisplayDialog.showAndWait();
|
||||
if(optButtonType.isPresent() && optButtonType.get().getButtonData() == ButtonBar.ButtonData.OK_DONE) {
|
||||
scanQr();
|
||||
}
|
||||
}
|
||||
|
||||
private PSBT buildBip322Psbt(Wallet signingWallet) {
|
||||
if(signingWallet.getPolicyType() == PolicyType.SINGLE_SP) {
|
||||
Keystore keystore = signingWallet.getKeystores().getFirst();
|
||||
ECKey spendPubKey = keystore.getSilentPaymentScanAddress().getSpendKey();
|
||||
KeyDerivation spendDerivation = new KeyDerivation(keystore.getKeyDerivation().getMasterFingerprint(), KeyDerivation.writePath(KeyDerivation.getBip352SpendDerivation(keystore.getKeyDerivation().getDerivation())));
|
||||
return Bip322.getBip322PsbtSp(walletNode.getAddress(), message.getText().trim(), walletNode.getSilentPaymentTweak(), Map.of(spendPubKey, spendDerivation));
|
||||
}
|
||||
|
||||
PSBT psbt = Bip322.getBip322Psbt(signingWallet.getScriptType(), walletNode.getAddress(), message.getText().trim());
|
||||
addBip322DerivationInfo(psbt, signingWallet);
|
||||
|
||||
return psbt;
|
||||
}
|
||||
|
||||
private String extractBip322Signature(PSBT signedPsbt) {
|
||||
String psbtMessage = signedPsbt.getGenericSignedMessage();
|
||||
if(psbtMessage != null && !psbtMessage.equals(message.getText().trim())) {
|
||||
Optional<ButtonType> response = AppServices.showWarningDialog("Message mismatch",
|
||||
"The message in the signed PSBT does not match the message in this dialog.\n\nPSBT message: " + psbtMessage +
|
||||
"\n\nContinue extracting the signature?", ButtonType.NO, ButtonType.YES);
|
||||
if(response.isEmpty() || response.get() != ButtonType.YES) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
Wallet signingWallet = walletNode.getWallet();
|
||||
if(signingWallet.getPolicyType() == PolicyType.SINGLE_SP) {
|
||||
return Bip322.getBip322SignatureFromPsbtSp(signedPsbt);
|
||||
}
|
||||
|
||||
ECKey pubKey = signingWallet.getKeystores().getFirst().getPubKey(walletNode);
|
||||
return Bip322.getBip322SignatureFromPsbt(signingWallet.getScriptType(), signedPsbt, pubKey);
|
||||
}
|
||||
|
||||
private void addBip322DerivationInfo(PSBT psbt, Wallet signingWallet) {
|
||||
ScriptType scriptType = signingWallet.getScriptType();
|
||||
PSBTInput psbtInput = psbt.getPsbtInputs().get(0);
|
||||
Keystore keystore = signingWallet.getKeystores().get(0);
|
||||
ECKey pubKey = keystore.getPubKey(walletNode);
|
||||
KeyDerivation fullDerivation = keystore.getKeyDerivation().extend(walletNode.getDerivation());
|
||||
|
||||
if(scriptType == ScriptType.P2TR) {
|
||||
psbtInput.setTapInternalKey(pubKey);
|
||||
psbtInput.getTapDerivedPublicKeys().put(ECKey.fromPublicOnly(pubKey.getPubKeyXCoord()), Map.of(fullDerivation, Collections.emptyList()));
|
||||
} else {
|
||||
psbtInput.getDerivedPublicKeys().put(scriptType.getOutputKey(signingWallet.getPolicyType(), pubKey), fullDerivation);
|
||||
}
|
||||
}
|
||||
|
||||
private void scanQr() {
|
||||
QRScanDialog qrScanDialog = new QRScanDialog();
|
||||
qrScanDialog.initOwner(getDialogPane().getScene().getWindow());
|
||||
Optional<QRScanDialog.Result> optionalResult = qrScanDialog.showAndWait();
|
||||
if(optionalResult.isPresent()) {
|
||||
QRScanDialog.Result result = optionalResult.get();
|
||||
if(result.payload != null) {
|
||||
if(result.psbt != null) {
|
||||
try {
|
||||
String sig = extractBip322Signature(result.psbt);
|
||||
if(sig != null) {
|
||||
signature.clear();
|
||||
signature.appendText(sig);
|
||||
}
|
||||
} catch(Exception e) {
|
||||
log.error("Error extracting BIP-322 signature from PSBT", e);
|
||||
AppServices.showErrorDialog("Error extracting signature", e.getMessage());
|
||||
}
|
||||
} else if(result.payload != null) {
|
||||
signature.clear();
|
||||
signature.appendText(result.payload);
|
||||
} else if(result.exception != null) {
|
||||
@@ -509,6 +612,11 @@ public class MessageSignDialog extends Dialog<ButtonBar.ButtonData> {
|
||||
return;
|
||||
}
|
||||
|
||||
if(isBip322()) {
|
||||
exportBip322File();
|
||||
return;
|
||||
}
|
||||
|
||||
StringJoiner joiner = new StringJoiner("\n");
|
||||
joiner.add(message.getText().trim().replaceAll("\r*\n*", ""));
|
||||
//Note we can expect a single keystore due to the check in the constructor
|
||||
@@ -537,20 +645,65 @@ public class MessageSignDialog extends Dialog<ButtonBar.ButtonData> {
|
||||
}
|
||||
}
|
||||
|
||||
private void exportBip322File() {
|
||||
Wallet signingWallet = walletNode.getWallet();
|
||||
PSBT psbt = buildBip322Psbt(signingWallet);
|
||||
|
||||
Stage window = new Stage();
|
||||
FileChooser fileChooser = new FileChooser();
|
||||
fileChooser.setTitle("Save PSBT File");
|
||||
fileChooser.setInitialFileName("bip322-signmessage.psbt");
|
||||
AppServices.moveToActiveWindowScreen(window, 800, 450);
|
||||
File file = fileChooser.showSaveDialog(window);
|
||||
if(file != null) {
|
||||
try(OutputStream os = new FileOutputStream(file)) {
|
||||
os.write(psbt.getForExport().serialize());
|
||||
} catch(IOException e) {
|
||||
log.error("Error saving BIP-322 PSBT", e);
|
||||
AppServices.showErrorDialog("Error saving PSBT", "Cannot write to " + file.getAbsolutePath());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void importFile() {
|
||||
Stage window = new Stage();
|
||||
|
||||
FileChooser fileChooser = new FileChooser();
|
||||
fileChooser.setTitle("Open Signed Text File");
|
||||
fileChooser.setTitle("Open Signed File");
|
||||
fileChooser.getExtensionFilters().addAll(
|
||||
new FileChooser.ExtensionFilter("All Files", OsType.getCurrent().equals(OsType.UNIX) ? "*" : "*.*"),
|
||||
new FileChooser.ExtensionFilter("Text Files", "*.txt")
|
||||
new FileChooser.ExtensionFilter("Text Files", "*.txt"),
|
||||
new FileChooser.ExtensionFilter("PSBT Files", "*.psbt")
|
||||
);
|
||||
|
||||
AppServices.moveToActiveWindowScreen(window, 800, 450);
|
||||
File file = fileChooser.showOpenDialog(window);
|
||||
|
||||
if(file != null) {
|
||||
if(file.getName().toLowerCase(Locale.ROOT).endsWith(".psbt") || isBip322()) {
|
||||
if(walletNode == null) {
|
||||
AppServices.showErrorDialog("Address not in wallet", "The provided address is not present in the currently selected wallet.");
|
||||
return;
|
||||
}
|
||||
try {
|
||||
byte[] psbtBytes = Files.readAllBytes(file.toPath());
|
||||
PSBT signedPsbt = new PSBT(psbtBytes, false);
|
||||
String sig = extractBip322Signature(signedPsbt);
|
||||
if(sig != null) {
|
||||
signature.clear();
|
||||
signature.appendText(sig);
|
||||
}
|
||||
return;
|
||||
} catch(Exception e) {
|
||||
if(file.getName().toLowerCase(Locale.ROOT).endsWith(".psbt")) {
|
||||
log.error("Error loading signed PSBT", e);
|
||||
AppServices.showErrorDialog("Error loading signed PSBT", e.getMessage());
|
||||
return;
|
||||
}
|
||||
//Fall through to text handling for non-.psbt files
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
String content = Files.readString(file.toPath(), StandardCharsets.UTF_8);
|
||||
Matcher matcher = signedMessagePattern.matcher(content);
|
||||
|
||||
@@ -6,10 +6,9 @@ import com.sparrowwallet.drongo.wallet.WalletModel;
|
||||
import javafx.beans.property.SimpleListProperty;
|
||||
import javafx.collections.FXCollections;
|
||||
import javafx.collections.ObservableList;
|
||||
import javafx.geometry.Orientation;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.layout.GridPane;
|
||||
import javafx.scene.layout.StackPane;
|
||||
import javafx.scene.layout.TilePane;
|
||||
import javafx.scene.layout.VBox;
|
||||
|
||||
import java.util.ArrayList;
|
||||
@@ -32,11 +31,9 @@ public class MnemonicKeystoreDisplayPane extends MnemonicKeystorePane {
|
||||
VBox vBox = new VBox();
|
||||
vBox.setSpacing(10);
|
||||
|
||||
wordsPane = new TilePane();
|
||||
wordsPane.setPrefRows(Math.ceilDiv(numWords, 3));
|
||||
wordsPane = new GridPane();
|
||||
wordsPane.setHgap(10);
|
||||
wordsPane.setVgap(10);
|
||||
wordsPane.setOrientation(Orientation.VERTICAL);
|
||||
|
||||
List<String> words = new ArrayList<>();
|
||||
for(int i = 0; i < numWords; i++) {
|
||||
@@ -53,7 +50,14 @@ public class MnemonicKeystoreDisplayPane extends MnemonicKeystorePane {
|
||||
wordEntries.get(i).setNextEntry(wordEntries.get(i + 1));
|
||||
wordEntries.get(i).setNextField(wordEntries.get(i + 1).getEditor());
|
||||
}
|
||||
wordsPane.getChildren().addAll(wordEntries);
|
||||
|
||||
int numCols = 3;
|
||||
int numRows = Math.ceilDiv(numWords, numCols);
|
||||
for(int i = 0; i < wordEntries.size(); i++) {
|
||||
int col = i / numRows;
|
||||
int row = i % numRows;
|
||||
wordsPane.add(wordEntries.get(i), col, row);
|
||||
}
|
||||
|
||||
vBox.getChildren().add(wordsPane);
|
||||
|
||||
|
||||
@@ -58,7 +58,7 @@ public class MnemonicKeystoreImportPane extends MnemonicKeystorePane {
|
||||
importButton = new SplitMenuButton();
|
||||
importButton.setAlignment(Pos.CENTER_RIGHT);
|
||||
importButton.setText("Import Keystore");
|
||||
importButton.getStyleClass().add("default-button");
|
||||
setDefaultButton(importButton);
|
||||
importButton.setOnAction(event -> {
|
||||
importButton.setDisable(true);
|
||||
importKeystore(getDefaultDerivation(), false);
|
||||
@@ -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);
|
||||
|
||||
@@ -15,20 +15,15 @@ import javafx.beans.property.SimpleStringProperty;
|
||||
import javafx.collections.FXCollections;
|
||||
import javafx.collections.ListChangeListener;
|
||||
import javafx.collections.ObservableList;
|
||||
import javafx.concurrent.ScheduledService;
|
||||
import javafx.concurrent.Task;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.Orientation;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.control.*;
|
||||
import javafx.scene.input.Clipboard;
|
||||
import javafx.scene.layout.*;
|
||||
import javafx.util.Callback;
|
||||
import javafx.util.Duration;
|
||||
import org.controlsfx.control.textfield.AutoCompletionBinding;
|
||||
import org.controlsfx.control.textfield.TextFields;
|
||||
import org.controlsfx.glyphfont.Glyph;
|
||||
import org.controlsfx.validation.ValidationResult;
|
||||
import org.controlsfx.validation.ValidationSupport;
|
||||
import org.controlsfx.validation.Validator;
|
||||
@@ -44,7 +39,7 @@ public class MnemonicKeystorePane extends TitledDescriptionPane {
|
||||
private static final Logger log = LoggerFactory.getLogger(MnemonicKeystorePane.class);
|
||||
|
||||
protected SplitMenuButton enterMnemonicButton;
|
||||
protected TilePane wordsPane;
|
||||
protected GridPane wordsPane;
|
||||
protected Label validLabel;
|
||||
protected Label invalidLabel;
|
||||
|
||||
@@ -167,11 +162,9 @@ public class MnemonicKeystorePane extends TitledDescriptionPane {
|
||||
VBox vBox = new VBox();
|
||||
vBox.setSpacing(10);
|
||||
|
||||
wordsPane = new TilePane();
|
||||
wordsPane.setPrefRows(Math.ceilDiv(numWords, 3));
|
||||
wordsPane = new GridPane();
|
||||
wordsPane.setHgap(10);
|
||||
wordsPane.setVgap(10);
|
||||
wordsPane.setOrientation(Orientation.VERTICAL);
|
||||
|
||||
List<String> words = new ArrayList<>();
|
||||
for(int i = 0; i < numWords; i++) {
|
||||
@@ -188,7 +181,14 @@ public class MnemonicKeystorePane extends TitledDescriptionPane {
|
||||
wordEntries.get(i).setNextEntry(wordEntries.get(i + 1));
|
||||
wordEntries.get(i).setNextField(wordEntries.get(i + 1).getEditor());
|
||||
}
|
||||
wordsPane.getChildren().addAll(wordEntries);
|
||||
|
||||
int numCols = 3;
|
||||
int numRows = Math.ceilDiv(numWords, numCols);
|
||||
for(int i = 0; i < wordEntries.size(); i++) {
|
||||
int col = i / numRows;
|
||||
int row = i % numRows;
|
||||
wordsPane.add(wordEntries.get(i), col, row);
|
||||
}
|
||||
|
||||
vBox.getChildren().add(wordsPane);
|
||||
|
||||
|
||||
+3
-3
@@ -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) {
|
||||
@@ -196,7 +196,7 @@ public class MnemonicShareKeystoreImportPane extends MnemonicKeystorePane {
|
||||
importButton = new SplitMenuButton();
|
||||
importButton.setAlignment(Pos.CENTER_RIGHT);
|
||||
importButton.setText("Import Keystore");
|
||||
importButton.getStyleClass().add("default-button");
|
||||
setDefaultButton(importButton);
|
||||
importButton.setOnAction(event -> {
|
||||
importButton.setDisable(true);
|
||||
importKeystore(getDefaultDerivation(), false);
|
||||
@@ -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);
|
||||
|
||||
+45
-25
@@ -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();
|
||||
@@ -134,15 +134,21 @@ public class MnemonicWalletKeystoreImportPane extends MnemonicKeystorePane {
|
||||
progressIndicator.progressProperty().bind(walletDiscoveryService.progressProperty());
|
||||
walletDiscoveryService.setOnSucceeded(successEvent -> {
|
||||
discoverButton.setGraphic(null);
|
||||
Optional<Wallet> optWallet = walletDiscoveryService.getValue();
|
||||
if(optWallet.isPresent()) {
|
||||
EventManager.get().post(new WalletImportEvent(optWallet.get()));
|
||||
Optional<List<Wallet>> optWallets = walletDiscoveryService.getValue();
|
||||
if(optWallets.isPresent()) {
|
||||
List<Wallet> discoveredWallets = optWallets.get();
|
||||
if(discoveredWallets.size() > 1) {
|
||||
for(Wallet wallet : discoveredWallets) {
|
||||
wallet.setName(wallet.getKeystores().getFirst().getLabel() + " " + wallet.getScriptType().getDescription());
|
||||
}
|
||||
}
|
||||
EventManager.get().post(new WalletImportEvent(discoveredWallets));
|
||||
} else {
|
||||
discoverButton.setDisable(false);
|
||||
Optional<ButtonType> optButtonType = AppServices.showErrorDialog("No existing wallet found",
|
||||
Config.get().getServerType() == ServerType.BITCOIN_CORE ? "The configured server type is Bitcoin Core, which does not support wallet discovery.\n\n" +
|
||||
"You can however import this wallet and scan the blockchain by supplying a start date. Do you want to import this wallet?" :
|
||||
"Could not find 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);
|
||||
@@ -157,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);
|
||||
@@ -202,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);
|
||||
@@ -225,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");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,6 +13,9 @@ import com.sparrowwallet.drongo.policy.PolicyType;
|
||||
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;
|
||||
@@ -28,8 +31,6 @@ import javafx.application.Platform;
|
||||
import javafx.collections.FXCollections;
|
||||
import javafx.event.ActionEvent;
|
||||
import javafx.scene.control.*;
|
||||
import javafx.scene.image.Image;
|
||||
import javafx.scene.image.ImageView;
|
||||
import javafx.scene.layout.HBox;
|
||||
import javafx.scene.layout.Priority;
|
||||
import javafx.scene.layout.StackPane;
|
||||
@@ -67,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();
|
||||
@@ -110,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>() {
|
||||
@@ -141,6 +144,7 @@ public class PrivateKeySweepDialog extends Dialog<Transaction> {
|
||||
toAddressField.setText("Sweep to:");
|
||||
toAddress = new ComboBoxTextField();
|
||||
toAddress.getStyleClass().add("fixed-width");
|
||||
toAddress.setSkin(new AddressTextFieldSkin(toAddress));
|
||||
toWallet = new ComboBox<>();
|
||||
toWallet.setItems(FXCollections.observableList(AppServices.get().getOpenWallets().keySet().stream()
|
||||
.filter(w -> !w.isWhirlpoolChildWallet() && !w.isBip47()).collect(Collectors.toList())));
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -398,14 +439,14 @@ public class PrivateKeySweepDialog extends Dialog<Transaction> {
|
||||
|
||||
double feeRate = feeRange.getFeeRate();
|
||||
long fee = (long)Math.ceil(noFeeTransaction.getVirtualSize() * feeRate);
|
||||
if(feeRate == Transaction.DEFAULT_MIN_RELAY_FEE) {
|
||||
if(feeRate == AppServices.getMinimumRelayFeeRate() && feeRate > 0d) {
|
||||
fee++;
|
||||
}
|
||||
|
||||
long dustThreshold = destAddress.getScriptType().getDustThreshold(sweepOutput, Transaction.DUST_RELAY_TX_FEE);
|
||||
if(total - fee <= dustThreshold) {
|
||||
feeRate = Transaction.DEFAULT_MIN_RELAY_FEE;
|
||||
fee = (long)Math.ceil(noFeeTransaction.getVirtualSize() * feeRate) + 1;
|
||||
feeRate = AppServices.getMinimumRelayFeeRate();
|
||||
fee = (long)Math.ceil(noFeeTransaction.getVirtualSize() * feeRate) + (feeRate > 0d ? 1 : 0);
|
||||
|
||||
if(total - fee <= dustThreshold) {
|
||||
AppServices.showErrorDialog("Insufficient funds", "The unspent outputs for this private key contain insufficient funds to spend (" + total + " sats).");
|
||||
@@ -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,12 +497,39 @@ 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());
|
||||
}
|
||||
|
||||
setResult(psbt.extractTransaction());
|
||||
try {
|
||||
setResult(psbt.extractTransaction());
|
||||
} catch(PSBTProofException e) {
|
||||
AppServices.showErrorDialog("Invalid Silent Payments Transaction", e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
|
||||
@@ -57,7 +57,11 @@ public class QRDisplayDialog extends Dialog<ButtonType> {
|
||||
|
||||
private final BBQR bbqr;
|
||||
private BBQREncoder bbqrEncoder;
|
||||
private boolean useBbqrEncoding;
|
||||
|
||||
private final String raw;
|
||||
private final boolean rawEncodable;
|
||||
|
||||
private QREncoding encoding = QREncoding.UR;
|
||||
|
||||
private final ImageView qrImageView;
|
||||
|
||||
@@ -72,14 +76,18 @@ public class QRDisplayDialog extends Dialog<ButtonType> {
|
||||
private static boolean initialDensityChange;
|
||||
|
||||
public QRDisplayDialog(String type, byte[] data, boolean addLegacyEncodingOption) throws UR.URException {
|
||||
this(UR.fromBytes(type, data), null, addLegacyEncodingOption, false, false);
|
||||
this(UR.fromBytes(type, data), null, addLegacyEncodingOption, false, QREncoding.UR);
|
||||
}
|
||||
|
||||
public QRDisplayDialog(UR ur) {
|
||||
this(ur, null, false, false, false);
|
||||
this(ur, null, false, false, QREncoding.UR);
|
||||
}
|
||||
|
||||
public QRDisplayDialog(UR ur, BBQR bbqr, boolean addLegacyEncodingOption, boolean addScanButton, boolean selectBbqrButton) {
|
||||
public QRDisplayDialog(UR ur, BBQR bbqr, boolean addLegacyEncodingOption, boolean addScanButton, QREncoding defaultEncoding) {
|
||||
this(ur, bbqr, null, addLegacyEncodingOption, addScanButton, defaultEncoding);
|
||||
}
|
||||
|
||||
public QRDisplayDialog(UR ur, BBQR bbqr, String raw, boolean addLegacyEncodingOption, boolean addScanButton, QREncoding defaultEncoding) {
|
||||
this.ur = ur;
|
||||
this.bbqr = bbqr;
|
||||
this.addLegacyEncodingOption = bbqr == null && addLegacyEncodingOption;
|
||||
@@ -88,11 +96,14 @@ public class QRDisplayDialog extends Dialog<ButtonType> {
|
||||
|
||||
if(bbqr != null) {
|
||||
this.bbqrEncoder = new BBQREncoder(bbqr.type(), DEFAULT_BBQR_ENCODING, bbqr.data(), Config.get().getQrDensity().getMaxBbqrFragmentLength(), 0);
|
||||
if(selectBbqrButton) {
|
||||
useBbqrEncoding = true;
|
||||
if(defaultEncoding == QREncoding.BBQR || Config.get().getQrEncoding() == QREncoding.BBQR) {
|
||||
encoding = QREncoding.BBQR;
|
||||
}
|
||||
}
|
||||
|
||||
this.raw = raw;
|
||||
this.rawEncodable = raw != null && getQrCode(raw, true) != null;
|
||||
|
||||
final DialogPane dialogPane = new QRDisplayDialogPane();
|
||||
setDialogPane(dialogPane);
|
||||
AppServices.setStageIcon(dialogPane.getScene().getWindow());
|
||||
@@ -141,6 +152,11 @@ public class QRDisplayDialog extends Dialog<ButtonType> {
|
||||
dialogPane.getButtonTypes().add(scanButtonType);
|
||||
}
|
||||
|
||||
ButtonBar buttonBar = (ButtonBar)dialogPane.lookup(".button-bar");
|
||||
if(buttonBar != null) {
|
||||
buttonBar.setButtonOrder("L+B+C+O");
|
||||
}
|
||||
|
||||
dialogPane.setPrefWidth(40 + qrSize + 40);
|
||||
dialogPane.setPrefHeight(40 + qrSize + 85);
|
||||
dialogPane.setMinHeight(dialogPane.getPrefHeight());
|
||||
@@ -156,8 +172,10 @@ public class QRDisplayDialog extends Dialog<ButtonType> {
|
||||
public QRDisplayDialog(String data, boolean addScanButton) {
|
||||
this.ur = null;
|
||||
this.bbqr = null;
|
||||
this.raw = data;
|
||||
this.urEncoder = null;
|
||||
this.bbqrEncoder = null;
|
||||
this.rawEncodable = true;
|
||||
|
||||
final DialogPane dialogPane = new QRDisplayDialogPane();
|
||||
setDialogPane(dialogPane);
|
||||
@@ -205,7 +223,9 @@ public class QRDisplayDialog extends Dialog<ButtonType> {
|
||||
}
|
||||
|
||||
private boolean isSinglePart() {
|
||||
if(useBbqrEncoding) {
|
||||
if(encoding == QREncoding.RAW) {
|
||||
return true;
|
||||
} else if(encoding == QREncoding.BBQR) {
|
||||
return bbqrEncoder.isSinglePart();
|
||||
} else if(!useLegacyEncoding) {
|
||||
return urEncoder.isSinglePart();
|
||||
@@ -215,7 +235,9 @@ public class QRDisplayDialog extends Dialog<ButtonType> {
|
||||
}
|
||||
|
||||
private void nextPart() {
|
||||
if(useBbqrEncoding) {
|
||||
if(encoding == QREncoding.RAW) {
|
||||
currentPart = raw;
|
||||
} else if(encoding == QREncoding.BBQR) {
|
||||
String fragment = bbqrEncoder.nextPart();
|
||||
currentPart = fragment.toUpperCase(Locale.ROOT);
|
||||
} else if(!useLegacyEncoding) {
|
||||
@@ -231,6 +253,10 @@ public class QRDisplayDialog extends Dialog<ButtonType> {
|
||||
}
|
||||
|
||||
protected Image getQrCode(String fragment) {
|
||||
return getQrCode(fragment, false);
|
||||
}
|
||||
|
||||
protected Image getQrCode(String fragment, boolean trial) {
|
||||
try {
|
||||
QRCodeWriter qrCodeWriter = new QRCodeWriter();
|
||||
BitMatrix qrMatrix = qrCodeWriter.encode(fragment, BarcodeFormat.QR_CODE, qrSize, qrSize, Map.of(EncodeHintType.MARGIN, "2"));
|
||||
@@ -241,7 +267,9 @@ public class QRDisplayDialog extends Dialog<ButtonType> {
|
||||
ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
|
||||
return new Image(bais);
|
||||
} catch(Exception e) {
|
||||
log.error("Error generating QR", e);
|
||||
if(!trial) {
|
||||
log.error("Error generating QR", e);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
@@ -265,18 +293,13 @@ public class QRDisplayDialog extends Dialog<ButtonType> {
|
||||
}
|
||||
}
|
||||
|
||||
public boolean isUseBbqrEncoding() {
|
||||
return useBbqrEncoding;
|
||||
public QREncoding getEncoding() {
|
||||
return encoding;
|
||||
}
|
||||
|
||||
private void setUseBbqrEncoding(boolean useBbqrEncoding) {
|
||||
if(useBbqrEncoding) {
|
||||
this.useBbqrEncoding = true;
|
||||
restartAnimation();
|
||||
} else {
|
||||
this.useBbqrEncoding = false;
|
||||
restartAnimation();
|
||||
}
|
||||
private void setEncoding(QREncoding encoding) {
|
||||
this.encoding = encoding;
|
||||
restartAnimation();
|
||||
}
|
||||
|
||||
private void changeQRDensity() {
|
||||
@@ -373,18 +396,29 @@ public class QRDisplayDialog extends Dialog<ButtonType> {
|
||||
|
||||
return scanButton;
|
||||
} else if(buttonType.getButtonData() == ButtonBar.ButtonData.BACK_PREVIOUS) {
|
||||
ToggleButton bbqr = new ToggleButton(buttonType.getText());
|
||||
bbqr.setGraphicTextGap(5);
|
||||
bbqr.setGraphic(getGlyph(FontAwesome5.Glyph.QRCODE));
|
||||
bbqr.setSelected(useBbqrEncoding);
|
||||
final ButtonBar.ButtonData buttonData = buttonType.getButtonData();
|
||||
ButtonBar.setButtonData(bbqr, buttonData);
|
||||
ComboBox<QREncoding> encodingComboBox = new ComboBox<>();
|
||||
if(ur != null) {
|
||||
encodingComboBox.getItems().add(QREncoding.UR);
|
||||
}
|
||||
if(bbqr != null) {
|
||||
encodingComboBox.getItems().add(QREncoding.BBQR);
|
||||
}
|
||||
if(raw != null) {
|
||||
encodingComboBox.getItems().add(QREncoding.RAW);
|
||||
}
|
||||
|
||||
bbqr.selectedProperty().addListener((observable, oldValue, newValue) -> {
|
||||
setUseBbqrEncoding(newValue);
|
||||
encodingComboBox.setCellFactory(_ -> new QREncodingListCell());
|
||||
encodingComboBox.setButtonCell(new QREncodingButtonCell());
|
||||
encodingComboBox.setValue(encoding);
|
||||
encodingComboBox.setOnAction(_ -> {
|
||||
setEncoding(encodingComboBox.getValue());
|
||||
Config.get().setQrEncoding(encodingComboBox.getValue());
|
||||
});
|
||||
|
||||
return bbqr;
|
||||
final ButtonBar.ButtonData buttonData = buttonType.getButtonData();
|
||||
ButtonBar.setButtonData(encodingComboBox, buttonData);
|
||||
|
||||
return encodingComboBox;
|
||||
}
|
||||
|
||||
return super.createButton(buttonType);
|
||||
@@ -399,7 +433,7 @@ public class QRDisplayDialog extends Dialog<ButtonType> {
|
||||
}
|
||||
|
||||
private void updateDensityButton(Button density) {
|
||||
density.setText(Config.get().getQrDensity() == QRDensity.NORMAL ? "Decrease Density" : "Increase Density");
|
||||
density.setText(Config.get().getQrDensity() == QRDensity.NORMAL ? "Less Dense" : "More Dense");
|
||||
if(Config.get().getQrDensity() == QRDensity.NORMAL) {
|
||||
density.setGraphic(getGlyph(FontAwesome5.Glyph.MAGNIFYING_GLASS_PLUS));
|
||||
} else {
|
||||
@@ -413,4 +447,38 @@ public class QRDisplayDialog extends Dialog<ButtonType> {
|
||||
glyph.setFontSize(11);
|
||||
return glyph;
|
||||
}
|
||||
|
||||
private class QREncodingListCell extends ListCell<QREncoding> {
|
||||
@Override
|
||||
protected void updateItem(QREncoding item, boolean empty) {
|
||||
super.updateItem(item, empty);
|
||||
if(empty || item == null) {
|
||||
setText(null);
|
||||
setGraphic(null);
|
||||
setDisable(false);
|
||||
setOpacity(1.0);
|
||||
} else {
|
||||
setText(item.getName() + " Encoding");
|
||||
setGraphic(item.getSVGImage());
|
||||
setGraphicTextGap(8.0d);
|
||||
setDisable(item == QREncoding.RAW && !rawEncodable);
|
||||
setOpacity(isDisabled() ? 0.5 : 1.0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static class QREncodingButtonCell extends ListCell<QREncoding> {
|
||||
@Override
|
||||
protected void updateItem(QREncoding item, boolean empty) {
|
||||
super.updateItem(item, empty);
|
||||
if(item == null || empty) {
|
||||
setText("");
|
||||
setGraphic(null);
|
||||
} else {
|
||||
setText(item.getName());
|
||||
setGraphic(item.getSVGImage());
|
||||
setGraphicTextGap(8.0d);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
package com.sparrowwallet.sparrow.control;
|
||||
|
||||
import com.sparrowwallet.sparrow.AppServices;
|
||||
import com.sparrowwallet.sparrow.Theme;
|
||||
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
|
||||
import com.sparrowwallet.sparrow.io.Config;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.scene.Node;
|
||||
import org.controlsfx.glyphfont.Glyph;
|
||||
import org.girod.javafx.svgimage.SVGLoader;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.net.URL;
|
||||
import java.util.Locale;
|
||||
|
||||
public enum QREncoding {
|
||||
UR("UR"), BBQR("BBQr"), RAW("Raw");
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(QREncoding.class);
|
||||
|
||||
QREncoding(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
private final String name;
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public Node getSVGImage() {
|
||||
try {
|
||||
URL url = AppServices.class.getResource("/image/qrencoding/" + getName().toLowerCase(Locale.ROOT) + "-icon" + (Config.get().getTheme() == Theme.DARK ? "-invert" : "") + ".svg");
|
||||
if(url != null) {
|
||||
return SVGLoader.load(url);
|
||||
} else {
|
||||
Glyph glyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.QRCODE);
|
||||
glyph.setFontSize(12);
|
||||
glyph.setPadding(new Insets(0, 2, 0, 0));
|
||||
return glyph;
|
||||
}
|
||||
} catch(Exception e) {
|
||||
log.error("Could not load QR encoding source image for " + name);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
@@ -189,6 +190,7 @@ public class QRScanDialog extends Dialog<QRScanDialog.Result> {
|
||||
});
|
||||
webcamDeviceProperty.addListener((_, _, newValue) -> {
|
||||
Config.get().setWebcamDevice(newValue.getName());
|
||||
Config.get().setWebcamDeviceId(newValue.getUniqueId());
|
||||
if(!Objects.equals(webcamService.getDevice(), newValue)) {
|
||||
webcamService.cancel();
|
||||
}
|
||||
@@ -581,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());
|
||||
|
||||
@@ -655,7 +665,7 @@ public class QRScanDialog extends Dialog<QRScanDialog.Result> {
|
||||
|
||||
List<ChildNumber> path = cryptoKeypath.getComponents().stream().map(comp -> (IndexPathComponent)comp)
|
||||
.map(comp -> new ChildNumber(comp.getIndex(), comp.isHardened())).collect(Collectors.toList());
|
||||
String fingerprint = cryptoKeypath.getSourceFingerprint() == null ? KeystoreController.DEFAULT_WATCH_ONLY_FINGERPRINT : Utils.bytesToHex(cryptoKeypath.getSourceFingerprint());
|
||||
String fingerprint = cryptoKeypath.getSourceFingerprint() == null ? KeyDerivation.DEFAULT_WATCH_ONLY_FINGERPRINT : Utils.bytesToHex(cryptoKeypath.getSourceFingerprint());
|
||||
return new KeyDerivation(fingerprint, KeyDerivation.writePath(path));
|
||||
}
|
||||
|
||||
@@ -678,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");
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
package com.sparrowwallet.sparrow.control;
|
||||
|
||||
import com.sparrowwallet.sparrow.AppServices;
|
||||
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
|
||||
import javafx.application.Platform;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.scene.control.*;
|
||||
import javafx.scene.layout.GridPane;
|
||||
import org.controlsfx.glyphfont.Glyph;
|
||||
|
||||
public class RangeInputDialog extends Dialog<Integer> {
|
||||
private final Spinner<Integer> spinner;
|
||||
|
||||
public RangeInputDialog(int min, int max, int initialValue) {
|
||||
final DialogPane dialogPane = getDialogPane();
|
||||
|
||||
setTitle("Select a Value");
|
||||
setHeaderText("Choose a value between " + min + " and " + max);
|
||||
|
||||
Glyph key = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.SORT_NUMERIC_DOWN);
|
||||
key.setFontSize(50);
|
||||
key.setPadding(new Insets(0, 0, 0, 10));
|
||||
dialogPane.setGraphic(key);
|
||||
dialogPane.getButtonTypes().addAll(ButtonType.OK, ButtonType.CANCEL);
|
||||
|
||||
spinner = new Spinner<>();
|
||||
spinner.setValueFactory(new SpinnerValueFactory.IntegerSpinnerValueFactory(min, max, initialValue));
|
||||
spinner.setPrefWidth(80);
|
||||
|
||||
GridPane grid = new GridPane();
|
||||
grid.setHgap(10);
|
||||
grid.setVgap(10);
|
||||
grid.setPadding(new Insets(20, 20, 10, 20));
|
||||
|
||||
grid.add(new Label("Enter value between " + min + " and " + max + ":"), 0, 0);
|
||||
grid.add(spinner, 1, 0);
|
||||
|
||||
dialogPane.setContent(grid);
|
||||
dialogPane.getStylesheets().add(AppServices.class.getResource("general.css").toExternalForm());
|
||||
AppServices.setStageIcon(dialogPane.getScene().getWindow());
|
||||
|
||||
Platform.runLater(spinner::requestFocus);
|
||||
|
||||
setResultConverter((dialogButton) -> {
|
||||
ButtonBar.ButtonData data = dialogButton == null ? null : dialogButton.getButtonData();
|
||||
return data == ButtonBar.ButtonData.OK_DONE ? spinner.getValue() : null;
|
||||
});
|
||||
|
||||
dialogPane.setPrefWidth(500);
|
||||
dialogPane.setPrefHeight(230);
|
||||
AppServices.moveToActiveWindowScreen(this);
|
||||
}
|
||||
|
||||
public void setValue(int value) {
|
||||
spinner.getValueFactory().setValue(value);
|
||||
}
|
||||
|
||||
public int getValue() {
|
||||
return spinner.getValue();
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@ package com.sparrowwallet.sparrow.control;
|
||||
|
||||
import com.sparrowwallet.drongo.protocol.Script;
|
||||
import com.sparrowwallet.drongo.protocol.ScriptChunk;
|
||||
import com.sparrowwallet.drongo.protocol.ScriptOpCodes;
|
||||
import javafx.geometry.Pos;
|
||||
import org.controlsfx.control.decoration.Decorator;
|
||||
import org.controlsfx.control.decoration.GraphicDecoration;
|
||||
@@ -53,7 +54,11 @@ public class ScriptArea extends CodeArea {
|
||||
for (int i = 0; i < script.getChunks().size(); i++) {
|
||||
ScriptChunk chunk = script.getChunks().get(i);
|
||||
if(chunk.isOpCode()) {
|
||||
append(chunk.toString(), "script-opcode");
|
||||
if(chunk.getOpcode() == ScriptOpCodes.OP_0 && witnessScript != null) {
|
||||
append("<empty>", "script-other");
|
||||
} else {
|
||||
append(chunk.toString(), "script-opcode");
|
||||
}
|
||||
} else if(chunk.isPubKey()) {
|
||||
append("<pubkey" + pubKeyCount++ + ">", "script-pubkey");
|
||||
} else if(chunk.isSignature()) {
|
||||
|
||||
@@ -74,6 +74,7 @@ public class SearchWalletDialog extends Dialog<Entry> {
|
||||
searchField.setText("Search:");
|
||||
search = TextFields.createClearableTextField();
|
||||
search.setPromptText("Label, address, value or transaction ID");
|
||||
search.setSkin(new AddressTextFieldSkin(search));
|
||||
searchField.getInputs().add(search);
|
||||
|
||||
fieldset.getChildren().addAll(searchField);
|
||||
@@ -113,7 +114,11 @@ public class SearchWalletDialog extends Dialog<Entry> {
|
||||
entryCol.setCellValueFactory((TreeTableColumn.CellDataFeatures<Entry, Entry> param) -> {
|
||||
return new ReadOnlyObjectWrapper<>(param.getValue().getValue());
|
||||
});
|
||||
entryCol.setCellFactory(p -> new SearchEntryCell());
|
||||
entryCol.setCellFactory(p -> {
|
||||
SearchEntryCell searchEntryCell = new SearchEntryCell();
|
||||
searchEntryCell.setSkin(new AddressTreeTableCellSkin<>(searchEntryCell));
|
||||
return searchEntryCell;
|
||||
});
|
||||
String address = walletForms.iterator().next().getNodeEntry(KeyPurpose.RECEIVE).getAddress().toString();
|
||||
entryCol.setMinWidth(TextUtils.computeTextWidth(AppServices.getMonospaceFont(), address, 0.0));
|
||||
results.getColumns().add(entryCol);
|
||||
|
||||
@@ -5,35 +5,58 @@ import com.sparrowwallet.drongo.BitcoinUnit;
|
||||
import com.sparrowwallet.drongo.OsType;
|
||||
import com.sparrowwallet.drongo.address.Address;
|
||||
import com.sparrowwallet.drongo.address.InvalidAddressException;
|
||||
import com.sparrowwallet.drongo.protocol.Transaction;
|
||||
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.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.glyphfont.FontAwesome5;
|
||||
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;
|
||||
import javafx.application.Platform;
|
||||
import javafx.collections.FXCollections;
|
||||
import javafx.collections.ObservableList;
|
||||
import javafx.concurrent.Service;
|
||||
import javafx.concurrent.Task;
|
||||
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;
|
||||
import org.controlsfx.control.spreadsheet.*;
|
||||
import org.controlsfx.glyphfont.Glyph;
|
||||
|
||||
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 AddressCellType ADDRESS = new AddressCellType();
|
||||
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);
|
||||
@@ -68,14 +91,16 @@ public class SendToManyDialog extends Dialog<List<Payment>> {
|
||||
dialogPane.setContent(stackPane);
|
||||
|
||||
dialogPane.getButtonTypes().addAll(ButtonType.OK, ButtonType.CANCEL);
|
||||
Button okButton = (Button) dialogPane.lookupButton(ButtonType.OK);
|
||||
okButton.addEventFilter(ActionEvent.ACTION, event -> {
|
||||
getPayments();
|
||||
event.consume();
|
||||
});
|
||||
|
||||
final ButtonType loadCsvButtonType = new javafx.scene.control.ButtonType("Load CSV", ButtonBar.ButtonData.LEFT);
|
||||
dialogPane.getButtonTypes().add(loadCsvButtonType);
|
||||
|
||||
setResultConverter((dialogButton) -> {
|
||||
ButtonBar.ButtonData data = dialogButton == null ? null : dialogButton.getButtonData();
|
||||
return data == ButtonBar.ButtonData.OK_DONE ? getPayments() : null;
|
||||
});
|
||||
setResultConverter((_) -> null);
|
||||
|
||||
dialogPane.setPrefWidth(850);
|
||||
dialogPane.setPrefHeight(500);
|
||||
@@ -85,22 +110,26 @@ public class SendToManyDialog extends Dialog<List<Payment>> {
|
||||
}
|
||||
|
||||
private Grid getGrid(List<Payment> payments) {
|
||||
int rowCount = payments.size();
|
||||
return createGrid(payments.stream().map(payment -> new SendToPayment(payment, SendToAddress.fromPayment(payment))).collect(Collectors.toList()));
|
||||
}
|
||||
|
||||
private Grid createGrid(List<SendToPayment> sendToPayments) {
|
||||
int rowCount = sendToPayments.size();
|
||||
int columnCount = 3;
|
||||
GridBase grid = new GridBase(rowCount, columnCount);
|
||||
ObservableList<ObservableList<SpreadsheetCell>> rows = FXCollections.observableArrayList();
|
||||
for(int row = 0; row < grid.getRowCount(); ++row) {
|
||||
SendToPayment sendToPayment = sendToPayments.get(row);
|
||||
final ObservableList<SpreadsheetCell> list = FXCollections.observableArrayList();
|
||||
|
||||
SpreadsheetCell addressCell = ADDRESS.createCell(row, 0, 1, 1, payments.get(row).getAddress());
|
||||
SendToAddress sendToAddress = sendToPayment.sendToAddress();
|
||||
SpreadsheetCell addressCell = SEND_TO_ADDRESS.createCell(row, 0, 1, 1, sendToAddress);
|
||||
addressCell.getStyleClass().add("fixed-width");
|
||||
list.add(addressCell);
|
||||
|
||||
double amount = (double)payments.get(row).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) {
|
||||
@@ -108,7 +137,7 @@ public class SendToManyDialog extends Dialog<List<Payment>> {
|
||||
}
|
||||
list.add(amountCell);
|
||||
|
||||
list.add(SpreadsheetCellType.STRING.createCell(row, 2, 1, 1, payments.get(row).getLabel()));
|
||||
list.add(SpreadsheetCellType.STRING.createCell(row, 2, 1, 1, sendToPayment.payment().getLabel()));
|
||||
rows.add(list);
|
||||
}
|
||||
grid.setRows(rows);
|
||||
@@ -117,32 +146,49 @@ public class SendToManyDialog extends Dialog<List<Payment>> {
|
||||
return grid;
|
||||
}
|
||||
|
||||
private List<Payment> getPayments() {
|
||||
List<Payment> payments = new ArrayList<>();
|
||||
Grid grid = spreadsheetView.getGrid();
|
||||
String firstLabel = null;
|
||||
for(int row = 0; row < grid.getRowCount(); row++) {
|
||||
private void getPayments() {
|
||||
if(needsResolution() && Config.get().hasServer() && !AppServices.isConnected() && !AppServices.isConnecting()) {
|
||||
if(Config.get().getConnectToResolve() == null || Config.get().getConnectToResolve() == Boolean.FALSE) {
|
||||
Platform.runLater(() -> {
|
||||
ConfirmationAlert confirmationAlert = new ConfirmationAlert("Connect to resolve?", "You are currently offline. Connect to resolve the addresses?", ButtonType.NO, ButtonType.YES);
|
||||
Optional<ButtonType> optType = confirmationAlert.showAndWait();
|
||||
if(confirmationAlert.isDontAskAgain() && optType.isPresent()) {
|
||||
Config.get().setConnectToResolve(optType.get() == ButtonType.YES);
|
||||
}
|
||||
if(optType.isPresent() && optType.get() == ButtonType.YES) {
|
||||
EventManager.get().post(new RequestConnectEvent());
|
||||
}
|
||||
});
|
||||
} else {
|
||||
Platform.runLater(() -> EventManager.get().post(new RequestConnectEvent()));
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
CreatePaymentsService createPaymentsService = new CreatePaymentsService();
|
||||
createPaymentsService.setOnSucceeded(_ -> {
|
||||
List<Payment> payments = createPaymentsService.getValue();
|
||||
if(payments != null) {
|
||||
setResult(payments);
|
||||
}
|
||||
});
|
||||
createPaymentsService.setOnFailed(event -> {
|
||||
Throwable ex = event.getSource().getException();
|
||||
AppServices.showErrorDialog("Error creating payments", ex.getMessage());
|
||||
});
|
||||
createPaymentsService.start();
|
||||
}
|
||||
|
||||
private boolean needsResolution() {
|
||||
for(int row = 0; row < spreadsheetView.getGrid().getRowCount(); row++) {
|
||||
ObservableList<SpreadsheetCell> rowCells = spreadsheetView.getItems().get(row);
|
||||
Address address = (Address)rowCells.get(0).getItem();
|
||||
Double value = (Double)rowCells.get(1).getItem();
|
||||
String label = (String)rowCells.get(2).getItem();
|
||||
if(firstLabel == null) {
|
||||
firstLabel = label;
|
||||
}
|
||||
if(label == null || label.isEmpty()) {
|
||||
label = firstLabel;
|
||||
}
|
||||
|
||||
if(address != null && value != null) {
|
||||
if(bitcoinUnit == BitcoinUnit.BTC) {
|
||||
value = value * Transaction.SATOSHIS_PER_BITCOIN;
|
||||
}
|
||||
|
||||
payments.add(new Payment(address, label, value.longValue(), false));
|
||||
SendToAddress sendToAddress = (SendToAddress)rowCells.getFirst().getItem();
|
||||
if(sendToAddress != null && sendToAddress.hrn != null && DnsPaymentCache.getDnsPayment(sendToAddress.hrn) == null) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return payments;
|
||||
return false;
|
||||
}
|
||||
|
||||
private class SendToManyDialogPane extends DialogPane {
|
||||
@@ -152,7 +198,7 @@ public class SendToManyDialog extends Dialog<List<Payment>> {
|
||||
if(buttonType.getButtonData() == ButtonBar.ButtonData.LEFT) {
|
||||
Button loadButton = new Button(buttonType.getText());
|
||||
loadButton.setGraphicTextGap(5);
|
||||
loadButton.setGraphic(getGlyph(FontAwesome5.Glyph.ARROW_UP));
|
||||
loadButton.setGraphic(GlyphUtils.getUpArrowGlyph());
|
||||
final ButtonBar.ButtonData buttonData = buttonType.getButtonData();
|
||||
ButtonBar.setButtonData(loadButton, buttonData);
|
||||
loadButton.setOnAction(event -> {
|
||||
@@ -167,7 +213,7 @@ public class SendToManyDialog extends Dialog<List<Payment>> {
|
||||
File file = fileChooser.showOpenDialog(this.getScene().getWindow());
|
||||
if(file != null) {
|
||||
try {
|
||||
List<Payment> csvPayments = new ArrayList<>();
|
||||
List<SendToPayment> csvPayments = new ArrayList<>();
|
||||
try(Reader reader = new FileReader(file, StandardCharsets.UTF_8)) {
|
||||
CsvReader csvReader = new CsvReader(reader);
|
||||
while(csvReader.readRecord()) {
|
||||
@@ -176,16 +222,32 @@ 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);
|
||||
}
|
||||
Address address = Address.fromString(csvReader.get(0));
|
||||
String label = csvReader.get(2);
|
||||
csvPayments.add(new Payment(address, label, amount, false));
|
||||
Optional<String> optDnsPaymentHrn = DnsPayment.getHrn(csvReader.get(0));
|
||||
if(optDnsPaymentHrn.isPresent()) {
|
||||
Payment payment = new Payment(null, label, amount, false);
|
||||
csvPayments.add(new SendToPayment(payment, new SendToAddress(optDnsPaymentHrn.get())));
|
||||
} else {
|
||||
try {
|
||||
SilentPaymentAddress silentPaymentAddress = SilentPaymentAddress.from(csvReader.get(0));
|
||||
Payment payment = new SilentPayment(silentPaymentAddress, label, amount, false);
|
||||
csvPayments.add(new SendToPayment(payment, SendToAddress.fromPayment(payment)));
|
||||
} catch(Exception e) {
|
||||
Address address = Address.fromString(csvReader.get(0));
|
||||
Payment payment = new Payment(address, label, amount, false);
|
||||
csvPayments.add(new SendToPayment(payment, SendToAddress.fromPayment(payment)));
|
||||
}
|
||||
}
|
||||
} catch(NumberFormatException e) {
|
||||
//ignore and continue - probably a header line
|
||||
} catch(InvalidAddressException e) {
|
||||
@@ -198,7 +260,7 @@ public class SendToManyDialog extends Dialog<List<Payment>> {
|
||||
return;
|
||||
}
|
||||
|
||||
spreadsheetView.setGrid(getGrid(csvPayments));
|
||||
spreadsheetView.setGrid(createGrid(csvPayments));
|
||||
}
|
||||
} catch(IOException e) {
|
||||
AppServices.showErrorDialog("Cannot load CSV", e.getMessage());
|
||||
@@ -213,24 +275,18 @@ public class SendToManyDialog extends Dialog<List<Payment>> {
|
||||
|
||||
return button;
|
||||
}
|
||||
|
||||
private Glyph getGlyph(FontAwesome5.Glyph glyphName) {
|
||||
Glyph glyph = new Glyph(FontAwesome5.FONT_NAME, glyphName);
|
||||
glyph.setFontSize(11);
|
||||
return glyph;
|
||||
}
|
||||
}
|
||||
|
||||
public static class AddressCellType extends SpreadsheetCellType<Address> {
|
||||
public AddressCellType() {
|
||||
this(new StringConverterWithFormat<>(new AddressStringConverter()) {
|
||||
public static class SendToAddressCellType extends SpreadsheetCellType<SendToAddress> {
|
||||
public SendToAddressCellType() {
|
||||
this(new StringConverterWithFormat<>(new SendToAddressStringConverter()) {
|
||||
@Override
|
||||
public String toString(Address item) {
|
||||
public String toString(SendToAddress item) {
|
||||
return toStringFormat(item, ""); //$NON-NLS-1$
|
||||
}
|
||||
|
||||
@Override
|
||||
public Address fromString(String str) {
|
||||
public SendToAddress fromString(String str) {
|
||||
if(str == null || str.isEmpty()) { //$NON-NLS-1$
|
||||
return null;
|
||||
} else {
|
||||
@@ -239,7 +295,7 @@ public class SendToManyDialog extends Dialog<List<Payment>> {
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toStringFormat(Address item, String format) {
|
||||
public String toStringFormat(SendToAddress item, String format) {
|
||||
try {
|
||||
if(item == null) {
|
||||
return ""; //$NON-NLS-1$
|
||||
@@ -253,7 +309,7 @@ public class SendToManyDialog extends Dialog<List<Payment>> {
|
||||
});
|
||||
}
|
||||
|
||||
public AddressCellType(StringConverter<Address> converter) {
|
||||
public SendToAddressCellType(StringConverter<SendToAddress> converter) {
|
||||
super(converter);
|
||||
}
|
||||
|
||||
@@ -263,7 +319,7 @@ public class SendToManyDialog extends Dialog<List<Payment>> {
|
||||
}
|
||||
|
||||
public SpreadsheetCell createCell(final int row, final int column, final int rowSpan, final int columnSpan,
|
||||
final Address value) {
|
||||
final SendToAddress value) {
|
||||
SpreadsheetCell cell = new SpreadsheetCellBase(row, column, rowSpan, columnSpan, this);
|
||||
cell.setItem(value);
|
||||
return cell;
|
||||
@@ -276,7 +332,7 @@ public class SendToManyDialog extends Dialog<List<Payment>> {
|
||||
|
||||
@Override
|
||||
public boolean match(Object value, Object... options) {
|
||||
if(value instanceof Address)
|
||||
if(value instanceof SendToAddress)
|
||||
return true;
|
||||
else {
|
||||
try {
|
||||
@@ -289,9 +345,9 @@ public class SendToManyDialog extends Dialog<List<Payment>> {
|
||||
}
|
||||
|
||||
@Override
|
||||
public Address convertValue(Object value) {
|
||||
if(value instanceof Address)
|
||||
return (Address)value;
|
||||
public SendToAddress convertValue(Object value) {
|
||||
if(value instanceof SendToAddress)
|
||||
return (SendToAddress)value;
|
||||
else {
|
||||
try {
|
||||
return converter.fromString(value == null ? null : value.toString());
|
||||
@@ -302,13 +358,305 @@ public class SendToManyDialog extends Dialog<List<Payment>> {
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString(Address item) {
|
||||
public String toString(SendToAddress item) {
|
||||
return converter.toString(item);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString(Address item, String format) {
|
||||
return ((StringConverterWithFormat<Address>)converter).toStringFormat(item, format);
|
||||
public String toString(SendToAddress item, String format) {
|
||||
return ((StringConverterWithFormat<SendToAddress>)converter).toStringFormat(item, format);
|
||||
}
|
||||
};
|
||||
|
||||
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;
|
||||
private final SilentPaymentAddress silentPaymentAddress;
|
||||
|
||||
public SendToAddress(String hrn) {
|
||||
this.hrn = hrn;
|
||||
this.address = null;
|
||||
this.silentPaymentAddress = null;
|
||||
}
|
||||
|
||||
public SendToAddress(Address address) {
|
||||
this.hrn = null;
|
||||
this.address = address;
|
||||
this.silentPaymentAddress = null;
|
||||
}
|
||||
|
||||
public SendToAddress(SilentPaymentAddress silentPaymentAddress) {
|
||||
this.hrn = null;
|
||||
this.address = null;
|
||||
this.silentPaymentAddress = silentPaymentAddress;
|
||||
}
|
||||
|
||||
public String toString() {
|
||||
return hrn == null ? silentPaymentAddress == null ? (address == null ? null : address.toString()) : silentPaymentAddress.toString() : hrn;
|
||||
}
|
||||
|
||||
public static SendToAddress fromPayment(Payment payment) {
|
||||
DnsPayment dnsPayment = DnsPaymentCache.getDnsPayment(payment);
|
||||
if(dnsPayment != null) {
|
||||
return new SendToAddress(dnsPayment.hrn());
|
||||
}
|
||||
return payment instanceof SilentPayment ? new SendToAddress(((SilentPayment)payment).getSilentPaymentAddress()) : new SendToAddress(payment.getAddress());
|
||||
}
|
||||
|
||||
public Payment toPayment(String label, long value, boolean sendMax) throws DnsPaymentValidationException, IOException, ExecutionException, InterruptedException, BitcoinURIParseException {
|
||||
if(hrn != null) {
|
||||
DnsPayment dnsPayment = DnsPaymentCache.getDnsPayment(hrn);
|
||||
if(dnsPayment == null) {
|
||||
DnsPaymentResolver resolver = new DnsPaymentResolver(hrn);
|
||||
Optional<DnsPayment> optDnsPayment = resolver.resolve();
|
||||
if(optDnsPayment.isPresent()) {
|
||||
dnsPayment = optDnsPayment.get();
|
||||
if(dnsPayment.hasAddress()) {
|
||||
DnsPaymentCache.putDnsPayment(dnsPayment.bitcoinURI().getAddress(), dnsPayment);
|
||||
} else if(dnsPayment.hasSilentPaymentAddress()) {
|
||||
DnsPaymentCache.putDnsPayment(dnsPayment.bitcoinURI().getSilentPaymentAddress(), dnsPayment);
|
||||
}
|
||||
return getPayment(optDnsPayment.get(), label, value, sendMax);
|
||||
} else {
|
||||
throw new IllegalArgumentException("Payment to " + hrn + " could not be resolved.");
|
||||
}
|
||||
} else {
|
||||
return getPayment(dnsPayment, label, value, sendMax);
|
||||
}
|
||||
}
|
||||
|
||||
if(silentPaymentAddress != null) {
|
||||
return new SilentPayment(silentPaymentAddress, label, value, sendMax);
|
||||
} else {
|
||||
return new Payment(address, label, value, sendMax);
|
||||
}
|
||||
}
|
||||
|
||||
private static Payment getPayment(DnsPayment dnsPayment, String label, long value, boolean sendMax) {
|
||||
if(dnsPayment.hasAddress()) {
|
||||
return new Payment(dnsPayment.bitcoinURI().getAddress(), label, value, sendMax);
|
||||
} else if(dnsPayment.hasSilentPaymentAddress()) {
|
||||
return new SilentPayment(dnsPayment.bitcoinURI().getSilentPaymentAddress(), label, value, sendMax);
|
||||
} else {
|
||||
throw new IllegalArgumentException("Payment to " + dnsPayment + " has no associated address.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static class SendToAddressStringConverter extends StringConverter<SendToAddress> {
|
||||
private final AddressStringConverter addressStringConverter = new AddressStringConverter();
|
||||
|
||||
@Override
|
||||
public SendToAddress fromString(String value) {
|
||||
Optional<String> optDnsPaymentHrn = DnsPayment.getHrn(value);
|
||||
if(optDnsPaymentHrn.isPresent()) {
|
||||
return new SendToAddress(optDnsPaymentHrn.get());
|
||||
}
|
||||
|
||||
try {
|
||||
SilentPaymentAddress silentPaymentAddress = SilentPaymentAddress.from(value);
|
||||
return new SendToAddress(silentPaymentAddress);
|
||||
} catch(Exception e) {
|
||||
Address address = addressStringConverter.fromString(value);
|
||||
return address == null ? null : new SendToAddress(address);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString(SendToAddress value) {
|
||||
return value.toString();
|
||||
}
|
||||
}
|
||||
|
||||
private class CreatePaymentsService extends Service<List<Payment>> {
|
||||
@Override
|
||||
protected Task<List<Payment>> createTask() {
|
||||
return new Task<>() {
|
||||
@Override
|
||||
protected List<Payment> call() throws Exception {
|
||||
return getPayments();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private List<Payment> getPayments() throws DnsPaymentValidationException, IOException, ExecutionException, InterruptedException, BitcoinURIParseException {
|
||||
List<Payment> payments = new ArrayList<>();
|
||||
Grid grid = spreadsheetView.getGrid();
|
||||
String firstLabel = null;
|
||||
for(int row = 0; row < grid.getRowCount(); row++) {
|
||||
ObservableList<SpreadsheetCell> rowCells = spreadsheetView.getItems().get(row);
|
||||
SendToAddress sendToAddress = (SendToAddress)rowCells.get(0).getItem();
|
||||
Double value = (Double)rowCells.get(1).getItem();
|
||||
String label = (String)rowCells.get(2).getItem();
|
||||
if(firstLabel == null) {
|
||||
firstLabel = label;
|
||||
}
|
||||
if(label == null || label.isEmpty()) {
|
||||
label = firstLabel;
|
||||
}
|
||||
|
||||
if(sendToAddress != null && value != null) {
|
||||
payments.add(sendToAddress.toPayment(label, bitcoinUnit.getSatsValue(value), false));
|
||||
}
|
||||
}
|
||||
|
||||
return payments;
|
||||
}
|
||||
}
|
||||
|
||||
private record SendToPayment(Payment payment, SendToAddress sendToAddress) {}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import com.sparrowwallet.drongo.wallet.Wallet;
|
||||
import com.sparrowwallet.drongo.wallet.WalletModel;
|
||||
import com.sparrowwallet.sparrow.AppServices;
|
||||
import javafx.application.Platform;
|
||||
import javafx.collections.ListChangeListener;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.Node;
|
||||
@@ -182,6 +183,22 @@ public class TitledDescriptionPane extends TitledPane {
|
||||
});
|
||||
}
|
||||
|
||||
protected static void setDefaultButton(ButtonBase button) {
|
||||
button.getStyleClass().add("default-button");
|
||||
if(button instanceof SplitMenuButton splitMenuButton) {
|
||||
for(MenuItem item : splitMenuButton.getItems()) {
|
||||
item.getStyleClass().add("default-button");
|
||||
}
|
||||
splitMenuButton.getItems().addListener((ListChangeListener<MenuItem>) c -> {
|
||||
while(c.next()) {
|
||||
for(MenuItem item : c.getAddedSubList()) {
|
||||
item.getStyleClass().add("default-button");
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
protected static int getAccount(Wallet wallet, KeyDerivation requiredDerivation) {
|
||||
if(wallet == null || requiredDerivation == null) {
|
||||
return 0;
|
||||
|
||||
@@ -1,11 +1,17 @@
|
||||
package com.sparrowwallet.sparrow.control;
|
||||
|
||||
import com.sparrowwallet.drongo.BitcoinUnit;
|
||||
import com.sparrowwallet.drongo.KeyPurpose;
|
||||
import com.sparrowwallet.drongo.OsType;
|
||||
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;
|
||||
import com.sparrowwallet.drongo.silentpayments.SilentPaymentAddress;
|
||||
import com.sparrowwallet.drongo.uri.BitcoinURI;
|
||||
import com.sparrowwallet.drongo.wallet.*;
|
||||
import com.sparrowwallet.sparrow.*;
|
||||
@@ -22,6 +28,7 @@ import javafx.beans.property.SimpleBooleanProperty;
|
||||
import javafx.beans.property.SimpleObjectProperty;
|
||||
import javafx.embed.swing.SwingFXUtils;
|
||||
import javafx.event.EventHandler;
|
||||
import javafx.geometry.HPos;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.Group;
|
||||
@@ -103,6 +110,7 @@ public class TransactionDiagram extends GridPane {
|
||||
expandedDiagram.setId("transactionDiagram");
|
||||
expandedDiagram.setExpanded(true);
|
||||
expandedDiagram.setFinal(isFinal());
|
||||
expandedDiagram.setMaxWidth(AppServices.getActiveWindow().getWidth() - 200);
|
||||
updateDerivedDiagram(expandedDiagram);
|
||||
|
||||
HBox buttonBox = new HBox();
|
||||
@@ -120,7 +128,7 @@ public class TransactionDiagram extends GridPane {
|
||||
AppServices.setStageIcon(stage);
|
||||
stage.setScene(scene);
|
||||
stage.setOnShowing(e -> {
|
||||
AppServices.moveToActiveWindowScreen(stage, 600, 460);
|
||||
AppServices.moveToActiveWindowScreen(stage, expandedDiagram.getMaxWidth(), 460);
|
||||
});
|
||||
stage.setOnHidden(e -> {
|
||||
expandedDiagram = null;
|
||||
@@ -137,6 +145,39 @@ public class TransactionDiagram extends GridPane {
|
||||
}
|
||||
};
|
||||
|
||||
public TransactionDiagram() {
|
||||
ColumnConstraints col1 = new ColumnConstraints();
|
||||
col1.setPrefWidth(22);
|
||||
col1.setHgrow(Priority.NEVER);
|
||||
|
||||
ColumnConstraints col2 = new ColumnConstraints();
|
||||
col2.setHgrow(Priority.ALWAYS);
|
||||
col2.setPercentWidth(25);
|
||||
col2.setFillWidth(true);
|
||||
|
||||
ColumnConstraints col3 = new ColumnConstraints();
|
||||
col3.setPrefWidth(140);
|
||||
col3.setHgrow(Priority.NEVER);
|
||||
|
||||
ColumnConstraints col4 = new ColumnConstraints();
|
||||
Label label = new Label();
|
||||
col4.setMinWidth(TextUtils.computeTextWidth(label.getFont(), "Transaction", 0) + 20);
|
||||
col4.setHgrow(Priority.NEVER);
|
||||
col4.setHalignment(HPos.CENTER);
|
||||
|
||||
ColumnConstraints col5 = new ColumnConstraints();
|
||||
col5.setPrefWidth(140);
|
||||
col5.setHgrow(Priority.NEVER);
|
||||
|
||||
ColumnConstraints col6 = new ColumnConstraints();
|
||||
col6.setHgrow(Priority.ALWAYS);
|
||||
col6.setPercentWidth(25);
|
||||
col6.setFillWidth(true);
|
||||
|
||||
getColumnConstraints().addAll(col1, col2, col3, col4, col5, col6);
|
||||
setPadding(new Insets(0, 0, 0, 40));
|
||||
}
|
||||
|
||||
public void update(WalletTransaction walletTx) {
|
||||
setMinHeight(getDiagramHeight());
|
||||
setMaxHeight(getDiagramHeight());
|
||||
@@ -165,7 +206,7 @@ public class TransactionDiagram extends GridPane {
|
||||
|
||||
VBox messagePane = new VBox();
|
||||
messagePane.setPrefHeight(getDiagramHeight());
|
||||
messagePane.setPadding(new Insets(0, 10, 0, 280));
|
||||
messagePane.setPadding(new Insets(0, 10, 0, 10));
|
||||
messagePane.setAlignment(Pos.CENTER);
|
||||
messagePane.getChildren().add(createSpacer());
|
||||
|
||||
@@ -188,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);
|
||||
@@ -216,16 +257,15 @@ 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();
|
||||
getChildren().addAll(inputsTypePane, inputsPane, inputsLinesPane, txPane, outputsLinesPane, outputsPane);
|
||||
|
||||
List<Payment> userPayments = getUserPayments();
|
||||
if(!isFinal() && userPayments.size() > 1) {
|
||||
@@ -234,6 +274,8 @@ public class TransactionDiagram extends GridPane {
|
||||
getChildren().add(totalsPane);
|
||||
}
|
||||
|
||||
getChildren().addAll(inputsTypePane, inputsPane, inputsLinesPane, txPane, outputsLinesPane, outputsPane);
|
||||
|
||||
if(contextMenu == null) {
|
||||
contextMenu = new ContextMenu();
|
||||
MenuItem menuItem = new MenuItem("Save as Image...");
|
||||
@@ -407,8 +449,6 @@ public class TransactionDiagram extends GridPane {
|
||||
|
||||
private Pane getInputsLabels(List<Map<BlockTransactionHashIndex, WalletNode>> displayedUtxoSets) {
|
||||
VBox inputsBox = new VBox();
|
||||
inputsBox.setMaxWidth(isExpanded() ? 300 : 150);
|
||||
inputsBox.setPrefWidth(isExpanded() ? 230 : 150);
|
||||
inputsBox.setPadding(new Insets(0, 10, 0, 10));
|
||||
inputsBox.minHeightProperty().bind(minHeightProperty());
|
||||
inputsBox.setAlignment(Pos.BASELINE_RIGHT);
|
||||
@@ -436,7 +476,7 @@ public class TransactionDiagram extends GridPane {
|
||||
inputValue = input.getValue();
|
||||
Wallet nodeWallet = walletNode.getWallet();
|
||||
StringJoiner joiner = new StringJoiner("\n");
|
||||
joiner.add("Spending " + getSatsValue(inputValue) + " sats from " + (isFinal() ? nodeWallet.getFullDisplayName() : (nodeWallet.isNested() ? nodeWallet.getDisplayName() : "")) + " " + walletNode);
|
||||
joiner.add("Spending " + getCoinValue(inputValue) + " from " + (isFinal() ? nodeWallet.getFullDisplayName() : (nodeWallet.isNested() ? nodeWallet.getDisplayName() : "")) + " " + walletNode);
|
||||
joiner.add(input.getHashAsString() + ":" + input.getIndex());
|
||||
joiner.add(walletNode.getAddress().toString());
|
||||
if(input.getLabel() != null) {
|
||||
@@ -462,7 +502,7 @@ public class TransactionDiagram extends GridPane {
|
||||
} else if(input instanceof AdditionalBlockTransactionHashIndex additionalReference) {
|
||||
inputValue = input.getValue();
|
||||
StringJoiner joiner = new StringJoiner("\n");
|
||||
joiner.add("Spending " + getSatsValue(inputValue) + " sats from" + (isExpanded() ? ":" : " (click to expand):"));
|
||||
joiner.add("Spending " + getCoinValue(inputValue) + " from" + (isExpanded() ? ":" : " (click to expand):"));
|
||||
for(BlockTransactionHashIndex additionalInput : additionalReference.getAdditionalInputs()) {
|
||||
joiner.add(getInputDescription(additionalInput));
|
||||
}
|
||||
@@ -475,7 +515,7 @@ public class TransactionDiagram extends GridPane {
|
||||
TransactionOutput txOutput = blockTransaction.getTransaction().getOutputs().get((int) input.getIndex());
|
||||
Address fromAddress = txOutput.getScript().getToAddress();
|
||||
inputValue = txOutput.getValue();
|
||||
tooltip.setText("Input of " + getSatsValue(inputValue) + " sats\n" + input.getHashAsString() + ":" + input.getIndex() + (fromAddress != null ? "\n" + fromAddress : ""));
|
||||
tooltip.setText("Input of " + getCoinValue(inputValue) + "\n" + input.getHashAsString() + ":" + input.getIndex() + (fromAddress != null ? "\n" + fromAddress : ""));
|
||||
|
||||
ContextMenu contextMenu = new LabelContextMenu(fromAddress, inputValue);
|
||||
label.setContextMenu(contextMenu);
|
||||
@@ -494,6 +534,7 @@ public class TransactionDiagram extends GridPane {
|
||||
tooltip.setShowDelay(new Duration(TOOLTIP_SHOW_DELAY));
|
||||
tooltip.setShowDuration(Duration.INDEFINITE);
|
||||
tooltip.setWrapText(true);
|
||||
tooltip.setSkin(new AddressTooltipSkin(tooltip));
|
||||
Window activeWindow = AppServices.getActiveWindow();
|
||||
if(activeWindow != null) {
|
||||
tooltip.setMaxWidth(activeWindow.getWidth());
|
||||
@@ -533,9 +574,22 @@ public class TransactionDiagram extends GridPane {
|
||||
return input.getLabel() != null && !input.getLabel().isEmpty() ? input.getLabel() : input.getHashAsString().substring(0, 8) + "..:" + input.getIndex();
|
||||
}
|
||||
|
||||
String getSatsValue(long amount) {
|
||||
String getCoinValue(long amount) {
|
||||
if(Config.get().isHideAmounts()) {
|
||||
return CoinLabel.HIDDEN_AMOUNT_TEXT;
|
||||
}
|
||||
|
||||
UnitFormat format = Config.get().getUnitFormat() == null ? UnitFormat.DOT : Config.get().getUnitFormat();
|
||||
return format.formatSatsValue(amount);
|
||||
BitcoinUnit unit = Config.get().getBitcoinUnit();
|
||||
if(unit == null || unit.equals(BitcoinUnit.AUTO)) {
|
||||
unit = (amount >= BitcoinUnit.getAutoThreshold() ? BitcoinUnit.BTC : BitcoinUnit.SATOSHIS);
|
||||
}
|
||||
|
||||
if(unit.equals(BitcoinUnit.BTC)) {
|
||||
return format.formatBtcValue(amount) + " BTC";
|
||||
}
|
||||
|
||||
return format.formatSatsValue(amount) + " sats";
|
||||
}
|
||||
|
||||
private Pane getInputsLines(List<Map<BlockTransactionHashIndex, WalletNode>> displayedUtxoSets) {
|
||||
@@ -599,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);
|
||||
@@ -640,9 +709,9 @@ public class TransactionDiagram extends GridPane {
|
||||
|
||||
double width = 140.0;
|
||||
long sum = walletTx.getTotal();
|
||||
List<Long> values = walletTx.getTransaction().getOutputs().stream().filter(txo -> txo.getScript().getToAddress() != null).map(TransactionOutput::getValue).collect(Collectors.toList());
|
||||
List<Long> values = 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");
|
||||
@@ -674,118 +743,28 @@ 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.setMaxWidth(isExpanded() ? 350 : 150);
|
||||
outputsBox.setPrefWidth(isExpanded() ? 230 : 150);
|
||||
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;
|
||||
Label recipientLabel = new Label(payment.getLabel() == null || payment.getType() == Payment.Type.FAKE_MIX || payment.getType() == Payment.Type.MIX ? payment.getAddress().toString().substring(0, 8) + "..." : payment.getLabel(), outputGlyph);
|
||||
recipientLabel.getStyleClass().add("output-label");
|
||||
recipientLabel.getStyleClass().add(labelledPayment ? "payment-label" : "recipient-label");
|
||||
Wallet toWallet = walletTx.getToWallet(AppServices.get().getOpenWallets().keySet(), payment);
|
||||
WalletNode toNode = walletTx.getWallet() != null && !walletTx.getWallet().isBip47() ? walletTx.getAddressNodeMap().get(payment.getAddress()) : null;
|
||||
Wallet toBip47Wallet = getBip47SendWallet(payment);
|
||||
Tooltip recipientTooltip = new Tooltip((toWallet == null ? (toNode != null ? "Consolidate " : "Pay ") : "Receive ")
|
||||
+ getSatsValue(payment.getAmount()) + " sats to "
|
||||
+ (payment instanceof AdditionalPayment ? (isExpanded() ? "\n" : "(click to expand)\n") + payment : (toWallet == null ? (payment.getLabel() == null ? (toNode != null ? toNode : (toBip47Wallet == null ? "external address" : toBip47Wallet.getDisplayName())) : payment.getLabel()) : toWallet.getFullDisplayName()) + "\n" + payment.getAddress().toString())
|
||||
+ (walletTx.isDuplicateAddress(payment) ? " (Duplicate)" : ""));
|
||||
recipientTooltip.getStyleClass().add("recipient-label");
|
||||
recipientTooltip.setShowDelay(new Duration(TOOLTIP_SHOW_DELAY));
|
||||
recipientTooltip.setShowDuration(Duration.INDEFINITE);
|
||||
recipientTooltip.setWrapText(true);
|
||||
Window activeWindow = AppServices.getActiveWindow();
|
||||
if(activeWindow != null) {
|
||||
recipientTooltip.setMaxWidth(activeWindow.getWidth());
|
||||
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));
|
||||
}
|
||||
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);
|
||||
}
|
||||
|
||||
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));
|
||||
}
|
||||
|
||||
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");
|
||||
Tooltip changeTooltip = new Tooltip("Change of " + getSatsValue(changeEntry.getValue()) + " sats 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);
|
||||
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) {
|
||||
outputsBox.getChildren().add(outputNode.outputLabel);
|
||||
outputsBox.getChildren().add(createSpacer());
|
||||
|
||||
ContextMenu contextMenu = new LabelContextMenu(outputNode.address, outputNode.amount, outputNode.paymentCode);
|
||||
ContextMenu contextMenu = new LabelContextMenu(outputNode.address, outputNode.amount, outputNode.paymentCode, outputNode.silentPaymentAddress);
|
||||
if(!outputNode.outputLabel.getChildren().isEmpty() && outputNode.outputLabel.getChildren().get(0) instanceof Label outputLabelControl) {
|
||||
outputLabelControl.setContextMenu(contextMenu);
|
||||
}
|
||||
@@ -795,7 +774,7 @@ public class TransactionDiagram extends GridPane {
|
||||
Label feeLabel = highFee ? new Label("High Fee", getFeeWarningGlyph()) : new Label("Fee", getFeeGlyph());
|
||||
feeLabel.getStyleClass().addAll("output-label", "fee-label");
|
||||
String percentage = walletTx.getFeePercentage() < 0.0001d ? "<0.01" : String.format("%.2f", walletTx.getFeePercentage() * 100.0);
|
||||
Tooltip feeTooltip = new Tooltip(walletTx.getFee() < 0 ? "Unknown fee" : "Fee of " + getSatsValue(walletTx.getFee()) + " sats (" + percentage + "%)");
|
||||
Tooltip feeTooltip = new Tooltip(walletTx.getFee() < 0 ? "Unknown fee" : "Fee of " + getCoinValue(walletTx.getFee()) + " (" + percentage + "%)");
|
||||
feeTooltip.getStyleClass().add("fee-tooltip");
|
||||
feeTooltip.setShowDelay(new Duration(TOOLTIP_SHOW_DELAY));
|
||||
feeTooltip.setShowDuration(Duration.INDEFINITE);
|
||||
@@ -825,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));
|
||||
@@ -906,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) + "]";
|
||||
}
|
||||
@@ -959,12 +1075,6 @@ public class TransactionDiagram extends GridPane {
|
||||
return spacer;
|
||||
}
|
||||
|
||||
private int getOutputIndex(Address address, long amount, Collection<Integer> seenIndexes) {
|
||||
List<TransactionOutput> addressOutputs = walletTx.getTransaction().getOutputs().stream().filter(txOutput -> txOutput.getScript().getToAddress() != null).collect(Collectors.toList());
|
||||
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()) {
|
||||
@@ -1111,7 +1221,7 @@ public class TransactionDiagram extends GridPane {
|
||||
}
|
||||
|
||||
public String toString() {
|
||||
return additionalPayments.stream().map(payment -> payment.getAddress().toString()).collect(Collectors.joining("\n"));
|
||||
return additionalPayments.stream().map(Payment::toString).collect(Collectors.joining("\n"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1120,25 +1230,27 @@ public class TransactionDiagram extends GridPane {
|
||||
public Address address;
|
||||
public long amount;
|
||||
public PaymentCode paymentCode;
|
||||
public SilentPaymentAddress silentPaymentAddress;
|
||||
|
||||
public OutputNode(Pane outputLabel, Address address, long amount) {
|
||||
this(outputLabel, address, amount, null);
|
||||
this(outputLabel, address, amount, null, null);
|
||||
}
|
||||
|
||||
public OutputNode(Pane outputLabel, Address address, long amount, PaymentCode paymentCode) {
|
||||
public OutputNode(Pane outputLabel, Address address, long amount, PaymentCode paymentCode, SilentPaymentAddress silentPaymentAddress) {
|
||||
this.outputLabel = outputLabel;
|
||||
this.address = address;
|
||||
this.amount = amount;
|
||||
this.paymentCode = paymentCode;
|
||||
this.silentPaymentAddress = silentPaymentAddress;
|
||||
}
|
||||
}
|
||||
|
||||
private class LabelContextMenu extends ContextMenu {
|
||||
public LabelContextMenu(Address address, long value) {
|
||||
this(address, value, null);
|
||||
this(address, value, null, null);
|
||||
}
|
||||
|
||||
public LabelContextMenu(Address address, long value, PaymentCode paymentCode) {
|
||||
public LabelContextMenu(Address address, long value, PaymentCode paymentCode, SilentPaymentAddress silentPaymentAddress) {
|
||||
if(address != null) {
|
||||
MenuItem copyAddress = new MenuItem("Copy Address");
|
||||
copyAddress.setOnAction(event -> {
|
||||
@@ -1186,6 +1298,17 @@ public class TransactionDiagram extends GridPane {
|
||||
});
|
||||
getItems().add(copyPaymentCode);
|
||||
}
|
||||
|
||||
if(silentPaymentAddress != null) {
|
||||
MenuItem copySilentPaymentAddress = new MenuItem("Copy Silent Payment Address");
|
||||
copySilentPaymentAddress.setOnAction(AE -> {
|
||||
hide();
|
||||
ClipboardContent content = new ClipboardContent();
|
||||
content.putString(silentPaymentAddress.toString());
|
||||
Clipboard.getSystemClipboard().setContent(content);
|
||||
});
|
||||
getItems().add(copySilentPaymentAddress);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -90,28 +90,44 @@ public class TransactionDiagramLabel extends HBox {
|
||||
outputLabels.add(mixOutputLabel);
|
||||
}
|
||||
} else if(walletTx.getPayments().size() >= 5 && walletTx.getPayments().stream().mapToLong(Payment::getAmount).distinct().count() <= 1 && walletTx.getWallet() != null
|
||||
&& walletTx.getWallet().getStandardAccountType() == StandardAccount.WHIRLPOOL_POSTMIX && walletTx.getPayments().stream().anyMatch(walletTx::isConsolidationSend)) {
|
||||
&& walletTx.getWallet().getStandardAccountType() == StandardAccount.WHIRLPOOL_POSTMIX && !walletTx.getWalletNodePayments().isEmpty()) {
|
||||
OutputLabel remixOutputLabel = getRemixOutputLabel(transactionDiagram, walletTx.getPayments());
|
||||
if(remixOutputLabel != null) {
|
||||
outputLabels.add(remixOutputLabel);
|
||||
}
|
||||
} else {
|
||||
List<Payment> payments = walletTx.getPayments().stream().filter(payment -> payment.getType() == Payment.Type.DEFAULT && !walletTx.isConsolidationSend(payment)).collect(Collectors.toList());
|
||||
List<OutputLabel> paymentLabels = payments.stream().map(payment -> getOutputLabel(transactionDiagram, payment)).collect(Collectors.toList());
|
||||
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.getPayments().stream().filter(payment -> payment.getType() == Payment.Type.DEFAULT && walletTx.isConsolidationSend(payment)).collect(Collectors.toList());
|
||||
outputLabels.addAll(consolidations.stream().map(consolidation -> getOutputLabel(transactionDiagram, consolidation)).collect(Collectors.toList()));
|
||||
|
||||
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) {
|
||||
@@ -147,10 +163,10 @@ public class TransactionDiagramLabel extends HBox {
|
||||
Glyph glyph = GlyphUtils.getOutputGlyph(transactionDiagram.getWalletTransaction(), premixOutput);
|
||||
String text;
|
||||
if(premixOutputs.size() == 1) {
|
||||
text = "Premix transaction with 1 output of " + transactionDiagram.getSatsValue(premixOutput.getAmount()) + " sats";
|
||||
text = "Premix transaction with 1 output of " + transactionDiagram.getCoinValue(premixOutput.getAmount());
|
||||
} else {
|
||||
text = "Premix transaction with " + premixOutputs.size() + " outputs of " + transactionDiagram.getSatsValue(premixOutput.getAmount()) + " sats each ("
|
||||
+ transactionDiagram.getSatsValue(total) + " sats)";
|
||||
text = "Premix transaction with " + premixOutputs.size() + " outputs of " + transactionDiagram.getCoinValue(premixOutput.getAmount()) + " each ("
|
||||
+ transactionDiagram.getCoinValue(total) + ")";
|
||||
}
|
||||
|
||||
return getOutputLabel(glyph, text);
|
||||
@@ -158,7 +174,7 @@ public class TransactionDiagramLabel extends HBox {
|
||||
|
||||
private OutputLabel getBadbankOutputLabel(TransactionDiagram transactionDiagram, Payment payment) {
|
||||
Glyph glyph = GlyphUtils.getOutputGlyph(transactionDiagram.getWalletTransaction(), payment);
|
||||
String text = "Badbank change of " + transactionDiagram.getSatsValue(payment.getAmount()) + " sats to " + payment.getAddress().toString();
|
||||
String text = "Badbank change of " + transactionDiagram.getCoinValue(payment.getAmount()) + " to " + payment.getAddress().toString();
|
||||
|
||||
return getOutputLabel(glyph, text);
|
||||
}
|
||||
@@ -167,7 +183,7 @@ public class TransactionDiagramLabel extends HBox {
|
||||
long total = premixOutputs.stream().mapToLong(Payment::getAmount).sum();
|
||||
double feePercentage = (double)whirlpoolFee.getAmount() / (total - whirlpoolFee.getAmount());
|
||||
Glyph glyph = GlyphUtils.getOutputGlyph(transactionDiagram.getWalletTransaction(), whirlpoolFee);
|
||||
String text = "Whirlpool fee of " + transactionDiagram.getSatsValue(whirlpoolFee.getAmount()) + " sats (" + String.format("%.2f", feePercentage * 100.0) + "% of total premix value)";
|
||||
String text = "Whirlpool fee of " + transactionDiagram.getCoinValue(whirlpoolFee.getAmount()) + " (" + String.format("%.2f", feePercentage * 100.0) + "% of total premix value)";
|
||||
|
||||
return getOutputLabel(glyph, text);
|
||||
}
|
||||
@@ -180,8 +196,8 @@ public class TransactionDiagramLabel extends HBox {
|
||||
Payment remixOutput = mixOutputs.get(0);
|
||||
long total = mixOutputs.stream().mapToLong(Payment::getAmount).sum();
|
||||
Glyph glyph = GlyphUtils.getPremixGlyph();
|
||||
String text = "Mix transaction with " + mixOutputs.size() + " outputs of " + transactionDiagram.getSatsValue(remixOutput.getAmount()) + " sats each ("
|
||||
+ transactionDiagram.getSatsValue(total) + " sats)";
|
||||
String text = "Mix transaction with " + mixOutputs.size() + " outputs of " + transactionDiagram.getCoinValue(remixOutput.getAmount()) + " each ("
|
||||
+ transactionDiagram.getCoinValue(total) + ")";
|
||||
|
||||
return getOutputLabel(glyph, text);
|
||||
}
|
||||
@@ -194,28 +210,37 @@ public class TransactionDiagramLabel extends HBox {
|
||||
Payment remixOutput = remixOutputs.get(0);
|
||||
long total = remixOutputs.stream().mapToLong(Payment::getAmount).sum();
|
||||
Glyph glyph = GlyphUtils.getPremixGlyph();
|
||||
String text = "Remix transaction with " + remixOutputs.size() + " outputs of " + transactionDiagram.getSatsValue(remixOutput.getAmount()) + " sats each ("
|
||||
+ transactionDiagram.getSatsValue(total) + " sats)";
|
||||
String text = "Remix transaction with " + remixOutputs.size() + " outputs of " + transactionDiagram.getCoinValue(remixOutput.getAmount()) + " each ("
|
||||
+ transactionDiagram.getCoinValue(total) + ")";
|
||||
|
||||
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 = walletTx.getWallet() != null && !walletTx.getWallet().isBip47() ? walletTx.getAddressNodeMap().get(payment.getAddress()) : null;
|
||||
WalletNode toNode = payment instanceof WalletNodePayment walletNodePayment ? walletNodePayment.getWalletNode() : null;
|
||||
|
||||
Glyph glyph = GlyphUtils.getOutputGlyph(transactionDiagram.getWalletTransaction(), payment);
|
||||
String text = (toWallet == null ? (toNode != null ? "Consolidate " : "Pay ") : "Receive ") + transactionDiagram.getSatsValue(payment.getAmount()) + " sats to " + payment.getAddress().toString();
|
||||
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.getSatsValue(changeEntry.getValue()) + " sats 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);
|
||||
}
|
||||
@@ -228,7 +253,7 @@ public class TransactionDiagramLabel extends HBox {
|
||||
|
||||
Glyph glyph = GlyphUtils.getFeeGlyph();
|
||||
String percentage = walletTx.getFeePercentage() < 0.0001d ? "<0.01" : String.format("%.2f", walletTx.getFeePercentage() * 100.0);
|
||||
String text = "Fee of " + transactionDiagram.getSatsValue(walletTx.getFee()) + " sats (" + percentage + "%)";
|
||||
String text = "Fee of " + transactionDiagram.getCoinValue(walletTx.getFee()) + " (" + percentage + "%)";
|
||||
|
||||
return getOutputLabel(glyph, text);
|
||||
}
|
||||
@@ -240,7 +265,8 @@ public class TransactionDiagramLabel extends HBox {
|
||||
icon.setGraphic(glyph);
|
||||
|
||||
CopyableLabel label = new CopyableLabel();
|
||||
label.setFont(Font.font("Roboto Mono Italic", 13));
|
||||
label.setSkin(new AddressTextFieldSkin(label));
|
||||
label.setFont(Font.font("Fragment Mono Italic", 13));
|
||||
label.setText(text);
|
||||
|
||||
HBox output = new HBox(5);
|
||||
|
||||
@@ -28,7 +28,7 @@ public class UsbStatusButton extends MenuButton {
|
||||
for(Device device : devices) {
|
||||
MenuItem deviceItem = new MenuItem(device.getModel().toDisplayString());
|
||||
if(!device.isNeedsPinSent() && (device.getModel() == WalletModel.TREZOR_1 || device.getModel() == WalletModel.TREZOR_T || device.getModel() == WalletModel.TREZOR_SAFE_3 ||
|
||||
device.getModel() == WalletModel.TREZOR_SAFE_5 || device.getModel() == WalletModel.KEEPKEY || device.getModel() == WalletModel.BITBOX_02 ||
|
||||
device.getModel() == WalletModel.TREZOR_SAFE_5 || device.getModel() == WalletModel.TREZOR_SAFE_7 || device.getModel() == WalletModel.KEEPKEY || device.getModel() == WalletModel.BITBOX_02 ||
|
||||
device.getModel() == WalletModel.ONEKEY_CLASSIC_1S || device.getModel() == WalletModel.ONEKEY_PRO)) {
|
||||
deviceItem = new Menu(device.getModel().toDisplayString());
|
||||
MenuItem toggleItem = new MenuItem("Toggle Passphrase" + (!device.getModel().externalPassphraseEntry() ? "" : (device.isNeedsPassphraseSent() ? " Off" : " On")));
|
||||
@@ -37,7 +37,8 @@ public class UsbStatusButton extends MenuButton {
|
||||
togglePassphraseService.setOnSucceeded(event1 -> {
|
||||
EventManager.get().post(new RequestOpenWalletsEvent());
|
||||
if(!device.getModel().externalPassphraseEntry()) {
|
||||
AppServices.showAlertDialog("Reconnect device", "Reconnect your " + device.getModel().toDisplayString() + " to reset the passphrase.", Alert.AlertType.INFORMATION);
|
||||
AppServices.showAlertDialog("Restart device", "Reconnect your " + device.getModel().toDisplayString() + " to reset the passphrase." +
|
||||
"\n\nIf it has a battery, hold down the power button until it restarts.", Alert.AlertType.INFORMATION);
|
||||
}
|
||||
});
|
||||
togglePassphraseService.setOnFailed(event1 -> {
|
||||
|
||||
@@ -90,6 +90,10 @@ public class UtxosChart extends BarChart<String, Number> {
|
||||
private void installTooltip(XYChart.Data<String, Number> item) {
|
||||
Tooltip.uninstall(item.getNode(), null);
|
||||
|
||||
if(Config.get().isHideAmounts()) {
|
||||
return;
|
||||
}
|
||||
|
||||
String satsValue = String.format(Locale.ENGLISH, "%,d", item.getYValue());
|
||||
Tooltip tooltip = new Tooltip(item.getXValue() + "\n" + satsValue + " sats");
|
||||
tooltip.setShowDelay(Duration.millis(TOOLTIP_SHOW_DELAY));
|
||||
@@ -129,4 +133,21 @@ public class UtxosChart extends BarChart<String, Number> {
|
||||
NumberAxis yaxis = (NumberAxis)getYAxis();
|
||||
yaxis.setTickLabelFormatter(new CoinAxisFormatter(yaxis, format, unit));
|
||||
}
|
||||
|
||||
public void refreshAxisLabels() {
|
||||
NumberAxis yaxis = (NumberAxis)getYAxis();
|
||||
// Force the axis to redraw by invalidating the upper and lower bounds
|
||||
yaxis.setAutoRanging(false);
|
||||
double lower = yaxis.getLowerBound();
|
||||
double upper = yaxis.getUpperBound();
|
||||
yaxis.setLowerBound(lower);
|
||||
yaxis.setUpperBound(upper);
|
||||
yaxis.setAutoRanging(true);
|
||||
}
|
||||
|
||||
public void refreshTooltips() {
|
||||
for(XYChart.Data<String, Number> data : utxoSeries.getData()) {
|
||||
installTooltip(data);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -57,7 +57,11 @@ public class UtxosTreeTable extends CoinTreeTable {
|
||||
addressCol.setCellValueFactory((TreeTableColumn.CellDataFeatures<Entry, UtxoEntry.AddressStatus> param) -> {
|
||||
return ((UtxoEntry)param.getValue().getValue()).addressStatusProperty();
|
||||
});
|
||||
addressCol.setCellFactory(p -> new AddressCell());
|
||||
addressCol.setCellFactory(p -> {
|
||||
AddressCell addressCell = new AddressCell();
|
||||
addressCell.setSkin(new AddressTreeTableCellSkin<>(addressCell));
|
||||
return addressCell;
|
||||
});
|
||||
addressCol.setSortable(true);
|
||||
addressCol.setComparator(Comparator.comparing(o -> o.getAddress().toString()));
|
||||
getColumns().add(addressCol);
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
@@ -21,8 +21,8 @@ import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
|
||||
public class WalletImportDialog extends Dialog<Wallet> {
|
||||
private Wallet wallet;
|
||||
public class WalletImportDialog extends Dialog<List<Wallet>> {
|
||||
private List<Wallet> wallets;
|
||||
private final Accordion importAccordion;
|
||||
private final Button scanButton;
|
||||
|
||||
@@ -52,7 +52,7 @@ public class WalletImportDialog extends Dialog<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 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);
|
||||
@@ -95,13 +95,13 @@ public class WalletImportDialog extends Dialog<Wallet> {
|
||||
dialogPane.setMinHeight(dialogPane.getPrefHeight());
|
||||
AppServices.moveToActiveWindowScreen(this);
|
||||
|
||||
setResultConverter(dialogButton -> dialogButton != cancelButtonType ? wallet : null);
|
||||
setResultConverter(dialogButton -> dialogButton != cancelButtonType ? wallets : null);
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void walletImported(WalletImportEvent event) {
|
||||
wallet = event.getWallet();
|
||||
setResult(wallet);
|
||||
wallets = event.getWallets();
|
||||
setResult(wallets);
|
||||
}
|
||||
|
||||
private void scan() {
|
||||
@@ -112,7 +112,7 @@ public class WalletImportDialog extends Dialog<Wallet> {
|
||||
List<Device> devices = enumerateService.getValue();
|
||||
importAccordion.getPanes().removeIf(titledPane -> titledPane instanceof DevicePane);
|
||||
for(Device device : devices) {
|
||||
DevicePane devicePane = new DevicePane(new Wallet(), device, devices.size() == 1, null);
|
||||
DevicePane devicePane = new DevicePane(new Wallet(), device, devices.size() == 1, null, null);
|
||||
importAccordion.getPanes().add(0, devicePane);
|
||||
}
|
||||
Platform.runLater(() -> EventManager.get().post(new UsbDeviceEvent(devices)));
|
||||
|
||||
@@ -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:");
|
||||
@@ -73,7 +78,7 @@ public class WalletNameDialog extends Dialog<WalletNameDialog.NameAndBirthDate>
|
||||
name = (CustomTextField)TextFields.createClearableTextField();
|
||||
name.setText(initialName);
|
||||
name.setTextFormatter(new TextFormatter<>((change) -> {
|
||||
change.setText(change.getText().replaceAll("[\\\\/:*?\"<>|]", "_"));
|
||||
change.setText(change.getText().replaceAll("[\\\\/:*?\"<>|;`]", "_"));
|
||||
return change;
|
||||
}));
|
||||
content.getChildren().add(name);
|
||||
|
||||
@@ -7,6 +7,7 @@ public enum WebcamPixelFormat {
|
||||
PIX_FMT_RGB24("RGB3", true),
|
||||
PIX_FMT_YUYV("YUYV", true),
|
||||
PIX_FMT_NV12("NV12", true),
|
||||
PIX_FMT_YU12("YU12", true),
|
||||
PIX_FMT_MJPG("MJPG", true);
|
||||
|
||||
private final String name;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import com.google.zxing.qrcode.QRCodeReader;
|
||||
import com.sparrowwallet.bokmakierie.Bokmakierie;
|
||||
import com.sparrowwallet.drongo.OsType;
|
||||
import com.sparrowwallet.sparrow.io.Config;
|
||||
import com.sparrowwallet.sparrow.io.ZBar;
|
||||
import javafx.beans.property.BooleanProperty;
|
||||
import javafx.beans.property.ObjectProperty;
|
||||
import javafx.beans.property.SimpleBooleanProperty;
|
||||
@@ -15,9 +16,7 @@ import javafx.concurrent.ScheduledService;
|
||||
import javafx.concurrent.Task;
|
||||
import javafx.embed.swing.SwingFXUtils;
|
||||
import javafx.scene.image.Image;
|
||||
import net.sourceforge.zbar.ZBar;
|
||||
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;
|
||||
}
|
||||
});
|
||||
@@ -137,14 +145,20 @@ public class WebcamService extends ScheduledService<Image> {
|
||||
|
||||
if(device != null) {
|
||||
for(CaptureDevice webcam : availableDevices) {
|
||||
if(webcam.getName().equals(device.getName())) {
|
||||
if(webcam.equals(device)) {
|
||||
selectedDevice = webcam;
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else if(Config.get().getWebcamDevice() != null) {
|
||||
for(CaptureDevice webcam : availableDevices) {
|
||||
if(webcam.getUniqueId().equals(Config.get().getWebcamDeviceId())) {
|
||||
selectedDevice = webcam;
|
||||
break;
|
||||
}
|
||||
if(webcam.getName().equals(Config.get().getWebcamDevice())) {
|
||||
selectedDevice = webcam;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -161,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));
|
||||
});
|
||||
}
|
||||
@@ -179,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);
|
||||
@@ -197,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");
|
||||
}
|
||||
@@ -279,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);
|
||||
}
|
||||
@@ -291,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);
|
||||
}
|
||||
@@ -313,9 +327,6 @@ public class WebcamService extends ScheduledService<Image> {
|
||||
}
|
||||
|
||||
private Result readQR(BufferedImage bufferedImage) {
|
||||
LuminanceSource source = new BufferedImageLuminanceSource(bufferedImage);
|
||||
BinaryBitmap bitmap = new BinaryBitmap(new HybridBinarizer(source));
|
||||
|
||||
try {
|
||||
com.sparrowwallet.bokmakierie.Result result = bokmakierie.scan(bufferedImage);
|
||||
if(result != null) {
|
||||
@@ -333,6 +344,8 @@ public class WebcamService extends ScheduledService<Image> {
|
||||
}
|
||||
|
||||
try {
|
||||
LuminanceSource source = new BufferedImageLuminanceSource(bufferedImage);
|
||||
BinaryBitmap bitmap = new BinaryBitmap(new HybridBinarizer(source));
|
||||
return qrReader.decode(bitmap, Map.of(DecodeHintType.TRY_HARDER, Boolean.TRUE));
|
||||
} catch(ReaderException e) {
|
||||
// fall thru, it means there is no QR code in image
|
||||
@@ -444,10 +457,27 @@ public class WebcamService extends ScheduledService<Image> {
|
||||
}
|
||||
|
||||
public static <T extends Enum<T>> T getNearestEnum(T target, T[] values) {
|
||||
int ordinal = target.ordinal();
|
||||
return Stream.concat(ordinal > 0 ? Stream.of(values[ordinal - 1]) : Stream.empty(), ordinal < values.length - 1 ? Stream.of(values[ordinal + 1]) : Stream.empty())
|
||||
.findFirst()
|
||||
.orElse(null);
|
||||
if(values == null || values.length == 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
int targetOrdinal = target.ordinal();
|
||||
if(values.length == 1) {
|
||||
return values[0];
|
||||
}
|
||||
|
||||
for(int i = 0; i < values.length; i++) {
|
||||
if(targetOrdinal < values[i].ordinal()) {
|
||||
if(i == 0) {
|
||||
return values[0];
|
||||
}
|
||||
int diffToPrev = Math.abs(targetOrdinal - values[i - 1].ordinal());
|
||||
int diffToNext = Math.abs(targetOrdinal - values[i].ordinal());
|
||||
return diffToPrev <= diffToNext ? values[i - 1] : values[i];
|
||||
}
|
||||
}
|
||||
|
||||
return values[values.length - 1];
|
||||
}
|
||||
|
||||
private static class CroppedDimension {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -36,7 +36,7 @@ public class XprvKeystoreImportPane extends TitledDescriptionPane {
|
||||
private ExtendedKey xprv;
|
||||
|
||||
public XprvKeystoreImportPane(Wallet wallet, KeystoreXprvImport importer, KeyDerivation defaultDerivation) {
|
||||
super(importer.getName(), "Extended key import", importer.getKeystoreImportDescription(), importer.getWalletModel());
|
||||
super(importer.getName(), "Enter master private key", importer.getKeystoreImportDescription(), importer.getWalletModel());
|
||||
this.wallet = wallet;
|
||||
this.importer = importer;
|
||||
this.defaultDerivation = defaultDerivation;
|
||||
@@ -79,7 +79,7 @@ public class XprvKeystoreImportPane extends TitledDescriptionPane {
|
||||
importButton = new SplitMenuButton();
|
||||
importButton.setAlignment(Pos.CENTER_RIGHT);
|
||||
importButton.setText("Import Keystore");
|
||||
importButton.getStyleClass().add("default-button");
|
||||
setDefaultButton(importButton);
|
||||
importButton.setOnAction(event -> {
|
||||
importButton.setDisable(true);
|
||||
importKeystore(getDefaultDerivation());
|
||||
@@ -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();
|
||||
@@ -151,6 +151,7 @@ public class XprvKeystoreImportPane extends TitledDescriptionPane {
|
||||
Button importXprvButton = new Button("Import");
|
||||
importXprvButton.setMinWidth(80);
|
||||
importXprvButton.setDisable(true);
|
||||
importXprvButton.setDefaultButton(true);
|
||||
importXprvButton.setOnAction(event -> {
|
||||
enterXprvButton.setVisible(false);
|
||||
importButton.setVisible(true);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package com.sparrowwallet.sparrow.event;
|
||||
|
||||
import com.sparrowwallet.drongo.protocol.BlockHeader;
|
||||
import com.sparrowwallet.sparrow.AppServices;
|
||||
import com.sparrowwallet.sparrow.net.MempoolRateSize;
|
||||
|
||||
import java.util.List;
|
||||
@@ -13,6 +14,7 @@ public class ConnectionEvent extends FeeRatesUpdatedEvent {
|
||||
private final int blockHeight;
|
||||
private final BlockHeader blockHeader;
|
||||
private final Double minimumRelayFeeRate;
|
||||
private final Double previousMinimumRelayFeeRate;
|
||||
|
||||
public ConnectionEvent(List<String> serverVersion, String serverBanner, int blockHeight, BlockHeader blockHeader, Map<Integer, Double> targetBlockFeeRates, Set<MempoolRateSize> mempoolRateSizes, Double minimumRelayFeeRate) {
|
||||
super(targetBlockFeeRates, mempoolRateSizes);
|
||||
@@ -21,6 +23,7 @@ public class ConnectionEvent extends FeeRatesUpdatedEvent {
|
||||
this.blockHeight = blockHeight;
|
||||
this.blockHeader = blockHeader;
|
||||
this.minimumRelayFeeRate = minimumRelayFeeRate;
|
||||
this.previousMinimumRelayFeeRate = AppServices.getMinimumRelayFeeRate();
|
||||
}
|
||||
|
||||
public List<String> getServerVersion() {
|
||||
@@ -42,4 +45,8 @@ public class ConnectionEvent extends FeeRatesUpdatedEvent {
|
||||
public Double getMinimumRelayFeeRate() {
|
||||
return minimumRelayFeeRate;
|
||||
}
|
||||
|
||||
public Double getPreviousMinimumRelayFeeRate() {
|
||||
return previousMinimumRelayFeeRate;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
package com.sparrowwallet.sparrow.event;
|
||||
|
||||
public class HideAmountsStatusEvent {
|
||||
private final boolean hideAmounts;
|
||||
|
||||
public HideAmountsStatusEvent(boolean hideAmounts) {
|
||||
this.hideAmounts = hideAmounts;
|
||||
}
|
||||
|
||||
public boolean isHideAmounts() {
|
||||
return hideAmounts;
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import com.sparrowwallet.drongo.BitcoinUnit;
|
||||
import com.sparrowwallet.drongo.wallet.BlockTransaction;
|
||||
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||
import com.sparrowwallet.sparrow.UnitFormat;
|
||||
import com.sparrowwallet.sparrow.control.CoinLabel;
|
||||
import com.sparrowwallet.sparrow.io.Config;
|
||||
import com.sparrowwallet.sparrow.wallet.Entry;
|
||||
import com.sparrowwallet.sparrow.wallet.HashIndexEntry;
|
||||
@@ -48,6 +49,10 @@ public class NewWalletTransactionsEvent {
|
||||
}
|
||||
|
||||
public String getValueAsText(long value) {
|
||||
if(Config.get().isHideAmounts()) {
|
||||
return CoinLabel.HIDDEN_AMOUNT_TEXT;
|
||||
}
|
||||
|
||||
UnitFormat format = Config.get().getUnitFormat();
|
||||
if(format == null) {
|
||||
format = UnitFormat.DOT;
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user