Skip to content

Commit

Permalink
Wallet: store quotes (#657)
Browse files Browse the repository at this point in the history
* wallet_quotes_wip

* fix quote in db

* fix subscription test

* clean up api

* fix api tests

* fix balance check
  • Loading branch information
callebtc authored Nov 1, 2024
1 parent 21418a1 commit 9262739
Show file tree
Hide file tree
Showing 31 changed files with 981 additions and 865 deletions.
60 changes: 42 additions & 18 deletions cashu/core/base.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import base64
import json
import math
import time
from abc import ABC, abstractmethod
from dataclasses import dataclass, field
from enum import Enum
Expand Down Expand Up @@ -260,20 +261,7 @@ def from_row(cls, row: Row):
)


# ------- LIGHTNING INVOICE -------


class Invoice(BaseModel):
amount: int
bolt11: str
id: str
out: Union[None, bool] = None
payment_hash: Union[None, str] = None
preimage: Union[str, None] = None
issued: Union[None, bool] = False
paid: Union[None, bool] = False
time_created: Union[None, str, int, float] = ""
time_paid: Union[None, str, int, float] = ""
# ------- Quotes -------


class MeltQuoteState(Enum):
Expand All @@ -297,9 +285,10 @@ class MeltQuote(LedgerEvent):
created_time: Union[int, None] = None
paid_time: Union[int, None] = None
fee_paid: int = 0
payment_preimage: str = ""
payment_preimage: Optional[str] = None
expiry: Optional[int] = None
change: Optional[List[BlindedSignature]] = None
mint: Optional[str] = None

@classmethod
def from_row(cls, row: Row):
Expand All @@ -314,6 +303,8 @@ def from_row(cls, row: Row):
paid_time = int(row["paid_time"].timestamp()) if row["paid_time"] else None
expiry = int(row["expiry"].timestamp()) if row["expiry"] else None

payment_preimage = row.get("payment_preimage") or row.get("proof") # type: ignore

# parse change from row as json
change = None
if row["change"]:
Expand All @@ -327,13 +318,30 @@ def from_row(cls, row: Row):
unit=row["unit"],
amount=row["amount"],
fee_reserve=row["fee_reserve"],
state=MeltQuoteState[row["state"]],
state=MeltQuoteState(row["state"]),
created_time=created_time,
paid_time=paid_time,
fee_paid=row["fee_paid"],
change=change,
expiry=expiry,
payment_preimage=row["proof"],
payment_preimage=payment_preimage,
)

@classmethod
def from_resp_wallet(
cls, melt_quote_resp, mint: str, amount: int, unit: str, request: str
):
return cls(
quote=melt_quote_resp.quote,
method="bolt11",
request=request,
checking_id="",
unit=unit,
amount=amount,
fee_reserve=melt_quote_resp.fee_reserve,
state=MeltQuoteState(melt_quote_resp.state),
mint=mint,
change=melt_quote_resp.change,
)

@property
Expand Down Expand Up @@ -397,6 +405,7 @@ class MintQuote(LedgerEvent):
created_time: Union[int, None] = None
paid_time: Union[int, None] = None
expiry: Optional[int] = None
mint: Optional[str] = None

@classmethod
def from_row(cls, row: Row):
Expand All @@ -417,11 +426,26 @@ def from_row(cls, row: Row):
checking_id=row["checking_id"],
unit=row["unit"],
amount=row["amount"],
state=MintQuoteState[row["state"]],
state=MintQuoteState(row["state"]),
created_time=created_time,
paid_time=paid_time,
)

@classmethod
def from_resp_wallet(cls, mint_quote_resp, mint: str, amount: int, unit: str):
return cls(
quote=mint_quote_resp.quote,
method="bolt11",
request=mint_quote_resp.request,
checking_id="",
unit=unit,
amount=amount,
state=MintQuoteState(mint_quote_resp.state),
mint=mint,
expiry=mint_quote_resp.expiry,
created_time=int(time.time()),
)

@property
def identifier(self) -> str:
"""Implementation of the abstract method from LedgerEventManager"""
Expand Down
13 changes: 8 additions & 5 deletions cashu/core/db.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ def big_int(self) -> str:
def table_with_schema(self, table: str):
return f"{self.references_schema if self.schema else ''}{table}"


# https://docs.sqlalchemy.org/en/14/core/connections.html#sqlalchemy.engine.CursorResult
class Connection(Compat):
def __init__(self, conn: AsyncSession, txn, typ, name, schema):
Expand All @@ -82,7 +83,9 @@ def rewrite_query(self, query) -> TextClause:

async def fetchall(self, query: str, values: dict = {}):
result = await self.conn.execute(self.rewrite_query(query), values)
return [r._mapping for r in result.all()] # will return [] if result list is empty
return [
r._mapping for r in result.all()
] # will return [] if result list is empty

