Compare commits
3 Commits
079ec730db
...
227e9cc356
| Author | SHA1 | Date | |
|---|---|---|---|
| 227e9cc356 | |||
| 9115faa6c8 | |||
| 2833c96d51 |
@@ -17,7 +17,30 @@ RUN python3 -m venv venv && \
|
||||
|
||||
# Compile Python CLI into a standalone binary
|
||||
RUN venv/bin/pip install --no-cache-dir pyinstaller && \
|
||||
venv/bin/pyinstaller --onefile --name cli src/cli.py && \
|
||||
venv/bin/pyinstaller \
|
||||
--onefile --name cli \
|
||||
--hidden-import _cffi_backend \
|
||||
--hidden-import coincurve._cffi_backend \
|
||||
--hidden-import src.crypto \
|
||||
--hidden-import src.hd_wallet \
|
||||
--hidden-import src.p2pk \
|
||||
--hidden-import src.p2pkh \
|
||||
--hidden-import src.p2sh \
|
||||
--hidden-import src.p2wpkh \
|
||||
--hidden-import src.p2tr \
|
||||
--hidden-import src.single_wallet \
|
||||
--collect-data bip_utils \
|
||||
--collect-submodules src \
|
||||
--collect-submodules coincurve \
|
||||
--collect-binaries coincurve \
|
||||
--collect-data coincurve \
|
||||
src/cli.py && \
|
||||
dist/cli hd_generate '{}' > /tmp/cli-hd-generate.json && \
|
||||
cat /tmp/cli-hd-generate.json && \
|
||||
grep -Eq '"ok"[[:space:]]*:[[:space:]]*true' /tmp/cli-hd-generate.json && \
|
||||
dist/cli p2pkh '{}' > /tmp/cli-p2pkh.json && \
|
||||
cat /tmp/cli-p2pkh.json && \
|
||||
grep -Eq '"ok"[[:space:]]*:[[:space:]]*true' /tmp/cli-p2pkh.json && \
|
||||
mkdir -p frontend/resources && \
|
||||
cp dist/cli frontend/resources/cli
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
FROM node:22.14.0-bookworm
|
||||
|
||||
ENV WINEPREFIX=/root/.wine
|
||||
ENV WINEPREFIX=/home/node/.wine
|
||||
ENV WINEDEBUG=-all
|
||||
|
||||
# System dependencies + Wine + Xvfb
|
||||
@@ -8,7 +8,7 @@ RUN dpkg --add-architecture i386 && \
|
||||
apt-get update && apt-get install -y --no-install-recommends \
|
||||
wine wine32 wine64 \
|
||||
python3 python3-pip python3-venv libpython3.11 \
|
||||
binutils wget xvfb xauth cabextract ca-certificates \
|
||||
binutils wget xvfb xauth cabextract ca-certificates unzip \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Install winetricks manually (removed from Debian bookworm apt)
|
||||
@@ -19,37 +19,93 @@ RUN wget -q https://raw.githubusercontent.com/Winetricks/winetricks/master/src/w
|
||||
WORKDIR /build
|
||||
|
||||
# Copy repo
|
||||
COPY . .
|
||||
COPY --chown=node:node . .
|
||||
RUN chown -R node:node /build
|
||||
|
||||
# Wine/winetricks are unstable as root; use the image's non-root user.
|
||||
USER node
|
||||
|
||||
# Python venv (Linux, for local tools only)
|
||||
RUN python3 -m venv venv && \
|
||||
venv/bin/pip install --no-cache-dir -r requirements.txt
|
||||
venv/bin/pip install --no-cache-dir -r requirements.txt pyinstaller
|
||||
|
||||
# Pre-download Windows wheels so Wine Python can install fully offline.
|
||||
RUN mkdir -p wheelhouse && \
|
||||
venv/bin/pip freeze > win-lock.txt && \
|
||||
printf "pip\nsetuptools\nwheel\n" >> win-lock.txt && \
|
||||
while read -r pkg; do \
|
||||
venv/bin/pip download \
|
||||
--dest wheelhouse \
|
||||
--platform win_amd64 --implementation cp --python-version 3.11 --abi cp311 \
|
||||
--no-deps "$pkg"; \
|
||||
done < win-lock.txt && \
|
||||
# PyInstaller has Windows-only deps not present in Linux freeze output.
|
||||
for win_pkg in pefile pywin32-ctypes colorama; do \
|
||||
venv/bin/pip download \
|
||||
--dest wheelhouse \
|
||||
--platform win_amd64 --implementation cp --python-version 3.11 --abi cp311 \
|
||||
--no-deps "$win_pkg"; \
|
||||
done
|
||||
|
||||
# Bootstrap Wine prefix
|
||||
RUN wineboot --init 2>/dev/null || true
|
||||
|
||||
# Install Visual C++ 2019 runtime (required by Python 3.11)
|
||||
RUN xvfb-run winetricks -q vcrun2019
|
||||
# vcrun2019 currently returns status 243 on Wine 8 (Debian bookworm).
|
||||
# Keep it best-effort so the build can proceed if Python installer already bundles needed runtime.
|
||||
RUN xvfb-run -a winetricks -q vcrun2019 || true
|
||||
|
||||
# Install Python for Windows under Wine (silent, no GUI)
|
||||
RUN wget -q "https://www.python.org/ftp/python/3.11.9/python-3.11.9-amd64.exe" -O /tmp/py.exe && \
|
||||
xvfb-run wine /tmp/py.exe /quiet InstallAllUsers=1 PrependPath=1 TargetDir="C:\\Python311" && \
|
||||
rm /tmp/py.exe
|
||||
# Install Python for Windows using embeddable zip (avoids installer/runtime failures under Wine)
|
||||
RUN mkdir -p "$WINEPREFIX/drive_c/Python311" && \
|
||||
wget -q "https://www.python.org/ftp/python/3.11.9/python-3.11.9-embed-amd64.zip" -O /tmp/pyembed.zip && \
|
||||
unzip -q /tmp/pyembed.zip -d "$WINEPREFIX/drive_c/Python311" && \
|
||||
rm /tmp/pyembed.zip && \
|
||||
sed -i 's/^#import site/import site/' "$WINEPREFIX/drive_c/Python311/python311._pth" && \
|
||||
wget -q "https://bootstrap.pypa.io/pip/pip.pyz" -O "$WINEPREFIX/drive_c/pip.pyz" && \
|
||||
xvfb-run -a wine "C:\\Python311\\python.exe" --version
|
||||
|
||||
# Install Python packaging toolchain first (needed for sdist like crcmod).
|
||||
RUN xvfb-run -a wine "C:\\Python311\\python.exe" "C:\\pip.pyz" install --no-index \
|
||||
--find-links="Z:\\build\\wheelhouse" \
|
||||
pip setuptools wheel
|
||||
|
||||
# Install Python deps + PyInstaller under Wine Python
|
||||
RUN xvfb-run wine "C:\\Python311\\python.exe" -m pip install --no-cache-dir -r requirements.txt pyinstaller
|
||||
RUN xvfb-run -a wine "C:\\Python311\\python.exe" "C:\\pip.pyz" install --no-index --no-build-isolation \
|
||||
--find-links="Z:\\build\\wheelhouse" \
|
||||
-r "Z:\\build\\win-lock.txt"
|
||||
|
||||
# Compile Python CLI into Windows binary
|
||||
RUN xvfb-run wine "C:\\Python311\\python.exe" -m PyInstaller \
|
||||
--onefile --name cli src/cli.py && \
|
||||
RUN xvfb-run -a wine "C:\\Python311\\python.exe" -m PyInstaller \
|
||||
--onefile --name cli \
|
||||
--hidden-import _cffi_backend \
|
||||
--hidden-import coincurve._cffi_backend \
|
||||
--hidden-import src.crypto \
|
||||
--hidden-import src.hd_wallet \
|
||||
--hidden-import src.p2pk \
|
||||
--hidden-import src.p2pkh \
|
||||
--hidden-import src.p2sh \
|
||||
--hidden-import src.p2wpkh \
|
||||
--hidden-import src.p2tr \
|
||||
--hidden-import src.single_wallet \
|
||||
--collect-data bip_utils \
|
||||
--collect-submodules src \
|
||||
--collect-submodules coincurve \
|
||||
--collect-binaries coincurve \
|
||||
--collect-data coincurve \
|
||||
src/cli.py && \
|
||||
script -qec 'xvfb-run -a wine cmd /c "Z:\\build\\dist\\cli.exe hd_generate {}"' /tmp/cli-hd-generate.log && \
|
||||
cat /tmp/cli-hd-generate.log && \
|
||||
grep -Eq '"ok"[[:space:]]*:[[:space:]]*true' /tmp/cli-hd-generate.log && \
|
||||
script -qec 'xvfb-run -a wine cmd /c "Z:\\build\\dist\\cli.exe p2pkh {}"' /tmp/cli-p2pkh.log && \
|
||||
cat /tmp/cli-p2pkh.log && \
|
||||
grep -Eq '"ok"[[:space:]]*:[[:space:]]*true' /tmp/cli-p2pkh.log && \
|
||||
mkdir -p frontend/resources && \
|
||||
cp dist/cli.exe frontend/resources/cli.exe
|
||||
|
||||
# JS dependencies + electron-builder
|
||||
RUN cd frontend && npm ci && npm install --no-save electron-builder
|
||||
|
||||
# Build Windows NSIS installer
|
||||
RUN cd frontend && npx vite build && npx electron-builder --win nsis --publish never
|
||||
# Build Windows installer + standalone portable executable
|
||||
RUN cd frontend && npx vite build && npx electron-builder --win nsis portable --publish never
|
||||
|
||||
# Export installer
|
||||
# Export build artifacts
|
||||
CMD cp frontend/release/*.exe /out/
|
||||
|
||||
@@ -14,5 +14,5 @@ docker run --rm \
|
||||
-v "$OUT_DIR:/out" \
|
||||
wallet-gen-builder-win
|
||||
|
||||
echo "Installer saved to: $OUT_DIR"
|
||||
echo "Windows artifacts saved to: $OUT_DIR"
|
||||
ls "$OUT_DIR"/*.exe
|
||||
|
||||
4
frontend/package-lock.json
generated
4
frontend/package-lock.json
generated
@@ -1,11 +1,11 @@
|
||||
{
|
||||
"name": "bitcoin-address-generator",
|
||||
"name": "wallet-gen",
|
||||
"version": "1.0.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "bitcoin-address-generator",
|
||||
"name": "wallet-gen",
|
||||
"version": "1.0.0",
|
||||
"dependencies": {
|
||||
"react": "^19.2.0",
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
"main": "electron/main.cjs",
|
||||
"scripts": {
|
||||
"vite": "vite",
|
||||
"dev": "concurrently --kill-others -n Vite,Electron \"vite\" \"wait-on http://localhost:5173 && cross-env NODE_ENV=development electron .\"",
|
||||
"dev": "concurrently --kill-others -n Vite,Electron \"vite\" \"wait-on http://localhost:5173 && cross-env NODE_ENV=development ELECTRON_RUN_AS_NODE= electron .\"",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview",
|
||||
"dist": "vite build && electron-builder"
|
||||
@@ -27,7 +27,7 @@
|
||||
"category": "Finance"
|
||||
},
|
||||
"win": {
|
||||
"target": "nsis"
|
||||
"target": ["nsis", "portable"]
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
|
||||
96
src/cli.py
96
src/cli.py
@@ -4,44 +4,84 @@ Usage: python src/cli.py <command> <json_args>
|
||||
Prints a single JSON line to stdout.
|
||||
"""
|
||||
|
||||
import importlib
|
||||
import json
|
||||
import sys
|
||||
|
||||
try:
|
||||
from src.hd_wallet import generate_hd_wallet, encrypt_wallet, decrypt_wallet
|
||||
from src.p2pk import generate_p2pk
|
||||
from src.p2pkh import generate_legacy_address
|
||||
from src.p2sh import generate_p2sh_multisig
|
||||
from src.p2wpkh import generate_segwit_address
|
||||
from src.p2tr import generate_p2tr_address
|
||||
from src.single_wallet import encrypt_single_wallet, decrypt_single_wallet
|
||||
except ImportError:
|
||||
from hd_wallet import generate_hd_wallet, encrypt_wallet, decrypt_wallet
|
||||
from p2pk import generate_p2pk
|
||||
from p2pkh import generate_legacy_address
|
||||
from p2sh import generate_p2sh_multisig
|
||||
from p2wpkh import generate_segwit_address
|
||||
from p2tr import generate_p2tr_address
|
||||
from single_wallet import encrypt_single_wallet, decrypt_single_wallet
|
||||
def _load_function(module_name, function_name):
|
||||
"""
|
||||
Import only what is needed for the selected command.
|
||||
This avoids hard-failing all commands when an optional dependency is missing.
|
||||
"""
|
||||
try:
|
||||
module = importlib.import_module(f"src.{module_name}")
|
||||
except ModuleNotFoundError as err:
|
||||
if err.name not in {f"src.{module_name}", "src"}:
|
||||
raise
|
||||
module = importlib.import_module(module_name)
|
||||
return getattr(module, function_name)
|
||||
|
||||
|
||||
def _run_hd_generate(args):
|
||||
return _load_function("hd_wallet", "generate_hd_wallet")(**args)
|
||||
|
||||
|
||||
def _run_hd_encrypt(args):
|
||||
return _load_function("hd_wallet", "encrypt_wallet")(args["wallet"], args["password"])
|
||||
|
||||
|
||||
def _run_hd_decrypt(args):
|
||||
return _load_function("hd_wallet", "decrypt_wallet")(args["wallet"], args["password"])
|
||||
|
||||
|
||||
def _run_p2pk(args):
|
||||
return _load_function("p2pk", "generate_p2pk")(**args)
|
||||
|
||||
|
||||
def _run_p2pkh(args):
|
||||
return _load_function("p2pkh", "generate_legacy_address")(**args)
|
||||
|
||||
|
||||
def _run_p2sh(args):
|
||||
return _load_function("p2sh", "generate_p2sh_multisig")(**args)
|
||||
|
||||
|
||||
def _run_p2wpkh(args):
|
||||
return _load_function("p2wpkh", "generate_segwit_address")(**args)
|
||||
|
||||
|
||||
def _run_p2tr(args):
|
||||
return _load_function("p2tr", "generate_p2tr_address")(**args)
|
||||
|
||||
|
||||
def _run_single_encrypt(args):
|
||||
return _load_function("single_wallet", "encrypt_single_wallet")(args["wallet"], args["password"])
|
||||
|
||||
|
||||
def _run_single_decrypt(args):
|
||||
return _load_function("single_wallet", "decrypt_single_wallet")(args["wallet"], args["password"])
|
||||
|
||||
|
||||
COMMANDS = {
|
||||
'hd_generate': lambda a: generate_hd_wallet(**a),
|
||||
'hd_encrypt': lambda a: encrypt_wallet(a['wallet'], a['password']),
|
||||
'hd_decrypt': lambda a: decrypt_wallet(a['wallet'], a['password']),
|
||||
'p2pk': lambda a: generate_p2pk(**a),
|
||||
'p2pkh': lambda a: generate_legacy_address(**a),
|
||||
'p2sh': lambda a: generate_p2sh_multisig(**a),
|
||||
'p2wpkh': lambda a: generate_segwit_address(**a),
|
||||
'p2tr': lambda a: generate_p2tr_address(**a),
|
||||
'single_encrypt': lambda a: encrypt_single_wallet(a['wallet'], a['password']),
|
||||
'single_decrypt': lambda a: decrypt_single_wallet(a['wallet'], a['password']),
|
||||
"hd_generate": _run_hd_generate,
|
||||
"hd_encrypt": _run_hd_encrypt,
|
||||
"hd_decrypt": _run_hd_decrypt,
|
||||
"p2pk": _run_p2pk,
|
||||
"p2pkh": _run_p2pkh,
|
||||
"p2sh": _run_p2sh,
|
||||
"p2wpkh": _run_p2wpkh,
|
||||
"p2tr": _run_p2tr,
|
||||
"single_encrypt": _run_single_encrypt,
|
||||
"single_decrypt": _run_single_decrypt,
|
||||
}
|
||||
|
||||
if __name__ == '__main__':
|
||||
command = sys.argv[1]
|
||||
args = json.loads(sys.argv[2]) if len(sys.argv) > 2 else {}
|
||||
try:
|
||||
if command not in COMMANDS:
|
||||
raise ValueError(f"Unsupported command: {command}")
|
||||
result = COMMANDS[command](args)
|
||||
print(json.dumps({'ok': True, 'data': result}))
|
||||
print(json.dumps({"ok": True, "data": result}))
|
||||
except Exception as e:
|
||||
print(json.dumps({'ok': False, 'error': str(e)}))
|
||||
print(json.dumps({"ok": False, "error": str(e)}))
|
||||
|
||||
Reference in New Issue
Block a user