Compare commits
29 Commits
temp
..
b5fa01edfc
| Author | SHA1 | Date | |
|---|---|---|---|
| b5fa01edfc | |||
| 0da9670a36 | |||
| b193282766 | |||
| 12881fc477 | |||
| f0654310e1 | |||
| 3f90a46fa5 | |||
| 013d234348 | |||
| f3c376d8f4 | |||
| 39d65bb454 | |||
| 13f8be46b3 | |||
| 4fc74d5510 | |||
| 63e76fb088 | |||
| 029ec7ab2d | |||
| 5ddbb637fa | |||
| 7e782baa73 | |||
| 55f2ba2586 | |||
| 2ab945833a | |||
| 2a7cf8278b | |||
| 1a09d60a95 | |||
| 99f11fc5cb | |||
| d22bd6c379 | |||
| 1ae12899f6 | |||
| 729a0081a5 | |||
| 90f567d57b | |||
| 645216003f | |||
| af19974381 | |||
| a959456683 | |||
| 374d1c6b60 | |||
| f4d2d0adea |
@@ -5,6 +5,7 @@
|
||||
build/
|
||||
dist/
|
||||
*.egg/
|
||||
*.egg-info/
|
||||
Electrum.egg-info/
|
||||
.devlocaltmp/
|
||||
*_trial_temp
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
ThomasV - Creator and maintainer.
|
||||
Davide Grilli <davide.grilli@outlook.com> - BitcoinPurple fork author and maintainer.
|
||||
|
||||
ThomasV - Creator and maintainer (original Electrum).
|
||||
Animazing / Tachikoma - Styled the new GUI. Mac version.
|
||||
Azelphur - GUI stuff.
|
||||
Coblee - Alternate coin support and py2app support.
|
||||
|
||||
@@ -0,0 +1,112 @@
|
||||
# Changelog — Electrum Purple
|
||||
|
||||
All notable changes to the Electrum Purple fork are documented here.
|
||||
Upstream Electrum changes are not listed; see the [upstream changelog](https://github.com/spesmilo/electrum/blob/master/CHANGELOG).
|
||||
|
||||
---
|
||||
|
||||
## [0.9.0] — 2026-05-06
|
||||
|
||||
First public release of Electrum Purple — an unofficial fork of Electrum 4.7.x
|
||||
with first-class support for the **BitcoinPurple (BTCP)** network.
|
||||
|
||||
### New network: BitcoinPurple (BTCP)
|
||||
|
||||
- Added `BitcoinPurple` and `BitcoinPurpleTestnet` network classes with all chain
|
||||
parameters: 1-minute blocks, 120-block difficulty retarget, adjusted PoW limits
|
||||
(`MAX_TARGET`, `POW_GENESIS_BITS`, `DIFFICULTY_ADJUSTMENT_INTERVAL`,
|
||||
`POW_TARGET_TIMESPAN`). (`e0d04af15`)
|
||||
- Generalized difficulty adjustment logic in `blockchain.py` to support
|
||||
per-chain PoW constants; in-chunk retarget (120-block boundary) reads headers
|
||||
from the in-RAM buffer instead of disk. (`d1088c036`)
|
||||
- Default network set to BitcoinPurple mainnet. (`8b8d958a4`)
|
||||
- `BIP44_COIN_TYPE` set to 13496 for BitcoinPurple. (`af1997438`)
|
||||
- Launch flags: `--bitcoinpurple` and `--bitcoinpurple_testnet`. (via constants)
|
||||
|
||||
### Lightning Network — block-scaled timeouts
|
||||
|
||||
- All LN timeout values expressed in blocks scaled ×10 to preserve real-world
|
||||
security windows with 1-minute blocks (e.g. `to_self_delay` 144 → 1440,
|
||||
`cltv_expiry_delta` 40 → 400).
|
||||
|
||||
### UI / branding
|
||||
|
||||
- Coin name and unit strings are now network-aware: QML and Qt GUIs display
|
||||
the correct coin name (BTCP/BTC) based on the active network. (`d51076cb0`,
|
||||
`5ddbb637f`, `029ec7ab2`)
|
||||
- All icons recolored blue → purple (hue 278°) to match BitcoinPurple branding.
|
||||
(`374d1c6b6`, `12881fc47`)
|
||||
- Desktop icon (`.ico`, `.png`) updated to purple in all sizes (16 → 256 px).
|
||||
- Qt wizard logo updated to use `electrum-purple.png`. (`63e76fb08`)
|
||||
|
||||
### Packaging and build
|
||||
|
||||
- Package renamed to `electrum-purple`; pip entry point renamed to
|
||||
`electrum-purple`. (`90f567d57`)
|
||||
- `setup.py` data files and PyInstaller spec updated for `electrum-purple`
|
||||
naming. (`55f2ba258`)
|
||||
- Broken `electrum-purple` symlink fixed (was `../run_electrum`, now
|
||||
`run_electrum`). (`4fc74d551`)
|
||||
- Desktop and metainfo files renamed to `electrum-purple.desktop` /
|
||||
`electrum-purple.metainfo.xml`. (`729a0081a`)
|
||||
|
||||
#### Windows
|
||||
|
||||
- NSIS installer script renamed to `electrum-purple.nsi`; produces
|
||||
`electrum-purple-<VERSION>-setup.exe` and `electrum-purple-<VERSION>-portable.exe`.
|
||||
(`2a7cf8278`)
|
||||
- PyInstaller spec updated: icon set to `electrum-purple.ico`, exe name to
|
||||
`electrum-purple-<VERSION>.exe`. (`55f2ba258`)
|
||||
- Docker build: added `--security-opt seccomp=unconfined` and
|
||||
`--cap-add SYS_PTRACE` to fix Wine wineserver socket failure on WSL2.
|
||||
(`f0654310e`)
|
||||
|
||||
#### Linux AppImage
|
||||
|
||||
- Build script updated: output renamed to
|
||||
`electrum-purple-<VERSION>-x86_64.AppImage`. (`1a09d60a9`)
|
||||
- `apprun.sh` corrected: launches `electrum-purple` (was `electrum`). (`7e782baa7`)
|
||||
- Desktop file and icon (`electrum-purple.png`) correctly referenced in AppDir.
|
||||
- `run_electrum` `is_local` check updated to look for `electrum-purple.desktop`.
|
||||
(`7e782baa7`)
|
||||
- type2-runtime xz pin updated. (`013d23434`)
|
||||
|
||||
#### Android
|
||||
|
||||
- Buildozer spec updated: `title = Electrum Purple`,
|
||||
`package.domain = org.electrumpurple`, `package.name = electrum_purple`.
|
||||
(`2ab945833`)
|
||||
- Java classes (`SimpleScannerActivity`, `BiometricActivity`) updated to import
|
||||
`org.electrumpurple.electrum_purple.res.R`. (`7e782baa7`)
|
||||
|
||||
### Bug fixes
|
||||
|
||||
- Fixed onion message queues: replaced `put_nowait` + `sleep` polling with
|
||||
`call_later` to eliminate busy-wait. (`9a93bfda8`)
|
||||
- Fixed flaky Lightning peer tests (retries, timeouts, MPP wait loop).
|
||||
(`49ac312c8`, `7d433d0b4`)
|
||||
- Tests now use `config.path` instead of `electrum_path` for network-aware
|
||||
temporary directories. (`5c406683b`)
|
||||
|
||||
### Tests
|
||||
|
||||
- Added full BitcoinPurple test suite: address encoding, difficulty calculation,
|
||||
header verification, retarget clamping (46 tests). (`41e4a8141`)
|
||||
- 1005 tests pass, 6 skipped (upstream suite + BitcoinPurple suite). (`f4d2d0ade`)
|
||||
|
||||
### Documentation
|
||||
|
||||
- `README.md` updated: identifies this as an unofficial BitcoinPurple fork,
|
||||
credits Davide Grilli as fork author, preserves upstream credits. (`13f8be46b`)
|
||||
- `LICENCE` updated: added Davide Grilli copyright for fork additions. (`39d65bb45`)
|
||||
- `AUTHORS` updated: Davide Grilli listed as fork author and maintainer. (`f3c376d8f`)
|
||||
- `CLAUDE.md` added with codebase and BitcoinPurple architecture documentation.
|
||||
(`88525ef51`, `7b39a89d1`)
|
||||
- `technical-data.md` added: complete BitcoinPurple parameter reference (ports,
|
||||
genesis, PoW, LN, ElectrumX). (`6db423282`, `a95945668`)
|
||||
- `quickstart.md` added (English). (`ea8f27358`)
|
||||
|
||||
### Based on
|
||||
|
||||
Electrum 4.7.x (upstream commit `bd5ac019c` — release notes 4.7.2),
|
||||
MIT Licence, © 2011-2024 Thomas Voegtlin and The Electrum developers.
|
||||
@@ -1,5 +1,6 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2024-2026 Davide Grilli (BitcoinPurple fork additions)
|
||||
Copyright (c) 2011-2024 The Electrum developers
|
||||
Copyright (c) 2011-2024 Thomas Voegtlin
|
||||
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
include LICENCE RELEASE-NOTES AUTHORS
|
||||
include README.md
|
||||
include electrum.desktop
|
||||
include electrum-purple.desktop
|
||||
include *.py
|
||||
include run_electrum
|
||||
include org.electrum.electrum.metainfo.xml
|
||||
include org.electrumpurple.electrum-purple.metainfo.xml
|
||||
recursive-include packages *.py
|
||||
recursive-include packages cacert.pem
|
||||
|
||||
|
||||
@@ -1,16 +1,46 @@
|
||||
# Electrum - Lightweight Bitcoin client
|
||||
# Electrum Purple - Lightweight BitcoinPurple Wallet
|
||||
|
||||
> **Unofficial fork** of [Electrum](https://github.com/spesmilo/electrum) with support for the [BitcoinPurple](https://bitcoinpurple.org) network.
|
||||
|
||||
```
|
||||
Licence: MIT Licence
|
||||
Author: Thomas Voegtlin
|
||||
Language: Python (>= 3.10)
|
||||
Homepage: https://electrum.org/
|
||||
Licence: MIT Licence
|
||||
Fork author: Davide Grilli <davide.grilli@outlook.com>
|
||||
Original author: Thomas Voegtlin
|
||||
Language: Python (>= 3.10)
|
||||
Upstream: https://github.com/spesmilo/electrum
|
||||
```
|
||||
|
||||
[](https://cirrus-ci.com/github/spesmilo/electrum)
|
||||
[](https://coveralls.io/github/spesmilo/electrum?branch=master)
|
||||
[](https://crowdin.com/project/electrum)
|
||||
---
|
||||
|
||||
## About this fork
|
||||
|
||||
This project is an **unofficial, independent fork** of Electrum, maintained by **Davide Grilli**.
|
||||
It adds first-class support for the **BitcoinPurple (BTCP)** network — a Bitcoin fork with
|
||||
1-minute blocks and a 120-block difficulty retarget window — while keeping full compatibility
|
||||
with the original Electrum codebase and all upstream bug fixes.
|
||||
|
||||
This fork is **not affiliated with, endorsed by, or supported by** the original Electrum project
|
||||
or its developers. For the official Bitcoin wallet, use [electrum.org](https://electrum.org/).
|
||||
|
||||
### What is different from upstream Electrum
|
||||
|
||||
- `--bitcoinpurple` and `--bitcoinpurple_testnet` launch flags
|
||||
- BitcoinPurple chain parameters (1-min blocks, 120-block retarget, adjusted PoW limits)
|
||||
- Lightning Network timeouts scaled for 1-minute block times
|
||||
- Branding and packaging renamed to `electrum-purple` / `Electrum Purple`
|
||||
|
||||
Everything else — wallet format, Lightning support, hardware wallets, plugins — is identical
|
||||
to upstream Electrum.
|
||||
|
||||
### Licence and credits
|
||||
|
||||
This software is released under the **MIT Licence**, the same licence as the original Electrum.
|
||||
All original copyright notices are preserved as required by the licence.
|
||||
|
||||
Original copyright: © 2011-2024 Thomas Voegtlin and The Electrum developers.
|
||||
Fork additions: © 2024-2026 Davide Grilli.
|
||||
|
||||
---
|
||||
|
||||
## Getting started
|
||||
|
||||
@@ -142,15 +172,13 @@ $ pytest tests/test_bitcoin.py -v
|
||||
|
||||
## Contributing
|
||||
|
||||
Any help testing the software, reporting or fixing bugs, reviewing pull requests
|
||||
and recent changes, writing tests, or helping with outstanding issues is very welcome.
|
||||
Implementing new features, or improving/refactoring the codebase, is of course
|
||||
also welcome, but to avoid wasted effort, especially for larger changes,
|
||||
we encourage discussing these on the issue tracker or IRC first.
|
||||
Bug reports, testing, and pull requests for BitcoinPurple-specific features are welcome.
|
||||
|
||||
Besides [GitHub](https://github.com/spesmilo/electrum),
|
||||
most communication about Electrum development happens on IRC, in the
|
||||
`#electrum` channel on Libera Chat. The easiest way to participate on IRC is
|
||||
with the web client, [web.libera.chat](https://web.libera.chat/#electrum).
|
||||
For issues unrelated to BitcoinPurple support (core wallet, Lightning, hardware wallets),
|
||||
please check the [upstream Electrum project](https://github.com/spesmilo/electrum) first —
|
||||
fixes merged upstream can be rebased into this fork.
|
||||
|
||||
Please improve translations on [Crowdin](https://crowdin.com/project/electrum).
|
||||
---
|
||||
|
||||
*Electrum Purple is an independent fork and is not affiliated with the Electrum project.*
|
||||
*Original Electrum translations are maintained on [Crowdin](https://crowdin.com/project/electrum).*
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
[app]
|
||||
|
||||
# (str) Title of your application
|
||||
title = Electrum
|
||||
title = Electrum Purple
|
||||
|
||||
# (str) Package name
|
||||
package.name = Electrum
|
||||
package.name = electrum_purple
|
||||
|
||||
# (str) Package domain (needed for android/ios packaging)
|
||||
package.domain = org.electrum
|
||||
package.domain = org.electrumpurple
|
||||
|
||||
# (str) Source code where the main.py live
|
||||
source.dir = .
|
||||
|
||||
@@ -8,4 +8,4 @@ export LD_LIBRARY_PATH="${APPDIR}/usr/lib/:${APPDIR}/usr/lib/x86_64-linux-gnu${L
|
||||
export PATH="${APPDIR}/usr/bin:${PATH}"
|
||||
export LDFLAGS="-L${APPDIR}/usr/lib/x86_64-linux-gnu -L${APPDIR}/usr/lib"
|
||||
|
||||
exec "${APPDIR}/usr/bin/python3" -s "${APPDIR}/usr/bin/electrum" "$@"
|
||||
exec "${APPDIR}/usr/bin/python3" -s "${APPDIR}/usr/bin/electrum-purple" "$@"
|
||||
|
||||
@@ -7,7 +7,7 @@ CONTRIB="$PROJECT_ROOT/contrib"
|
||||
CONTRIB_APPIMAGE="$CONTRIB/build-linux/appimage"
|
||||
DISTDIR="$PROJECT_ROOT/dist"
|
||||
BUILDDIR="$CONTRIB_APPIMAGE/build/appimage"
|
||||
APPDIR="$BUILDDIR/electrum.AppDir"
|
||||
APPDIR="$BUILDDIR/electrum-purple.AppDir"
|
||||
CACHEDIR="$CONTRIB_APPIMAGE/.cache/appimage"
|
||||
TYPE2_RUNTIME_REPO_DIR="$CACHEDIR/type2-runtime"
|
||||
export DLL_TARGET_DIR="$CACHEDIR/dlls"
|
||||
@@ -25,7 +25,7 @@ PY_VER_MAJOR="3.12" # as it appears in fs paths
|
||||
PKG2APPIMAGE_COMMIT="a9c85b7e61a3a883f4a35c41c5decb5af88b6b5d"
|
||||
|
||||
VERSION=$(git describe --tags --dirty --always)
|
||||
APPIMAGE="$DISTDIR/electrum-$VERSION-x86_64.AppImage"
|
||||
APPIMAGE="$DISTDIR/electrum-purple-$VERSION-x86_64.AppImage"
|
||||
|
||||
rm -rf "$BUILDDIR"
|
||||
mkdir -p "$APPDIR" "$CACHEDIR" "$PIP_CACHE_DIR" "$DISTDIR" "$DLL_TARGET_DIR"
|
||||
@@ -159,8 +159,8 @@ info "installing electrum and its dependencies."
|
||||
|
||||
|
||||
info "desktop integration."
|
||||
cp "$PROJECT_ROOT/electrum.desktop" "$APPDIR/electrum.desktop"
|
||||
cp "$PROJECT_ROOT/electrum/gui/icons/electrum.png" "$APPDIR/electrum.png"
|
||||
cp "$PROJECT_ROOT/electrum-purple.desktop" "$APPDIR/electrum-purple.desktop"
|
||||
cp "$PROJECT_ROOT/electrum/gui/icons/electrum-purple.png" "$APPDIR/electrum-purple.png"
|
||||
|
||||
|
||||
# add launcher
|
||||
|
||||
@@ -108,7 +108,7 @@ index 07b6533..fba9c6e 100644
|
||||
+ autoconf=2.72-r0 \
|
||||
+ automake=1.17-r0 \
|
||||
+ libtool=2.4.7-r3 \
|
||||
+ xz=5.6.3-r1 \
|
||||
+ xz=5.8.3-r0 \
|
||||
+ eudev-dev=3.2.14-r5 \
|
||||
+ gettext-dev=0.22.5-r0 \
|
||||
+ linux-headers=6.6-r1 \
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
#!/bin/bash
|
||||
|
||||
NAME_ROOT=electrum
|
||||
NAME_ROOT=electrum-purple
|
||||
PROJECT_ROOT="$WINEPREFIX/drive_c/electrum"
|
||||
|
||||
export PYTHONDONTWRITEBYTECODE=1 # don't create __pycache__/ folders with .pyc files
|
||||
@@ -70,11 +70,11 @@ find -exec touch -h -d '2000-11-11T11:11:11+00:00' {} +
|
||||
popd
|
||||
|
||||
info "building NSIS installer"
|
||||
# $VERSION could be passed to the electrum.nsi script, but this would require some rewriting in the script itself.
|
||||
makensis -DPRODUCT_VERSION=$VERSION electrum.nsi
|
||||
# $VERSION could be passed to the electrum-purple.nsi script, but this would require some rewriting in the script itself.
|
||||
makensis -DPRODUCT_VERSION=$VERSION electrum-purple.nsi
|
||||
|
||||
cd dist
|
||||
mv electrum-setup.exe $NAME_ROOT-$VERSION-setup.exe
|
||||
mv electrum-purple-setup.exe $NAME_ROOT-$VERSION-setup.exe
|
||||
cd ..
|
||||
|
||||
info "Padding binaries to 8-byte boundaries, and fixing COFF image checksum in PE header"
|
||||
|
||||
@@ -48,10 +48,10 @@ else
|
||||
info "not doing fresh clone."
|
||||
fi
|
||||
|
||||
DOCKER_RUN_FLAGS=""
|
||||
DOCKER_RUN_FLAGS="--security-opt seccomp=unconfined --cap-add SYS_PTRACE"
|
||||
if sh -c ": >/dev/tty" >/dev/null 2>/dev/null; then
|
||||
info "/dev/tty is available and usable"
|
||||
DOCKER_RUN_FLAGS="-it"
|
||||
DOCKER_RUN_FLAGS="$DOCKER_RUN_FLAGS -it"
|
||||
fi
|
||||
|
||||
info "building binary..."
|
||||
|
||||
@@ -6,9 +6,9 @@
|
||||
;--------------------------------
|
||||
;Variables
|
||||
|
||||
!define PRODUCT_NAME "Electrum"
|
||||
!define PRODUCT_WEB_SITE "https://github.com/spesmilo/electrum"
|
||||
!define PRODUCT_PUBLISHER "Electrum Technologies GmbH"
|
||||
!define PRODUCT_NAME "Electrum Purple"
|
||||
!define PRODUCT_WEB_SITE "https://github.com/DavideGrilli/electrum"
|
||||
!define PRODUCT_PUBLISHER "Electrum Purple"
|
||||
!define PRODUCT_UNINST_KEY "Software\Microsoft\Windows\CurrentVersion\Uninstall\${PRODUCT_NAME}"
|
||||
|
||||
;--------------------------------
|
||||
@@ -16,7 +16,7 @@
|
||||
|
||||
;Name and file
|
||||
Name "${PRODUCT_NAME}"
|
||||
OutFile "dist/electrum-setup.exe"
|
||||
OutFile "dist/electrum-purple-setup.exe"
|
||||
|
||||
;Default installation folder
|
||||
InstallDir "$PROGRAMFILES64\${PRODUCT_NAME}"
|
||||
@@ -72,7 +72,7 @@
|
||||
!define MUI_ABORTWARNING
|
||||
!define MUI_ABORTWARNING_TEXT "Are you sure you wish to abort the installation of ${PRODUCT_NAME}?"
|
||||
|
||||
!define MUI_ICON "..\..\electrum\gui\icons\electrum.ico"
|
||||
!define MUI_ICON "..\..\electrum\gui\icons\electrum-purple.ico"
|
||||
|
||||
;--------------------------------
|
||||
;Pages
|
||||
@@ -168,7 +168,7 @@ Section
|
||||
|
||||
;Files to pack into the installer
|
||||
File /r "dist\electrum\*.*"
|
||||
File "..\..\electrum\gui\icons\electrum.ico"
|
||||
File "..\..\electrum\gui\icons\electrum-purple.ico"
|
||||
|
||||
;Store installation folder
|
||||
WriteRegStr HKCU "Software\${PRODUCT_NAME}" "" $INSTDIR
|
||||
@@ -179,33 +179,33 @@ Section
|
||||
|
||||
;Create desktop shortcut
|
||||
DetailPrint "Creating desktop shortcut..."
|
||||
CreateShortCut "$DESKTOP\${PRODUCT_NAME}.lnk" "$INSTDIR\electrum-${PRODUCT_VERSION}.exe" ""
|
||||
CreateShortCut "$DESKTOP\${PRODUCT_NAME}.lnk" "$INSTDIR\electrum-purple-${PRODUCT_VERSION}.exe" ""
|
||||
|
||||
;Create start-menu items
|
||||
DetailPrint "Creating start-menu items..."
|
||||
CreateDirectory "$SMPROGRAMS\${PRODUCT_NAME}"
|
||||
CreateShortCut "$SMPROGRAMS\${PRODUCT_NAME}\Uninstall.lnk" "$INSTDIR\Uninstall.exe" "" "$INSTDIR\Uninstall.exe" 0
|
||||
CreateShortCut "$SMPROGRAMS\${PRODUCT_NAME}\${PRODUCT_NAME}.lnk" "$INSTDIR\electrum-${PRODUCT_VERSION}.exe" "" "$INSTDIR\electrum-${PRODUCT_VERSION}.exe" 0
|
||||
CreateShortCut "$SMPROGRAMS\${PRODUCT_NAME}\${PRODUCT_NAME} Testnet.lnk" "$INSTDIR\electrum-${PRODUCT_VERSION}.exe" "--testnet" "$INSTDIR\electrum-${PRODUCT_VERSION}.exe" 0
|
||||
CreateShortCut "$SMPROGRAMS\${PRODUCT_NAME}\${PRODUCT_NAME}.lnk" "$INSTDIR\electrum-purple-${PRODUCT_VERSION}.exe" "" "$INSTDIR\electrum-purple-${PRODUCT_VERSION}.exe" 0
|
||||
CreateShortCut "$SMPROGRAMS\${PRODUCT_NAME}\${PRODUCT_NAME} Testnet.lnk" "$INSTDIR\electrum-purple-${PRODUCT_VERSION}.exe" "--testnet" "$INSTDIR\electrum-purple-${PRODUCT_VERSION}.exe" 0
|
||||
|
||||
|
||||
;Links bitcoin:, lightning: and lnurl LUD-17 URIs to Electrum
|
||||
WriteRegStr HKCU "Software\Classes\bitcoin" "" "URL:bitcoin Protocol"
|
||||
WriteRegStr HKCU "Software\Classes\bitcoin" "URL Protocol" ""
|
||||
WriteRegStr HKCU "Software\Classes\bitcoin" "DefaultIcon" "$\"$INSTDIR\electrum.ico, 0$\""
|
||||
WriteRegStr HKCU "Software\Classes\bitcoin\shell\open\command" "" "$\"$INSTDIR\electrum-${PRODUCT_VERSION}.exe$\" $\"%1$\""
|
||||
WriteRegStr HKCU "Software\Classes\bitcoin" "DefaultIcon" "$\"$INSTDIR\electrum-purple.ico, 0$\""
|
||||
WriteRegStr HKCU "Software\Classes\bitcoin\shell\open\command" "" "$\"$INSTDIR\electrum-purple-${PRODUCT_VERSION}.exe$\" $\"%1$\""
|
||||
WriteRegStr HKCU "Software\Classes\lightning" "" "URL:lightning Protocol"
|
||||
WriteRegStr HKCU "Software\Classes\lightning" "URL Protocol" ""
|
||||
WriteRegStr HKCU "Software\Classes\lightning" "DefaultIcon" "$\"$INSTDIR\electrum.ico, 0$\""
|
||||
WriteRegStr HKCU "Software\Classes\lightning\shell\open\command" "" "$\"$INSTDIR\electrum-${PRODUCT_VERSION}.exe$\" $\"%1$\""
|
||||
WriteRegStr HKCU "Software\Classes\lightning" "DefaultIcon" "$\"$INSTDIR\electrum-purple.ico, 0$\""
|
||||
WriteRegStr HKCU "Software\Classes\lightning\shell\open\command" "" "$\"$INSTDIR\electrum-purple-${PRODUCT_VERSION}.exe$\" $\"%1$\""
|
||||
WriteRegStr HKCU "Software\Classes\lnurlp" "" "URL:lnurlp Protocol"
|
||||
WriteRegStr HKCU "Software\Classes\lnurlp" "URL Protocol" ""
|
||||
WriteRegStr HKCU "Software\Classes\lnurlp" "DefaultIcon" "$\"$INSTDIR\electrum.ico, 0$\""
|
||||
WriteRegStr HKCU "Software\Classes\lnurlp\shell\open\command" "" "$\"$INSTDIR\electrum-${PRODUCT_VERSION}.exe$\" $\"%1$\""
|
||||
WriteRegStr HKCU "Software\Classes\lnurlp" "DefaultIcon" "$\"$INSTDIR\electrum-purple.ico, 0$\""
|
||||
WriteRegStr HKCU "Software\Classes\lnurlp\shell\open\command" "" "$\"$INSTDIR\electrum-purple-${PRODUCT_VERSION}.exe$\" $\"%1$\""
|
||||
WriteRegStr HKCU "Software\Classes\lnurlw" "" "URL:lnurlw Protocol"
|
||||
WriteRegStr HKCU "Software\Classes\lnurlw" "URL Protocol" ""
|
||||
WriteRegStr HKCU "Software\Classes\lnurlw" "DefaultIcon" "$\"$INSTDIR\electrum.ico, 0$\""
|
||||
WriteRegStr HKCU "Software\Classes\lnurlw\shell\open\command" "" "$\"$INSTDIR\electrum-${PRODUCT_VERSION}.exe$\" $\"%1$\""
|
||||
WriteRegStr HKCU "Software\Classes\lnurlw" "DefaultIcon" "$\"$INSTDIR\electrum-purple.ico, 0$\""
|
||||
WriteRegStr HKCU "Software\Classes\lnurlw\shell\open\command" "" "$\"$INSTDIR\electrum-purple-${PRODUCT_VERSION}.exe$\" $\"%1$\""
|
||||
|
||||
;Adds an uninstaller possibility to Windows Uninstall or change a program section
|
||||
WriteRegStr HKCU "${PRODUCT_UNINST_KEY}" "DisplayName" "$(^Name)"
|
||||
@@ -213,7 +213,7 @@ Section
|
||||
WriteRegStr HKCU "${PRODUCT_UNINST_KEY}" "DisplayVersion" "${PRODUCT_VERSION}"
|
||||
WriteRegStr HKCU "${PRODUCT_UNINST_KEY}" "URLInfoAbout" "${PRODUCT_WEB_SITE}"
|
||||
WriteRegStr HKCU "${PRODUCT_UNINST_KEY}" "Publisher" "${PRODUCT_PUBLISHER}"
|
||||
WriteRegStr HKCU "${PRODUCT_UNINST_KEY}" "DisplayIcon" "$INSTDIR\electrum.ico"
|
||||
WriteRegStr HKCU "${PRODUCT_UNINST_KEY}" "DisplayIcon" "$INSTDIR\electrum-purple.ico"
|
||||
|
||||
;Fixes Windows broken size estimates
|
||||
${GetSize} "$INSTDIR" "/S=0K" $0 $1 $2
|
||||
@@ -12,7 +12,7 @@ if TYPE_CHECKING:
|
||||
PYPKG="electrum"
|
||||
MAIN_SCRIPT="run_electrum"
|
||||
PROJECT_ROOT = "C:/electrum"
|
||||
ICONS_FILE=f"{PROJECT_ROOT}/{PYPKG}/gui/icons/electrum.ico"
|
||||
ICONS_FILE=f"{PROJECT_ROOT}/{PYPKG}/gui/icons/electrum-purple.ico"
|
||||
|
||||
cmdline_name = os.environ.get("ELECTRUM_CMDLINE_NAME")
|
||||
if not cmdline_name:
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
run_electrum
|
||||
@@ -1,18 +1,18 @@
|
||||
# If you want Electrum to appear in a Linux app launcher ("start menu"), install this by doing:
|
||||
# sudo desktop-file-install electrum.desktop
|
||||
# sudo desktop-file-install electrum-purple.desktop
|
||||
# Note: This assumes $HOME/.local/bin is in your $PATH
|
||||
|
||||
[Desktop Entry]
|
||||
Comment=Lightweight Bitcoin Client
|
||||
Exec=electrum %u
|
||||
Comment=Lightweight Bitcoin client with BitcoinPurple support
|
||||
Exec=electrum-purple %u
|
||||
GenericName[en_US]=Bitcoin Wallet
|
||||
GenericName=Bitcoin Wallet
|
||||
Icon=electrum
|
||||
Name[en_US]=Electrum Bitcoin Wallet
|
||||
Name=Electrum Bitcoin Wallet
|
||||
Icon=electrum-purple
|
||||
Name[en_US]=Electrum Purple Bitcoin Wallet
|
||||
Name=Electrum Purple Bitcoin Wallet
|
||||
Categories=Finance;Network;
|
||||
StartupNotify=true
|
||||
StartupWMClass=electrum
|
||||
StartupWMClass=electrum-purple
|
||||
Terminal=false
|
||||
Type=Application
|
||||
MimeType=x-scheme-handler/bitcoin;x-scheme-handler/lightning;x-scheme-handler/lnurlp;x-scheme-handler/lnurlw;
|
||||
@@ -20,5 +20,5 @@ Actions=Testnet;
|
||||
Keywords=crypto;currency;BTC
|
||||
|
||||
[Desktop Action Testnet]
|
||||
Exec=electrum --testnet %u
|
||||
Exec=electrum-purple --testnet %u
|
||||
Name=Testnet mode
|
||||
@@ -304,8 +304,7 @@ class BitcoinPurple(AbstractNet):
|
||||
}
|
||||
XPUB_HEADERS_INV = inv_dict(XPUB_HEADERS)
|
||||
|
||||
# Provisional BIP44 coin type (not SLIP-0044 registered; matches BTCP P2P port)
|
||||
BIP44_COIN_TYPE = 13496
|
||||
BIP44_COIN_TYPE = 13496 # provisional private constant (not SLIP-0044 registered)
|
||||
LN_REALM_BYTE = 0
|
||||
LN_DNS_SEEDS = []
|
||||
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
../run_electrum
|
||||
|
Before Width: | Height: | Size: 170 KiB After Width: | Height: | Size: 117 KiB |
|
Before Width: | Height: | Size: 172 KiB After Width: | Height: | Size: 107 KiB |
|
Before Width: | Height: | Size: 23 KiB After Width: | Height: | Size: 25 KiB |
|
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 8.7 KiB After Width: | Height: | Size: 9.4 KiB |
|
After Width: | Height: | Size: 34 KiB |
|
After Width: | Height: | Size: 9.7 KiB |
|
Before Width: | Height: | Size: 62 KiB |
|
Before Width: | Height: | Size: 24 KiB |
@@ -69,11 +69,11 @@
|
||||
<linearGradient
|
||||
id="linearGradient3987">
|
||||
<stop
|
||||
style="stop-color:#1382ef;stop-opacity:1;"
|
||||
style="stop-color:#8b5cf6;stop-opacity:1;"
|
||||
offset="0"
|
||||
id="stop4032" />
|
||||
<stop
|
||||
style="stop-color:#0056c0;stop-opacity:1;"
|
||||
style="stop-color:#5b21b6;stop-opacity:1;"
|
||||
offset="1"
|
||||
id="stop3991" />
|
||||
</linearGradient>
|
||||
|
||||
|
Before Width: | Height: | Size: 7.5 KiB After Width: | Height: | Size: 7.5 KiB |
|
Before Width: | Height: | Size: 5.1 KiB After Width: | Height: | Size: 5.2 KiB |
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 12 KiB |
@@ -63,11 +63,11 @@
|
||||
<linearGradient
|
||||
id="linearGradient3987">
|
||||
<stop
|
||||
style="stop-color:#41b3ec;stop-opacity:1;"
|
||||
style="stop-color:#c4b5fd;stop-opacity:1;"
|
||||
offset="0"
|
||||
id="stop4032" />
|
||||
<stop
|
||||
style="stop-color:#0581c4;stop-opacity:1;"
|
||||
style="stop-color:#7c3aed;stop-opacity:1;"
|
||||
offset="1"
|
||||
id="stop3991" />
|
||||
</linearGradient>
|
||||
|
||||
|
Before Width: | Height: | Size: 7.3 KiB After Width: | Height: | Size: 7.3 KiB |
|
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 20 KiB |
|
Before Width: | Height: | Size: 7.3 KiB After Width: | Height: | Size: 6.6 KiB |
@@ -15,7 +15,7 @@ Pane {
|
||||
|
||||
padding: 0
|
||||
|
||||
property var _baseunits: ['BTC','mBTC','bits','sat']
|
||||
property var _baseunits: Config.baseUnitsList
|
||||
|
||||
ColumnLayout {
|
||||
anchors.fill: parent
|
||||
|
||||
@@ -36,7 +36,7 @@ Item {
|
||||
|
||||
Image {
|
||||
visible: _qrprops.valid
|
||||
source: '../../../icons/electrum.png'
|
||||
source: '../../../icons/electrum-purple.png'
|
||||
x: 1
|
||||
y: 1
|
||||
width: parent.width - 2
|
||||
|
||||
@@ -81,7 +81,7 @@ ApplicationWindow
|
||||
|
||||
MenuItem {
|
||||
icon.color: action.enabled ? 'transparent' : Material.iconDisabledColor
|
||||
icon.source: '../../icons/electrum.png'
|
||||
icon.source: '../../icons/electrum-purple.png'
|
||||
action: Action {
|
||||
text: qsTr('About');
|
||||
onTriggered: menu.openPage(Qt.resolvedUrl('About.qml'))
|
||||
|
||||
@@ -17,7 +17,7 @@ ElDialog {
|
||||
|
||||
title: (pages.currentItem.wizard_title ? pages.currentItem.wizard_title : wizardTitle) +
|
||||
(pages.currentItem.title ? ' - ' + pages.currentItem.title : '')
|
||||
iconSource: '../../../icons/electrum.png'
|
||||
iconSource: '../../../icons/electrum-purple.png'
|
||||
|
||||
// android back button triggers close() on Popups. Disabling close here,
|
||||
// we handle that via Keys.onReleased event handler in the root layout.
|
||||
|
||||
@@ -22,7 +22,7 @@ import javax.crypto.KeyGenerator;
|
||||
import javax.crypto.SecretKey;
|
||||
import javax.crypto.spec.IvParameterSpec;
|
||||
|
||||
import org.electrum.electrum.res.R;
|
||||
import org.electrumpurple.electrum_purple.res.R;
|
||||
|
||||
public class BiometricActivity extends Activity {
|
||||
private static final String TAG = "BiometricActivity";
|
||||
@@ -54,7 +54,7 @@ public class BiometricActivity extends Activity {
|
||||
|
||||
Executor executor = getMainExecutor();
|
||||
BiometricPrompt biometricPrompt = new BiometricPrompt.Builder(this)
|
||||
.setTitle("Electrum Wallet")
|
||||
.setTitle("Electrum Purple")
|
||||
.setAllowedAuthenticators(BiometricManager.Authenticators.BIOMETRIC_STRONG | BiometricManager.Authenticators.DEVICE_CREDENTIAL)
|
||||
.setSubtitle(authMessage)
|
||||
.build();
|
||||
|
||||
@@ -27,7 +27,7 @@ import de.markusfisch.android.zxingcpp.ZxingCpp.Result;
|
||||
import de.markusfisch.android.zxingcpp.ZxingCpp.ContentType;
|
||||
|
||||
|
||||
import org.electrum.electrum.res.R; // package set in build.gradle
|
||||
import org.electrumpurple.electrum_purple.res.R; // package set in build.gradle
|
||||
|
||||
public class SimpleScannerActivity extends Activity {
|
||||
private static final int MY_PERMISSIONS_CAMERA = 1002;
|
||||
|
||||
@@ -170,7 +170,7 @@ class QEAppController(BaseCrashReporter, QObject):
|
||||
icon = "" # plyer wants image to be in .ico format on Windows
|
||||
else:
|
||||
icon = os.path.join(
|
||||
os.path.dirname(os.path.dirname(os.path.realpath(__file__))), "icons", "electrum.png",
|
||||
os.path.dirname(os.path.dirname(os.path.realpath(__file__))), "icons", "electrum-purple.png",
|
||||
)
|
||||
try:
|
||||
# TODO: lazy load not in UI thread please
|
||||
|
||||
@@ -7,7 +7,7 @@ from PyQt6.QtCore import pyqtProperty, pyqtSignal, pyqtSlot, QObject, QRegularEx
|
||||
from electrum.bitcoin import TOTAL_COIN_SUPPLY_LIMIT_IN_BTC
|
||||
from electrum.i18n import set_language, get_gui_lang_names
|
||||
from electrum.logging import get_logger
|
||||
from electrum.util import base_unit_name_to_decimal_point
|
||||
from electrum.util import base_unit_name_to_decimal_point, get_base_units_list
|
||||
from electrum.gui import messages
|
||||
|
||||
from .qetypes import QEAmount
|
||||
@@ -89,6 +89,11 @@ class QEConfig(AuthMixin, QObject):
|
||||
self.config.set_base_unit(unit)
|
||||
self.baseUnitChanged.emit()
|
||||
|
||||
@pyqtProperty('QVariantList', notify=baseUnitChanged)
|
||||
def baseUnitsList(self):
|
||||
from electrum.util import get_base_units_list
|
||||
return get_base_units_list()
|
||||
|
||||
@pyqtProperty('QRegularExpression', notify=baseUnitChanged)
|
||||
def btcAmountRegex(self):
|
||||
return self._btcAmountRegex()
|
||||
@@ -101,7 +106,7 @@ class QEConfig(AuthMixin, QObject):
|
||||
decimal_point = base_unit_name_to_decimal_point(self.config.get_base_unit())
|
||||
max_digits_before_dp = (
|
||||
len(str(TOTAL_COIN_SUPPLY_LIMIT_IN_BTC))
|
||||
+ (base_unit_name_to_decimal_point("BTC") - decimal_point))
|
||||
+ (base_unit_name_to_decimal_point(get_base_units_list()[0]) - decimal_point))
|
||||
exp = '^[0-9]{0,%d}' % max_digits_before_dp
|
||||
decimal_point += extra_precision
|
||||
if decimal_point > 0:
|
||||
|
||||
@@ -164,7 +164,7 @@ class ElectrumGui(BaseElectrumGui, Logger):
|
||||
self.app.installEventFilter(self.screenshot_protection_efilter)
|
||||
# explicitly set 'AA_DontShowIconsInMenus' False so menu icons are shown on MacOS
|
||||
self.app.setAttribute(Qt.ApplicationAttribute.AA_DontShowIconsInMenus, on=False)
|
||||
self.app.setWindowIcon(read_QIcon("electrum.png"))
|
||||
self.app.setWindowIcon(read_QIcon("electrum-purple.png"))
|
||||
self.translator = ElectrumTranslator()
|
||||
self.app.installTranslator(self.translator)
|
||||
self._cleaned_up = False
|
||||
|
||||
@@ -251,7 +251,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger, QtEventListener):
|
||||
if self.config.GUI_QT_WINDOW_IS_MAXIMIZED:
|
||||
self.showMaximized()
|
||||
|
||||
self.setWindowIcon(read_QIcon("electrum.png"))
|
||||
self.setWindowIcon(read_QIcon("electrum-purple.png"))
|
||||
self.init_menubar()
|
||||
|
||||
wrtabs = weakref.proxy(tabs)
|
||||
|
||||
@@ -110,7 +110,7 @@ class QEAbstractWizard(QDialog, MessageBoxMixin):
|
||||
self.setTabOrder(self.back_button, self.next_button)
|
||||
|
||||
self.icon_filename = None
|
||||
self.set_icon('electrum.png')
|
||||
self.set_icon('electrum-purple.png')
|
||||
|
||||
self.start_viewstate = start_viewstate
|
||||
|
||||
@@ -196,7 +196,7 @@ class QEAbstractWizard(QDialog, MessageBoxMixin):
|
||||
self.please_wait_l.setText(page.busy_msg if page.busy_msg else _("Please wait..."))
|
||||
self.error_msg.setText(str(page.error))
|
||||
self.error.setVisible(not page.busy and bool(page.error))
|
||||
icon = page.params.get('icon', icon_path('electrum.png'))
|
||||
icon = page.params.get('icon', icon_path('electrum-purple.png'))
|
||||
if icon:
|
||||
if icon != self.icon_filename:
|
||||
self.set_icon(icon)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
ELECTRUM_VERSION = '4.7.2' # version of the client package
|
||||
ELECTRUM_VERSION = '1.0.0' # version of the client package
|
||||
|
||||
PROTOCOL_VERSION_MIN = '1.4' # electrum protocol
|
||||
PROTOCOL_VERSION_MAX = '1.6'
|
||||
|
||||
@@ -9,9 +9,9 @@
|
||||
-->
|
||||
|
||||
<component type="desktop-application">
|
||||
<id>org.electrum.electrum</id>
|
||||
<id>org.electrumpurple.electrum-purple</id>
|
||||
|
||||
<name>Electrum</name>
|
||||
<name>Electrum Purple</name>
|
||||
<summary>Bitcoin Wallet</summary>
|
||||
|
||||
<metadata_license>MIT</metadata_license>
|
||||
@@ -29,7 +29,7 @@
|
||||
<name>The Electrum developers</name>
|
||||
</developer>
|
||||
|
||||
<launchable type="desktop-id">electrum.desktop</launchable>
|
||||
<launchable type="desktop-id">electrum-purple.desktop</launchable>
|
||||
|
||||
<content_rating type="oars-1.1" />
|
||||
</component>
|
||||
@@ -47,7 +47,7 @@ is_appimage = 'APPIMAGE' in os.environ
|
||||
is_binary_distributable = is_pyinstaller or is_android or is_appimage
|
||||
# is_local: unpacked tar.gz but not pip installed, or git clone
|
||||
is_local = (not is_binary_distributable
|
||||
and os.path.exists(os.path.join(script_dir, "electrum.desktop")))
|
||||
and os.path.exists(os.path.join(script_dir, "electrum-purple.desktop")))
|
||||
is_git_clone = is_local and os.path.exists(os.path.join(script_dir, ".git"))
|
||||
|
||||
if is_git_clone:
|
||||
|
||||
@@ -35,9 +35,9 @@ data_files = []
|
||||
if platform.system() in ['Linux', 'FreeBSD', 'DragonFly']:
|
||||
# note: we can't use absolute paths here. see #7787
|
||||
data_files += [
|
||||
(os.path.join('share', 'applications'), ['electrum.desktop']),
|
||||
(os.path.join('share', 'pixmaps'), ['electrum/gui/icons/electrum.png']),
|
||||
(os.path.join('share', 'icons/hicolor/128x128/apps'), ['electrum/gui/icons/electrum.png']),
|
||||
(os.path.join('share', 'applications'), ['electrum-purple.desktop']),
|
||||
(os.path.join('share', 'pixmaps'), ['electrum/gui/icons/electrum-purple.png']),
|
||||
(os.path.join('share', 'icons/hicolor/128x128/apps'), ['electrum/gui/icons/electrum-purple.png']),
|
||||
]
|
||||
|
||||
extras_require = {
|
||||
@@ -56,7 +56,7 @@ extras_require['fast'] = extras_require['crypto']
|
||||
|
||||
|
||||
setup(
|
||||
name="Electrum",
|
||||
name="electrum-purple",
|
||||
version=version.ELECTRUM_VERSION,
|
||||
python_requires='>={}'.format(MIN_PYTHON_VERSION),
|
||||
install_requires=requirements,
|
||||
@@ -71,7 +71,7 @@ setup(
|
||||
# package_data kwarg lists what gets put in site-packages when pip installing the tar.gz.
|
||||
# By specifying include_package_data=True, MANIFEST.in becomes responsible for both.
|
||||
include_package_data=True,
|
||||
scripts=['electrum/electrum'],
|
||||
scripts=['electrum-purple'],
|
||||
data_files=data_files,
|
||||
description="Lightweight Bitcoin Wallet",
|
||||
author="Thomas Voegtlin",
|
||||
|
||||
@@ -404,7 +404,15 @@ For `servers.json`, replace `your-server.example.com` with a real DNS name or
|
||||
public IP. The current file only documents the format; it does not configure a
|
||||
real public server.
|
||||
|
||||
### 6.2 Docker Patch Snippet
|
||||
### 6.2 Tested Component Versions
|
||||
|
||||
| Component | Version |
|
||||
|-----------|---------|
|
||||
| ElectrumX (`e-x`) | **1.18.0** — [spesmilo/electrumx](https://github.com/spesmilo/electrumx) |
|
||||
| Python (container) | **3.13.5** |
|
||||
| Base Docker image | `lukechilds/electrumx:latest` (unpinned) |
|
||||
|
||||
### 6.3 Docker Patch Snippet
|
||||
|
||||
```dockerfile
|
||||
COPY electrumx-patch/coins_btcp.py /tmp/coins_btcp.py
|
||||
@@ -431,7 +439,7 @@ print('>> Patched ElectrumX with BitcoinPurple coin classes')
|
||||
PATCH
|
||||
```
|
||||
|
||||
### 6.3 Environment Variables
|
||||
### 6.4 Environment Variables
|
||||
|
||||
```env
|
||||
# ── Identity ──────────────────────────────────────────────────────────────────
|
||||
@@ -479,7 +487,7 @@ ulimits:
|
||||
hard: 1048576
|
||||
```
|
||||
|
||||
### 6.4 ZMQ Notification Ports
|
||||
### 6.5 ZMQ Notification Ports
|
||||
|
||||
These are recommended local ports if you enable ZMQ notifications. BitcoinPurple
|
||||
Core does not assign default ZMQ bind ports; the port only exists if you set the
|
||||
@@ -519,7 +527,7 @@ Modelled after the `AbstractNet` interface (see `pallectrum` for a working examp
|
||||
| `BOLT11_HRP` | `"btcp"` | `"tbtcp"` | LN invoice prefix |
|
||||
| `GENESIS` | `000003823f…c015` | `000002fdc3…d998` | full hashes in §2.5 / §3 |
|
||||
| `DEFAULT_PORTS` | `{'t':'50001','s':'50002'}` | `{'t':'60001','s':'60002'}` | |
|
||||
| `BIP44_COIN_TYPE` | **TBD / private project constant** | `1` | not registered for BitcoinPurple — see note |
|
||||
| `BIP44_COIN_TYPE` | `13496` (provisional — not SLIP-0044 registered) | `1` | matches BTCP P2P port; update when registered |
|
||||
| `LN_REALM_BYTE` | `0` | `1` | LN DNS realm byte; unused while `LN_DNS_SEEDS=[]` |
|
||||
| `LN_DNS_SEEDS` | `[]` | `[]` | no LN seeds configured |
|
||||
| `SKIP_POW_DIFFICULTY_VALIDATION` | `False` only after BTCP retarget support | `False` only after BTCP retarget support | see §7.7 |
|
||||
@@ -1,443 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Sweep the addresses associated with a BitcoinPurple WIF.
|
||||
|
||||
Examples:
|
||||
python temp/sweep_p2wpkh.py
|
||||
python temp/sweep_p2wpkh.py "p2wpkh:WIF..." btcp1destination... all
|
||||
python temp/sweep_p2wpkh.py "WIF..." btcp1destination... 3 --fee-rate 2
|
||||
|
||||
By default the script creates and prints a signed raw transaction. It only
|
||||
broadcasts if --broadcast is passed.
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import asyncio
|
||||
import json
|
||||
import os
|
||||
import ssl
|
||||
import sys
|
||||
from dataclasses import dataclass
|
||||
from decimal import Decimal, InvalidOperation, ROUND_CEILING
|
||||
from typing import Iterable, Optional
|
||||
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
|
||||
|
||||
import electrum.constants as constants
|
||||
from electrum.constants import BitcoinPurple
|
||||
|
||||
BitcoinPurple.set_as_network()
|
||||
|
||||
from electrum import bitcoin
|
||||
from electrum.bitcoin import (
|
||||
address_to_script,
|
||||
deserialize_privkey,
|
||||
dust_threshold,
|
||||
is_address,
|
||||
pubkey_to_address,
|
||||
)
|
||||
from electrum.descriptor import get_singlesig_descriptor_from_legacy_leaf
|
||||
from electrum.transaction import (
|
||||
PartialTransaction,
|
||||
PartialTxInput,
|
||||
PartialTxOutput,
|
||||
TxOutput,
|
||||
TxOutpoint,
|
||||
)
|
||||
from electrum_ecc import ECPrivkey
|
||||
|
||||
|
||||
DEFAULT_FEE_RATE = Decimal("2")
|
||||
CLIENT_NAME = "btcp_sweep_tool"
|
||||
MAX_RPC_RESPONSE_BYTES = 64 * 1024 * 1024
|
||||
UTXO_PREVIEW_LIMIT = 50
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class DerivedAddress:
|
||||
script_type: str
|
||||
address: str
|
||||
pubkey: bytes
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class SpendableUtxo:
|
||||
tx_hash: str
|
||||
tx_pos: int
|
||||
value: int
|
||||
height: int
|
||||
derived: DerivedAddress
|
||||
|
||||
@property
|
||||
def outpoint(self) -> str:
|
||||
return f"{self.tx_hash}:{self.tx_pos}"
|
||||
|
||||
|
||||
class ElectrumXClient:
|
||||
def __init__(self, reader: asyncio.StreamReader, writer: asyncio.StreamWriter):
|
||||
self.reader = reader
|
||||
self.writer = writer
|
||||
self.request_id = 0
|
||||
|
||||
@classmethod
|
||||
async def connect(cls, servers: Iterable[tuple[str, int]]) -> "ElectrumXClient":
|
||||
ctx = ssl.create_default_context()
|
||||
ctx.check_hostname = False
|
||||
ctx.verify_mode = ssl.CERT_NONE
|
||||
|
||||
last_error: Optional[BaseException] = None
|
||||
for host, port in servers:
|
||||
try:
|
||||
print(f"Connecting to {host}:{port} ... ", end="", flush=True)
|
||||
reader, writer = await asyncio.wait_for(
|
||||
asyncio.open_connection(
|
||||
host,
|
||||
port,
|
||||
ssl=ctx,
|
||||
limit=MAX_RPC_RESPONSE_BYTES,
|
||||
),
|
||||
timeout=10,
|
||||
)
|
||||
client = cls(reader, writer)
|
||||
await client.rpc("server.version", [CLIENT_NAME, "1.4"])
|
||||
print("OK")
|
||||
return client
|
||||
except Exception as e:
|
||||
last_error = e
|
||||
print(f"failed ({e})")
|
||||
|
||||
raise RuntimeError(f"no ElectrumX server reachable: {last_error}")
|
||||
|
||||
async def rpc(self, method: str, params: list):
|
||||
self.request_id += 1
|
||||
payload = {"id": self.request_id, "method": method, "params": params}
|
||||
self.writer.write((json.dumps(payload) + "\n").encode("ascii"))
|
||||
await self.writer.drain()
|
||||
|
||||
line = await asyncio.wait_for(self.reader.readline(), timeout=30)
|
||||
if not line:
|
||||
raise RuntimeError("ElectrumX connection closed")
|
||||
response = json.loads(line)
|
||||
if response.get("error"):
|
||||
raise RuntimeError(f"ElectrumX error for {method}: {response['error']}")
|
||||
return response["result"]
|
||||
|
||||
async def close(self) -> None:
|
||||
self.writer.close()
|
||||
try:
|
||||
await self.writer.wait_closed()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
def decimal_fee_rate(value: str) -> Decimal:
|
||||
try:
|
||||
fee_rate = Decimal(value)
|
||||
except InvalidOperation as e:
|
||||
raise argparse.ArgumentTypeError("fee rate must be a number") from e
|
||||
if fee_rate <= 0:
|
||||
raise argparse.ArgumentTypeError("fee rate must be greater than zero")
|
||||
return fee_rate
|
||||
|
||||
|
||||
def load_servers() -> list[tuple[str, int]]:
|
||||
servers = []
|
||||
for host, data in constants.net.DEFAULT_SERVERS.items():
|
||||
ssl_port = data.get("s")
|
||||
if ssl_port:
|
||||
servers.append((host, int(ssl_port)))
|
||||
if not servers:
|
||||
raise RuntimeError("no SSL ElectrumX servers configured")
|
||||
return servers
|
||||
|
||||
|
||||
def parse_args() -> argparse.Namespace:
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Create a signed sweep transaction for addresses derived from a BTCP WIF.",
|
||||
)
|
||||
parser.add_argument("wif", nargs="?", help="BTCP WIF private key")
|
||||
parser.add_argument("destination", nargs="?", help="destination BTCP address")
|
||||
parser.add_argument(
|
||||
"utxo_count",
|
||||
nargs="?",
|
||||
help="number of UTXOs to spend, or 'all' (default: prompt, Enter = all)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--fee-rate",
|
||||
type=decimal_fee_rate,
|
||||
default=DEFAULT_FEE_RATE,
|
||||
help=f"fee rate in sat/vbyte (default: {DEFAULT_FEE_RATE})",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--script-types",
|
||||
default=None,
|
||||
help="comma-separated script types to scan (default: p2wpkh,p2wpkh-p2sh,p2pkh)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--broadcast",
|
||||
action="store_true",
|
||||
help="broadcast the signed transaction after creating it",
|
||||
)
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
def prompt_missing_args(args: argparse.Namespace) -> argparse.Namespace:
|
||||
if not args.wif:
|
||||
args.wif = input("Private key WIF: ").strip()
|
||||
if not args.destination:
|
||||
args.destination = input("Destination address: ").strip()
|
||||
return args
|
||||
|
||||
|
||||
def get_script_types(args_value: Optional[str], compressed: bool) -> list[str]:
|
||||
if args_value:
|
||||
script_types = [item.strip() for item in args_value.split(",") if item.strip()]
|
||||
elif compressed:
|
||||
script_types = ["p2wpkh", "p2wpkh-p2sh", "p2pkh"]
|
||||
else:
|
||||
script_types = ["p2pkh"]
|
||||
|
||||
unsupported = sorted(set(script_types) - {"p2wpkh", "p2wpkh-p2sh", "p2pkh"})
|
||||
if unsupported:
|
||||
raise ValueError(f"unsupported script type(s): {', '.join(unsupported)}")
|
||||
if not compressed and any(t in {"p2wpkh", "p2wpkh-p2sh"} for t in script_types):
|
||||
raise ValueError("segwit script types require a compressed WIF")
|
||||
return script_types
|
||||
|
||||
|
||||
def derive_addresses(wif: str, script_types_arg: Optional[str]) -> tuple[bytes, list[DerivedAddress]]:
|
||||
_txin_type, privkey, compressed = deserialize_privkey(wif)
|
||||
ec_privkey = ECPrivkey(privkey)
|
||||
|
||||
addresses = []
|
||||
for script_type in get_script_types(script_types_arg, compressed):
|
||||
use_compressed = script_type in {"p2wpkh", "p2wpkh-p2sh"} or compressed
|
||||
pubkey = ec_privkey.get_public_key_bytes(compressed=use_compressed)
|
||||
address = pubkey_to_address(script_type, pubkey.hex())
|
||||
addresses.append(DerivedAddress(script_type=script_type, address=address, pubkey=pubkey))
|
||||
|
||||
return privkey, addresses
|
||||
|
||||
|
||||
async def fetch_utxos(client: ElectrumXClient, derived: DerivedAddress) -> list[SpendableUtxo]:
|
||||
script_hash = bitcoin.address_to_scripthash(derived.address)
|
||||
rows = await client.rpc("blockchain.scripthash.listunspent", [script_hash])
|
||||
utxos = []
|
||||
for row in rows:
|
||||
utxos.append(
|
||||
SpendableUtxo(
|
||||
tx_hash=row["tx_hash"],
|
||||
tx_pos=int(row["tx_pos"]),
|
||||
value=int(row["value"]),
|
||||
height=int(row.get("height", 0)),
|
||||
derived=derived,
|
||||
)
|
||||
)
|
||||
return utxos
|
||||
|
||||
|
||||
def sort_utxos(utxos: list[SpendableUtxo]) -> list[SpendableUtxo]:
|
||||
return sorted(
|
||||
utxos,
|
||||
key=lambda u: (
|
||||
u.height <= 0,
|
||||
-u.value,
|
||||
u.derived.script_type,
|
||||
u.tx_hash,
|
||||
u.tx_pos,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def print_addresses(addresses: list[DerivedAddress]) -> None:
|
||||
print("\nDerived addresses:")
|
||||
for derived in addresses:
|
||||
print(f" {derived.script_type:<12} {derived.address}")
|
||||
|
||||
|
||||
def print_utxos(utxos: list[SpendableUtxo]) -> None:
|
||||
print("\nSpendable UTXOs:")
|
||||
print(f"{'#':>4} {'type':<12} {'outpoint':<69} {'sat':>14} height")
|
||||
print("-" * 112)
|
||||
for index, utxo in enumerate(utxos[:UTXO_PREVIEW_LIMIT], start=1):
|
||||
print(
|
||||
f"{index:>4} {utxo.derived.script_type:<12} "
|
||||
f"{utxo.outpoint:<69} {utxo.value:>14,} {utxo.height}"
|
||||
)
|
||||
if len(utxos) > UTXO_PREVIEW_LIMIT:
|
||||
print(f"... {len(utxos) - UTXO_PREVIEW_LIMIT:,} more UTXOs not shown")
|
||||
print("-" * 112)
|
||||
print(f"Total: {len(utxos)} UTXO, {sum(u.value for u in utxos):,} sat")
|
||||
|
||||
|
||||
def parse_utxo_count(raw_count: Optional[str], max_count: int) -> int:
|
||||
raw = raw_count
|
||||
if raw is None:
|
||||
raw = input(f"How many UTXOs to spend? (1-{max_count}, Enter = all): ").strip()
|
||||
if raw == "" or raw.lower() == "all":
|
||||
return max_count
|
||||
try:
|
||||
count = int(raw)
|
||||
except ValueError as e:
|
||||
raise ValueError("UTXO count must be an integer or 'all'") from e
|
||||
if not 1 <= count <= max_count:
|
||||
raise ValueError(f"UTXO count must be between 1 and {max_count}")
|
||||
return count
|
||||
|
||||
|
||||
def make_inputs_and_keypairs(
|
||||
selected_utxos: list[SpendableUtxo],
|
||||
privkey: bytes,
|
||||
) -> tuple[list[PartialTxInput], dict[bytes, bytes]]:
|
||||
inputs = []
|
||||
keypairs = {}
|
||||
|
||||
for utxo in selected_utxos:
|
||||
desc = get_singlesig_descriptor_from_legacy_leaf(
|
||||
pubkey=utxo.derived.pubkey.hex(),
|
||||
script_type=utxo.derived.script_type,
|
||||
)
|
||||
txin = PartialTxInput(prevout=TxOutpoint.from_str(utxo.outpoint))
|
||||
txin.script_descriptor = desc
|
||||
txin.witness_utxo = TxOutput(
|
||||
value=utxo.value,
|
||||
scriptpubkey=address_to_script(utxo.derived.address),
|
||||
)
|
||||
txin._trusted_value_sats = utxo.value
|
||||
txin._trusted_address = utxo.derived.address
|
||||
txin.block_height = utxo.height
|
||||
inputs.append(txin)
|
||||
keypairs[utxo.derived.pubkey] = privkey
|
||||
|
||||
return inputs, keypairs
|
||||
|
||||
|
||||
def fee_from_vbytes(vbytes: int, fee_rate: Decimal) -> int:
|
||||
return int((Decimal(vbytes) * fee_rate).to_integral_value(rounding=ROUND_CEILING))
|
||||
|
||||
|
||||
def build_signed_transaction(
|
||||
*,
|
||||
selected_utxos: list[SpendableUtxo],
|
||||
privkey: bytes,
|
||||
destination: str,
|
||||
fee_rate: Decimal,
|
||||
) -> tuple[PartialTransaction, str, int, int]:
|
||||
total_in = sum(utxo.value for utxo in selected_utxos)
|
||||
fee = 0
|
||||
|
||||
for _attempt in range(5):
|
||||
inputs, keypairs = make_inputs_and_keypairs(selected_utxos, privkey)
|
||||
output_value = total_in - fee
|
||||
output = PartialTxOutput.from_address_and_value(destination, output_value)
|
||||
tx = PartialTransaction.from_io(inputs, [output], locktime=0)
|
||||
|
||||
estimated_vbytes = tx.estimated_size()
|
||||
next_fee = fee_from_vbytes(estimated_vbytes, fee_rate)
|
||||
if next_fee != fee:
|
||||
fee = next_fee
|
||||
if total_in - fee < dust_threshold():
|
||||
raise ValueError(
|
||||
f"not enough funds: total={total_in} sat, fee={fee} sat, "
|
||||
f"dust={dust_threshold()} sat"
|
||||
)
|
||||
continue
|
||||
|
||||
tx.sign(keypairs)
|
||||
if not tx.is_complete():
|
||||
raise RuntimeError("transaction is incomplete after signing")
|
||||
raw = tx.serialize()
|
||||
actual_vbytes = tx.estimated_size()
|
||||
return tx, raw, fee, actual_vbytes
|
||||
|
||||
raise RuntimeError("fee calculation did not converge")
|
||||
|
||||
|
||||
async def broadcast(raw_tx: str, servers: list[tuple[str, int]]) -> str:
|
||||
last_error: Optional[BaseException] = None
|
||||
for host, port in servers:
|
||||
client = None
|
||||
try:
|
||||
client = await ElectrumXClient.connect([(host, port)])
|
||||
txid = await client.rpc("blockchain.transaction.broadcast", [raw_tx])
|
||||
return txid
|
||||
except Exception as e:
|
||||
last_error = e
|
||||
print(f"Broadcast via {host}:{port} failed: {e}")
|
||||
finally:
|
||||
if client:
|
||||
await client.close()
|
||||
raise RuntimeError(f"broadcast failed on all servers: {last_error}")
|
||||
|
||||
|
||||
async def main() -> int:
|
||||
args = prompt_missing_args(parse_args())
|
||||
|
||||
if not is_address(args.destination):
|
||||
raise ValueError("destination is not a valid BitcoinPurple address")
|
||||
|
||||
privkey, addresses = derive_addresses(args.wif, args.script_types)
|
||||
servers = load_servers()
|
||||
|
||||
print("\nBitcoinPurple WIF sweep")
|
||||
print(f"Fee rate: {args.fee_rate} sat/vbyte")
|
||||
print_addresses(addresses)
|
||||
|
||||
client = await ElectrumXClient.connect(servers)
|
||||
try:
|
||||
utxos: list[SpendableUtxo] = []
|
||||
for derived in addresses:
|
||||
found = await fetch_utxos(client, derived)
|
||||
print(f"Found {len(found)} UTXO for {derived.script_type} {derived.address}")
|
||||
utxos.extend(found)
|
||||
finally:
|
||||
await client.close()
|
||||
|
||||
utxos = sort_utxos(utxos)
|
||||
if not utxos:
|
||||
print("\nNo spendable UTXOs found for the derived addresses.")
|
||||
return 1
|
||||
|
||||
print_utxos(utxos)
|
||||
count = parse_utxo_count(args.utxo_count, len(utxos))
|
||||
selected = utxos[:count]
|
||||
total_in = sum(utxo.value for utxo in selected)
|
||||
|
||||
tx, raw, fee, actual_vbytes = build_signed_transaction(
|
||||
selected_utxos=selected,
|
||||
privkey=privkey,
|
||||
destination=args.destination,
|
||||
fee_rate=args.fee_rate,
|
||||
)
|
||||
|
||||
print("\nTransaction created")
|
||||
print(f"Inputs : {len(selected)}")
|
||||
print(f"Total : {total_in:,} sat")
|
||||
print(f"Fee : {fee:,} sat ({actual_vbytes} vbytes)")
|
||||
print(f"Output : {total_in - fee:,} sat")
|
||||
print(f"TxID : {tx.txid()}")
|
||||
print(f"\nRaw TX:\n{raw}")
|
||||
|
||||
should_broadcast = args.broadcast
|
||||
if not should_broadcast:
|
||||
answer = input("\nBroadcast transaction? Type 'yes' to send, or Enter/no to keep raw tx only: ").strip().lower()
|
||||
should_broadcast = answer == "yes"
|
||||
|
||||
if should_broadcast:
|
||||
print("\nBroadcasting...")
|
||||
txid = await broadcast(raw, servers)
|
||||
print(f"Broadcast OK: {txid}")
|
||||
else:
|
||||
print("\nBroadcast skipped. Raw transaction only.")
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
try:
|
||||
raise SystemExit(asyncio.run(main()))
|
||||
except KeyboardInterrupt:
|
||||
raise SystemExit(130)
|
||||
except Exception as e:
|
||||
print(f"ERROR: {e}", file=sys.stderr)
|
||||
raise SystemExit(1)
|
||||
@@ -172,7 +172,7 @@ class TestBitcoinPurpleConstants(ElectrumTestCase):
|
||||
|
||||
def test_bip44_coin_type(self):
|
||||
self.assertEqual(13496, BitcoinPurple.BIP44_COIN_TYPE)
|
||||
self.assertEqual(1, BitcoinPurpleTestnet.BIP44_COIN_TYPE)
|
||||
self.assertEqual(1, BitcoinPurpleTestnet.BIP44_COIN_TYPE)
|
||||
|
||||
# --- NETS_LIST integrity ---
|
||||
|
||||
|
||||
@@ -1375,6 +1375,7 @@ class TestPeerDirect(TestPeer):
|
||||
for i in range(num_payments):
|
||||
lnaddr, pay_req = self.prepare_invoice(w2, amount_msat=payment_value_msat)
|
||||
await group.spawn(single_payment(pay_req))
|
||||
await asyncio.sleep(0) # flush pending revoke_and_ack before stopping message loops
|
||||
gath.cancel()
|
||||
gath = asyncio.gather(many_payments(), p1._message_loop(), p2._message_loop(), p1.htlc_switch(), p2.htlc_switch())
|
||||
with self.assertRaises(asyncio.CancelledError):
|
||||
|
||||