mirror of
https://github.com/aljazceru/nutshell.git
synced 2025-12-21 02:54:20 +01:00
Wallet: Lightning interface (#318)
* mint does not start yet * fix import * revert mint db migrations * handle zero fee case * cli: adjust fee message * wallet: replace requests with httpx * clean up * rename http client decorator * fix pending check in main, todo: TEST PROXIES WITH HTTPX * fix up * use httpx for nostr as well * update packages to same versions as https://github.com/lnbits/lnbits/pull/1609/files * fix proof deserialization * check for string * tests passing * adjust wallet api tests * lockfile * add correct responses to Lightning interface and delete melt_id for proofs for which the payent has failed * fix create_invoice checking_id response * migrations atomic * proofs are stored automatically when created * make format * use bolt11 lib * stricter type checking * add fee response to payments * assert fees in test_melt * test that mint_id and melt_id is stored correctly in proofs and proofs_used * remove traces * refactor: Lightning interface into own file and LedgerCrud with typing * fix tests * fix payment response * rename variable
This commit is contained in:
@@ -11,6 +11,12 @@ from fastapi import APIRouter, Query
|
||||
from ...core.base import TokenV3
|
||||
from ...core.helpers import sum_proofs
|
||||
from ...core.settings import settings
|
||||
from ...lightning.base import (
|
||||
InvoiceResponse,
|
||||
PaymentResponse,
|
||||
PaymentStatus,
|
||||
StatusResponse,
|
||||
)
|
||||
from ...nostr.client.client import NostrClient
|
||||
from ...tor.tor import TorProxy
|
||||
from ...wallet.crud import get_lightning_invoices, get_reserved_proofs
|
||||
@@ -23,16 +29,15 @@ from ...wallet.helpers import (
|
||||
)
|
||||
from ...wallet.nostr import receive_nostr, send_nostr
|
||||
from ...wallet.wallet import Wallet as Wallet
|
||||
from ..lightning.lightning import LightningWallet
|
||||
from .api_helpers import verify_mints
|
||||
from .responses import (
|
||||
BalanceResponse,
|
||||
BurnResponse,
|
||||
InfoResponse,
|
||||
InvoiceResponse,
|
||||
InvoicesResponse,
|
||||
LockResponse,
|
||||
LocksResponse,
|
||||
PayResponse,
|
||||
PendingResponse,
|
||||
ReceiveResponse,
|
||||
RestoreResponse,
|
||||
@@ -44,17 +49,19 @@ from .responses import (
|
||||
router: APIRouter = APIRouter()
|
||||
|
||||
|
||||
async def mint_wallet(mint_url: Optional[str] = None):
|
||||
wallet: Wallet = await Wallet.with_db(
|
||||
async def mint_wallet(
|
||||
mint_url: Optional[str] = None, raise_connection_error: bool = True
|
||||
) -> LightningWallet:
|
||||
lightning_wallet = await LightningWallet.with_db(
|
||||
mint_url or settings.mint_url,
|
||||
db=os.path.join(settings.cashu_dir, settings.wallet_name),
|
||||
name=settings.wallet_name,
|
||||
)
|
||||
await wallet.load_mint()
|
||||
return wallet
|
||||
await lightning_wallet.async_init(raise_connection_error=raise_connection_error)
|
||||
return lightning_wallet
|
||||
|
||||
|
||||
wallet: Wallet = Wallet(
|
||||
wallet = LightningWallet(
|
||||
settings.mint_url,
|
||||
db=os.path.join(settings.cashu_dir, settings.wallet_name),
|
||||
name=settings.wallet_name,
|
||||
@@ -64,87 +71,101 @@ wallet: Wallet = Wallet(
|
||||
@router.on_event("startup")
|
||||
async def start_wallet():
|
||||
global wallet
|
||||
wallet = await Wallet.with_db(
|
||||
settings.mint_url,
|
||||
db=os.path.join(settings.cashu_dir, settings.wallet_name),
|
||||
name=settings.wallet_name,
|
||||
)
|
||||
|
||||
wallet = await mint_wallet(settings.mint_url, raise_connection_error=False)
|
||||
if settings.tor and not TorProxy().check_platform():
|
||||
raise Exception("tor not working.")
|
||||
await wallet.load_mint()
|
||||
|
||||
|
||||
@router.post("/pay", name="Pay lightning invoice", response_model=PayResponse)
|
||||
@router.post(
|
||||
"/lightning/pay_invoice",
|
||||
name="Pay lightning invoice",
|
||||
response_model=PaymentResponse,
|
||||
)
|
||||
async def pay(
|
||||
invoice: str = Query(default=..., description="Lightning invoice to pay"),
|
||||
bolt11: str = Query(default=..., description="Lightning invoice to pay"),
|
||||
mint: str = Query(
|
||||
default=None,
|
||||
description="Mint URL to pay from (None for default mint)",
|
||||
),
|
||||
):
|
||||
if not settings.lightning:
|
||||
raise Exception("lightning not enabled.")
|
||||
|
||||
) -> PaymentResponse:
|
||||
global wallet
|
||||
wallet = await mint_wallet(mint)
|
||||
await wallet.load_proofs(reload=True)
|
||||
|
||||
total_amount, fee_reserve_sat = await wallet.get_pay_amount_with_fees(invoice)
|
||||
assert total_amount > 0, "amount has to be larger than zero."
|
||||
assert wallet.available_balance >= total_amount, "balance is too low."
|
||||
_, send_proofs = await wallet.split_to_send(wallet.proofs, total_amount)
|
||||
await wallet.pay_lightning(send_proofs, invoice, fee_reserve_sat)
|
||||
return PayResponse(
|
||||
amount=total_amount - fee_reserve_sat,
|
||||
fee=fee_reserve_sat,
|
||||
amount_with_fee=total_amount,
|
||||
)
|
||||
if mint:
|
||||
wallet = await mint_wallet(mint)
|
||||
payment_response = await wallet.pay_invoice(bolt11)
|
||||
return payment_response
|
||||
|
||||
|
||||
@router.post(
|
||||
"/invoice", name="Request lightning invoice", response_model=InvoiceResponse
|
||||
@router.get(
|
||||
"/lightning/payment_state",
|
||||
name="Request lightning invoice",
|
||||
response_model=PaymentStatus,
|
||||
)
|
||||
async def invoice(
|
||||
amount: int = Query(default=..., description="Amount to request in invoice"),
|
||||
hash: str = Query(default=None, description="Hash of paid invoice"),
|
||||
async def payment_state(
|
||||
payment_hash: str = Query(default=None, description="Id of paid invoice"),
|
||||
mint: str = Query(
|
||||
default=None,
|
||||
description="Mint URL to create an invoice at (None for default mint)",
|
||||
),
|
||||
split: int = Query(
|
||||
default=None, description="Split minted tokens with a specific amount."
|
||||
),
|
||||
):
|
||||
# in case the user wants a specific split, we create a list of amounts
|
||||
optional_split = None
|
||||
if split:
|
||||
assert amount % split == 0, "split must be divisor or amount"
|
||||
assert amount >= split, "split must smaller or equal amount"
|
||||
n_splits = amount // split
|
||||
optional_split = [split] * n_splits
|
||||
print(f"Requesting split with {n_splits}*{split} sat tokens.")
|
||||
|
||||
) -> PaymentStatus:
|
||||
global wallet
|
||||
wallet = await mint_wallet(mint)
|
||||
if not settings.lightning:
|
||||
await wallet.mint(amount, split=optional_split)
|
||||
return InvoiceResponse(
|
||||
amount=amount,
|
||||
)
|
||||
elif amount and not hash:
|
||||
invoice = await wallet.request_mint(amount)
|
||||
return InvoiceResponse(
|
||||
amount=amount,
|
||||
invoice=invoice,
|
||||
)
|
||||
elif amount and hash:
|
||||
await wallet.mint(amount, split=optional_split, hash=hash)
|
||||
return InvoiceResponse(
|
||||
amount=amount,
|
||||
hash=hash,
|
||||
)
|
||||
return
|
||||
if mint:
|
||||
wallet = await mint_wallet(mint)
|
||||
state = await wallet.get_payment_status(payment_hash)
|
||||
return state
|
||||
|
||||
|
||||
@router.post(
|
||||
"/lightning/create_invoice",
|
||||
name="Request lightning invoice",
|
||||
response_model=InvoiceResponse,
|
||||
)
|
||||
async def create_invoice(
|
||||
amount: int = Query(default=..., description="Amount to request in invoice"),
|
||||
mint: str = Query(
|
||||
default=None,
|
||||
description="Mint URL to create an invoice at (None for default mint)",
|
||||
),
|
||||
) -> InvoiceResponse:
|
||||
global wallet
|
||||
if mint:
|
||||
wallet = await mint_wallet(mint)
|
||||
invoice = await wallet.create_invoice(amount)
|
||||
return invoice
|
||||
|
||||
|
||||
@router.get(
|
||||
"/lightning/invoice_state",
|
||||
name="Request lightning invoice",
|
||||
response_model=PaymentStatus,
|
||||
)
|
||||
async def invoice_state(
|
||||
payment_hash: str = Query(default=None, description="Payment hash of paid invoice"),
|
||||
mint: str = Query(
|
||||
default=None,
|
||||
description="Mint URL to create an invoice at (None for default mint)",
|
||||
),
|
||||
) -> PaymentStatus:
|
||||
global wallet
|
||||
if mint:
|
||||
wallet = await mint_wallet(mint)
|
||||
state = await wallet.get_invoice_status(payment_hash)
|
||||
return state
|
||||
|
||||
|
||||
@router.get(
|
||||
"/lightning/balance",
|
||||
name="Balance",
|
||||
summary="Display balance.",
|
||||
response_model=StatusResponse,
|
||||
)
|
||||
async def lightning_balance() -> StatusResponse:
|
||||
try:
|
||||
await wallet.load_proofs(reload=True)
|
||||
except Exception as exc:
|
||||
return StatusResponse(error_message=str(exc), balance_msat=0)
|
||||
return StatusResponse(
|
||||
error_message=None, balance_msat=wallet.available_balance * 1000
|
||||
)
|
||||
|
||||
|
||||
@router.post(
|
||||
@@ -171,7 +192,7 @@ async def swap(
|
||||
# pay invoice from outgoing mint
|
||||
await outgoing_wallet.load_proofs(reload=True)
|
||||
total_amount, fee_reserve_sat = await outgoing_wallet.get_pay_amount_with_fees(
|
||||
invoice.pr
|
||||
invoice.bolt11
|
||||
)
|
||||
assert total_amount > 0, "amount must be positive"
|
||||
if outgoing_wallet.available_balance < total_amount:
|
||||
@@ -180,10 +201,10 @@ async def swap(
|
||||
_, send_proofs = await outgoing_wallet.split_to_send(
|
||||
outgoing_wallet.proofs, total_amount, set_reserved=True
|
||||
)
|
||||
await outgoing_wallet.pay_lightning(send_proofs, invoice.pr, fee_reserve_sat)
|
||||
await outgoing_wallet.pay_lightning(send_proofs, invoice.bolt11, fee_reserve_sat)
|
||||
|
||||
# mint token in incoming mint
|
||||
await incoming_wallet.mint(amount, hash=invoice.hash)
|
||||
await incoming_wallet.mint(amount, id=invoice.id)
|
||||
await incoming_wallet.load_proofs(reload=True)
|
||||
mint_balances = await incoming_wallet.balance_per_minturl()
|
||||
return SwapResponse(
|
||||
@@ -223,6 +244,8 @@ async def send_command(
|
||||
),
|
||||
):
|
||||
global wallet
|
||||
if mint:
|
||||
wallet = await mint_wallet(mint)
|
||||
if not nostr:
|
||||
balance, token = await send(
|
||||
wallet, amount=amount, lock=lock, legacy=False, split=not nosplit
|
||||
@@ -284,7 +307,7 @@ async def burn(
|
||||
if not (all or token or force or delete) or (token and all):
|
||||
raise Exception(
|
||||
"enter a token or use --all to burn all pending tokens, --force to"
|
||||
" check all tokensor --delete with send ID to force-delete pending"
|
||||
" check all tokens or --delete with send ID to force-delete pending"
|
||||
" token from list if mint is unavailable.",
|
||||
)
|
||||
if all:
|
||||
|
||||
Reference in New Issue
Block a user