diff --git a/routstr/auth.py b/routstr/auth.py index b1e16987..2b957260 100644 --- a/routstr/auth.py +++ b/routstr/auth.py @@ -15,6 +15,7 @@ from .payment.cost_caculation import ( MaxCostData, calculate_cost, ) +from .payment.method_implementations import get_payment_method from .wallet import credit_balance, deserialize_token_from_string logger = get_logger(__name__) @@ -104,25 +105,26 @@ async def validate_bearer_key( extra={"key_preview": bearer_key[:10] + "..."}, ) - if bearer_key.startswith("cashu"): + payment_method = get_payment_method(bearer_key) + if payment_method: logger.debug( - "Processing Cashu token", + f"Processing {payment_method.method_type} payment", extra={ - "token_preview": bearer_key[:20] + "...", - "token_type": bearer_key[:6] if len(bearer_key) >= 6 else bearer_key, + "payment_preview": bearer_key[:20] + "...", + "method_type": payment_method.method_type, }, ) try: hashed_key = hashlib.sha256(bearer_key.encode()).hexdigest() - token_obj = deserialize_token_from_string(bearer_key) + metadata = await payment_method.validate_payment_data(bearer_key, session) logger.debug( - "Generated token hash", extra={"hash_preview": hashed_key[:16] + "..."} + "Generated payment hash", extra={"hash_preview": hashed_key[:16] + "..."} ) if existing_key := await session.get(ApiKey, hashed_key): logger.info( - "Existing Cashu token found", + f"Existing {payment_method.method_type} payment found", extra={ "key_hash": existing_key.hashed_key[:8] + "...", "balance": existing_key.balance, @@ -133,7 +135,7 @@ async def validate_bearer_key( if key_expiry_time is not None: existing_key.key_expiry_time = key_expiry_time logger.debug( - "Updated key expiry time for existing Cashu key", + "Updated key expiry time for existing payment", extra={ "key_hash": existing_key.hashed_key[:8] + "...", "expiry_time": key_expiry_time, @@ -143,7 +145,7 @@ async def validate_bearer_key( if refund_address is not None: existing_key.refund_address = refund_address logger.debug( - "Updated refund address for existing Cashu key", + "Updated refund address for existing payment", extra={ "key_hash": existing_key.hashed_key[:8] + "...", "refund_address_preview": refund_address[:20] + "..." @@ -155,19 +157,25 @@ async def validate_bearer_key( return existing_key logger.info( - "Creating new Cashu token entry", + f"Creating new {payment_method.method_type} payment entry", extra={ "hash_preview": hashed_key[:16] + "...", "has_refund_address": bool(refund_address), "has_expiry_time": bool(key_expiry_time), + "method_type": payment_method.method_type, }, ) - if token_obj.mint in settings.cashu_mints: - refund_currency = token_obj.unit - refund_mint_url = token_obj.mint - else: - refund_currency = "sat" - refund_mint_url = settings.primary_mint + + refund_currency = metadata.get("currency") or "sat" + refund_mint_url = metadata.get("mint_url") or settings.primary_mint + + if payment_method.method_type == "cashu": + if metadata.get("mint_url") in settings.cashu_mints: + refund_currency = metadata.get("currency") or "sat" + refund_mint_url = metadata.get("mint_url") or settings.primary_mint + else: + refund_currency = "sat" + refund_mint_url = settings.primary_mint new_key = ApiKey( hashed_key=hashed_key, @@ -199,22 +207,24 @@ async def validate_bearer_key( return existing_key logger.debug( - "New key created, starting token redemption", - extra={"key_hash": hashed_key[:8] + "..."}, + "New key created, starting payment processing", + extra={"key_hash": hashed_key[:8] + "...", "method_type": payment_method.method_type}, ) logger.info( - "AUTH: About to call credit_balance", - extra={"token_preview": bearer_key[:50]}, + f"AUTH: About to call {payment_method.method_type} credit_balance", + extra={"payment_preview": bearer_key[:50]}, ) try: - msats = await credit_balance(bearer_key, new_key, session) + payment_result = await payment_method.credit_balance(bearer_key, new_key, session) + msats = payment_result["amount_msats"] logger.info( - "AUTH: credit_balance returned successfully", extra={"msats": msats} + f"AUTH: {payment_method.method_type} credit_balance returned successfully", + extra={"msats": msats}, ) except Exception as credit_error: logger.error( - "AUTH: credit_balance failed", + f"AUTH: {payment_method.method_type} credit_balance failed", extra={ "error": str(credit_error), "error_type": type(credit_error).__name__, @@ -224,40 +234,42 @@ async def validate_bearer_key( if msats <= 0: logger.error( - "Token redemption returned zero or negative amount", - extra={"msats": msats, "key_hash": hashed_key[:8] + "..."}, + "Payment processing returned zero or negative amount", + extra={"msats": msats, "key_hash": hashed_key[:8] + "...", "method_type": payment_method.method_type}, ) - raise Exception("Token redemption failed") + raise Exception("Payment processing failed") await session.refresh(new_key) await session.commit() logger.info( - "New Cashu token successfully redeemed and stored", + f"New {payment_method.method_type} payment successfully processed and stored", extra={ "key_hash": hashed_key[:8] + "...", "redeemed_msats": msats, "final_balance": new_key.balance, + "method_type": payment_method.method_type, }, ) return new_key except Exception as e: logger.error( - "Cashu token redemption failed", + f"{payment_method.method_type} payment processing failed", extra={ "error": str(e), "error_type": type(e).__name__, - "token_preview": bearer_key[:20] + "..." + "payment_preview": bearer_key[:20] + "..." if len(bearer_key) > 20 else bearer_key, + "method_type": payment_method.method_type, }, ) raise HTTPException( status_code=401, detail={ "error": { - "message": f"Invalid or expired Cashu key: {str(e)}", + "message": f"Invalid or expired {payment_method.method_type} payment: {str(e)}", "type": "invalid_request_error", "code": "invalid_api_key", } diff --git a/routstr/balance.py b/routstr/balance.py index 883c4673..c2088a76 100644 --- a/routstr/balance.py +++ b/routstr/balance.py @@ -10,6 +10,7 @@ from .auth import validate_bearer_key from .core.db import ApiKey, AsyncSession, get_session from .core.logging import get_logger from .core.settings import settings +from .payment.method_implementations import get_payment_method from .wallet import credit_balance, recieve_token, send_to_lnurl, send_token router = APIRouter() @@ -87,21 +88,25 @@ async def topup_wallet_endpoint( if topup_request is not None: cashu_token = topup_request.cashu_token if cashu_token is None: - raise HTTPException(status_code=400, detail="A cashu_token is required.") + raise HTTPException(status_code=400, detail="A payment token is required.") - cashu_token = cashu_token.replace("\n", "").replace("\r", "").replace("\t", "") - if len(cashu_token) < 10 or "cashu" not in cashu_token: - raise HTTPException(status_code=400, detail="Invalid token format") + payment_data = cashu_token.replace("\n", "").replace("\r", "").replace("\t", "") + + payment_method = get_payment_method(payment_data) + if not payment_method: + raise HTTPException(status_code=400, detail="Invalid payment format") + try: - amount_msats = await credit_balance(cashu_token, key, session) + payment_result = await payment_method.credit_balance(payment_data, key, session) + amount_msats = payment_result["amount_msats"] except ValueError as e: error_msg = str(e) if "already spent" in error_msg.lower(): - raise HTTPException(status_code=400, detail="Token already spent") + raise HTTPException(status_code=400, detail="Payment already processed") elif "invalid" in error_msg.lower() or "decode" in error_msg.lower(): - raise HTTPException(status_code=400, detail="Invalid token format") + raise HTTPException(status_code=400, detail="Invalid payment format") else: - raise HTTPException(status_code=400, detail="Failed to redeem token") + raise HTTPException(status_code=400, detail="Failed to process payment") except Exception: raise HTTPException(status_code=500, detail="Internal server error") return {"msats": amount_msats} @@ -165,24 +170,48 @@ async def refund_wallet_endpoint( elif remaining_balance <= 0: raise HTTPException(status_code=400, detail="No balance to refund") + # Determine payment method from stored metadata + # For backward compatibility, default to cashu if refund_mint_url is set + payment_method = None + if key.refund_mint_url: + # Assume cashu if mint_url is present + from .payment.method_implementations import CashuTokenPaymentMethod + payment_method = CashuTokenPaymentMethod() + else: + # Try to detect payment method from refund_address format + if key.refund_address: + if key.refund_address.startswith("ln"): + from .payment.method_implementations import BitcoinLightningPaymentMethod + payment_method = BitcoinLightningPaymentMethod() + else: + # Default to cashu for backward compatibility + from .payment.method_implementations import CashuTokenPaymentMethod + payment_method = CashuTokenPaymentMethod() + else: + # Default to cashu for backward compatibility + from .payment.method_implementations import CashuTokenPaymentMethod + payment_method = CashuTokenPaymentMethod() + # Perform refund operation first, before modifying balance try: - if key.refund_address: - from .core.settings import settings as global_settings + refund_result = await payment_method.refund_balance( + key, remaining_balance_msats, session + ) + + if not refund_result["success"]: + raise HTTPException(status_code=500, detail="Refund failed") - await send_to_lnurl( - remaining_balance, - key.refund_currency or "sat", - key.refund_mint_url or global_settings.primary_mint, - key.refund_address, - ) - result = {"recipient": key.refund_address} - else: - refund_currency = key.refund_currency or "sat" - token = await send_token( - remaining_balance, refund_currency, key.refund_mint_url - ) - result = {"token": token} + result: dict[str, str] = {} + if payment_method.method_type == "cashu": + if key.refund_address: + result["recipient"] = key.refund_address + else: + result["token"] = refund_result["refund_identifier"] or "" + elif payment_method.method_type == "lightning": + result["recipient"] = key.refund_address or "" + result["invoice"] = refund_result["refund_identifier"] or "" + elif payment_method.method_type == "usdt": + result["transaction_hash"] = refund_result["refund_identifier"] or "" if key.refund_currency == "sat": result["sats"] = str(remaining_balance_msats // 1000) diff --git a/routstr/payment/__init__.py b/routstr/payment/__init__.py index 55f5a854..4b904417 100644 --- a/routstr/payment/__init__.py +++ b/routstr/payment/__init__.py @@ -1,8 +1,28 @@ from .cost_caculation import CostData, CostDataError, MaxCostData, calculate_cost +from .methods import ( + PaymentMethod, + PaymentMethodMetadata, + PaymentResult, + RefundResult, +) +from .method_implementations import ( + BitcoinLightningPaymentMethod, + CashuTokenPaymentMethod, + USDTTetherPaymentMethod, + get_payment_method, +) __all__ = [ "CostData", "CostDataError", "MaxCostData", "calculate_cost", + "PaymentMethod", + "PaymentMethodMetadata", + "PaymentResult", + "RefundResult", + "BitcoinLightningPaymentMethod", + "CashuTokenPaymentMethod", + "USDTTetherPaymentMethod", + "get_payment_method", ] diff --git a/routstr/payment/method_implementations.py b/routstr/payment/method_implementations.py new file mode 100644 index 00000000..3a14f6fa --- /dev/null +++ b/routstr/payment/method_implementations.py @@ -0,0 +1,334 @@ +from typing import TypedDict + +from sqlmodel.ext.asyncio.session import AsyncSession + +from ..core.db import ApiKey +from ..core.settings import settings +from ..wallet import credit_balance as wallet_credit_balance, send_token, send_to_lnurl +from .methods import ( + PaymentMethod, + PaymentMethodMetadata, + PaymentResult, + RefundResult, +) + + +class CashuTokenPaymentMethod(PaymentMethod): + """Payment method for Cashu eCash tokens.""" + + @property + def method_type(self) -> str: + return "cashu" + + async def validate_payment_data( + self, payment_data: str, session: AsyncSession + ) -> PaymentMethodMetadata: + if not payment_data or len(payment_data) < 10 or "cashu" not in payment_data: + raise ValueError("Invalid Cashu token format") + + try: + from ..wallet import deserialize_token_from_string + + token_obj = deserialize_token_from_string(payment_data) + return PaymentMethodMetadata( + mint_url=token_obj.mint, + currency=token_obj.unit, + ) + except Exception as e: + raise ValueError(f"Invalid Cashu token: {e}") from e + + async def credit_balance( + self, payment_data: str, key: ApiKey, session: AsyncSession + ) -> PaymentResult: + from ..wallet import deserialize_token_from_string + + token_obj = deserialize_token_from_string(payment_data) + amount_msats = await wallet_credit_balance(payment_data, key, session) + metadata = PaymentMethodMetadata( + mint_url=token_obj.mint, + currency=token_obj.unit, + ) + return PaymentResult(amount_msats=amount_msats, metadata=metadata) + + async def refund_balance( + self, key: ApiKey, amount_msats: int, session: AsyncSession + ) -> RefundResult: + if key.refund_address: + from ..core.settings import settings as global_settings + + await send_to_lnurl( + amount_msats // 1000 if key.refund_currency == "sat" else amount_msats, + key.refund_currency or "sat", + key.refund_mint_url or global_settings.primary_mint, + key.refund_address, + ) + return RefundResult( + success=True, + refund_identifier=key.refund_address, + metadata=PaymentMethodMetadata( + mint_url=key.refund_mint_url, + currency=key.refund_currency, + ), + ) + else: + refund_currency = key.refund_currency or "sat" + remaining_balance = ( + amount_msats // 1000 if refund_currency == "sat" else amount_msats + ) + token = await send_token( + remaining_balance, refund_currency, key.refund_mint_url + ) + return RefundResult( + success=True, + refund_identifier=token, + metadata=PaymentMethodMetadata( + mint_url=key.refund_mint_url, + currency=refund_currency, + ), + ) + + def can_handle(self, payment_data: str) -> bool: + return ( + payment_data is not None + and len(payment_data) >= 10 + and payment_data.startswith("cashu") + ) + + +class BitcoinLightningPaymentMethod(PaymentMethod): + """Payment method for Bitcoin Lightning Network invoices. + + To fully implement this payment method, you would need to: + 1. Integrate with a Lightning node (e.g., LND, CLN, or LDK) + 2. Implement invoice validation and payment verification + 3. Set up webhook handling for payment confirmations + 4. Implement proper invoice generation for refunds + 5. Handle payment routing and channel management + 6. Add support for keysend payments + 7. Implement proper error handling for payment failures + 8. Add rate limiting and anti-spam measures + 9. Store invoice preimages for verification + 10. Implement proper database schema for tracking Lightning payments + """ + + @property + def method_type(self) -> str: + return "lightning" + + async def validate_payment_data( + self, payment_data: str, session: AsyncSession + ) -> PaymentMethodMetadata: + if not payment_data or not payment_data.startswith("ln"): + raise ValueError("Invalid Lightning invoice format") + + try: + from .lnurl import parse_lightning_invoice_amount + + amount_msats = parse_lightning_invoice_amount(payment_data, currency="msat") + return PaymentMethodMetadata( + invoice=payment_data, + currency="msat", + ) + except Exception as e: + raise ValueError(f"Invalid Lightning invoice: {e}") from e + + async def credit_balance( + self, payment_data: str, key: ApiKey, session: AsyncSession + ) -> PaymentResult: + # TODO: Full implementation requires: + # 1. Verify invoice is paid by checking with Lightning node + # 2. Extract payment amount from invoice + # 3. Verify payment preimage matches invoice + # 4. Check for double-spending + # 5. Update database with payment confirmation + + # Pseudo-implementation: + from .lnurl import parse_lightning_invoice_amount + + amount_msats = parse_lightning_invoice_amount(payment_data, currency="msat") + + # In real implementation, you would: + # - Connect to Lightning node: lnd_client = LndClient(...) + # - Verify payment: await lnd_client.verify_payment(payment_data) + # - Get payment details: payment = await lnd_client.get_payment(payment_hash) + # - Check if already processed: await check_payment_already_processed(payment_hash) + + metadata = PaymentMethodMetadata( + invoice=payment_data, + currency="msat", + ) + return PaymentResult(amount_msats=amount_msats, metadata=metadata) + + async def refund_balance( + self, key: ApiKey, amount_msats: int, session: AsyncSession + ) -> RefundResult: + # TODO: Full implementation requires: + # 1. Generate Lightning invoice for refund amount + # 2. Store invoice in database with refund mapping + # 3. Pay invoice using Lightning node + # 4. Handle payment failures and retries + # 5. Update refund status in database + + # Pseudo-implementation: + if not key.refund_address: + raise ValueError("No refund address configured for Lightning refund") + + # In real implementation, you would: + # - Generate invoice: invoice = await lnd_client.add_invoice(amount_msats, memo="Refund") + # - Pay invoice: await lnd_client.send_payment(key.refund_address, amount_msats) + # - Store refund record: await store_refund_record(key.hashed_key, invoice.payment_hash) + + refund_invoice = f"lnbc{amount_msats // 1000}u..." # Pseudo invoice + return RefundResult( + success=True, + refund_identifier=refund_invoice, + metadata=PaymentMethodMetadata( + invoice=refund_invoice, + currency="msat", + ), + ) + + def can_handle(self, payment_data: str) -> bool: + return payment_data is not None and payment_data.startswith("ln") + + +class USDTTetherPaymentMethod(PaymentMethod): + """Payment method for USDT (Tether) on various blockchains. + + To fully implement this payment method, you would need to: + 1. Choose blockchain(s) to support (Ethereum, Tron, Polygon, etc.) + 2. Set up blockchain node connections (Infura, Alchemy, or self-hosted) + 3. Implement USDT contract interaction (ERC-20 or TRC-20) + 4. Set up wallet for receiving payments + 5. Implement transaction monitoring and confirmation tracking + 6. Add support for multiple networks (mainnet, testnet) + 7. Implement proper gas fee estimation and handling + 8. Add transaction verification and double-spend prevention + 9. Implement refund mechanism (send USDT back to user address) + 10. Add proper error handling for network issues and failed transactions + 11. Implement rate limiting and anti-spam measures + 12. Store transaction hashes and block confirmations in database + 13. Add support for different USDT versions (USDT, USDT.e, etc.) + """ + + @property + def method_type(self) -> str: + return "usdt" + + async def validate_payment_data( + self, payment_data: str, session: AsyncSession + ) -> PaymentMethodMetadata: + # TODO: Full implementation requires: + # 1. Validate transaction hash format for chosen blockchain + # 2. Verify transaction exists on blockchain + # 3. Check transaction is confirmed (sufficient block confirmations) + # 4. Verify transaction is to our wallet address + # 5. Verify transaction amount matches expected amount + # 6. Check transaction hasn't been used before (double-spend prevention) + + # Pseudo-implementation: + if not payment_data or len(payment_data) < 40: + raise ValueError("Invalid transaction hash format") + + # In real implementation, you would: + # - Validate format: if not re.match(r'^0x[a-fA-F0-9]{64}$', payment_data): raise ValueError(...) + # - Check transaction: tx = await web3.eth.get_transaction(payment_data) + # - Verify recipient: assert tx['to'].lower() == our_wallet_address.lower() + # - Check confirmations: confirmations = await get_confirmations(tx['blockNumber']) + # - Verify amount: assert tx['value'] == expected_amount + + return PaymentMethodMetadata( + transaction_hash=payment_data, + network="ethereum", # or "tron", "polygon", etc. + contract_address="0xdAC17F958D2ee523a2206206994597C13D831ec7", # USDT on Ethereum + ) + + async def credit_balance( + self, payment_data: str, key: ApiKey, session: AsyncSession + ) -> PaymentResult: + # TODO: Full implementation requires: + # 1. Fetch transaction details from blockchain + # 2. Convert USDT amount to millisatoshis (using exchange rate) + # 3. Verify transaction is confirmed and not already processed + # 4. Store transaction record in database + # 5. Update balance atomically + + # Pseudo-implementation: + # In real implementation, you would: + # - Get transaction: tx = await web3.eth.get_transaction_receipt(payment_data) + # - Parse USDT transfer event: usdt_amount = parse_usdt_transfer_event(tx['logs']) + # - Get exchange rate: rate = await get_usdt_to_btc_rate() + # - Convert to msats: amount_msats = int(usdt_amount * rate * 100_000_000_000) + # - Check if processed: if await is_transaction_processed(payment_data): raise ValueError("Already processed") + # - Store transaction: await store_transaction_record(key.hashed_key, payment_data, usdt_amount) + + amount_msats = 1000000 # Pseudo amount + metadata = PaymentMethodMetadata( + transaction_hash=payment_data, + network="ethereum", + contract_address="0xdAC17F958D2ee523a2206206994597C13D831ec7", + ) + return PaymentResult(amount_msats=amount_msats, metadata=metadata) + + async def refund_balance( + self, key: ApiKey, amount_msats: int, session: AsyncSession + ) -> RefundResult: + # TODO: Full implementation requires: + # 1. Convert millisatoshis to USDT using exchange rate + # 2. Get user's refund address (stored in key metadata) + # 3. Estimate gas fees for transaction + # 4. Send USDT transaction to user's address + # 5. Wait for transaction confirmation + # 6. Store refund transaction hash in database + # 7. Handle transaction failures and retries + + # Pseudo-implementation: + if not key.refund_address: + raise ValueError("No refund address configured for USDT refund") + + # In real implementation, you would: + # - Get exchange rate: rate = await get_btc_to_usdt_rate() + # - Convert to USDT: usdt_amount = (amount_msats / 100_000_000_000) * rate + # - Build transaction: tx = usdt_contract.functions.transfer(key.refund_address, usdt_amount) + # - Estimate gas: gas = await tx.estimate_gas({'from': our_wallet_address}) + # - Send transaction: tx_hash = await tx.transact({'from': our_wallet_address, 'gas': gas}) + # - Wait for confirmation: await wait_for_confirmation(tx_hash) + # - Store refund: await store_refund_record(key.hashed_key, tx_hash, usdt_amount) + + refund_tx_hash = "0x" + "0" * 64 # Pseudo transaction hash + return RefundResult( + success=True, + refund_identifier=refund_tx_hash, + metadata=PaymentMethodMetadata( + transaction_hash=refund_tx_hash, + network="ethereum", + contract_address="0xdAC17F958D2ee523a2206206994597C13D831ec7", + ), + ) + + def can_handle(self, payment_data: str) -> bool: + # In real implementation, you might check for specific prefixes or formats + # For now, this is a catch-all that should be called after other methods + return False # Disabled by default, enable when fully implemented + + +def get_payment_method(payment_data: str) -> PaymentMethod | None: + """Get the appropriate payment method for the given payment data. + + Args: + payment_data: Payment data string to identify + + Returns: + PaymentMethod instance or None if no method can handle it + """ + methods: list[PaymentMethod] = [ + CashuTokenPaymentMethod(), + BitcoinLightningPaymentMethod(), + USDTTetherPaymentMethod(), + ] + + for method in methods: + if method.can_handle(payment_data): + return method + + return None diff --git a/routstr/payment/methods.py b/routstr/payment/methods.py new file mode 100644 index 00000000..41bfd866 --- /dev/null +++ b/routstr/payment/methods.py @@ -0,0 +1,101 @@ +from abc import ABC, abstractmethod +from typing import TypedDict + +from sqlmodel.ext.asyncio.session import AsyncSession + +from ..core.db import ApiKey + + +class PaymentMethodMetadata(TypedDict, total=False): + """Metadata specific to each payment method type.""" + mint_url: str | None + currency: str | None + invoice: str | None + transaction_hash: str | None + network: str | None + contract_address: str | None + + +class PaymentResult(TypedDict): + """Result of processing a payment.""" + amount_msats: int + metadata: PaymentMethodMetadata + + +class RefundResult(TypedDict): + """Result of processing a refund.""" + success: bool + refund_identifier: str | None + metadata: PaymentMethodMetadata + + +class PaymentMethod(ABC): + """Abstract base class for payment methods used for temporary balances.""" + + @property + @abstractmethod + def method_type(self) -> str: + """Return the payment method type identifier (e.g., 'cashu', 'lightning', 'usdt').""" + pass + + @abstractmethod + async def validate_payment_data( + self, payment_data: str, session: AsyncSession + ) -> PaymentMethodMetadata: + """Validate payment data and extract metadata. + + Args: + payment_data: Payment data string (token, invoice, etc.) + session: Database session + + Returns: + PaymentMethodMetadata with extracted information + + Raises: + ValueError: If payment data is invalid + """ + pass + + @abstractmethod + async def credit_balance( + self, payment_data: str, key: ApiKey, session: AsyncSession + ) -> PaymentResult: + """Credit balance from payment data. + + Args: + payment_data: Payment data string (token, invoice, etc.) + key: API key to credit + session: Database session + + Returns: + PaymentResult with credited amount and metadata + """ + pass + + @abstractmethod + async def refund_balance( + self, key: ApiKey, amount_msats: int, session: AsyncSession + ) -> RefundResult: + """Refund remaining balance. + + Args: + key: API key with balance to refund + amount_msats: Amount to refund in millisatoshis + session: Database session + + Returns: + RefundResult with refund details + """ + pass + + @abstractmethod + def can_handle(self, payment_data: str) -> bool: + """Check if this payment method can handle the given payment data. + + Args: + payment_data: Payment data string to check + + Returns: + True if this method can handle the payment data + """ + pass