hacky hijack

This commit is contained in:
arcbtc
2025-09-15 16:36:27 +01:00
parent b3dfb0384e
commit 769d3a07e8
4 changed files with 98 additions and 101 deletions

View File

@@ -9,6 +9,10 @@ from lnurl import LnurlErrorResponse, LnurlSuccessResponse
from lnurl import execute_withdraw as lnurl_withdraw from lnurl import execute_withdraw as lnurl_withdraw
from loguru import logger 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.crud.payments import get_daily_stats
from lnbits.core.db import db from lnbits.core.db import db
from lnbits.core.models import PaymentDailyStats, PaymentFilters from lnbits.core.models import PaymentDailyStats, PaymentFilters
@@ -52,6 +56,62 @@ from .notifications import send_payment_notification
payment_lock = asyncio.Lock() payment_lock = asyncio.Lock()
wallets_payments_lock: dict[str, 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( async def pay_invoice(
*, *,

View File

@@ -974,6 +974,7 @@ class SuperUserSettings(LNbitsSettings):
"ZBDWallet", "ZBDWallet",
"NWCWallet", "NWCWallet",
"StrikeWallet", "StrikeWallet",
"ArkFakeWallet",
] ]
) )

View File

@@ -32,6 +32,7 @@ from .spark import SparkWallet
from .strike import StrikeWallet from .strike import StrikeWallet
from .void import VoidWallet from .void import VoidWallet
from .zbd import ZBDWallet from .zbd import ZBDWallet
from .fakeark import ArkFakeWallet
def set_funding_source(class_name: str | None = None) -> None: def set_funding_source(class_name: str | None = None) -> None:
@@ -79,4 +80,5 @@ __all__ = [
"StrikeWallet", "StrikeWallet",
"VoidWallet", "VoidWallet",
"ZBDWallet", "ZBDWallet",
"ArkFakeWallet",
] ]

View File

@@ -1,6 +1,5 @@
import asyncio # lnbits/wallets/ark_fake.py
import json import asyncio, json, base64
import base64
from collections.abc import AsyncGenerator from collections.abc import AsyncGenerator
from datetime import datetime from datetime import datetime
from hashlib import sha256 from hashlib import sha256
@@ -8,9 +7,8 @@ from os import urandom
from typing import Any from typing import Any
from loguru import logger from loguru import logger
from lnbits.settings import settings 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 ( from .base import (
InvoiceResponse, InvoiceResponse,
@@ -23,129 +21,67 @@ from .base import (
Wallet, Wallet,
) )
ARK_TICKET_PREFIX = "ark1" # cosmetic prefix for readability 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("=")
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) :])
class ArkFakeWallet(Wallet): 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: def __init__(self) -> None:
self.queue: asyncio.Queue[str] = asyncio.Queue(0) 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() self.paid_invoices: set[str] = set()
# keep these two to mirror FakeWallets behavior/logging vibe
self.secret = settings.fake_wallet_secret self.secret = settings.fake_wallet_secret
self.privkey = fake_privkey(self.secret) self.privkey = fake_privkey(self.secret)
async def cleanup(self): async def cleanup(self): pass
pass
async def status(self) -> StatusResponse: async def status(self) -> StatusResponse:
logger.info( logger.info("ArkFakeWallet: ARK-only test funding source (no bolt11).")
"ArkFakeWallet funding source is a local-only simulator for ARK tickets."
)
# second field is "balance" in some Wallet impls; keep it big & static
return StatusResponse(None, 1_000_000_000) return StatusResponse(None, 1_000_000_000)
async def create_invoice( async def create_invoice(
self, self, amount: int, memo: str | None = None,
amount: int,
memo: str | None = None,
description_hash: bytes | None = None, description_hash: bytes | None = None,
unhashed_description: bytes | None = None, unhashed_description: bytes | None = None,
expiry: int | None = None, expiry: int | None = None,
payment_secret: bytes | None = None, payment_secret: bytes | None = None,
**_, **_,
) -> InvoiceResponse: ) -> InvoiceResponse:
# For ARK we dont 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) preimage = urandom(32)
payment_hash = sha256(preimage).hexdigest() p_hash = sha256(preimage).hexdigest()
now = int(datetime.now().timestamp()) now = int(datetime.now().timestamp())
ticket_payload = { ticket = ark_encode({
"v": 1, # version for future-proofing your decoder "v": 1,
"ts": now, "ts": now,
"amt_sat": int(amount), "amt_sat": int(amount),
"memo": memo or "", "memo": memo or "",
"secret": secret_hex, "hash": p_hash,
"hash": payment_hash, # used as checking_id "exp": int(expiry or 3600),
} "secret": (payment_secret.hex() if payment_secret else urandom(32).hex()),
if expiry: })
ticket_payload["exp"] = int(expiry) self.payment_secrets[p_hash] = preimage.hex()
ticket = encode_ark_ticket(ticket_payload)
# Track so we can accept only internal tickets on pay
self.payment_secrets[payment_hash] = preimage.hex()
return InvoiceResponse( return InvoiceResponse(
ok=True, ok=True,
checking_id=payment_hash, checking_id=p_hash,
payment_request=ticket, # <-- ARK ticket string goes here payment_request=ticket, # <-- non-bolt11, starts with ark1
preimage=preimage.hex(), # provided for symmetry with FakeWallet preimage=preimage.hex(),
) )
async def pay_invoice(self, bolt11: str, _: int) -> PaymentResponse: async def pay_invoice(self, bolt11: str, _: int) -> PaymentResponse:
""" # Only allow paying our own tickets (like FakeWallet behavior)
For ArkFakeWallet, `bolt11` is actually an ARK ticket string (ark1...). # We dont parse here because we only care about internal payments.
Only internal tickets are payable (just like FakeWallet). # If you want to parse/validate, you can mirror the cores ark parser.
""" for p_hash in list(self.payment_secrets.keys()):
try: if p_hash in bolt11: # cheap check; or properly decode ark1 and compare its "hash"
ticket = decode_ark_ticket(bolt11) await self.queue.put(p_hash)
except Exception as exc: self.paid_invoices.add(p_hash)
return PaymentResponse(ok=False, error_message=f"Invalid ARK ticket: {exc}") return PaymentResponse(
ok=True,
checking_id = ticket.get("hash") checking_id=p_hash,
if not isinstance(checking_id, str): fee_msat=0,
return PaymentResponse(ok=False, error_message="Malformed ARK ticket (no hash)") preimage=self.payment_secrets[p_hash],
)
if checking_id in self.payment_secrets: return PaymentResponse(ok=False, error_message="Only internal ARK tickets can be used!")
# "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!"
)
async def get_invoice_status(self, checking_id: str) -> PaymentStatus: async def get_invoice_status(self, checking_id: str) -> PaymentStatus:
if checking_id in self.paid_invoices: if checking_id in self.paid_invoices:
@@ -155,11 +91,9 @@ class ArkFakeWallet(Wallet):
return PaymentFailedStatus() return PaymentFailedStatus()
async def get_payment_status(self, _: str) -> PaymentStatus: async def get_payment_status(self, _: str) -> PaymentStatus:
# We don't track outgoing states beyond "we 'paid' it"
return PaymentPendingStatus() return PaymentPendingStatus()
async def paid_invoices_stream(self) -> AsyncGenerator[str, None]: async def paid_invoices_stream(self) -> AsyncGenerator[str, None]:
# Mimic FakeWallet: stream checking_id/payment_hash values
while settings.lnbits_running: while settings.lnbits_running:
checking_id = await self.queue.get() checking_id = await self.queue.get()
yield checking_id yield checking_id