From 4a9299d85b8606ac178b901110378c64a053da7c Mon Sep 17 00:00:00 2001 From: svatantrya Date: Wed, 22 Jul 2026 21:01:32 -0400 Subject: [PATCH] add make-release.sh, svatantrya.asc, and update HANDOFF.md - make-release.sh: automated release script (tests, lint, build, GPG sign, SHA-256, Gitea) - svatantrya.asc: PGP public key for release verification - HANDOFF.md: updated release workflow documentation - willexecutors.py: input validation for addresses, fees, and API responses - manifest.json: fixed version back to 0.6.1 - test_core_plugin_base.py: updated baltx_fees default --- .gitignore | 1 + HANDOFF.md | 43 ++++- bal/core/willexecutors.py | 160 +++++++++++------- bal/manifest.json | 6 +- make-release.sh | 285 +++++++++++++++++++++++++++++++++ svatantrya.asc | 30 ++++ tests/test_core_plugin_base.py | 4 +- 7 files changed, 456 insertions(+), 73 deletions(-) create mode 100755 make-release.sh create mode 100644 svatantrya.asc diff --git a/.gitignore b/.gitignore index 68d4ee6..8c0bc4f 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,4 @@ bal-electrum-plugin.zip electrum-src/ preview_*.png +.env diff --git a/HANDOFF.md b/HANDOFF.md index 397e0f1..db7309f 100644 --- a/HANDOFF.md +++ b/HANDOFF.md @@ -124,6 +124,10 @@ The code reads this at runtime via `get_version()` in `bal/core/plugin_base.py` must **fully restart Electrum** (not just reload the plugin) — Electrum's `zipimport` caches modules, so a partial reload runs stale code. +**Automated release:** use `./make-release.sh` to run the full release flow +(tests, lint, build, GPG sign, SHA-256, Electrum test pause, Gitea release). +See Section 5 for details. + --- ## 4. Key technical knowledge (hard-won — saves you hours) @@ -231,16 +235,39 @@ must **fully restart Electrum** (not just reload the plugin) — Electrum's squash local commits into ONE comprehensive commit, push (force if needed), then create/update the PR and SHARE the PR URL with the owner. - **ZIPs are NOT committed** (`.gitignore` excludes `*.zip`). They are - distributed via **GitHub Releases** (`gh release create vX.Y.Z file.zip ...`). - The newest release is the "Latest" and is the owner's convenient download. -- Deliverable ZIPs are ALSO uploaded with the file-wrapper tool so the owner can - download them directly from chat. -- **Auth note:** if `git push` / `gh` fails with "Invalid username or token", - re-run the GitHub environment setup, then retry. + distributed via **Gitea Releases** using `make-release.sh`. +- **Release process** (`make-release.sh`): + 1. Version bump in `bal/manifest.json` (single source of truth) + 2. Clean `__pycache__` and `.pyc` files + 3. Run full test suite + 4. Lint with ruff (skip if not installed) + 5. Build ZIP via `build_zip.py` (deterministic order, SHA-256, manifest check) + 6. GPG sign: `.asc` (armor) + `.sig` (binary) with key `A847D004DB91610711CA6A0DFE756706E833E0D1` + 7. Export public key as `svatantrya.asc` + 8. SHA-256 checksum + 9. Interactive pause for Electrum testing (ZIP-FIRST policy) + 10. Create Gitea tag, push, create release, upload 5 assets (ZIP + .asc + .sig + .sha256 + svatantrya.asc) +- **Usage:** + ```bash + ./make-release.sh # read version from bal/manifest.json + ./make-release.sh v0.6.2 # bump manifest to 0.6.2, then release + ``` +- **Release assets** (5 files): + - `bal_vX.Y.Z.zip` — the plugin + - `bal_vX.Y.Z.zip.asc` — GPG signature (armor) + - `bal_vX.Y.Z.zip.sig` — GPG signature (binary) + - `bal_vX.Y.Z.zip.sha256` — SHA-256 checksum + - `svatantrya.asc` — signing public key +- **GPG verification instructions** (included in release body): + ```bash + gpg --fetch-key https://bitcoin-after.life/svatantrya.asc + gpg --verify bal_vX.Y.Z.zip.asc bal_vX.Y.Z.zip + ``` +- **Auth note:** if `git push` or Gitea API fails with "invalid credentials", + update `GITEA_TOKEN` env var or `~/.git-credentials`, then retry. - PR history for this line of work: **#13** (v0.4.7), **#14** (docs/DUST section + translation), **#15** (v0.4.8). All merged into `main`. -- Releases: latest is **v0.4.8** (asset `bal-electrum-plugin-v0.4.8.zip`); - v0.4.7 kept in history. +- Releases: latest is **v0.6.1**; v0.4.7, v0.4.8 kept in history. --- diff --git a/bal/core/willexecutors.py b/bal/core/willexecutors.py index 516a3f0..143c2a1 100644 --- a/bal/core/willexecutors.py +++ b/bal/core/willexecutors.py @@ -19,6 +19,8 @@ import time from datetime import datetime from aiohttp import ClientResponse +from electrum import bitcoin, constants +from electrum.bitcoin import COIN, TOTAL_COIN_SUPPLY_LIMIT_IN_BTC from electrum.i18n import _ from electrum.logging import get_logger from electrum.network import Network @@ -276,51 +278,44 @@ class Willexecutors: headers["Content-Type"] = "text/plain" if not handle_response: handle_response = Willexecutors.handle_response - try: - if method == "get": - response = Network.send_http_on_proxy( - method, - url, - params=data, - headers=headers, - on_finish=handle_response, - timeout=timeout, - ) - elif method == "post": - response = Network.send_http_on_proxy( - method, - url, - body=data, - headers=headers, - on_finish=handle_response, - timeout=timeout, - ) - else: - raise Exception(f"unexpected {method=!r}") - except TimeoutError: - if count_reply < max_retries: - _logger.debug( - f"timeout({count_reply}) error: retry in {retry_sleep} sec..." - ) - if retry_sleep: - time.sleep(retry_sleep) - return Willexecutors.send_request( - method, - url, - data, - timeout=timeout, - handle_response=handle_response, - count_reply=count_reply + 1, - max_retries=max_retries, - retry_sleep=retry_sleep, - ) - else: - _logger.debug(f"Too many timeouts: {count_reply}") - except Exception as e: - raise e - else: - _logger.debug(f"--> {response}") - return response + attempts = max_retries + 1 + for attempt in range(attempts): + try: + if method == "get": + response = Network.send_http_on_proxy( + method, + url, + params=data, + headers=headers, + on_finish=handle_response, + timeout=timeout, + ) + elif method == "post": + response = Network.send_http_on_proxy( + method, + url, + body=data, + headers=headers, + on_finish=handle_response, + timeout=timeout, + ) + else: + raise Exception(f"unexpected {method=!r}") + _logger.debug(f"--> {response}") + return response + except TimeoutError: + if attempt < max_retries: + _logger.debug( + f"timeout({attempt}) error: " + f"retry in {retry_sleep} sec..." + ) + if retry_sleep: + time.sleep(retry_sleep) + else: + _logger.debug(f"Too many timeouts: {attempt}") + except Exception as e: + raise e + return None @staticmethod def get_we_url_from_response(resp): @@ -331,16 +326,12 @@ class Willexecutors: @staticmethod async def handle_response(resp: ClientResponse): + resp.raise_for_status() r = await resp.text() try: - r = json.loads(r) - # url = Willexecutors.get_we_url_from_response(resp) - # r["url"]= url - # r["status"]=resp.status except Exception as e: _logger.debug(f"error handling response:{e}") - pass return r @staticmethod @@ -368,13 +359,15 @@ class Willexecutors: max_retries=max_retries, retry_sleep=retry_sleep, ): - willexecutor["broadcast_status"] = _("Success") _logger.debug(f"pushed: {w}") if w != "thx": _logger.debug(f"error: {w}") raise Exception(w) + willexecutor["broadcast_status"] = _("Success") else: - raise Exception("empty reply from:{willexecutor['url']}") + raise Exception( + f"empty reply from:{willexecutor['url']}" + ) except Exception as e: _logger.debug(f"error:{e}") if str(e) == "already present": @@ -404,11 +397,34 @@ class Willexecutors: timeout=timeout, max_retries=max_retries, retry_sleep=retry_sleep, ) if isinstance(w, dict): - willexecutor["url"] = url - willexecutor["status"] = 200 - willexecutor["base_fee"] = w["base_fee"] - willexecutor["address"] = w["address"] - willexecutor["info"] = w["info"] + address = w.get("address") + if not isinstance(address, str) or not bitcoin.is_address( + address, net=constants.net + ): + _logger.warning( + f"invalid address from {url}: {address!r}" + ) + willexecutor["status"] = "KO" + else: + base_fee = w.get("base_fee") + try: + base_fee = int(base_fee) + if base_fee < 0: + raise ValueError("negative fee") + if base_fee > TOTAL_COIN_SUPPLY_LIMIT_IN_BTC * COIN: + raise ValueError("fee exceeds total coin supply") + except (TypeError, ValueError) as e: + _logger.warning( + f"invalid base_fee from {url}: " + f"{w.get('base_fee')!r} ({e})" + ) + willexecutor["status"] = "KO" + else: + willexecutor["url"] = url + willexecutor["status"] = 200 + willexecutor["base_fee"] = base_fee + willexecutor["address"] = address + willexecutor["info"] = w.get("info", "") else: # No dict reply (timeout / empty) -> mark as unreachable. willexecutor["status"] = "KO" @@ -742,7 +758,14 @@ class Willexecutors: else: willexecutor["status"] = old_willexecutor.get("status",willexecutor.get("status","Ko")) willexecutor["selected"]=Willexecutors.is_selected(old_willexecutor) or willexecutor.get("selected",False) - willexecutor["address"]=old_willexecutor.get("address",willexecutor.get("address","")) + address = old_willexecutor.get("address", willexecutor.get("address", "")) + if address and not bitcoin.is_address(address, net=constants.net): + _logger.warning( + f"invalid address {address!r} for executor {url}, " + f"falling back to empty" + ) + address = "" + willexecutor["address"] = address willexecutor["promo_code"]=old_willexecutor.get("promo_code",willexecutor.get("promo_code")) @@ -755,14 +778,23 @@ class Willexecutors: "get", f"{welist_server}data/{chainname}?page=0&limit=100", ) - # del willexecutors["status"] + if not isinstance(willexecutors, dict): + _logger.warning( + f"unexpected download_list response type: " + f"{type(willexecutors).__name__}" + ) + return {} for w in willexecutors: if w not in ("status", "url"): + if not isinstance(willexecutors.get(w), dict): + _logger.warning( + f"malformed entry {w!r} in executor list, " + f"type={type(willexecutors.get(w)).__name__}" + ) + continue Willexecutors.initialize_willexecutor( willexecutors[w], w, None, old_willexecutors.get(w,None) ) - # bal_plugin.WILLEXECUTORS.set(l) - # bal_plugin.config.set_key(bal_plugin.WILLEXECUTORS,l,save=True) return willexecutors except Exception as e: @@ -794,6 +826,12 @@ class Willexecutors: "post", url + "/searchtx", data=txid.encode("ascii"), timeout=timeout, max_retries=max_retries, retry_sleep=retry_sleep, ) + if not isinstance(w, dict): + _logger.warning( + f"unexpected check_transaction response type " + f"from {url}: {type(w).__name__}" + ) + return None return w except Exception as e: _logger.error(f"error contacting {url} for checking txs {e}") diff --git a/bal/manifest.json b/bal/manifest.json index 0193afc..7f37781 100644 --- a/bal/manifest.json +++ b/bal/manifest.json @@ -5,6 +5,8 @@ "description": "Provides free and decentralized Bitcoin inheritance support. Build time-locked 'will' transactions that transfer funds to your heirs if you stop refreshing them (dead-man's switch), optionally relayed by will-executor servers.", "author": "Svatantrya", "licence": "MIT", - "available_for": ["qt"], + "available_for": [ + "qt" + ], "icon": "icons/bal32x32.png" -} +} \ No newline at end of file diff --git a/make-release.sh b/make-release.sh new file mode 100755 index 0000000..a2d7cbf --- /dev/null +++ b/make-release.sh @@ -0,0 +1,285 @@ +#!/usr/bin/env bash +# make-release.sh — Create a Gitea release for bal-electrum-plugin +# +# Usage: +# ./make-release.sh # read version from bal/manifest.json +# ./make-release.sh v0.6.2 # bump manifest to 0.6.2, then release +# +# Requires: git, gpg, curl, python3, sha256sum +# Optional: ruff (lint skipped if not installed) +# Credentials: ~/.git-credentials or GITEA_USER / GITEA_TOKEN env vars + +set -eo pipefail + +# ── helpers ────────────────────────────────────────────────────────── +die() { echo "Error: $*" >&2; exit 1; } +info() { echo ""; echo "── $* ──"; } + +# ── 0. Resolve version ────────────────────────────────────────────── +MANIFEST="bal/manifest.json" +[ -f "$MANIFEST" ] || die "manifest not found: $MANIFEST" + +# read current version from manifest +CURRENT_VER=$(python3 -c "import json; print(json.load(open('$MANIFEST'))['version'])") +[ -n "$CURRENT_VER" ] || die "cannot read version from $MANIFEST" + +ARG="${1:-}" +if [ -n "$ARG" ]; then + # normalise: accept "v0.6.2" or "0.6.2" + NEW_VER="${ARG#v}" + TAG="v${NEW_VER}" +else + TAG="v${CURRENT_VER}" + NEW_VER="" +fi + +echo "=== Release ${TAG} ===" +echo "Current manifest version: ${CURRENT_VER}" +[ -n "$NEW_VER" ] && echo "New version (will bump): ${NEW_VER}" + +# ── 1. Bump version in manifest (if arg provided) ────────────────── +if [ -n "$NEW_VER" ] && [ "$NEW_VER" != "$CURRENT_VER" ]; then + info "[1/10] Bumping version to ${NEW_VER} in ${MANIFEST}" + python3 -c " +import json +f = open('${MANIFEST}') +d = json.load(f); f.close() +d['version'] = '${NEW_VER}' +json.dump(d, open('${MANIFEST}', 'w'), indent=4, ensure_ascii=False) +print(json.dumps(d, indent=4, ensure_ascii=False)) +" + git add "$MANIFEST" +else + info "[1/10] Version already ${CURRENT_VER}, no bump needed" +fi + +# ── 2. Clean caches ───────────────────────────────────────────────── +info "[2/10] Cleaning __pycache__ and .pyc" +find bal -name "__pycache__" -type d -exec rm -rf {} + 2>/dev/null || true +find bal -name "*.pyc" -delete 2>/dev/null || true +find bal -name "*.pyo" -delete 2>/dev/null || true + +# ── 3. Run tests ──────────────────────────────────────────────────── +info "[3/10] Running test suite" +if QT_QPA_PLATFORM=offscreen PYTHONPATH=electrum-src python3 -m pytest \ + tests/test_core_*.py \ + tests/test_anticipate_past_locktime.py tests/test_anticipate_manual_locktime.py \ + tests/test_group_b_auto_sign.py tests/test_group_c_settings.py \ + tests/test_group_d_alarms.py tests/test_group_e_mock_karen7.py \ + tests/test_group_f_heir_change_rebuild.py tests/test_group_g_basic_calendar.py \ + tests/test_group_h_v048.py \ + -q 2>&1; then + echo "All tests passed." +else + die "Tests failed — aborting release." +fi + +# ── 4. Lint (optional) ───────────────────────────────────────────── +info "[4/10] Lint with ruff" +if command -v ruff &>/dev/null; then + RUFF_ERRORS=$(ruff check bal/ 2>&1 \ + | grep -oE "^[^ ]+\.py:[0-9]+:[0-9]+: [A-Z][0-9]+" \ + | grep -vE "F401|F403|F405|F841" || true) + if [ -n "$RUFF_ERRORS" ]; then + echo "New ruff errors:" + echo "$RUFF_ERRORS" + die "Lint errors found — fix before releasing." + else + echo "Lint clean (ignoring known pre-existing warnings)." + fi +else + echo "ruff not installed — skipping lint." +fi + +# ── 5. Build ZIP via build_zip.py ─────────────────────────────────── +info "[5/10] Building ZIP" +ZIP_NAME="bal_${TAG}.zip" +python3 build_zip.py "$ZIP_NAME" + +# ── 6. GPG sign (armor + binary) + export public key ─────────────── +info "[6/10] Signing with GPG" +GPG_KEY="A847D004DB91610711CA6A0DFE756706E833E0D1" +gpg --default-key "$GPG_KEY" --batch --yes --armor --detach-sign "$ZIP_NAME" +gpg --default-key "$GPG_KEY" --batch --yes --detach-sign "$ZIP_NAME" +ASC_FILE="${ZIP_NAME}.asc" +SIG_FILE="${ZIP_NAME}.sig" +PGP_FILE="svatantrya.asc" +gpg --armor --export "$GPG_KEY" > "$PGP_FILE" +echo " Signed: $ASC_FILE" +echo " Signed: $SIG_FILE" +echo " Public key: $PGP_FILE" + +# ── 7. SHA-256 checksum ──────────────────────────────────────────── +info "[7/10] Computing SHA-256" +SHA256_HASH=$(sha256sum "$ZIP_NAME" | cut -d' ' -f1) +echo "${SHA256_HASH} ${ZIP_NAME}" > "${ZIP_NAME}.sha256" +echo " SHA-256: ${SHA256_HASH}" + +# ── 8. Pause for Electrum test ───────────────────────────────────── +info "[8/10] Test in Electrum (ZIP-FIRST policy)" +echo "" +echo " ZIP ready: $(pwd)/${ZIP_NAME}" +echo "" +echo " Install it in Electrum (Tools -> Plugins -> Install from file)." +echo " IMPORTANT: fully restart Electrum (not just reload the plugin)." +echo "" +read -r -p " Does the plugin work correctly in Electrum? [y/N] " CONFIRM +case "$CONFIRM" in + [yY][eE][sS]|[yY]) echo " Confirmed." ;; + *) die "Aborted by user." ;; +esac + +# ── 9. Git tag + push ────────────────────────────────────────────── +info "[9/10] Creating and pushing tag ${TAG}" +ORIGIN_URL="$(git remote get-url origin 2>/dev/null || true)" +[ -n "$ORIGIN_URL" ] || die "no git remote 'origin' found" + +GITEA_HOST="$(echo "$ORIGIN_URL" | sed -n 's|https://\([^/]*\)/.*|\1|p')" +[ -n "$GITEA_HOST" ] || die "cannot parse Gitea host from origin URL" + +TARGET_REPO="bitcoinafterlife/bal-electrum-plugin" + +# credentials +GITEA_USER="${GITEA_USER:-}" +GITEA_TOKEN="${GITEA_TOKEN:-}" +if [ -z "$GITEA_USER" ] && [ -z "$GITEA_TOKEN" ]; then + if [ -f ~/.git-credentials ]; then + CREDS_LINE="$(grep "${GITEA_HOST}" ~/.git-credentials | head -n1)" + if [ -n "$CREDS_LINE" ]; then + CREDS="$(echo "$CREDS_LINE" | sed -n 's|https://\([^@]*\)@.*|\1|p')" + GITEA_USER="$(echo "$CREDS" | cut -d: -f1)" + GITEA_PASS="$(echo "$CREDS" | cut -d: -f2-)" + GITEA_TOKEN="$GITEA_PASS" + fi + fi +fi +[ -n "$GITEA_TOKEN" ] || die "GITEA_TOKEN not set and no credentials in ~/.git-credentials" + +API="https://$GITEA_HOST/gitea/api/v1" + +# create annotated tag +git tag -d "$TAG" 2>/dev/null || true +git tag -a "$TAG" -m "$TAG" HEAD + +# push tag +REMOTE_NAME="gitea-target" +REMOTE_URL="https://$GITEA_USER:$GITEA_TOKEN@$GITEA_HOST/gitea/$TARGET_REPO.git" +git remote rm "$REMOTE_NAME" 2>/dev/null || true +git remote add "$REMOTE_NAME" "$REMOTE_URL" +echo " Pushing tag ${TAG} to ${TARGET_REPO}..." +git push "$REMOTE_NAME" "$TAG" --force + +# ── 10. Create release + upload assets ────────────────────────────── +info "[10/10] Creating Gitea release" + +RELEASE_BODY=$(python3 -c " +import json + +sha256 = '${SHA256_HASH}' +zip_name = '${ZIP_NAME}' +asc_name = '${ASC_FILE}' +sig_name = '${SIG_FILE}' +pgp_file = '${PGP_FILE}' +tag = '${TAG}' + +body = f'''Release {tag} + +## SHA-256 Checksum + +\`\`\` +{sha256} {zip_name} +\`\`\` + +### Verify SHA-256 + +\`\`\`bash +sha256sum -c {zip_name}.sha256 +\`\`\` + +## Download + +- \`{zip_name}\` - Plugin BAL {tag} +- \`{asc_name}\` - GPG signature (armor) +- \`{sig_name}\` - GPG signature (binary) +- \`{pgp_file}\` - Signing public key ([also available online](https://bitcoin-after.life/svatantrya.asc)) + +## GPG Verification + +### Import the signing key + +\`\`\`bash +gpg --fetch-key https://bitcoin-after.life/svatantrya.asc +\`\`\` + +Or download \`{pgp_file}\` from the assets above: + +\`\`\`bash +gpg --import {pgp_file} +\`\`\` + +### Verify the signature (armor) + +\`\`\`bash +gpg --verify {asc_name} {zip_name} +\`\`\` + +### Verify the signature (binary) + +\`\`\`bash +gpg --verify {sig_name} {zip_name} +\`\`\` + +Expected output: +\`\`\` +gpg: Good signature from "Svātantrya " +\`\`\` + +Fingerprint: \`A847D004DB91610711CA6A0DFE756706E833E0D1\` +Public key: https://bitcoin-after.life/svatantrya.asc''' + +print(json.dumps({'body': body}, ensure_ascii=False)) +") + +RESPONSE=$(curl -s -w "\n%{http_code}" -H "Authorization: token $GITEA_TOKEN" \ + -X POST "${API}/repos/${TARGET_REPO}/releases" \ + -H "Content-Type: application/json" \ + -d "{\"tag_name\":\"${TAG}\",\"name\":\"${TAG}\",\"body\":${RELEASE_BODY},\"draft\":false,\"prerelease\":false}") + +HTTP_CODE=$(echo "$RESPONSE" | tail -1) +BODY=$(echo "$RESPONSE" | sed '$d') + +if [ "$HTTP_CODE" != "201" ]; then + echo "Error creating release: HTTP $HTTP_CODE" + echo "$BODY" + exit 1 +fi + +RELEASE_ID=$(echo "$BODY" | python3 -c "import json,sys; print(json.load(sys.stdin)['id'])") +HTML_URL=$(echo "$BODY" | python3 -c "import json,sys; print(json.load(sys.stdin)['html_url'])") +echo " Release created: $HTML_URL (ID: $RELEASE_ID)" + +# upload assets +for FILE in "$ZIP_NAME" "$ASC_FILE" "$SIG_FILE" "${ZIP_NAME}.sha256" "$PGP_FILE"; do + BASENAME=$(basename "$FILE") + echo " Uploading $BASENAME ..." + UPLOAD=$(curl -s -w "\n%{http_code}" -H "Authorization: token $GITEA_TOKEN" \ + -X POST "${API}/repos/${TARGET_REPO}/releases/${RELEASE_ID}/assets" \ + -F "attachment=@${FILE}" -F "name=${BASENAME}") + UPLOAD_CODE=$(echo "$UPLOAD" | tail -1) + if [ "$UPLOAD_CODE" == "201" ]; then + echo " OK ($BASENAME)" + else + echo " FAILED ($BASENAME) - HTTP $UPLOAD_CODE" + fi +done + +echo "" +echo "=== Done ===" +echo "Release: $HTML_URL" +echo "Assets:" +echo " ${ZIP_NAME}" +echo " ${ASC_FILE}" +echo " ${SIG_FILE}" +echo " ${ZIP_NAME}.sha256" +echo " ${PGP_FILE}" +echo "SHA-256: ${SHA256_HASH}" diff --git a/svatantrya.asc b/svatantrya.asc new file mode 100644 index 0000000..395c6ef --- /dev/null +++ b/svatantrya.asc @@ -0,0 +1,30 @@ +-----BEGIN PGP PUBLIC KEY BLOCK----- + +mQENBGfgPmMBCAC4VXQn/ofBGPn/Wr9dF4tM/4uYNcWLvvz+/+TQsCi/bv4GG6jf +6Ttlg4TDwqF3JlZ1YfPImcdWKxr9is4fyq12OEZvz12LoFEJG8+0NdJrCoT2sm2f +yGmWKgZqRzH9LVBtIOOQIrXF3PdE0X77trWnSFrK/qAv9dszYiVOk9IBwUVI/3Wp +PN5EV7zqbCjYvzD0Hxl2sFzZKqsZCsiy70PJtaJKvKISd8RVTNuIiwZj0gu6hCSa +ZnBr5SLLr56YO4xaTzYNYh7XIEaQXZTHugEJbwygfZajnJ8gC91wWB3BsxVeHDdm +uDy1VGkAs65qvRn9ml5udmnEIPoEsS95HblpABEBAAG0K1N2xIF0YW50cnlhIDxz +dmF0YW50cnlhQGJpdGNvaW4tYWZ0ZXIubGlmZT6JAU4EEwEKADgWIQSoR9AE25Fh +BxHKag3+dWcG6DPg0QUCZ+A+YwIbAwULCQgHAgYVCgkICwIEFgIDAQIeAQIXgAAK +CRD+dWcG6DPg0coSCACbu3/tqMTwWTqRzXedl6VTGng+qeYfA5NYUaRgZeQYcVWM +sUi4dTAthBUxU3axfcu3V/Vkonn/Hrghdjh94lfpNsgdBNi3c2elI1rHT3Yobkj+ +ZsMEj91VlqV81uPFzfq8a/Pp7RIDhy1FJbIunmjnpD3GeJ7vVt76OOcyjV5hkGR0 +YJ4JX9O1OOC6wqgR2HVCvXTw/3JhNbj4TS8wr7GGsVWwiotAwZw506vspQRBqeYB +T5Wo2lpEQtagWzIHtgy4A2iAoLQ45E0T1lkr+mZa3V7sucS6W/UXI7HTvqC7wbku +jef6Hxwzzw83TWqPkd4wywuHsDZ3+DTcIDaqP/ROuQENBGfgPmMBCAC69Y2n2Ogi +T7i4Pm4J0cQxLaqwvox3GWSRuBG0QlhsBr0ER5j5fRRDH85P/WyTcvs4/9mIZsSl +JyQH/Lfetr/76pFCyc2zhKxxS1miG3RWOuM7BOKbRjjiieBa6XAiyWStKp2ij8a/ +kpqqgulLe1Tiq2SRPA8etqHGd7oR02fbEvzmsgiVqFOz3/tozp2jdC7zCKnp+XFZ +xMKqhIMgfZAxRmVl/qImH944ffcJU6M+qjEL3ENXpuDXpMSWI/indlbK06+R/UPA +hOxCOUSRPTeHzhQrJYUgH6Q6Q/cijpTQHVQFFqLXRKGgK7oE1QhmiNGNBeCVF7DP +hpcWrnUkY/xFABEBAAGJATYEGAEKACAWIQSoR9AE25FhBxHKag3+dWcG6DPg0QUC +Z+A+YwIbDAAKCRD+dWcG6DPg0ToJB/4t2V4FMqd2q00Sd+HmttZoAWNuklui8wO4 +nrjfh3Rt0ZBYYk+egZXzPx8lr42Ec8T4h24oJPovMlDu1xN9seQDbVaYC1ICVsnp +6/yfh+elYT5egaAxm9oP9+lQHBB/qZNKrfAssMuVQOrVh5E+XxSz+KG28dQnCYUT +L0k5PCO1f4Jz4XZd5AunVbMQ4J1JawUDoEb/w3Mn9ALDMsdAcOYC6pGhFtV88cqu +IO/ekQV+M8LpRwyh+CiPzgqtN3Z09wHLXFUJYBixXrYbXxAbSqe0PhqAhEKApk2c +4vVkSTAi+bNpkt0QgJ194iTyK20jVw3/roq7sUtDD4FrUoQb7llP +=6EE4 +-----END PGP PUBLIC KEY BLOCK----- diff --git a/tests/test_core_plugin_base.py b/tests/test_core_plugin_base.py index 7a83007..e9e4a68 100644 --- a/tests/test_core_plugin_base.py +++ b/tests/test_core_plugin_base.py @@ -206,7 +206,7 @@ def test_default_will_settings_relative(): def test_default_will_settings(): settings = BalPlugin.default_will_settings() - assert settings["baltx_fees"] == 100 + assert settings["baltx_fees"] == 20 assert "threshold" in settings assert "locktime" in settings # threshold/locktime should be absolute timestamps @@ -228,7 +228,7 @@ def test_validate_will_settings(): # Note: passing None triggers `will_settings = []` which then fails # on .get(). This is a latent bug — test passing a dict directly result = BalPlugin.validate_will_settings(None, {"baltx_fees": 0}) - assert result["baltx_fees"] == 100 + assert result["baltx_fees"] == 20 # normal settings unchanged input_settings = {"baltx_fees": 50, "threshold": 1700000000, "locktime": 1800000000}