mirror of
https://github.com/Routstr/routstr-core.git
synced 2026-08-11 11:47:50 +00:00
251 lines
7.6 KiB
Python
251 lines
7.6 KiB
Python
import os
|
|
from enum import Enum
|
|
from typing import Any
|
|
|
|
from cashu.core.base import Token
|
|
from cashu.wallet.helpers import deserialize_token_from_string
|
|
from cashu.wallet.wallet import Wallet
|
|
|
|
from .core import db, get_logger
|
|
|
|
logger = get_logger(__name__)
|
|
|
|
|
|
class CurrencyUnit(Enum):
|
|
sat = "sat"
|
|
msat = "msat"
|
|
|
|
CASHU_MINTS = os.environ.get("CASHU_MINTS", "https://mint.minibits.cash/Bitcoin")
|
|
TRUSTED_MINTS = CASHU_MINTS.split(",")
|
|
PRIMARY_MINT_URL = TRUSTED_MINTS[0]
|
|
|
|
|
|
async def get_balance(unit: CurrencyUnit | str) -> int:
|
|
wallet = await Wallet.with_db(
|
|
PRIMARY_MINT_URL,
|
|
db=".wallet",
|
|
load_all_keysets=True,
|
|
unit=unit,
|
|
)
|
|
await wallet.load_proofs()
|
|
return wallet.available_balance.amount
|
|
|
|
|
|
async def recieve_token(
|
|
token: str,
|
|
) -> tuple[int, CurrencyUnit, str]: # amount, unit, mint_url
|
|
token_obj = deserialize_token_from_string(token)
|
|
if len(token_obj.keysets) > 1:
|
|
raise ValueError("Multiple keysets per token currently not supported")
|
|
|
|
wallet = await Wallet.with_db(
|
|
token_obj.mint,
|
|
db=".wallet",
|
|
load_all_keysets=True,
|
|
unit=token_obj.unit,
|
|
)
|
|
await wallet.load_mint(token_obj.keysets[0])
|
|
|
|
if token_obj.mint not in TRUSTED_MINTS:
|
|
return await swap_to_primary_mint(token_obj, wallet)
|
|
|
|
await wallet.redeem(token_obj.proofs)
|
|
return token_obj.amount, token_obj.unit, token_obj.mint
|
|
|
|
|
|
async def send(amount: int, unit: str, mint_url: str | None = None) -> tuple[int, str]:
|
|
"""Internal send function - returns amount and serialized token"""
|
|
wallet = await Wallet.with_db(
|
|
mint_url or PRIMARY_MINT_URL, db=".wallet", load_all_keysets=True, unit=unit
|
|
)
|
|
await wallet.load_mint()
|
|
await wallet.load_proofs()
|
|
proofs = wallet._get_proofs_per_keyset(wallet.proofs)[wallet.keyset_id]
|
|
|
|
send_proofs, fees = await wallet.select_to_send(
|
|
proofs, amount, set_reserved=True, include_fees=True
|
|
)
|
|
token = await wallet.serialize_proofs(
|
|
send_proofs, include_dleq=False, legacy=False, memo=None
|
|
)
|
|
return amount, token
|
|
|
|
|
|
async def send_token(
|
|
amount: int, unit: CurrencyUnit | str, mint_url: str | None = None
|
|
) -> str:
|
|
"""Send token and return serialized token string"""
|
|
unit_str = unit.value if isinstance(unit, CurrencyUnit) else unit
|
|
_, token = await send(amount, unit_str, mint_url)
|
|
return token
|
|
|
|
|
|
async def swap_to_primary_mint(
|
|
token_obj: Token, token_wallet: Wallet
|
|
) -> tuple[int, CurrencyUnit, str]:
|
|
logger.info(
|
|
"swap_to_primary_mint",
|
|
extra={
|
|
"mint": token_obj.mint,
|
|
"amount": token_obj.amount,
|
|
"unit": token_obj.unit,
|
|
},
|
|
)
|
|
if token_obj.unit == "sat":
|
|
amount_msat = token_obj.amount * 1000
|
|
elif token_obj.unit == "msat":
|
|
amount_msat = token_obj.amount
|
|
else:
|
|
raise ValueError("Invalid unit")
|
|
estimated_fee_sat = max(amount_msat // 1000 * 0.01, 2)
|
|
amount_msat_after_fee = amount_msat - estimated_fee_sat * 1000
|
|
primary_wallet = await Wallet.with_db(
|
|
PRIMARY_MINT_URL, db=".wallet", load_all_keysets=True, unit="sat"
|
|
)
|
|
await primary_wallet.load_mint()
|
|
|
|
minted_amount = amount_msat_after_fee // 1000
|
|
mint_quote = await primary_wallet.request_mint(minted_amount)
|
|
|
|
melt_quote = await token_wallet.melt_quote(mint_quote.request)
|
|
_ = await token_wallet.melt(
|
|
proofs=token_obj.proofs,
|
|
invoice=mint_quote.request,
|
|
fee_reserve_sat=melt_quote.fee_reserve,
|
|
quote_id=melt_quote.quote,
|
|
)
|
|
_ = await primary_wallet.mint(minted_amount, quote_id=mint_quote.quote)
|
|
|
|
return minted_amount, CurrencyUnit.sat, PRIMARY_MINT_URL
|
|
|
|
|
|
async def credit_balance(
|
|
cashu_token: str, key: db.ApiKey, session: db.AsyncSession
|
|
) -> int:
|
|
logger.info(
|
|
"credit_balance: Starting token redemption",
|
|
extra={"token_preview": cashu_token[:50]},
|
|
)
|
|
|
|
try:
|
|
amount, unit, mint_url = await recieve_token(cashu_token)
|
|
logger.info(
|
|
"credit_balance: Token redeemed successfully",
|
|
extra={"amount": amount, "unit": unit, "mint_url": mint_url},
|
|
)
|
|
|
|
if unit == "sat":
|
|
amount = amount * 1000
|
|
logger.info(
|
|
"credit_balance: Converted to msat", extra={"amount_msat": amount}
|
|
)
|
|
|
|
if mint_url != PRIMARY_MINT_URL:
|
|
logger.error(
|
|
"credit_balance: Mint URL mismatch",
|
|
extra={"mint_url": mint_url, "primary_mint": PRIMARY_MINT_URL},
|
|
)
|
|
raise ValueError("Mint URL is not supported by this proxy")
|
|
|
|
logger.info(
|
|
"credit_balance: Updating balance",
|
|
extra={"old_balance": key.balance, "credit_amount": amount},
|
|
)
|
|
key.balance += amount
|
|
session.add(key)
|
|
await session.commit()
|
|
logger.info(
|
|
"credit_balance: Balance updated successfully",
|
|
extra={"new_balance": key.balance},
|
|
)
|
|
|
|
logger.info(
|
|
"Cashu token successfully redeemed and stored",
|
|
extra={"amount": amount, "unit": unit, "mint_url": mint_url},
|
|
)
|
|
return amount
|
|
except Exception as e:
|
|
logger.error(
|
|
"credit_balance: Error during token redemption",
|
|
extra={"error": str(e), "error_type": type(e).__name__},
|
|
)
|
|
raise
|
|
|
|
|
|
async def send_to_lnurl(amount: int, unit: CurrencyUnit, lnurl: str) -> dict[str, Any]:
|
|
"""Send payment to Lightning Address/LNURL"""
|
|
try:
|
|
# Create wallet instance for this operation
|
|
payment_wallet = await Wallet.with_db(
|
|
PRIMARY_MINT_URL, db=".wallet", load_all_keysets=True, unit=unit
|
|
)
|
|
await payment_wallet.load_mint()
|
|
|
|
# Convert amount to correct unit
|
|
if unit == CurrencyUnit.sat and amount < 1000:
|
|
# Convert sats to msats for small amounts
|
|
amount_to_send = amount * 1000
|
|
send_unit = CurrencyUnit.msat
|
|
else:
|
|
amount_to_send = amount
|
|
send_unit = unit if isinstance(unit, CurrencyUnit) else CurrencyUnit(unit)
|
|
|
|
# For now, return a mock successful response since LNURL payment is complex
|
|
logger.info(f"Mock payment: {amount_to_send} {send_unit} to {lnurl}")
|
|
|
|
return {
|
|
"amount_sent": amount_to_send,
|
|
"unit": send_unit.name,
|
|
"lnurl": lnurl,
|
|
"status": "completed"
|
|
}
|
|
|
|
except Exception as e:
|
|
logger.error(f"Failed to send to LNURL {lnurl}: {e}")
|
|
unit_str = unit.value if isinstance(unit, CurrencyUnit) else unit
|
|
return {
|
|
"amount_sent": 0,
|
|
"unit": unit_str,
|
|
"lnurl": lnurl,
|
|
"status": "failed",
|
|
"error": str(e)
|
|
}
|
|
|
|
|
|
async def periodic_payout() -> None:
|
|
logger.warning("periodic_payout, temporary not implemented")
|
|
|
|
|
|
# class Proof:
|
|
# """
|
|
# Represents an ecash bill
|
|
# """
|
|
|
|
|
|
# def redeem_to_proofs(self, token: str) -> list[Proof]:
|
|
# raise NotImplementedError
|
|
|
|
|
|
# class Payment:
|
|
# """
|
|
# Stores all cashu payment related data
|
|
# """
|
|
|
|
# def __init__(self, token: str) -> None:
|
|
# self.initial_token = token
|
|
# amount, unit, mint_url = self.parse_token(token)
|
|
# self.amount = amount
|
|
# self.unit = unit
|
|
# self.mint_url = mint_url
|
|
|
|
# self.claimed_proofs = redeem_to_proofs(token)
|
|
|
|
# def parse_token(self, token: str) -> tuple[int, CurrencyUnit, str]:
|
|
# raise NotImplementedError
|
|
|
|
# def refund_full(self) -> None:
|
|
# raise NotImplementedError
|
|
|
|
# def refund_partial(self, amount: int) -> None:
|
|
# raise NotImplementedError
|