From 10b6e9fa70de8c3e18f9dbfe951fa10918308d24 Mon Sep 17 00:00:00 2001 From: lollerfirst Date: Thu, 1 Aug 2024 10:39:39 +0200 Subject: [PATCH] rename `DiscreteLogContract` to `DiscreetLogContract`, added `status_dlc` --- cashu/core/base.py | 12 ++++++++- cashu/core/errors.py | 8 +++++- cashu/core/models.py | 10 +++---- cashu/mint/crud.py | 16 ++++++----- cashu/mint/db/read.py | 16 +++++++++-- cashu/mint/db/write.py | 14 +++++----- cashu/mint/ledger.py | 32 +++++++++++++++++++--- cashu/mint/router.py | 29 +++++++++++++++----- tests/test_dlc.py | 61 +++++++++++++++++++++++++++++++++++++++--- 9 files changed, 161 insertions(+), 37 deletions(-) diff --git a/cashu/core/base.py b/cashu/core/base.py index 5147ddf4..8a68dc9f 100644 --- a/cashu/core/base.py +++ b/cashu/core/base.py @@ -1212,7 +1212,7 @@ def parse_obj(cls, token_dict: dict): # -------- DLC STUFF -------- -class DiscreteLogContract(BaseModel): +class DiscreetLogContract(BaseModel): """ A discrete log contract """ @@ -1223,6 +1223,16 @@ class DiscreteLogContract(BaseModel): inputs: Optional[List[Proof]] = None # Need to verify these are indeed SCT proofs debts: Optional[Dict[str, int]] = None # We save who we owe money to here + @classmethod + def from_row(cls, row: Row): + return cls( + dlc_root=row["dlc_root"], + settled=bool(row["settled"]), + funding_amount=int(row["funding_amount"]), + unit=row["unit"], + debts=row["debts"] or None, + ) + class DlcBadInput(BaseModel): index: int detail: str diff --git a/cashu/core/errors.py b/cashu/core/errors.py index 1d4701c4..1ec3ddfc 100644 --- a/cashu/core/errors.py +++ b/cashu/core/errors.py @@ -113,4 +113,10 @@ class DlcAlreadyRegisteredError(CashuError): def __init__(self, **kwargs): super().__init__(self.detail, self.code) - \ No newline at end of file + +class DlcNotFoundError(CashuError): + detail = "dlc not found" + code = 30002 + + def __init__(self, **kwargs): + super().__init__(self.detail, self.code) \ No newline at end of file diff --git a/cashu/core/models.py b/cashu/core/models.py index b6b2808f..8acdb71b 100644 --- a/cashu/core/models.py +++ b/cashu/core/models.py @@ -6,7 +6,7 @@ BlindedMessage, BlindedMessage_Deprecated, BlindedSignature, - DiscreteLogContract, + DiscreetLogContract, DlcFundingProof, DlcPayout, DlcPayoutForm, @@ -334,7 +334,7 @@ def __init__(self, **data): # ------- API: DLC REGISTRATION ------- class PostDlcRegistrationRequest(BaseModel): - registrations: List[DiscreteLogContract] + registrations: List[DiscreetLogContract] class PostDlcRegistrationResponse(BaseModel): funded: List[DlcFundingProof] = [] @@ -351,7 +351,6 @@ class PostDlcSettleResponse(BaseModel): # ------- API: DLC PAYOUT ------- class PostDlcPayoutRequest(BaseModel): - atomic: Optional[bool] payouts: List[DlcPayoutForm] class PostDlcPayoutResponse(BaseModel): @@ -362,5 +361,6 @@ class PostDlcPayoutResponse(BaseModel): class GetDlcStatusResponse(BaseModel): settled: bool - funding_amount: Optional[int] - debts: Optional[Dict[str, int]] + unit: Optional[str] = None + funding_amount: Optional[int] = None + debts: Optional[Dict[str, int]] = None diff --git a/cashu/mint/crud.py b/cashu/mint/crud.py index b27245e6..58cf54e2 100644 --- a/cashu/mint/crud.py +++ b/cashu/mint/crud.py @@ -1,7 +1,7 @@ import json from abc import ABC, abstractmethod from typing import Any, Dict, List, Optional -from ..core.base import DiscreteLogContract +from ..core.base import DiscreetLogContract from ..core.base import ( BlindedSignature, @@ -250,13 +250,13 @@ async def get_registered_dlc( dlc_root: str, db: Database, conn: Optional[Connection] = None, - ) -> Optional[DiscreteLogContract]: + ) -> Optional[DiscreetLogContract]: ... @abstractmethod async def store_dlc( self, - dlc: DiscreteLogContract, + dlc: DiscreetLogContract, db: Database, conn: Optional[Connection] = None, ) -> None: @@ -765,17 +765,19 @@ async def get_registered_dlc( dlc_root: str, db: Database, conn: Optional[Connection] = None, - ) -> Optional[DiscreteLogContract]: + ) -> Optional[DiscreetLogContract]: query = f""" SELECT * from {db.table_with_schema('dlc')} WHERE dlc_root = :dlc_root """ - result = await (conn or db).fetchone(query, {"dlc_root": dlc_root}) - return result + row = await (conn or db).fetchone(query, {"dlc_root": dlc_root}) + if not row: + return None + return DiscreetLogContract.from_row(row) async def store_dlc( self, - dlc: DiscreteLogContract, + dlc: DiscreetLogContract, db: Database, conn: Optional[Connection] = None, ) -> None: diff --git a/cashu/mint/db/read.py b/cashu/mint/db/read.py index 6b77d05e..b85cc743 100644 --- a/cashu/mint/db/read.py +++ b/cashu/mint/db/read.py @@ -2,7 +2,11 @@ from ...core.base import Proof, ProofSpentState, ProofState from ...core.db import Connection, Database -from ...core.errors import TokenAlreadySpentError, DlcAlreadyRegisteredError +from ...core.errors import ( + TokenAlreadySpentError, + DlcAlreadyRegisteredError, + DlcNotFoundError, +) from ..crud import LedgerCrud @@ -97,4 +101,12 @@ async def _verify_dlc_registrable( ): async with self.db.get_connection(conn) as conn: if await self.crud.get_registered_dlc(dlc_root, self.db, conn) is not None: - raise DlcAlreadyRegisteredError() \ No newline at end of file + raise DlcAlreadyRegisteredError() + + async def _get_registered_dlc(self, dlc_root: str, conn: Optional[Connection] = None): + async with self.db.get_connection(conn) as conn: + dlc = await self.crud.get_registered_dlc(dlc_root, self.db, conn) + if dlc is None: + raise DlcNotFoundError() + return dlc + \ No newline at end of file diff --git a/cashu/mint/db/write.py b/cashu/mint/db/write.py index c8e4a28c..80b84f92 100644 --- a/cashu/mint/db/write.py +++ b/cashu/mint/db/write.py @@ -10,7 +10,7 @@ Proof, ProofSpentState, ProofState, - DiscreteLogContract, + DiscreetLogContract, DlcFundingProof, DlcBadInput, ) @@ -231,19 +231,19 @@ async def _unset_melt_quote_pending( async def _verify_proofs_and_dlc_registrations( self, - registrations: List[Tuple[DiscreteLogContract, DlcFundingProof]], - ) -> Tuple[List[Tuple[DiscreteLogContract, DlcFundingProof]], List[DlcFundingProof]]: + registrations: List[Tuple[DiscreetLogContract, DlcFundingProof]], + ) -> Tuple[List[Tuple[DiscreetLogContract, DlcFundingProof]], List[DlcFundingProof]]: """ Method to check if proofs are already spent or registrations already registered. If they are not, we set them as spent and registered respectively Args: - registrations (List[Tuple[DiscreteLogContract, DlcFundingProof]]): List of registrations. + registrations (List[Tuple[DiscreetLogContract, DlcFundingProof]]): List of registrations. Returns: - List[Tuple[DiscreteLogContract, DlcFundingProof]]: a list of registered DLCs + List[Tuple[DiscreetLogContract, DlcFundingProof]]: a list of registered DLCs List[DlcFundingProof]: a list of errors """ - checked: List[Tuple[DiscreteLogContract, DlcFundingProof]] = [] - registered: List[Tuple[DiscreteLogContract, DlcFundingProof]] = [] + checked: List[Tuple[DiscreetLogContract, DlcFundingProof]] = [] + registered: List[Tuple[DiscreetLogContract, DlcFundingProof]] = [] errors: List[DlcFundingProof]= [] if len(registrations) == 0: logger.trace("Received 0 registrations") diff --git a/cashu/mint/ledger.py b/cashu/mint/ledger.py index 820d4753..6539695a 100644 --- a/cashu/mint/ledger.py +++ b/cashu/mint/ledger.py @@ -23,7 +23,7 @@ DlcBadInput, DlcFundingProof, DLCWitness, - DiscreteLogContract + DiscreetLogContract ) from ..core.crypto import b_dhke from ..core.crypto.dlc import sign_dlc @@ -51,6 +51,7 @@ PostMintQuoteRequest, PostDlcRegistrationRequest, PostDlcRegistrationResponse, + GetDlcStatusResponse, ) from ..core.settings import settings from ..core.split import amount_split @@ -1095,6 +1096,31 @@ async def _generate_promises( signatures.append(signature) return signatures + async def status_dlc(self, dlc_root: str) -> GetDlcStatusResponse: + """Gets the status of a particular DLC + + Args: + dlc_root (str): the root hash of the contract + Returns: + GetDlcStatusResponse: a response containing the status of the DLC, if it was found. + Raises: + DlcNotFoundError: no DLC with dlc_root was found + """ + logger.trace("status_dlc called") + dlc = await self.db_read._get_registered_dlc(dlc_root) + if not dlc.settled: + return GetDlcStatusResponse( + settled=dlc.settled, + funding_amount=dlc.funding_amount, + unit=dlc.unit, + debts=None + ) + else: + return GetDlcStatusResponse( + settled=dlc.settled, + debts=dlc.debts, + ) + async def register_dlc(self, request: PostDlcRegistrationRequest) -> PostDlcRegistrationResponse: """Validates and registers DiscreteLogContracts Args: @@ -1103,7 +1129,7 @@ async def register_dlc(self, request: PostDlcRegistrationRequest) -> PostDlcRegi PostDlcRegistrationResponse: Indicating the funded and registered DLCs as well as the errors. """ logger.trace("register called") - funded: List[Tuple[DiscreteLogContract, DlcFundingProof]] = [] + funded: List[Tuple[DiscreetLogContract, DlcFundingProof]] = [] errors: List[DlcFundingProof] = [] for registration in request.registrations: try: @@ -1136,7 +1162,7 @@ async def register_dlc(self, request: PostDlcRegistrationRequest) -> PostDlcRegi dlc_root=registration.dlc_root, signature=signature.hex() ) - dlc = DiscreteLogContract( + dlc = DiscreetLogContract( settled=False, dlc_root=registration.dlc_root, funding_amount=amount_provided, diff --git a/cashu/mint/router.py b/cashu/mint/router.py index f0da8bbe..db4434a3 100644 --- a/cashu/mint/router.py +++ b/cashu/mint/router.py @@ -25,7 +25,8 @@ PostSwapRequest, PostSwapResponse, PostDlcRegistrationRequest, - PostDlcRegistrationResponse + PostDlcRegistrationResponse, + GetDlcStatusResponse, ) from ..core.settings import settings from ..mint.startup import ledger @@ -379,16 +380,30 @@ async def restore(payload: PostRestoreRequest) -> PostRestoreResponse: return PostRestoreResponse(outputs=outputs, signatures=signatures) @router.post( - "v1/register", - name="Register", - summary="Register a DLC batch", + "v1/dlc/fund", + name="Fund", + summary="Register and fund a DLC batch", response_model=PostDlcRegistrationResponse, response_description=( "Two lists describing which DLC were registered and which encountered errors respectively." ) ) @limiter.limit(f"{settings.mint_transaction_rate_limit_per_minute}/minute") -async def register(request: Request, payload: PostDlcRegistrationRequest) -> PostDlcRegistrationResponse: - logger.trace(f"> POST /v1/register: {payload}") +async def dlc_fund(request: Request, payload: PostDlcRegistrationRequest) -> PostDlcRegistrationResponse: + logger.trace(f"> POST /v1/dlc/fund: {payload}") assert len(payload.registrations) > 0, "No registrations provided" - return await ledger.register_dlc(payload) \ No newline at end of file + return await ledger.register_dlc(payload) + +@router.get( + "v1/dlc/status/{dlc_root}", + name="", + summary="Register a DLC batch", + response_model=GetDlcStatusResponse, + response_description=( + "Two lists describing which DLC were registered and which encountered errors respectively." + ) +) +@limiter.limit(f"{settings.mint_transaction_rate_limit_per_minute}/minute") +async def dlc_status(request: Request, dlc_root: str) -> GetDlcStatusResponse: + logger.trace(f"> GET /v1/dlc/status/{dlc_root}") + return await ledger.status_dlc(dlc_root) \ No newline at end of file diff --git a/tests/test_dlc.py b/tests/test_dlc.py index e849cc38..08b8439a 100644 --- a/tests/test_dlc.py +++ b/tests/test_dlc.py @@ -4,7 +4,7 @@ from cashu.wallet.wallet import Wallet from cashu.core.secret import Secret, SecretKind from cashu.core.errors import CashuError -from cashu.core.base import DLCWitness, Proof, TokenV4, Unit, DiscreteLogContract +from cashu.core.base import DLCWitness, Proof, TokenV4, Unit, DiscreetLogContract from cashu.core.models import PostDlcRegistrationRequest, PostDlcRegistrationResponse from cashu.mint.ledger import Ledger from cashu.wallet.helpers import send @@ -283,7 +283,7 @@ async def test_registration_vanilla_proofs(wallet: Wallet, ledger: Ledger): pubkey = next(iter(active_keyset_for_unit.public_keys.values())) dlc_root = sha256("TESTING".encode()).hexdigest() - dlc = DiscreteLogContract( + dlc = DiscreetLogContract( funding_amount=64, unit="sat", dlc_root=dlc_root, @@ -319,7 +319,7 @@ async def test_registration_dlc_locked_proofs(wallet: Wallet, ledger: Ledger): active_keyset_for_unit = next(filter(lambda k: k.active and k.unit == Unit["sat"], keysets)) pubkey = next(iter(active_keyset_for_unit.public_keys.values())) - dlc = DiscreteLogContract( + dlc = DiscreetLogContract( funding_amount=64, unit="sat", dlc_root=dlc_root, @@ -335,4 +335,57 @@ async def test_registration_dlc_locked_proofs(wallet: Wallet, ledger: Ledger): assert ( verify_dlc_signature(dlc_root, 64, bytes.fromhex(funding_proof.signature), pubkey), "Could not verify funding proof" - ) \ No newline at end of file + ) + +@pytest.mark.asyncio +async def test_fund_same_dlc_twice(wallet: Wallet, ledger: Ledger): + invoice = await wallet.request_mint(128) + await pay_if_regtest(invoice.bolt11) + minted = await wallet.mint(128, id=invoice.id) + + dlc_root = sha256("TESTING".encode()).hexdigest() + proofs2, proofs1 = await wallet.split(minted, 64) + + dlc1 = DiscreetLogContract( + funding_amount=64, + unit="sat", + dlc_root=dlc_root, + inputs=proofs1, + ) + dlc2 = DiscreetLogContract( + funding_amount=64, + unit="sat", + dlc_root=dlc_root, + inputs=proofs2, + ) + request = PostDlcRegistrationRequest(registrations=[dlc1]) + response = await ledger.register_dlc(request) + assert response.errors is None, f"Funding proofs error: {response.errors[0].bad_inputs}" + request = PostDlcRegistrationRequest(registrations=[dlc2]) + response = await ledger.register_dlc(request) + assert response.errors and response.errors[0].bad_inputs[0].detail == "dlc already registered" + +@pytest.mark.asyncio +async def test_fund_same_dlc_twice_same_batch(wallet: Wallet, ledger: Ledger): + invoice = await wallet.request_mint(128) + await pay_if_regtest(invoice.bolt11) + minted = await wallet.mint(128, id=invoice.id) + + dlc_root = sha256("TESTING".encode()).hexdigest() + proofs2, proofs1 = await wallet.split(minted, 64) + + dlc1 = DiscreetLogContract( + funding_amount=64, + unit="sat", + dlc_root=dlc_root, + inputs=proofs1, + ) + dlc2 = DiscreetLogContract( + funding_amount=64, + unit="sat", + dlc_root=dlc_root, + inputs=proofs2, + ) + request = PostDlcRegistrationRequest(registrations=[dlc1, dlc2]) + response = await ledger.register_dlc(request) + assert response.errors and len(response.errors) == 1, f"Funding proofs error: {response.errors[0].bad_inputs}" \ No newline at end of file