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
87 changed files with 717 additions and 436 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" "$@"
+10 -10
View File
@@ -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"
@@ -132,9 +132,9 @@ info "Installing build dependencies."
# and I am not quite sure how to break the circular dependence there (I guess we could introduce
# "requirements-build-base-base.txt" with just wheel in it...)
"$python" -m pip install --no-build-isolation --no-dependencies --no-warn-script-location \
--timeout 120 --cache-dir "$PIP_CACHE_DIR" -r "$CONTRIB/deterministic-build/requirements-build-base.txt"
--cache-dir "$PIP_CACHE_DIR" -r "$CONTRIB/deterministic-build/requirements-build-base.txt"
"$python" -m pip install --no-build-isolation --no-dependencies --no-binary :all: --no-warn-script-location \
--timeout 120 --cache-dir "$PIP_CACHE_DIR" -r "$CONTRIB/deterministic-build/requirements-build-appimage.txt"
--cache-dir "$PIP_CACHE_DIR" -r "$CONTRIB/deterministic-build/requirements-build-appimage.txt"
# opt out of compiling C extensions
@@ -145,22 +145,22 @@ export ELECTRUM_ECC_DONT_COMPILE=1
info "installing electrum and its dependencies."
"$python" -m pip install --no-build-isolation --no-dependencies --no-binary :all: --no-warn-script-location \
--timeout 120 --cache-dir "$PIP_CACHE_DIR" -r "$CONTRIB/deterministic-build/requirements.txt"
--cache-dir "$PIP_CACHE_DIR" -r "$CONTRIB/deterministic-build/requirements.txt"
"$python" -m pip install --no-build-isolation --no-dependencies --no-binary :all: --only-binary PyQt6,PyQt6-Qt6,cryptography --no-warn-script-location \
--timeout 120 --cache-dir "$PIP_CACHE_DIR" -r "$CONTRIB/deterministic-build/requirements-binaries.txt"
--cache-dir "$PIP_CACHE_DIR" -r "$CONTRIB/deterministic-build/requirements-binaries.txt"
"$python" -m pip install --no-build-isolation --no-dependencies --no-binary :all: --no-warn-script-location \
--timeout 120 --cache-dir "$PIP_CACHE_DIR" -r "$CONTRIB/deterministic-build/requirements-hw.txt"
--cache-dir "$PIP_CACHE_DIR" -r "$CONTRIB/deterministic-build/requirements-hw.txt"
"$python" -m pip install --no-build-isolation --no-dependencies --no-warn-script-location \
--timeout 120 --cache-dir "$PIP_CACHE_DIR" "$PROJECT_ROOT"
--cache-dir "$PIP_CACHE_DIR" "$PROJECT_ROOT"
# was only needed during build time, not runtime
"$python" -m pip uninstall -y Cython
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 \
+7 -7
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
@@ -37,15 +37,15 @@ export ELECTRUM_ECC_DONT_COMPILE=1
info "Installing requirements..."
$WINE_PYTHON -m pip install --no-build-isolation --no-dependencies --no-binary :all: --no-warn-script-location \
--timeout 120 --cache-dir "$WINE_PIP_CACHE_DIR" -r "$CONTRIB"/deterministic-build/requirements.txt
--cache-dir "$WINE_PIP_CACHE_DIR" -r "$CONTRIB"/deterministic-build/requirements.txt
info "Installing dependencies specific to binaries..."
# TODO tighten "--no-binary :all:" (but we don't have a C compiler...)
$WINE_PYTHON -m pip install --no-build-isolation --no-dependencies --no-warn-script-location \
--timeout 120 --no-binary :all: --only-binary cffi,cryptography,PyQt6,PyQt6-Qt6,PyQt6-sip \
--no-binary :all: --only-binary cffi,cryptography,PyQt6,PyQt6-Qt6,PyQt6-sip \
--cache-dir "$WINE_PIP_CACHE_DIR" -r "$CONTRIB"/deterministic-build/requirements-binaries.txt
info "Installing hardware wallet requirements..."
$WINE_PYTHON -m pip install --no-build-isolation --no-dependencies --no-warn-script-location \
--timeout 120 --no-binary :all: --only-binary cffi,cryptography,hidapi \
--no-binary :all: --only-binary cffi,cryptography,hidapi \
--cache-dir "$WINE_PIP_CACHE_DIR" -r "$CONTRIB"/deterministic-build/requirements-hw.txt
pushd "$PROJECT_ROOT"
@@ -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
+3 -4
View File
@@ -5,13 +5,12 @@ from decimal import Decimal
from typing import Optional
from . import bitcoin
from . import constants
from .util import format_satoshis_plain
from .bitcoin import COIN, TOTAL_COIN_SUPPLY_LIMIT_IN_BTC
from .bolt11 import decode_bolt11_invoice, BOLT11DecodeException
# note: when checking against these, use .lower() to support case-insensitivity
BITCOIN_BIP21_URI_SCHEME = 'bitcoin' # kept for backward-compat imports
BITCOIN_BIP21_URI_SCHEME = 'bitcoin'
LIGHTNING_URI_SCHEME = 'lightning'
# note: URI scheme handler registrations are duplicated all over the codebase:
@@ -37,7 +36,7 @@ def parse_bip21_URI(uri: str) -> dict:
return {'address': uri}
u = urllib.parse.urlparse(uri)
if u.scheme.lower() != constants.net.BIP21_URI_SCHEME:
if u.scheme.lower() != BITCOIN_BIP21_URI_SCHEME:
raise InvalidBitcoinURI("Not a bitcoin URI")
address = u.path
@@ -128,7 +127,7 @@ def create_bip21_uri(addr, amount_sat: Optional[int], message: Optional[str],
v = urllib.parse.quote(v)
query.append(f"{k}={v}")
p = urllib.parse.ParseResult(
scheme=constants.net.BIP21_URI_SCHEME,
scheme=BITCOIN_BIP21_URI_SCHEME,
netloc='',
path=addr,
params='',
+2 -3
View File
@@ -83,7 +83,6 @@ class AbstractNet:
COIN_SYMBOL: str = "BTC"
COIN_NAME: str = "Bitcoin"
BIP21_URI_SCHEME: str = "bitcoin"
# PoW difficulty parameters (Bitcoin defaults; override per chain as needed)
DIFFICULTY_ADJUSTMENT_INTERVAL: int = 2016 # blocks per retarget window
@@ -284,7 +283,6 @@ class BitcoinPurple(AbstractNet):
ADDRTYPE_P2SH = 55
SEGWIT_HRP = "btcp"
BOLT11_HRP = SEGWIT_HRP
BIP21_URI_SCHEME = "btcp"
GENESIS = "000003823fbf82ea4906cbe214617ce7a70a5da29c19ecb1d65618bcf04ec015"
DEFAULT_PORTS = {'t': '50001', 's': '50002'}
BLOCK_HEIGHT_FIRST_LIGHTNING_CHANNELS = 0
@@ -306,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

+7 -2
View File
@@ -6,7 +6,7 @@ import QtQuick.Controls.Material
Pane {
objectName: 'About'
property string title: qsTr("About Electrum Purple")
property string title: qsTr("About Electrum")
Flickable {
anchors.fill: parent
@@ -72,7 +72,7 @@ Pane {
Layout.alignment: Qt.AlignRight
}
Label {
text: '<a href="https://bitcoinpurpleblockchain.com/">https://bitcoinpurpleblockchain.com/</a>'
text: '<a href="https://electrum.org">https://electrum.org</a>'
textFormat: Text.RichText
onLinkActivated: Qt.openUrlExternally(link)
}
@@ -88,6 +88,11 @@ Pane {
height: constants.paddingXLarge
Layout.columnSpan: 2
}
Label {
text: qsTr('Distributed by Electrum Technologies GmbH')
Layout.columnSpan: 2
Layout.alignment: Qt.AlignHCenter
}
}
}
@@ -41,7 +41,7 @@ Pane {
visible: Daemon.currentWallet.synchronizing || !Network.isConnected
text: Daemon.currentWallet.synchronizing
? qsTr('Your wallet is not synchronized. The displayed balance may be inaccurate.')
: qsTr('Your wallet is not connected to an Electrum Purple server. The displayed balance may be outdated.')
: qsTr('Your wallet is not connected to an Electrum server. The displayed balance may be outdated.')
iconStyle: InfoTextArea.IconStyle.Warn
}
@@ -122,7 +122,7 @@ ElDialog {
text_qr: dialog.channelBackup,
text_help: qsTr('The channel you created is not recoverable from seed.')
+ ' ' + qsTr('To prevent fund losses, please save this backup on another device.')
+ ' ' + qsTr('It may be imported in another Electrum Purple wallet with the same seed.')
+ ' ' + qsTr('It may be imported in another Electrum wallet with the same seed.')
})
sharedialog.open()
}
@@ -42,7 +42,7 @@ ElDialog
Label {
Layout.fillWidth: true
text: qsTr('Something went wrong while executing Electrum Purple.')
text: qsTr('Something went wrong while executing Electrum.')
}
Label {
Layout.fillWidth: true
@@ -45,7 +45,7 @@ ElDialog {
visible: !Daemon.currentWallet.lightningHasDeterministicNodeId
iconStyle: InfoTextArea.IconStyle.Warn
text: Daemon.currentWallet.seedType == 'segwit'
? [ qsTr('Your channels cannot be recovered from seed, because they were created with an old version of Electrum Purple.'), ' ',
? [ qsTr('Your channels cannot be recovered from seed, because they were created with an old version of Electrum.'), ' ',
qsTr('This means that you must save a backup of your wallet every time you create a new channel.'),
'\n\n',
qsTr('If you want this wallet to have recoverable channels, you must close your existing channels and restore this wallet from seed.')
@@ -53,7 +53,7 @@ ElDialog {
: [ qsTr('Your channels cannot be recovered from seed.'), ' ',
qsTr('This means that you must save a backup of your wallet every time you create a new channel.'),
'\n\n',
qsTr('If you want to have recoverable channels, you must create a new wallet with an Electrum Purple seed')
qsTr('If you want to have recoverable channels, you must create a new wallet with an Electrum seed')
].join('')
backgroundColor: constants.darkerDialogBackground
}
+3 -3
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
@@ -55,7 +55,7 @@ Pane {
if (Config.language != currentValue) {
Config.language = currentValue
var dialog = app.messageDialog.createObject(app, {
text: qsTr('Please restart Electrum Purple to activate the new GUI settings')
text: qsTr('Please restart Electrum to activate the new GUI settings')
})
dialog.open()
}
@@ -407,7 +407,7 @@ Pane {
if (!checked) {
var dialog = app.messageDialog.createObject(app, {
title: qsTr('Are you sure?'),
text: qsTr('Electrum Purple will have to download the Lightning Network graph, which is not recommended on mobile.'),
text: qsTr('Electrum will have to download the Lightning Network graph, which is not recommended on mobile.'),
yesno: true
})
dialog.accepted.connect(function() {
+1 -1
View File
@@ -71,7 +71,7 @@ ElDialog {
HelpButton {
heading: qsTr('Sweep private keys')
helptext: qsTr('This will create a transaction sending all funds associated with the private keys to the current wallet') +
'<br/><br/>' + qsTr('WIF keys are typed in Electrum Purple, based on script type.') + '<br/><br/>' +
'<br/><br/>' + qsTr('WIF keys are typed in Electrum, based on script type.') + '<br/><br/>' +
qsTr('A few examples') + ':<br/>' +
'<tt><b>p2pkh</b>:KxZcY47uGp9a... \t-> 1DckmggQM...<br/>' +
'<b>p2wpkh-p2sh</b>:KxZcY47uGp9a... \t-> 3NhNeZQXF...<br/>' +
@@ -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
+2 -2
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'))
@@ -616,7 +616,7 @@ ApplicationWindow
stack.pop()
} else {
var dialog = app.messageDialog.createObject(app, {
title: qsTr('Close Electrum Purple?'),
title: qsTr('Close Electrum?'),
yesno: true
})
dialog.accepted.connect(function() {
@@ -50,15 +50,15 @@ WizardComponent {
var t = {
'electrum': [
// not shown as electrum is the default seed type anyways and the name is self-explanatory
qsTr('Electrum Purple seeds are the default seed type.'),
qsTr('If you are restoring from a seed previously created by Electrum Purple, choose this option')
qsTr('Electrum seeds are the default seed type.'),
qsTr('If you are restoring from a seed previously created by Electrum, choose this option')
].join(' '),
'bip39': [
qsTr('BIP39 seeds can be imported in Electrum Purple, so that users can access funds locked in other wallets.'),
qsTr('BIP39 seeds can be imported in Electrum, so that users can access funds locked in other wallets.'),
qsTr('BIP39 seeds do not include a version number, which compromises compatibility with future software.'),
].join(' '),
'slip39': [
qsTr('SLIP39 seeds can be imported in Electrum Purple, so that users can access funds locked in other wallets.'),
qsTr('SLIP39 seeds can be imported in Electrum, so that users can access funds locked in other wallets.'),
].join(' ')
}
infotext.text = t[seed_variant_cb.currentValue]
@@ -31,7 +31,7 @@ WizardComponent {
InfoTextArea {
Layout.preferredWidth: parent.width
backgroundColor: constants.darkerDialogBackground
text: qsTr('Enter a list of Bitcoin Purple addresses (this will create a watching-only wallet), or a list of private keys.')
text: qsTr('Enter a list of Bitcoin addresses (this will create a watching-only wallet), or a list of private keys.')
}
RowLayout {
@@ -55,7 +55,7 @@ WizardComponent {
Layout.fillWidth: true
ButtonGroup.group: wallettypegroup
property string wallettype: 'imported'
text: qsTr('Import Bitcoin Purple addresses or private keys')
text: qsTr('Import Bitcoin addresses or private keys')
}
}
}
@@ -47,7 +47,7 @@ WizardComponent {
Label {
Layout.alignment: Qt.AlignHCenter
Layout.preferredWidth: parent.width
text: qsTr("If you are unsure what this is, leave them unchecked and Electrum Purple will automatically select servers.")
text: qsTr("If you are unsure what this is, leave them unchecked and Electrum will automatically select servers.")
wrapMode: Text.WordWrap
horizontalAlignment: Text.AlignHLeft
font.pixelSize: constants.fontSizeMedium
@@ -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 -2
View File
@@ -16,7 +16,6 @@ except ImportError:
# Note: missing QtMultimedia will lead to errors when using QR scanner on desktop
from PyQt6.QtCore import QObject as QVideoSink
from electrum import constants
from electrum.logging import get_logger
from electrum.qrreader import get_qr_reader
from electrum.i18n import _
@@ -145,7 +144,7 @@ class QEQRImageProvider(QQuickImageProvider):
# (unknown schemes might be found when a colon is in a serialized TX, which
# leads to mangling of the tx, so we check for supported schemes.)
uri = urllib.parse.urlparse(qstr)
if uri.scheme and uri.scheme in [constants.net.BIP21_URI_SCHEME, 'lightning']:
if uri.scheme and uri.scheme in ['bitcoin', 'lightning']:
# urlencode request parameters
query = urllib.parse.parse_qs(uri.query)
query = urllib.parse.urlencode(query, doseq=True, quote_via=urllib.parse.quote)
+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
@@ -1207,7 +1207,7 @@ class ConfirmTxDialog(TxEditor):
grid.addWidget(HelpLabel(_("Amount to be sent") + ": ", msg), 0, 0)
grid.addWidget(self.amount_label, 0, 1)
msg = _('Bitcoin Purple transactions are in general not free. A transaction fee is paid by the sender of the funds.') + '\n\n'\
msg = _('Bitcoin transactions are in general not free. A transaction fee is paid by the sender of the funds.') + '\n\n'\
+ _('The amount of fee can be decided freely by the sender. However, transactions with low fees take more time to be processed.') + '\n\n'\
+ _('A suggested fee is automatically added to this field. You may override it. The suggested fee increases with the size of the transaction.')
+1 -1
View File
@@ -52,7 +52,7 @@ class Exception_Window(BaseCrashReporter, QWidget, MessageBoxMixin, Logger):
self.config = config
QWidget.__init__(self)
self.setWindowTitle('Electrum Purple - ' + _('An Error Occurred'))
self.setWindowTitle('Electrum - ' + _('An Error Occurred'))
self.setMinimumSize(600, 300)
Logger.__init__(self)
+6 -6
View File
@@ -636,11 +636,11 @@ class HistoryList(MyTreeView, AcceptFileDragDrop):
grid.addWidget(QLabel(self.format_date(start_date)), 1, 1)
grid.addWidget(QLabel(self.format_date(end_date)), 1, 2)
#
grid.addWidget(QLabel(_("BTCP balance")), 2, 0)
grid.addWidget(QLabel(_("BTC balance")), 2, 0)
grid.addWidget(QLabel(format_amount(start['BTC_balance'])), 2, 1)
grid.addWidget(QLabel(format_amount(end['BTC_balance'])), 2, 2)
#
grid.addWidget(QLabel(_("BTCP Fiat price")), 3, 0)
grid.addWidget(QLabel(_("BTC Fiat price")), 3, 0)
grid.addWidget(QLabel(format_fiat(start.get('BTC_fiat_price'))), 3, 1)
grid.addWidget(QLabel(format_fiat(end.get('BTC_fiat_price'))), 3, 2)
#
@@ -657,11 +657,11 @@ class HistoryList(MyTreeView, AcceptFileDragDrop):
grid.addWidget(QLabel(format_fiat(end.get('unrealized_gains', ''))), 6, 2)
#
grid2 = QGridLayout()
grid2.addWidget(QLabel(_("BTCP incoming")), 0, 0)
grid2.addWidget(QLabel(_("BTC incoming")), 0, 0)
grid2.addWidget(QLabel(format_amount(flow['BTC_incoming'])), 0, 1)
grid2.addWidget(QLabel(_("Fiat incoming")), 1, 0)
grid2.addWidget(QLabel(format_fiat(flow.get('fiat_incoming'))), 1, 1)
grid2.addWidget(QLabel(_("BTCP outgoing")), 2, 0)
grid2.addWidget(QLabel(_("BTC outgoing")), 2, 0)
grid2.addWidget(QLabel(format_amount(flow['BTC_outgoing'])), 2, 1)
grid2.addWidget(QLabel(_("Fiat outgoing")), 3, 0)
grid2.addWidget(QLabel(format_fiat(flow.get('fiat_outgoing'))), 3, 1)
@@ -682,8 +682,8 @@ class HistoryList(MyTreeView, AcceptFileDragDrop):
_logger.error(f"could not import electrum.plot. This feature needs matplotlib to be installed. exc={e!r}")
self.main_window.show_message("\n\n".join([
_("This feature requires the 'matplotlib' Python library which is not "
"included in Electrum Purple by default."),
_("If you run Electrum Purple from source you can install matplotlib to use this feature."),
"included in Electrum by default."),
_("If you run Electrum from source you can install matplotlib to use this feature."),
_("It is not possible to install matplotlib inside the binary executables "
"(e.g. AppImage or Windows installation).")
]))
+1 -1
View File
@@ -179,7 +179,7 @@ class InvoiceList(MyTreeView):
copy_menu = self.add_copy_menu(menu, idx)
address = invoice.get_address()
if address:
copy_menu.addAction(_("Address"), lambda: self.main_window.do_copy(invoice.get_address(), title='Bitcoin Purple Address'))
copy_menu.addAction(_("Address"), lambda: self.main_window.do_copy(invoice.get_address(), title='Bitcoin Address'))
status = wallet.get_invoice_status(invoice)
if status == PR_UNPAID:
if bool(invoice.get_amount_sat()):
+73 -20
View File
@@ -95,6 +95,7 @@ from .util import (read_QIcon, ColorScheme, text_dialog, icon_path, WaitingDialo
getOpenFileName, getSaveFileName, ShowQRLineEdit, scan_qr_from_screenshot)
from .wizard.wallet import WIF_HELP_TEXT
from .history_list import HistoryList, HistoryModel
from .update_checker import UpdateCheck, UpdateCheckThread
from .channels_list import ChannelsList
from .confirm_tx_dialog import ConfirmTxDialog, TxEditorContext
from .rbf_dialog import BumpFeeDialog, DSCancelDialog
@@ -250,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)
@@ -295,6 +296,26 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger, QtEventListener):
self.contacts.fetch_openalias(self.config)
# If the option hasn't been set yet
if not config.cv.AUTOMATIC_CENTRALIZED_UPDATE_CHECKS.is_set():
choice = self.question(title="Electrum - " + _("Enable update check"),
msg=_("For security reasons we advise that you always use the latest version of Electrum.") + " " +
_("Would you like to be notified when there is a newer version of Electrum available?"))
config.AUTOMATIC_CENTRALIZED_UPDATE_CHECKS = bool(choice)
self._update_check_thread = None
if config.AUTOMATIC_CENTRALIZED_UPDATE_CHECKS:
# The references to both the thread and the window need to be stored somewhere
# to prevent GC from getting in our way.
def on_version_received(v):
if UpdateCheck.is_newer(v):
self.update_check_button.setText(_("Update to Electrum {} is available").format(v))
self.update_check_button.clicked.connect(lambda: self.show_update_check(v))
self.update_check_button.show()
self._update_check_thread = UpdateCheckThread()
self._update_check_thread.checked.connect(on_version_received)
self._update_check_thread.start()
def run_coroutine_dialog(self, coro, text):
""" run coroutine in a waiting dialog, with a Cancel button that cancels the coroutine"""
from .util import RunCoroutineDialog
@@ -708,7 +729,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger, QtEventListener):
try:
new_path = self.wallet.save_backup(backup_dir)
except BaseException as reason:
self.show_critical(_("Electrum Purple was unable to copy your wallet file to the specified location.") + "\n" + str(reason), title=_("Unable to create backup"))
self.show_critical(_("Electrum was unable to copy your wallet file to the specified location.") + "\n" + str(reason), title=_("Unable to create backup"))
return
msg = _("A copy of your wallet file was created in")+" '%s'" % str(new_path)
self.show_message(msg, title=_("Wallet backup created"))
@@ -827,11 +848,12 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger, QtEventListener):
about_action.setMenuRole(QAction.MenuRole.AboutRole) # make sure OS recognizes it as "About"
self.help_menu.addAction(about_action)
self.help_menu.addAction(_("&Changelog"), lambda: webopen(constants.RELEASE_NOTES_URL))
self.help_menu.addAction(_("&Official website"), lambda: webopen("https://bitcoinpurpleblockchain.com/"))
self.help_menu.addAction(_("&Check for updates"), self.show_update_check)
self.help_menu.addAction(_("&Official website"), lambda: webopen("https://electrum.org"))
self.help_menu.addSeparator()
self.help_menu.addAction(_("&Documentation"), lambda: webopen("http://docs.electrum.org/")).setShortcut(QKeySequence.StandardKey.HelpContents)
if not constants.net.TESTNET:
self.help_menu.addAction(_("&Bitcoin Purple Whitepaper"), lambda: webopen("https://github.com/BitcoinPurpleBlockchain/purple-whitepaper/blob/main/whitepaper.pdf"))
self.help_menu.addAction(_("&Bitcoin Paper"), self.show_bitcoin_paper)
self.help_menu.addAction(_("&Report Bug"), self.show_report_bug)
self.help_menu.addSeparator()
if self.network:
@@ -850,24 +872,47 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger, QtEventListener):
self.show_error(_('No donation address for this server'))
def show_about(self):
QMessageBox.about(self, "Electrum Purple",
QMessageBox.about(self, "Electrum",
(_("Version")+" %s" % ELECTRUM_VERSION + "\n\n" +
_("Electrum Purple's focus is speed, with low resource usage and simplifying Bitcoin Purple.") + " " +
_("Electrum's focus is speed, with low resource usage and simplifying Bitcoin.") + " " +
_("You do not need to perform regular backups, because your wallet can be "
"recovered from a secret phrase that you can memorize or write on paper.") + " " +
_("Startup times are instant because it operates in conjunction with high-performance "
"servers that handle the most complicated parts of the Bitcoin Purple system.") + "\n\n" +
"servers that handle the most complicated parts of the Bitcoin system.") + "\n\n" +
_("Uses icons from the Icons8 icon pack (icons8.com).")))
def show_bitcoin_paper(self):
filename = os.path.join(self.config.path, 'bitcoin.pdf')
if not os.path.exists(filename):
def fetch_bitcoin_paper():
s = self._fetch_tx_from_network("54e48e5f5c656b26c3bca14a8c95aa583d07ebe84dde3b7dd4a78f4e4186e713")
if not s:
raise concurrent.futures.CancelledError
s = s.split("0100000000000000")[1:-1]
out = ''.join(x[6:136] + x[138:268] + x[270:400] if len(x) > 136 else x[6:] for x in s)[16:-20]
with open(filename, 'wb') as f:
f.write(bytes.fromhex(out))
WaitingDialog(
self,
_("Fetching Bitcoin Paper..."),
fetch_bitcoin_paper,
on_success=lambda _: webopen('file:///' + filename),
on_error=self.on_error,
)
return
webopen('file:///' + filename)
def show_update_check(self, version=None):
self.gui_object._update_check = UpdateCheck(latest_version=version)
def show_report_bug(self):
msg = ' '.join([
_("Please report any bugs as issues on github:<br/>"),
f'''<a href="{constants.GIT_REPO_ISSUES_URL}">{constants.GIT_REPO_ISSUES_URL}</a><br/><br/>''',
_("Before reporting a bug, upgrade to the most recent version of Electrum Purple (latest release or git HEAD), and include the version number in your report."),
_("Before reporting a bug, upgrade to the most recent version of Electrum (latest release or git HEAD), and include the version number in your report."),
_("Try to explain not only what the bug is, but how it occurs.")
])
self.show_message(msg, title="Electrum Purple - " + _("Reporting Bugs"), rich_text=True)
self.show_message(msg, title="Electrum - " + _("Reporting Bugs"), rich_text=True)
def notify_transactions(self):
if self.tx_notification_queue.qsize() == 0:
@@ -892,7 +937,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger, QtEventListener):
def notify(self, message):
if self.tray:
self.tray.showMessage("Electrum Purple", message, read_QIcon("electrum_dark_icon"), 20000)
self.tray.showMessage("Electrum", message, read_QIcon("electrum_dark_icon"), 20000)
def timer_actions(self):
# refresh invoices and requests because they show ETA
@@ -1256,7 +1301,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger, QtEventListener):
if not self.config.SWAPSERVER_URL and not self.config.SWAPSERVER_NPUB:
if not self.question('\n'.join([
_('Electrum Purple uses Nostr in order to find liquidity providers.'),
_('Electrum uses Nostr in order to find liquidity providers.'),
_('Do you want to enable Nostr?'),
])):
return None
@@ -1767,6 +1812,12 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger, QtEventListener):
self.search_box.hide()
sb.addPermanentWidget(self.search_box)
self.update_check_button = QPushButton("")
self.update_check_button.setFlat(True)
self.update_check_button.setCursor(QCursor(Qt.CursorShape.PointingHandCursor))
self.update_check_button.setIcon(read_QIcon("update.png"))
self.update_check_button.hide()
sb.addPermanentWidget(self.update_check_button)
self.password_required_button = QPushButton(_('Password required'))
self.password_required_button.setFlat(True)
@@ -1955,7 +2006,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger, QtEventListener):
line2 = QLineEdit()
line2.setFixedWidth(32 * char_width_in_lineedit())
address_label = QLabel(_("Address"))
address_label.setToolTip(_("Bitcoin Purple- or Lightning address"))
address_label.setToolTip(_("Bitcoin- or Lightning address"))
grid.addWidget(address_label, 1, 0)
grid.addWidget(line1, 1, 1)
grid.addWidget(QLabel(_("Name")), 2, 0)
@@ -1969,7 +2020,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger, QtEventListener):
assert not self.wallet.has_lightning()
if self.wallet.can_have_deterministic_lightning():
msg = _(
"Lightning is not enabled because this wallet was created with an old version of Electrum Purple. "
"Lightning is not enabled because this wallet was created with an old version of Electrum. "
"Create lightning keys?")
else:
msg = _(
@@ -2076,7 +2127,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger, QtEventListener):
"private key, and verifying with the corresponding public key. The "
"address you have entered does not have a unique public key, so these "
"operations cannot be performed.") + '\n\n' + \
_('The operation is undefined. Not just in Electrum Purple, but in general.')
_('The operation is undefined. Not just in Electrum, but in general.')
@protected
def do_sign(self, address, message, signature, password):
@@ -2238,7 +2289,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger, QtEventListener):
try:
return tx_from_any(data)
except BaseException as e:
self.show_critical(_("Electrum Purple was unable to parse your transaction") + ":\n" + repr(e))
self.show_critical(_("Electrum was unable to parse your transaction") + ":\n" + repr(e))
return
def import_channel_backup(self, encrypted: str):
@@ -2301,7 +2352,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger, QtEventListener):
with open(fileName, "rb") as f:
file_content = f.read() # type: bytes
except (ValueError, IOError, os.error) as reason:
self.show_critical(_("Electrum Purple was unable to open your transaction file") + "\n" + str(reason),
self.show_critical(_("Electrum was unable to open your transaction file") + "\n" + str(reason),
title=_("Unable to read file or no transaction found"))
if file_content is None:
return None
@@ -2460,7 +2511,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger, QtEventListener):
self.do_export_privkeys(filename, private_keys, csv_button.isChecked())
except (IOError, os.error) as reason:
txt = "\n".join([
_("Electrum Purple was unable to produce a private key-export."),
_("Electrum was unable to produce a private key-export."),
str(reason)
])
self.show_critical(txt, title=_("Unable to create csv"))
@@ -2649,7 +2700,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger, QtEventListener):
self.fx.trigger_update()
run_hook('close_settings_dialog')
if d.need_restart:
self.show_warning(_('Please restart Electrum Purple to activate the new GUI settings'), title=_('Success'))
self.show_warning(_('Please restart Electrum to activate the new GUI settings'), title=_('Success'))
else:
# Some values might need to be updated if settings have changed.
# For example 'Can send' in the lightning tab will change if the fees config is changed.
@@ -2665,7 +2716,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger, QtEventListener):
for warning in list(warnings)[:3]:
warning = ''.join([
_("Are you sure you want to close Electrum Purple?"),
_("Are you sure you want to close Electrum?"),
'\n\n',
_("An ongoing operation requires you to stay online."),
'\n',
@@ -2770,6 +2821,8 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger, QtEventListener):
self.qr_window.close()
self.close_wallet()
if self._update_check_thread:
self._update_check_thread.stop()
if self.tray:
self.tray = None
self.timer.stop()
@@ -2910,7 +2963,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger, QtEventListener):
self.showing_cert_mismatch_error = True
self.show_critical(title=_("Certificate mismatch"),
msg=_("The SSL certificate provided by the main server did not match the fingerprint passed in with the --serverfingerprint option.") + "\n\n" +
_("Electrum Purple will now exit."))
_("Electrum will now exit."))
self.showing_cert_mismatch_error = False
self.close()
+1 -1
View File
@@ -243,7 +243,7 @@ class ProxyWidget(QWidget):
grid.addWidget(self.proxy_cb, 0, 0, 1, 4)
proxy_helpbutton = HelpButton(
_('Proxy settings apply to all connections: with Electrum Purple servers, but also with third-party services.'))
_('Proxy settings apply to all connections: with Electrum servers, but also with third-party services.'))
grid.addWidget(proxy_helpbutton, 0, 4, alignment=Qt.AlignmentFlag.AlignRight)
grid.addWidget(self.proxy_mode, 1, 0, 1, 1)
grid.addWidget(self.proxy_host, 1, 1, 1, 3)
+1 -1
View File
@@ -243,7 +243,7 @@ class ChangePasswordDialogForSW(ChangePasswordDialogBase):
msg += ' ' + _('Use this dialog to add a password to your wallet.')
else:
if not is_encrypted:
msg = _('Your Bitcoin Purple coins are password protected. However, your wallet file is not encrypted.')
msg = _('Your bitcoins are password protected. However, your wallet file is not encrypted.')
else:
msg = _('Your wallet is password protected and encrypted.')
msg += ' ' + _('Use this dialog to change your password.')
+2 -2
View File
@@ -100,7 +100,7 @@ class PluginDialog(WindowModalDialog):
if not self.plugins.is_available(self.name):
msg = "\n".join([
_('This plugin requires installation of additional dependencies.'),
_('For Electrum Purple to recognize external packages, you need to run it from source.')
_('For Electrum to recognize external packages, you need to run it from source.')
])
self.window.show_message(msg)
return
@@ -161,7 +161,7 @@ class PluginsDialog(WindowModalDialog, MessageBoxMixin):
_logger = get_logger(__name__)
def __init__(self, config: 'SimpleConfig', plugins: 'Plugins', *, gui_object: Optional['ElectrumGui'] = None):
WindowModalDialog.__init__(self, None, _('Electrum Purple Plugins'))
WindowModalDialog.__init__(self, None, _('Electrum Plugins'))
self.gui_object = gui_object
self.config = config
self.plugins = plugins
+1 -1
View File
@@ -36,7 +36,7 @@ class QR_Window(QWidget):
def __init__(self, win):
QWidget.__init__(self)
self.main_window = win
self.setWindowTitle('Electrum Purple - '+_('Payment Request'))
self.setWindowTitle('Electrum - '+_('Payment Request'))
self.setMinimumSize(800, 800)
self.setFocusPolicy(Qt.FocusPolicy.NoFocus)
main_box = QHBoxLayout()
+3 -3
View File
@@ -193,8 +193,8 @@ class ReceiveTab(QWidget, MessageBoxMixin, Logger):
_('This information is seen by the recipient if you send them a signed payment request.'),
'\n\n',
_('For on-chain requests, the address gets reserved until expiration. After that, it might get reused.'), ' ',
_('The Bitcoin Purple address never expires and will always be part of this Electrum Purple wallet.'), ' ',
_('You can reuse a Bitcoin Purple address any number of times but it is not good for your privacy.'),
_('The bitcoin address never expires and will always be part of this electrum wallet.'), ' ',
_('You can reuse a bitcoin address any number of times but it is not good for your privacy.'),
'\n\n',
_('For Lightning requests, payments will not be accepted after the expiration.'),
])
@@ -284,7 +284,7 @@ class ReceiveTab(QWidget, MessageBoxMixin, Logger):
def get_tab_data(self):
if self.URI:
out = self.URI, self.URI, self.URI_help, _('Bitcoin Purple URI')
out = self.URI, self.URI, self.URI_help, _('Bitcoin URI')
elif self.addr:
out = self.addr, self.addr, self.address_help, _('Address')
else:
+2 -2
View File
@@ -200,9 +200,9 @@ class RequestList(MyTreeView):
menu = QMenu(self)
copy_menu = self.add_copy_menu(menu, idx)
if req.get_address():
copy_menu.addAction(_("Address"), lambda: self.main_window.do_copy(req.get_address(), title='Bitcoin Purple Address'))
copy_menu.addAction(_("Address"), lambda: self.main_window.do_copy(req.get_address(), title='Bitcoin Address'))
if URI := self.wallet.get_request_URI(req):
copy_menu.addAction(_("Bitcoin Purple URI"), lambda: self.main_window.do_copy(URI, title='Bitcoin Purple URI'))
copy_menu.addAction(_("Bitcoin URI"), lambda: self.main_window.do_copy(URI, title='Bitcoin URI'))
if req.is_lightning():
copy_menu.addAction(_("Lightning Request"), lambda: self.main_window.do_copy(self.wallet.get_bolt11_invoice(req), title='Lightning Request'))
#if 'view_url' in req:
+5 -5
View File
@@ -52,7 +52,7 @@ MSG_PASSPHRASE_WARN_ISSUE4566 = _("Warning") + ": "\
+ _("You have multiple consecutive whitespaces or leading/trailing "
"whitespaces in your passphrase.") + " " \
+ _("This is discouraged.") + " " \
+ _("Due to a bug, old versions of Electrum Purple will NOT be creating the "
+ _("Due to a bug, old versions of Electrum will NOT be creating the "
"same wallet as newer versions or other software.")
@@ -233,15 +233,15 @@ class SeedWidget(QWidget):
if self.seed_type == 'bip39':
message = ' '.join([
'<b>' + _('Warning') + ':</b> ',
_('BIP39 seeds can be imported in Electrum Purple, so that users can access funds locked in other wallets.'),
_('BIP39 seeds can be imported in Electrum, so that users can access funds locked in other wallets.'),
_('However, we do not generate BIP39 seeds, because they do not meet our safety standard.'),
_('BIP39 seeds do not include a version number, which compromises compatibility with future software.'),
_('We do not guarantee that BIP39 imports will always be supported in Electrum Purple.'),
_('We do not guarantee that BIP39 imports will always be supported in Electrum.'),
])
elif self.seed_type == 'slip39':
message = ' '.join([
'<b>' + _('Warning') + ':</b> ',
_('SLIP39 seeds can be imported in Electrum Purple, so that users can access funds locked in other wallets.'),
_('SLIP39 seeds can be imported in Electrum, so that users can access funds locked in other wallets.'),
_('However, we do not generate SLIP39 seeds.'),
])
else:
@@ -420,7 +420,7 @@ class KeysWidget(QWidget):
class SeedDialog(WindowModalDialog):
def __init__(self, parent, seed, passphrase, *, config: 'SimpleConfig'):
WindowModalDialog.__init__(self, parent, ('Electrum Purple - ' + _('Seed')))
WindowModalDialog.__init__(self, parent, ('Electrum - ' + _('Seed')))
self.setMinimumWidth(400)
vbox = QVBoxLayout(self)
title = _("Your wallet generation seed is:")
+2 -2
View File
@@ -73,7 +73,7 @@ class SendTab(QWidget, MessageBoxMixin, Logger):
msg = (_("Recipient of the funds.")
+ "\n\n"
+ _("This field can contain:") + "\n"
+ _("- a Bitcoin Purple address or BIP21 URI") + "\n"
+ _("- a Bitcoin address or BIP21 URI") + "\n"
+ _("- a Lightning invoice") + "\n"
+ _("- a label from your list of contacts") + "\n"
+ _("- an openalias") + "\n"
@@ -620,7 +620,7 @@ class SendTab(QWidget, MessageBoxMixin, Logger):
for o in outputs:
if o.scriptpubkey is None:
self.show_error(_('Bitcoin Purple Address is None'))
self.show_error(_('Bitcoin Address is None'))
return True
if o.value is None:
self.show_error(_('Invalid Amount'))
+1 -1
View File
@@ -122,7 +122,7 @@ class SettingsDialog(QDialog, QtEventListener):
if not use_trampoline:
if not window.question('\n'.join([
_("Are you sure you want to disable trampoline?"),
_("Without this option, Electrum Purple will need to sync with the Lightning network on every start."),
_("Without this option, Electrum will need to sync with the Lightning network on every start."),
_("This may impact the reliability of your payments."),
]), parent=self):
trampoline_cb.setCheckState(Qt.CheckState.Checked)
+1 -1
View File
@@ -449,7 +449,7 @@ def show_transaction(
d.broadcast_button.setVisible(False)
except SerializationError as e:
_logger.exception('unable to deserialize the transaction')
parent.show_critical(_("Electrum Purple was unable to deserialize the transaction:") + "\n" + str(e))
parent.show_critical(_("Electrum was unable to deserialize the transaction:") + "\n" + str(e))
except UserCancelled:
return
else:
+3 -3
View File
@@ -30,7 +30,7 @@ class UpdateCheck(QDialog, Logger):
def __init__(self, *, latest_version=None):
QDialog.__init__(self)
self.setWindowTitle('Electrum Purple - ' + _('Update Check'))
self.setWindowTitle('Electrum - ' + _('Update Check'))
self.content = QVBoxLayout()
self.content.setContentsMargins(*[10]*4)
@@ -88,10 +88,10 @@ class UpdateCheck(QDialog, Logger):
self.detail_label.setText(_("You can download the new version from {}.").format(url))
else:
self.heading_label.setText('<h2>' + _("Already up to date") + '</h2>')
self.detail_label.setText(_("You are already on the latest version of Electrum Purple."))
self.detail_label.setText(_("You are already on the latest version of Electrum."))
else:
self.heading_label.setText('<h2>' + _("Checking for updates...") + '</h2>')
self.detail_label.setText(_("Please wait while Electrum Purple checks for available updates."))
self.detail_label.setText(_("Please wait while Electrum checks for available updates."))
class UpdateCheckThread(QThread, Logger):
+2 -2
View File
@@ -82,13 +82,13 @@ class WalletInfoDialog(WindowModalDialog):
label.setIcon(read_QIcon('cloud_no'))
grid.addWidget(label, cur_row, 1)
if wallet.get_seed_type() == 'segwit':
msg = _("Your channels cannot be recovered from seed, because they were created with an old version of Electrum Purple. "
msg = _("Your channels cannot be recovered from seed, because they were created with an old version of Electrum. "
"This means that you must save a backup of your wallet every time you create a new channel.\n\n"
"If you want this wallet to have recoverable channels, you must close your existing channels and restore this wallet from seed")
else:
msg = _("Your channels cannot be recovered from seed. "
"This means that you must save a backup of your wallet every time you create a new channel.\n\n"
"If you want to have recoverable channels, you must create a new wallet with an Electrum Purple seed")
"If you want to have recoverable channels, you must create a new wallet with an Electrum seed")
grid.addWidget(HelpButton(msg), cur_row, 3)
cur_row += 1
grid.addWidget(WWLabel(_('Lightning Node ID:')), cur_row, 0)
+2 -2
View File
@@ -36,7 +36,7 @@ class QEServerConnectWizard(ServerConnectWizard, QEAbstractWizard):
class WCWelcome(WizardComponent):
def __init__(self, parent, wizard):
WizardComponent.__init__(self, parent, wizard, title='Network Configuration')
self.wizard_title = _('Electrum Purple Wallet')
self.wizard_title = _('Electrum Bitcoin Wallet')
self.first_help_label = QLabel()
self.first_help_label.setText(_("Optional settings to customize your network connection") + ":")
@@ -45,7 +45,7 @@ class WCWelcome(WizardComponent):
self.config_proxy_w = QCheckBox(_('Use Proxy'))
self.config_proxy_w.setChecked(False)
self.config_proxy_w.stateChanged.connect(self.on_updated)
self.config_server_w = QCheckBox(_('Select Electrum Purple Server'))
self.config_server_w = QCheckBox(_('Select Electrum Server'))
self.config_server_w.setChecked(False)
self.config_server_w.stateChanged.connect(self.on_updated)
options_w = QWidget()
+1 -1
View File
@@ -33,7 +33,7 @@ class QETermsOfUseWizard(TermsOfUseWizard, QEAbstractWizard):
class WCTermsOfUseScreen(WizardComponent):
def __init__(self, parent, wizard):
WizardComponent.__init__(self, parent, wizard, title='')
self.wizard_title = _('Electrum Purple Terms of Use')
self.wizard_title = _('Electrum Terms of Use')
self.img_label = QLabel()
pixmap = QPixmap(icon_path('electrum_darkblue_1.png'))
self.img_label.setPixmap(pixmap)
+5 -5
View File
@@ -37,7 +37,7 @@ if TYPE_CHECKING:
from electrum.plugin import Plugins, DeviceInfo
from electrum.gui.qt import QElectrumApplication
WIF_HELP_TEXT = (_('WIF keys are typed in Electrum Purple, based on script type.') + '\n\n' +
WIF_HELP_TEXT = (_('WIF keys are typed in Electrum, based on script type.') + '\n\n' +
_('A few examples') + ':\n' +
'p2pkh:KxZcY47uGp9a... \t-> 1DckmggQM...\n' +
'p2wpkh-p2sh:KxZcY47uGp9a... \t-> 3NhNeZQXF...\n' +
@@ -243,7 +243,7 @@ class WalletWizardComponent(WizardComponent, ABC):
class WCWalletName(WalletWizardComponent, Logger):
def __init__(self, parent, wizard):
WalletWizardComponent.__init__(self, parent, wizard, title=_('Electrum Purple wallet'))
WalletWizardComponent.__init__(self, parent, wizard, title=_('Electrum wallet'))
Logger.__init__(self)
path = wizard._path
@@ -393,7 +393,7 @@ class WCWalletType(WalletWizardComponent):
ChoiceItem(key='standard', label=_('Standard wallet')),
ChoiceItem(key='2fa', label=_('Wallet with two-factor authentication')),
ChoiceItem(key='multisig', label=_('Multi-signature wallet')),
ChoiceItem(key='imported', label=_('Import Bitcoin Purple addresses or private keys')),
ChoiceItem(key='imported', label=_('Import Bitcoin addresses or private keys')),
]
choices = [c for c in wallet_kinds if c.key in wallet_types]
@@ -962,9 +962,9 @@ class WCMultisig(WalletWizardComponent):
class WCImport(WalletWizardComponent):
def __init__(self, parent, wizard):
WalletWizardComponent.__init__(self, parent, wizard, title=_('Import Bitcoin Purple Addresses or Private Keys'))
WalletWizardComponent.__init__(self, parent, wizard, title=_('Import Bitcoin Addresses or Private Keys'))
message = _(
'Enter a list of Bitcoin Purple addresses (this will create a watching-only wallet), or a list of private keys.')
'Enter a list of Bitcoin addresses (this will create a watching-only wallet), or a list of private keys.')
header_layout = QHBoxLayout()
label = WWLabel(message)
label.setMinimumWidth(400)
+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
@@ -1562,7 +1562,7 @@ class Interface(Logger):
return ''
if not isinstance(res, str):
raise RequestCorrupted(f'{res!r} should be a str')
address = res.removeprefix(constants.net.BIP21_URI_SCHEME + ':')
address = res.removeprefix('bitcoin:')
if not bitcoin.is_address(address):
# note: do not hard-fail -- allow server to use future-type
# bitcoin address we do not recognize
+1 -1
View File
@@ -341,7 +341,7 @@ class Request(BaseInvoice):
if lightning_invoice:
extra['lightning'] = lightning_invoice
if not addr and lightning_invoice:
return f"{constants.net.BIP21_URI_SCHEME}:?lightning=" + lightning_invoice
return "bitcoin:?lightning="+lightning_invoice
if not addr and not lightning_invoice:
return None
uri = create_bip21_uri(addr, amount, message, extra_query_params=extra)
+1 -2
View File
@@ -7,7 +7,6 @@ from enum import IntEnum
from typing import NamedTuple, Optional, Callable, List, TYPE_CHECKING, Tuple, Union
from . import bitcoin
from . import constants
from .contacts import AliasNotFoundException
from .i18n import _
from .invoices import Invoice
@@ -250,7 +249,7 @@ class PaymentIdentifier(Logger):
self._type = PaymentIdentifierType.LNURL
self.lnurl = lnurl_url
self.set_state(PaymentIdentifierState.NEED_RESOLVE)
elif text.lower().startswith(constants.net.BIP21_URI_SCHEME + ':'):
elif text.lower().startswith(BITCOIN_BIP21_URI_SCHEME + ':'):
try:
out = parse_bip21_URI(text)
except InvalidBitcoinURI as e:
+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>
+23 -81
View File
@@ -1,40 +1,19 @@
# Quickstart - Electrum Purple from source
This guide creates a complete local `.venv` on Ubuntu for:
- desktop Qt GUI
- QML GUI
- hardware-wallet Python dependencies
- tests and coverage
# Quickstart Electrum (running from source)
## System prerequisites
```bash
sudo apt update
sudo apt install -y \
git python3 python3-venv python3-dev build-essential pkg-config automake libtool gettext \
libsecp256k1-dev libusb-1.0-0-dev libudev-dev libhidapi-dev libzbar0 \
libgl1 libegl1 libxkbcommon-x11-0 libxcb-cursor0 libxcb-xinerama0 \
libxcb-icccm4 libxcb-image0 libxcb-keysyms1 libxcb-randr0 \
libxcb-render-util0 libxcb-shape0 libxcb-xfixes0 qt6-wayland xvfb
sudo apt-get install git python3.12 python3.12-venv libsecp256k1-dev xvfb
```
Notes:
- `libsecp256k1-dev` avoids recompiling secp256k1 via `electrum_ecc`.
- `libzbar0` enables QR scanning/reading support.
- `xvfb` is useful for GUI/QML tests on headless systems.
- The `libxcb-*`, `libgl1`, `libegl1`, and `qt6-wayland` packages avoid common PyQt6 runtime errors on Ubuntu.
> `libsecp256k1-dev` avoids recompiling the C library locally.
> `xvfb` is only needed to run QML tests without a physical display.
---
## 1. Clone the repository
If you are already inside the repository, just run:
```bash
git submodule update --init --recursive
```
---
## 2. Create and activate the virtual environment
@@ -43,40 +22,29 @@ python3 -m venv .venv
source .venv/bin/activate
```
Upgrade packaging tools:
```bash
python -m pip install --upgrade pip setuptools wheel
python -m pip install -r contrib/requirements/requirements-build-base.txt
python -m pip install "Cython>=0.27"
```
---
## 3. Install dependencies
### Complete development environment
### Tests only (no GUI)
```bash
ELECTRUM_ECC_DONT_COMPILE=1 python -m pip install -e ".[full,qml_gui,tests]"
python -m pip install -r contrib/requirements/requirements-ci.txt
python -m pip install pytest-xdist pillow
ELECTRUM_ECC_DONT_COMPILE=1 pip install -r contrib/requirements/requirements.txt \
"cryptography>=2.6" "dnspython[DNSSEC]>=2.2,<2.5" \
pytest coverage \
"pycryptodomex>=3.7" pyaes \
&& ELECTRUM_ECC_DONT_COMPILE=1 pip install -e .
```
What this installs:
- base runtime dependencies from `contrib/requirements/requirements.txt`
- `full`: Qt GUI, crypto, and hardware-wallet Python dependencies
- `qml_gui`: PyQt6/Qt6 packages suitable for the QML GUI
- `tests`: extra Python packages used by the test suite
- `requirements-ci.txt`: `pytest`, `coverage`, and `coveralls`
- `pytest-xdist`: optional parallel test execution with `-n auto`
- `pillow`: optional image support used by some hardware-wallet/plugin flows
Verify the main imports:
### Tests + Qt/QML GUI (Android)
```bash
python -c "import PyQt6.QtCore, PyQt6.QtQml, PyQt6.QtQuick, PyQt6.QtMultimedia, electrum_ecc, cryptography; print('ok')"
ELECTRUM_ECC_DONT_COMPILE=1 pip install -r contrib/requirements/requirements.txt \
"cryptography>=2.6" "dnspython[DNSSEC]>=2.2,<2.5" \
pytest coverage \
"pycryptodomex>=3.7" pyaes \
"pyqt6~=6.10" "pyqt6-qt6~=6.10" \
&& ELECTRUM_ECC_DONT_COMPILE=1 pip install -e .
```
---
@@ -87,17 +55,14 @@ python -c "import PyQt6.QtCore, PyQt6.QtQml, PyQt6.QtQuick, PyQt6.QtMultimedia,
# Qt GUI (default)
./run_electrum
# Qt GUI on BitcoinPurple network
./run_electrum --bitcoinpurple -g qt
# BitcoinPurple network
./run_electrum --bitcoinpurple
# BitcoinPurple testnet
./run_electrum --bitcoinpurple_testnet
# QML GUI
./run_electrum --bitcoinpurple -g qml
# QML GUI (Android-style)
./run_electrum --gui qml
# Text UI (terminal)
./run_electrum --gui text
@@ -106,13 +71,6 @@ python -c "import PyQt6.QtCore, PyQt6.QtQml, PyQt6.QtQuick, PyQt6.QtMultimedia,
./run_electrum daemon -d
```
After the editable install, the generated command should also work while the venv is active:
```bash
electrum-purple --bitcoinpurple -g qt
electrum-purple --bitcoinpurple -g qml
```
---
## 5. Run tests
@@ -130,27 +88,11 @@ pytest tests/test_bitcoin.py -v
# BitcoinPurple tests
pytest tests/test_bitcoinpurple.py -v
# Blockchain + Bitcoin + BitcoinPurple together
# blockchain + bitcoin + BitcoinPurple together
pytest tests/test_blockchain.py tests/test_bitcoin.py tests/test_bitcoinpurple.py -v
# QML tests
QT_QPA_PLATFORM=offscreen pytest tests/qml -v
# QML tests on a headless system
xvfb-run -a pytest tests/qml -v
```
Run tests with coverage:
```bash
# Full suite with coverage
coverage run --source=electrum -m pytest tests -v
coverage report -m
coverage html
# QML tests with coverage
QT_QPA_PLATFORM=offscreen coverage run --source=electrum -m pytest tests/qml -v
coverage report -m
# QML tests (requires PyQt6 and xvfb)
xvfb-run pytest tests/qml/ -v
```
---
+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):