async def fetchone(self, query: str, values: dict = {}):
result = await self.conn.execute(self.rewrite_query(query), values)
Expand Down Expand Up @@ -134,13 +137,13 @@ def __init__(self, db_name: str, db_location: str):
if not settings.db_connection_pool:
kwargs["poolclass"] = NullPool
elif self.type == POSTGRES:
kwargs["poolclass"] = AsyncAdaptedQueuePool # type: ignore[assignment]
kwargs["pool_size"] = 50 # type: ignore[assignment]
kwargs["max_overflow"] = 100 # type: ignore[assignment]
kwargs["poolclass"] = AsyncAdaptedQueuePool # type: ignore[assignment]
kwargs["pool_size"] = 50 # type: ignore[assignment]
kwargs["max_overflow"] = 100 # type: ignore[assignment]

self.engine = create_async_engine(database_uri, **kwargs)
self.async_session = sessionmaker(
self.engine,
self.engine, # type: ignore
expire_on_commit=False,
class_=AsyncSession, # type: ignore
)
Expand Down
16 changes: 8 additions & 8 deletions cashu/mint/crud.py
Original file line number Diff line number Diff line change
Expand Up @@ -304,7 +304,7 @@ async def get_promise(
""",
{"b_": str(b_)},
)
return BlindedSignature.from_row(row) if row else None
return BlindedSignature.from_row(row) if row else None # type: ignore

async def get_promises(
self,
Expand All @@ -320,7 +320,7 @@ async def get_promises(
""",
{f"b_{i}": b_s[i] for i in range(len(b_s))},
)
return [BlindedSignature.from_row(r) for r in rows] if rows else []
return [BlindedSignature.from_row(r) for r in rows] if rows else [] # type: ignore

