2 Commits

Author SHA1 Message Date
davide 368bc2329c script per ripulire indirizzi su electrum 2026-05-05 13:54:10 +02:00
davide 1ebad68b75 docs: add test suite report for BitcoinPurple Electrum (1005 passed, 6 skipped)
Full run: pytest tests -v, Python 3.12.3, pytest 9.0.3, ~3:30 min.
Documents pass/skip counts per file, reasons for the 6 upstream-skipped tests,
BTCP-specific coverage, and flaky test fixes applied in this session.
2026-05-05 09:45:09 +02:00
52 changed files with 542 additions and 256 deletions
-1
View File
@@ -5,7 +5,6 @@
build/
dist/
*.egg/
*.egg-info/
Electrum.egg-info/
.devlocaltmp/
*_trial_temp
+1 -3
View File
@@ -1,6 +1,4 @@
Davide Grilli <davide.grilli@outlook.com> - BitcoinPurple fork author and maintainer.
ThomasV - Creator and maintainer (original Electrum).
ThomasV - Creator and maintainer.
Animazing / Tachikoma - Styled the new GUI. Mac version.
Azelphur - GUI stuff.
Coblee - Alternate coin support and py2app support.
-112
View File
@@ -1,112 +0,0 @@
# 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
View File
@@ -1,6 +1,5 @@
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
+2 -2
View File
@@ -1,9 +1,9 @@
include LICENCE RELEASE-NOTES AUTHORS
include README.md
include electrum-purple.desktop
include electrum.desktop
include *.py
include run_electrum
include org.electrumpurple.electrum-purple.metainfo.xml
include org.electrum.electrum.metainfo.xml
recursive-include packages *.py
recursive-include packages cacert.pem
+18 -46
View File
@@ -1,46 +1,16 @@
# Electrum Purple - Lightweight BitcoinPurple Wallet
> **Unofficial fork** of [Electrum](https://github.com/spesmilo/electrum) with support for the [BitcoinPurple](https://bitcoinpurple.org) network.
# Electrum - Lightweight Bitcoin client
```
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
Licence: MIT Licence
Author: Thomas Voegtlin
Language: Python (>= 3.10)
Homepage: https://electrum.org/
```
---
[![Build Status](https://api.cirrus-ci.com/github/spesmilo/electrum.svg?branch=master)](https://cirrus-ci.com/github/spesmilo/electrum)
[![Test coverage statistics](https://coveralls.io/repos/github/spesmilo/electrum/badge.svg?branch=master)](https://coveralls.io/github/spesmilo/electrum?branch=master)
[![Help translate Electrum online](https://d322cqt584bo4o.cloudfront.net/electrum/localized.svg)](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
@@ -172,13 +142,15 @@ $ pytest tests/test_bitcoin.py -v
## Contributing
Bug reports, testing, and pull requests for BitcoinPurple-specific features are welcome.
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.
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.
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).
---
*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).*
Please improve translations on [Crowdin](https://crowdin.com/project/electrum).
+3 -3
View File
@@ -1,13 +1,13 @@
[app]
# (str) Title of your application
title = Electrum Purple
title = Electrum
# (str) Package name
package.name = electrum_purple
package.name = Electrum
# (str) Package domain (needed for android/ios packaging)
package.domain = org.electrumpurple
package.domain = org.electrum
# (str) Source code where the main.py live
source.dir = .
+1 -1
View File
@@ -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-purple" "$@"
exec "${APPDIR}/usr/bin/python3" -s "${APPDIR}/usr/bin/electrum" "$@"
@@ -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-purple.AppDir"
APPDIR="$BUILDDIR/electrum.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-purple-$VERSION-x86_64.AppImage"
APPIMAGE="$DISTDIR/electrum-$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-purple.desktop" "$APPDIR/electrum-purple.desktop"
cp "$PROJECT_ROOT/electrum/gui/icons/electrum-purple.png" "$APPDIR/electrum-purple.png"
cp "$PROJECT_ROOT/electrum.desktop" "$APPDIR/electrum.desktop"
cp "$PROJECT_ROOT/electrum/gui/icons/electrum.png" "$APPDIR/electrum.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.8.3-r0 \
+ xz=5.6.3-r1 \
+ eudev-dev=3.2.14-r5 \
+ gettext-dev=0.22.5-r0 \
+ linux-headers=6.6-r1 \
+4 -4
View File
@@ -1,6 +1,6 @@
#!/bin/bash
NAME_ROOT=electrum-purple
NAME_ROOT=electrum
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-purple.nsi script, but this would require some rewriting in the script itself.
makensis -DPRODUCT_VERSION=$VERSION electrum-purple.nsi
# $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
cd dist
mv electrum-purple-setup.exe $NAME_ROOT-$VERSION-setup.exe
mv electrum-setup.exe $NAME_ROOT-$VERSION-setup.exe
cd ..
info "Padding binaries to 8-byte boundaries, and fixing COFF image checksum in PE header"
+2 -2
View File
@@ -48,10 +48,10 @@ else
info "not doing fresh clone."
fi
DOCKER_RUN_FLAGS="--security-opt seccomp=unconfined --cap-add SYS_PTRACE"
DOCKER_RUN_FLAGS=""
if sh -c ": >/dev/tty" >/dev/null 2>/dev/null; then
info "/dev/tty is available and usable"
DOCKER_RUN_FLAGS="$DOCKER_RUN_FLAGS -it"
DOCKER_RUN_FLAGS="-it"
fi
info "building binary..."
@@ -6,9 +6,9 @@
;--------------------------------
;Variables
!define PRODUCT_NAME "Electrum Purple"
!define PRODUCT_WEB_SITE "https://github.com/DavideGrilli/electrum"
!define PRODUCT_PUBLISHER "Electrum Purple"
!define PRODUCT_NAME "Electrum"
!define PRODUCT_WEB_SITE "https://github.com/spesmilo/electrum"
!define PRODUCT_PUBLISHER "Electrum Technologies GmbH"
!define PRODUCT_UNINST_KEY "Software\Microsoft\Windows\CurrentVersion\Uninstall\${PRODUCT_NAME}"
;--------------------------------
@@ -16,7 +16,7 @@
;Name and file
Name "${PRODUCT_NAME}"
OutFile "dist/electrum-purple-setup.exe"
OutFile "dist/electrum-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-purple.ico"
!define MUI_ICON "..\..\electrum\gui\icons\electrum.ico"
;--------------------------------
;Pages
@@ -168,7 +168,7 @@ Section
;Files to pack into the installer
File /r "dist\electrum\*.*"
File "..\..\electrum\gui\icons\electrum-purple.ico"
File "..\..\electrum\gui\icons\electrum.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-purple-${PRODUCT_VERSION}.exe" ""
CreateShortCut "$DESKTOP\${PRODUCT_NAME}.lnk" "$INSTDIR\electrum-${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-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
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
;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-purple.ico, 0$\""
WriteRegStr HKCU "Software\Classes\bitcoin\shell\open\command" "" "$\"$INSTDIR\electrum-purple-${PRODUCT_VERSION}.exe$\" $\"%1$\""
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\lightning" "" "URL:lightning Protocol"
WriteRegStr HKCU "Software\Classes\lightning" "URL Protocol" ""
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\lightning" "DefaultIcon" "$\"$INSTDIR\electrum.ico, 0$\""
WriteRegStr HKCU "Software\Classes\lightning\shell\open\command" "" "$\"$INSTDIR\electrum-${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-purple.ico, 0$\""
WriteRegStr HKCU "Software\Classes\lnurlp\shell\open\command" "" "$\"$INSTDIR\electrum-purple-${PRODUCT_VERSION}.exe$\" $\"%1$\""
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\lnurlw" "" "URL:lnurlw Protocol"
WriteRegStr HKCU "Software\Classes\lnurlw" "URL Protocol" ""
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$\""
WriteRegStr HKCU "Software\Classes\lnurlw" "DefaultIcon" "$\"$INSTDIR\electrum.ico, 0$\""
WriteRegStr HKCU "Software\Classes\lnurlw\shell\open\command" "" "$\"$INSTDIR\electrum-${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-purple.ico"
WriteRegStr HKCU "${PRODUCT_UNINST_KEY}" "DisplayIcon" "$INSTDIR\electrum.ico"
;Fixes Windows broken size estimates
${GetSize} "$INSTDIR" "/S=0K" $0 $1 $2
+1 -1
View File
@@ -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-purple.ico"
ICONS_FILE=f"{PROJECT_ROOT}/{PYPKG}/gui/icons/electrum.ico"
cmdline_name = os.environ.get("ELECTRUM_CMDLINE_NAME")
if not cmdline_name:
-1
View File
@@ -1 +0,0 @@
run_electrum
+8 -8
View File
@@ -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-purple.desktop
# sudo desktop-file-install electrum.desktop
# Note: This assumes $HOME/.local/bin is in your $PATH
[Desktop Entry]
Comment=Lightweight Bitcoin client with BitcoinPurple support
Exec=electrum-purple %u
Comment=Lightweight Bitcoin Client
Exec=electrum %u
GenericName[en_US]=Bitcoin Wallet
GenericName=Bitcoin Wallet
Icon=electrum-purple
Name[en_US]=Electrum Purple Bitcoin Wallet
Name=Electrum Purple Bitcoin Wallet
Icon=electrum
Name[en_US]=Electrum Bitcoin Wallet
Name=Electrum Bitcoin Wallet
Categories=Finance;Network;
StartupNotify=true
StartupWMClass=electrum-purple
StartupWMClass=electrum
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-purple --testnet %u
Exec=electrum --testnet %u
Name=Testnet mode
+2 -1
View File
@@ -304,7 +304,8 @@ class BitcoinPurple(AbstractNet):
}
XPUB_HEADERS_INV = inv_dict(XPUB_HEADERS)
BIP44_COIN_TYPE = 13496 # provisional private constant (not SLIP-0044 registered)
# Provisional BIP44 coin type (not SLIP-0044 registered; matches BTCP P2P port)
BIP44_COIN_TYPE = 13496
LN_REALM_BYTE = 0
LN_DNS_SEEDS = []
+1
View File
@@ -0,0 +1 @@
../run_electrum
Binary file not shown.

Before

Width:  |  Height:  |  Size: 117 KiB

After

Width:  |  Height:  |  Size: 170 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 107 KiB

After

Width:  |  Height:  |  Size: 172 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.4 KiB

After

Width:  |  Height:  |  Size: 8.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

+2 -2
View File
@@ -69,11 +69,11 @@
<linearGradient
id="linearGradient3987">
<stop
style="stop-color:#8b5cf6;stop-opacity:1;"
style="stop-color:#1382ef;stop-opacity:1;"
offset="0"
id="stop4032" />
<stop
style="stop-color:#5b21b6;stop-opacity:1;"
style="stop-color:#0056c0;stop-opacity:1;"
offset="1"
id="stop3991" />
</linearGradient>

Before

Width:  |  Height:  |  Size: 7.5 KiB

After

Width:  |  Height:  |  Size: 7.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.2 KiB

After

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 12 KiB

+2 -2
View File
@@ -63,11 +63,11 @@
<linearGradient
id="linearGradient3987">
<stop
style="stop-color:#c4b5fd;stop-opacity:1;"
style="stop-color:#41b3ec;stop-opacity:1;"
offset="0"
id="stop4032" />
<stop
style="stop-color:#7c3aed;stop-opacity:1;"
style="stop-color:#0581c4;stop-opacity:1;"
offset="1"
id="stop3991" />
</linearGradient>

Before

Width:  |  Height:  |  Size: 7.3 KiB

After

Width:  |  Height:  |  Size: 7.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.6 KiB

After

Width:  |  Height:  |  Size: 7.3 KiB

+1 -1
View File
@@ -15,7 +15,7 @@ Pane {
padding: 0
property var _baseunits: Config.baseUnitsList
property var _baseunits: ['BTC','mBTC','bits','sat']
ColumnLayout {
anchors.fill: parent
@@ -36,7 +36,7 @@ Item {
Image {
visible: _qrprops.valid
source: '../../../icons/electrum-purple.png'
source: '../../../icons/electrum.png'
x: 1
y: 1
width: parent.width - 2
+1 -1
View File
@@ -81,7 +81,7 @@ ApplicationWindow
MenuItem {
icon.color: action.enabled ? 'transparent' : Material.iconDisabledColor
icon.source: '../../icons/electrum-purple.png'
icon.source: '../../icons/electrum.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-purple.png'
iconSource: '../../../icons/electrum.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.electrumpurple.electrum_purple.res.R;
import org.electrum.electrum.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 Purple")
.setTitle("Electrum Wallet")
.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.electrumpurple.electrum_purple.res.R; // package set in build.gradle
import org.electrum.electrum.res.R; // package set in build.gradle
public class SimpleScannerActivity extends Activity {
private static final int MY_PERMISSIONS_CAMERA = 1002;
+1 -1
View File
@@ -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-purple.png",
os.path.dirname(os.path.dirname(os.path.realpath(__file__))), "icons", "electrum.png",
)
try:
# TODO: lazy load not in UI thread please
+2 -7
View File
@@ -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, get_base_units_list
from electrum.util import base_unit_name_to_decimal_point
from electrum.gui import messages
from .qetypes import QEAmount
@@ -89,11 +89,6 @@ 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()
@@ -106,7 +101,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(get_base_units_list()[0]) - decimal_point))
+ (base_unit_name_to_decimal_point("BTC") - decimal_point))
exp = '^[0-9]{0,%d}' % max_digits_before_dp
decimal_point += extra_precision
if decimal_point > 0:
+1 -1
View File
@@ -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-purple.png"))
self.app.setWindowIcon(read_QIcon("electrum.png"))
self.translator = ElectrumTranslator()
self.app.installTranslator(self.translator)
self._cleaned_up = False
+1 -1
View File
@@ -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-purple.png"))
self.setWindowIcon(read_QIcon("electrum.png"))
self.init_menubar()
wrtabs = weakref.proxy(tabs)
+2 -2
View File
@@ -110,7 +110,7 @@ class QEAbstractWizard(QDialog, MessageBoxMixin):
self.setTabOrder(self.back_button, self.next_button)
self.icon_filename = None
self.set_icon('electrum-purple.png')
self.set_icon('electrum.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-purple.png'))
icon = page.params.get('icon', icon_path('electrum.png'))
if icon:
if icon != self.icon_filename:
self.set_icon(icon)
+1 -1
View File
@@ -1,4 +1,4 @@
ELECTRUM_VERSION = '1.0.0' # version of the client package
ELECTRUM_VERSION = '4.7.2' # 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.electrumpurple.electrum-purple</id>
<id>org.electrum.electrum</id>
<name>Electrum Purple</name>
<name>Electrum</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-purple.desktop</launchable>
<launchable type="desktop-id">electrum.desktop</launchable>
<content_rating type="oars-1.1" />
</component>
+1 -1
View File
@@ -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-purple.desktop")))
and os.path.exists(os.path.join(script_dir, "electrum.desktop")))
is_git_clone = is_local and os.path.exists(os.path.join(script_dir, ".git"))
if is_git_clone:
+5 -5
View File
@@ -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-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']),
(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']),
]
extras_require = {
@@ -56,7 +56,7 @@ extras_require['fast'] = extras_require['crypto']
setup(
name="electrum-purple",
name="Electrum",
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-purple'],
scripts=['electrum/electrum'],
data_files=data_files,
description="Lightweight Bitcoin Wallet",
author="Thomas Voegtlin",
+4 -12
View File
@@ -404,15 +404,7 @@ 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 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
### 6.2 Docker Patch Snippet
```dockerfile
COPY electrumx-patch/coins_btcp.py /tmp/coins_btcp.py
@@ -439,7 +431,7 @@ print('>> Patched ElectrumX with BitcoinPurple coin classes')
PATCH
```
### 6.4 Environment Variables
### 6.3 Environment Variables
```env
# ── Identity ──────────────────────────────────────────────────────────────────
@@ -487,7 +479,7 @@ ulimits:
hard: 1048576
```
### 6.5 ZMQ Notification Ports
### 6.4 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
@@ -527,7 +519,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` | `13496` (provisional — not SLIP-0044 registered) | `1` | matches BTCP P2P port; update when registered |
| `BIP44_COIN_TYPE` | **TBD / private project constant** | `1` | not registered for BitcoinPurple — see note |
| `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 |
+443
View File
@@ -0,0 +1,443 @@
#!/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)
+1 -1
View File
@@ -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 ---
-1
View File
@@ -1375,7 +1375,6 @@ 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):