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 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(
*,

View File

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

View File

@@ -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",
]

View File

@@ -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 FakeWallets 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 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)
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 dont parse here because we only care about internal payments.
# If you want to parse/validate, you can mirror the cores 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