async def invalidate_proof(
self,
Expand Down Expand Up @@ -455,7 +455,7 @@ async def store_mint_quote(
"amount": quote.amount,
"paid": quote.paid, # this is deprecated! we need to store it because we have a NOT NULL constraint | we could also remove the column but sqlite doesn't support that (we would have to make a new table)
"issued": quote.issued, # this is deprecated! we need to store it because we have a NOT NULL constraint | we could also remove the column but sqlite doesn't support that (we would have to make a new table)
"state": quote.state.name,
"state": quote.state.value,
"created_time": db.to_timestamp(
db.timestamp_from_seconds(quote.created_time) or ""
),
Expand Down Expand Up @@ -497,7 +497,7 @@ async def get_mint_quote(
)
if row is None:
return None
return MintQuote.from_row(row) if row else None
return MintQuote.from_row(row) if row else None # type: ignore

async def get_mint_quote_by_request(
self,
Expand All @@ -513,7 +513,7 @@ async def get_mint_quote_by_request(
""",
{"request": request},
)
return MintQuote.from_row(row) if row else None
return MintQuote.from_row(row) if row else None # type: ignore

async def update_mint_quote(
self,
Expand All @@ -525,7 +525,7 @@ async def update_mint_quote(
await (conn or db).execute(
f"UPDATE {db.table_with_schema('mint_quotes')} SET state = :state, paid_time = :paid_time WHERE quote = :quote",
{
"state": quote.state.name,
"state": quote.state.value,
"paid_time": db.to_timestamp(
db.timestamp_from_seconds(quote.paid_time) or ""
),
Expand Down Expand Up @@ -554,7 +554,7 @@ async def store_melt_quote(
"unit": quote.unit,
"amount": quote.amount,
"fee_reserve": quote.fee_reserve or 0,
"state": quote.state.name,
"state": quote.state.value,
"paid": quote.paid, # this is deprecated! we need to store it because we have a NOT NULL constraint | we could also remove the column but sqlite doesn't support that (we would have to make a new table)
"created_time": db.to_timestamp(
db.timestamp_from_seconds(quote.created_time) or ""
Expand Down Expand Up @@ -631,7 +631,7 @@ async def update_melt_quote(
UPDATE {db.table_with_schema('melt_quotes')} SET state = :state, fee_paid = :fee_paid, paid_time = :paid_time, proof = :proof, change = :change, checking_id = :checking_id WHERE quote = :quote
""",
{
"state": quote.state.name,
"state": quote.state.value,
"fee_paid": quote.fee_paid,
"paid_time": db.to_timestamp(
db.timestamp_from_seconds(quote.paid_time) or ""
Expand Down
14 changes: 13 additions & 1 deletion cashu/mint/migrations.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import copy
from typing import Dict, List

from ..core.base import MintKeyset, Proof
from ..core.base import MeltQuoteState, MintKeyset, MintQuoteState, Proof
from ..core.crypto.keys import derive_keyset_id, derive_keyset_id_deprecated
from ..core.db import Connection, Database
from ..core.settings import settings
Expand Down Expand Up @@ -826,3 +826,15 @@ async def m021_add_change_and_expiry_to_melt_quotes(db: Database):
await conn.execute(
f"ALTER TABLE {db.table_with_schema('melt_quotes')} ADD COLUMN expiry TIMESTAMP"
)


async def m022_quote_set_states_to_values(db: Database):
async with db.connect() as conn:
for melt_quote_states in MeltQuoteState:
await conn.execute(
f"UPDATE {db.table_with_schema('melt_quotes')} SET state = '{melt_quote_states.value}' WHERE state = '{melt_quote_states.name}'"
)
for mint_quote_states in MintQuoteState:
await conn.execute(
f"UPDATE {db.table_with_schema('mint_quotes')} SET state = '{mint_quote_states.value}' WHERE state = '{mint_quote_states.name}'"
)
17 changes: 4 additions & 13 deletions cashu/wallet/api/responses.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,23 +2,13 @@

from pydantic import BaseModel

from ...core.base import Invoice


class PayResponse(BaseModel):
ok: Optional[bool] = None


class InvoiceResponse(BaseModel):
amount: Optional[int] = None
invoice: Optional[Invoice] = None
id: Optional[str] = None
from ...core.base import MeltQuote, MintQuote


class SwapResponse(BaseModel):
outgoing_mint: str
incoming_mint: str
invoice: Invoice
mint_quote: MintQuote
balances: Dict


Expand Down Expand Up @@ -56,7 +46,8 @@ class LocksResponse(BaseModel):


class InvoicesResponse(BaseModel):
invoices: List[Invoice]
mint_quotes: List[MintQuote]
melt_quotes: List[MeltQuote]


class WalletsResponse(BaseModel):
Expand Down
25 changes: 15 additions & 10 deletions cashu/wallet/api/router.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,11 @@
)
from ...nostr.client.client import NostrClient
from ...tor.tor import TorProxy
from ...wallet.crud import get_lightning_invoices, get_reserved_proofs
from ...wallet.crud import (
get_bolt11_melt_quotes,
get_bolt11_mint_quotes,
get_reserved_proofs,
)
from ...wallet.helpers import (
deserialize_token_from_string,
init_wallet,
Expand Down Expand Up @@ -141,7 +145,7 @@ async def create_invoice(
response_model=PaymentStatus,
)
async def invoice_state(
payment_hash: str = Query(default=None, description="Payment hash of paid invoice"),
payment_request: str = Query(default=None, description="Payment request to check"),
mint: str = Query(
default=None,
description="Mint URL to create an invoice at (None for default mint)",
Expand All @@ -150,7 +154,7 @@ async def invoice_state(
global wallet
if mint:
wallet = await mint_wallet(mint)
state = await wallet.get_invoice_status(payment_hash)
state = await wallet.get_invoice_status(payment_request)
return state


Expand Down Expand Up @@ -185,11 +189,11 @@ async def swap(
raise Exception("mints for swap have to be different")

# request invoice from incoming mint
invoice = await incoming_wallet.request_mint(amount)
mint_quote = await incoming_wallet.request_mint(amount)

# pay invoice from outgoing mint
await outgoing_wallet.load_proofs(reload=True)
quote = await outgoing_wallet.melt_quote(invoice.bolt11)
quote = await outgoing_wallet.melt_quote(mint_quote.request)
total_amount = quote.amount + quote.fee_reserve
if outgoing_wallet.available_balance < total_amount:
raise Exception("balance too low")
Expand All @@ -198,17 +202,17 @@ async def swap(
outgoing_wallet.proofs, total_amount, set_reserved=True
)
await outgoing_wallet.melt(
send_proofs, invoice.bolt11, quote.fee_reserve, quote.quote
send_proofs, mint_quote.request, quote.fee_reserve, quote.quote
)

# mint token in incoming mint
await incoming_wallet.mint(amount, id=invoice.id)
await incoming_wallet.mint(amount, quote_id=mint_quote.quote)
await incoming_wallet.load_proofs(reload=True)
mint_balances = await incoming_wallet.balance_per_minturl()
return SwapResponse(
outgoing_mint=outgoing_mint,
incoming_mint=incoming_mint,
invoice=invoice,
mint_quote=mint_quote,
balances=mint_balances,
)

Expand Down Expand Up @@ -386,8 +390,9 @@ async def locks():
"/invoices", name="List all pending invoices", response_model=InvoicesResponse
)
async def invoices():
invoices = await get_lightning_invoices(db=wallet.db)
return InvoicesResponse(invoices=invoices)
mint_quotes = await get_bolt11_mint_quotes(db=wallet.db)
melt_quotes = await get_bolt11_melt_quotes(db=wallet.db)
return InvoicesResponse(mint_quotes=mint_quotes, melt_quotes=melt_quotes)


@router.get(
Expand Down
Loading

0 comments on commit 9262739

Please sign in to comment.