From 769d3a07e85704091c5814851860abd1b5c5f593 Mon Sep 17 00:00:00 2001 From: arcbtc Date: Mon, 15 Sep 2025 16:36:27 +0100 Subject: [PATCH] hacky hijack --- lnbits/core/services/payments.py | 60 ++++++++++++++ lnbits/settings.py | 1 + lnbits/wallets/__init__.py | 2 + lnbits/wallets/fakeark.py | 136 ++++++++----------------------- 4 files changed, 98 insertions(+), 101 deletions(-) diff --git a/lnbits/core/services/payments.py b/lnbits/core/services/payments.py index 5a55a4511..9af73044a 100644 --- a/lnbits/core/services/payments.py +++ b/lnbits/core/services/payments.py @@ -9,6 +9,10 @@ from lnurl import LnurlErrorResponse, LnurlSuccessResponse from lnurl import execute_withdraw as lnurl_withdraw from loguru import logger +from bolt11.exceptions import Bolt11Bech32InvalidException +import base64, json, time +from types import SimpleNamespace + from lnbits.core.crud.payments import get_daily_stats from lnbits.core.db import db from lnbits.core.models import PaymentDailyStats, PaymentFilters @@ -52,6 +56,62 @@ from .notifications import send_payment_notification payment_lock = asyncio.Lock() wallets_payments_lock: dict[str, asyncio.Lock] = {} +def _parse_ark_ticket(ticket: str) -> SimpleNamespace: + """ + Accepts 'ark1' + base64url(JSON) and returns an object that mimics the + attributes LNbits reads from a decoded BOLT11 invoice. + Required JSON fields we honor: + - amt_sat: int + - hash: str (payment hash / checking_id) + - ts: int (unix timestamp when created) + - exp: int (seconds until expiry) + - memo: str (optional) + """ + if not isinstance(ticket, str) or not ticket.startswith("ark1"): + raise ValueError("Not an ARK ticket") + + b64u = ticket[4:] + b64u += "=" * (-len(b64u) % 4) # restore padding + payload = json.loads(base64.urlsafe_b64decode(b64u).decode()) + + amt_sat = int(payload.get("amt_sat", 0)) + ts = int(payload.get("ts", int(time.time()))) + exp = int(payload.get("exp", 3600)) + memo = payload.get("memo") or "" + p_hash = payload.get("hash") or payload.get("payment_hash") + if not isinstance(p_hash, str): + raise ValueError("ARK ticket missing 'hash'") + + # Match attributes accessed later in this module: + # - amount_msat (int) + # - payment_hash (str) + # - description (str) + # - expiry_date (int unix ts expected by CreatePayment) + # - tags.get(...) is sometimes called; return a harmless stub + return SimpleNamespace( + amount_msat=amt_sat * 1000, + payment_hash=p_hash, + description=memo, + expiry_date=ts + exp, # unix timestamp + timestamp=ts, # not strictly required here but nice to have + tags=SimpleNamespace(get=lambda *a, **k: None), + ) + +# Keep a reference to the real decoder, then override the name used below +_real_bolt11_decode = bolt11_decode + +def _safe_decode_payment_request(pr: str): + try: + return _real_bolt11_decode(pr) + except Bolt11Bech32InvalidException: + # Fallback: accept ARK tickets + if isinstance(pr, str) and pr.startswith("ark1"): + return _parse_ark_ticket(pr) + # Not ARK? re-raise to keep existing behavior + raise + +# Override the imported name used throughout this file: +bolt11_decode = _safe_decode_payment_request async def pay_invoice( *, diff --git a/lnbits/settings.py b/lnbits/settings.py index a9743b76d..2e4ab7743 100644 --- a/lnbits/settings.py +++ b/lnbits/settings.py @@ -974,6 +974,7 @@ class SuperUserSettings(LNbitsSettings): "ZBDWallet", "NWCWallet", "StrikeWallet", + "ArkFakeWallet", ] ) diff --git a/lnbits/wallets/__init__.py b/lnbits/wallets/__init__.py index 4833345f9..745a39152 100644 --- a/lnbits/wallets/__init__.py +++ b/lnbits/wallets/__init__.py @@ -32,6 +32,7 @@ from .spark import SparkWallet from .strike import StrikeWallet from .void import VoidWallet from .zbd import ZBDWallet +from .fakeark import ArkFakeWallet def set_funding_source(class_name: str | None = None) -> None: @@ -79,4 +80,5 @@ __all__ = [ "StrikeWallet", "VoidWallet", "ZBDWallet", + "ArkFakeWallet", ] diff --git a/lnbits/wallets/fakeark.py b/lnbits/wallets/fakeark.py index 134bfdebf..9c2a93771 100644 --- a/lnbits/wallets/fakeark.py +++ b/lnbits/wallets/fakeark.py @@ -1,6 +1,5 @@ -import asyncio -import json -import base64 +# lnbits/wallets/ark_fake.py +import asyncio, json, base64 from collections.abc import AsyncGenerator from datetime import datetime from hashlib import sha256 @@ -8,9 +7,8 @@ from os import urandom from typing import Any from loguru import logger - from lnbits.settings import settings -from lnbits.utils.crypto import fake_privkey # kept for parity/log symmetry +from lnbits.utils.crypto import fake_privkey # unused but kept for parity/logs from .base import ( InvoiceResponse, @@ -23,129 +21,67 @@ from .base import ( Wallet, ) -ARK_TICKET_PREFIX = "ark1" # cosmetic prefix for readability - - -def _b64u_encode(d: dict[str, Any]) -> str: - raw = json.dumps(d, separators=(",", ":"), ensure_ascii=False).encode() - return base64.urlsafe_b64encode(raw).decode().rstrip("=") - - -def _b64u_decode(s: str) -> dict[str, Any]: - # Restore missing padding - pad = "=" * (-len(s) % 4) - raw = base64.urlsafe_b64decode(s + pad) - return json.loads(raw.decode()) - - -def encode_ark_ticket(payload: dict[str, Any]) -> str: - return f"{ARK_TICKET_PREFIX}{_b64u_encode(payload)}" - - -def decode_ark_ticket(ticket: str) -> dict[str, Any]: - if not ticket.startswith(ARK_TICKET_PREFIX): - raise ValueError("Not an ARK ticket") - return _b64u_decode(ticket[len(ARK_TICKET_PREFIX) :]) - +def ark_encode(payload: dict[str, Any]) -> str: + raw = json.dumps(payload, separators=(",", ":"), ensure_ascii=False).encode() + return "ark1" + base64.urlsafe_b64encode(raw).decode().rstrip("=") class ArkFakeWallet(Wallet): - """ - A fake ARK funding source: - - create_invoice() returns an ARK 'ticket' string in `payment_request`. - - pay_invoice() accepts the ARK ticket string (only if it was created internally). - - Status/streams mimic FakeWallet for compatibility in tests/dev. - """ - def __init__(self) -> None: self.queue: asyncio.Queue[str] = asyncio.Queue(0) - self.payment_secrets: dict[str, str] = {} # payment_hash -> preimage hex + self.payment_secrets: dict[str, str] = {} self.paid_invoices: set[str] = set() - # keep these two to mirror FakeWallet’s behavior/logging vibe self.secret = settings.fake_wallet_secret self.privkey = fake_privkey(self.secret) - async def cleanup(self): - pass + async def cleanup(self): pass async def status(self) -> StatusResponse: - logger.info( - "ArkFakeWallet funding source is a local-only simulator for ARK tickets." - ) - # second field is "balance" in some Wallet impls; keep it big & static + logger.info("ArkFakeWallet: ARK-only test funding source (no bolt11).") return StatusResponse(None, 1_000_000_000) async def create_invoice( - self, - amount: int, - memo: str | None = None, + self, amount: int, memo: str | None = None, description_hash: bytes | None = None, unhashed_description: bytes | None = None, expiry: int | None = None, payment_secret: bytes | None = None, **_, ) -> InvoiceResponse: - # For ARK we don’t use BOLT11 tags; we just build a ticket payload. - if payment_secret: - secret_hex = payment_secret.hex() - else: - secret_hex = urandom(32).hex() - - # In Lightning this would be the payment preimage; we keep the same idea. preimage = urandom(32) - payment_hash = sha256(preimage).hexdigest() - + p_hash = sha256(preimage).hexdigest() now = int(datetime.now().timestamp()) - ticket_payload = { - "v": 1, # version for future-proofing your decoder + ticket = ark_encode({ + "v": 1, "ts": now, "amt_sat": int(amount), "memo": memo or "", - "secret": secret_hex, - "hash": payment_hash, # used as checking_id - } - if expiry: - ticket_payload["exp"] = int(expiry) - - ticket = encode_ark_ticket(ticket_payload) - - # Track so we can accept only internal tickets on pay - self.payment_secrets[payment_hash] = preimage.hex() - + "hash": p_hash, + "exp": int(expiry or 3600), + "secret": (payment_secret.hex() if payment_secret else urandom(32).hex()), + }) + self.payment_secrets[p_hash] = preimage.hex() return InvoiceResponse( ok=True, - checking_id=payment_hash, - payment_request=ticket, # <-- ARK ticket string goes here - preimage=preimage.hex(), # provided for symmetry with FakeWallet + checking_id=p_hash, + payment_request=ticket, # <-- non-bolt11, starts with ark1 + preimage=preimage.hex(), ) async def pay_invoice(self, bolt11: str, _: int) -> PaymentResponse: - """ - For ArkFakeWallet, `bolt11` is actually an ARK ticket string (ark1...). - Only internal tickets are payable (just like FakeWallet). - """ - try: - ticket = decode_ark_ticket(bolt11) - except Exception as exc: - return PaymentResponse(ok=False, error_message=f"Invalid ARK ticket: {exc}") - - checking_id = ticket.get("hash") - if not isinstance(checking_id, str): - return PaymentResponse(ok=False, error_message="Malformed ARK ticket (no hash)") - - if checking_id in self.payment_secrets: - # "Pay" it: mark as paid and enqueue for stream consumers - await self.queue.put(checking_id) - self.paid_invoices.add(checking_id) - return PaymentResponse( - ok=True, - checking_id=checking_id, - fee_msat=0, - preimage=self.payment_secrets.get(checking_id) or "0" * 64, - ) - else: - return PaymentResponse( - ok=False, error_message="Only internal ARK tickets can be used!" - ) + # Only allow paying our own tickets (like FakeWallet behavior) + # We don’t parse here because we only care about internal payments. + # If you want to parse/validate, you can mirror the core’s ark parser. + for p_hash in list(self.payment_secrets.keys()): + if p_hash in bolt11: # cheap check; or properly decode ark1 and compare its "hash" + await self.queue.put(p_hash) + self.paid_invoices.add(p_hash) + return PaymentResponse( + ok=True, + checking_id=p_hash, + fee_msat=0, + preimage=self.payment_secrets[p_hash], + ) + return PaymentResponse(ok=False, error_message="Only internal ARK tickets can be used!") async def get_invoice_status(self, checking_id: str) -> PaymentStatus: if checking_id in self.paid_invoices: @@ -155,11 +91,9 @@ class ArkFakeWallet(Wallet): return PaymentFailedStatus() async def get_payment_status(self, _: str) -> PaymentStatus: - # We don't track outgoing states beyond "we 'paid' it" return PaymentPendingStatus() async def paid_invoices_stream(self) -> AsyncGenerator[str, None]: - # Mimic FakeWallet: stream checking_id/payment_hash values while settings.lnbits_running: checking_id = await self.queue.get() yield checking_id