Fake Wallet Ark, for playing with ark internally

This commit is contained in:
arcbtc
2025-09-15 15:00:52 +01:00
parent c054e47913
commit b3dfb0384e

165
lnbits/wallets/fakeark.py Normal file
View File

@@ -0,0 +1,165 @@
import asyncio
import json
import base64
from collections.abc import AsyncGenerator
from datetime import datetime
from hashlib import sha256
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 .base import (
InvoiceResponse,
PaymentFailedStatus,
PaymentPendingStatus,
PaymentResponse,
PaymentStatus,
PaymentSuccessStatus,
StatusResponse,
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) :])
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.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 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
return StatusResponse(None, 1_000_000_000)
async def create_invoice(
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()
now = int(datetime.now().timestamp())
ticket_payload = {
"v": 1, # version for future-proofing your decoder
"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()
return InvoiceResponse(
ok=True,
checking_id=payment_hash,
payment_request=ticket, # <-- ARK ticket string goes here
preimage=preimage.hex(), # provided for symmetry with FakeWallet
)
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!"
)
async def get_invoice_status(self, checking_id: str) -> PaymentStatus:
if checking_id in self.paid_invoices:
return PaymentSuccessStatus()
if checking_id in self.payment_secrets:
return PaymentPendingStatus()
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