Add custom error types (#290)

* add error types

* better subclassing

* add response models

* fix comments

* add response_description to mint endpoints
This commit is contained in:
callebtc
2023-07-25 23:26:50 +02:00
committed by GitHub
parent b196c34427
commit 3d676dd35f
9 changed files with 294 additions and 151 deletions

View File

@@ -1,21 +1,83 @@
from pydantic import BaseModel
from typing import Optional
class CashuError(BaseModel):
class CashuError(Exception):
code: int
error: str
detail: str
def __init__(self, detail, code=0):
super().__init__(detail)
self.code = code
self.detail = detail
class MintException(CashuError):
code = 100
error = "Mint"
class NotAllowedError(CashuError):
detail = "Not allowed."
code = 10000
def __init__(self, detail: Optional[str] = None, code: Optional[int] = None):
super().__init__(detail or self.detail, code=code or self.code)
class LightningException(MintException):
code = 200
error = "Lightning"
class TransactionError(CashuError):
detail = "Transaction error."
code = 11000
def __init__(self, detail: Optional[str] = None, code: Optional[int] = None):
super().__init__(detail or self.detail, code=code or self.code)
class InvoiceNotPaidException(LightningException):
code = 201
error = "invoice not paid."
class TokenAlreadySpentError(TransactionError):
detail = "Token already spent."
code = 11001
def __init__(self):
super().__init__(self.detail, code=self.code)
class SecretTooLongError(TransactionError):
detail = "Secret too long."
code = 11003
def __init__(self):
super().__init__(self.detail, code=self.code)
class NoSecretInProofsError(TransactionError):
detail = "No secret in proofs."
code = 11004
def __init__(self):
super().__init__(self.detail, code=self.code)
class KeysetError(CashuError):
detail = "Keyset error."
code = 12000
def __init__(self, detail: Optional[str] = None, code: Optional[int] = None):
super().__init__(detail or self.detail, code=code or self.code)
class KeysetNotFoundError(KeysetError):
detail = "Keyset not found."
code = 12001
def __init__(self):
super().__init__(self.detail, code=self.code)
class LightningError(CashuError):
detail = "Lightning error."
code = 20000
def __init__(self, detail: Optional[str] = None, code: Optional[int] = None):
super().__init__(detail or self.detail, code=code or self.code)
class InvoiceNotPaidError(CashuError):
detail = "Lightning invoice not paid yet."
code = 20001
def __init__(self):
super().__init__(self.detail, code=2001)

View File

@@ -1,14 +1,19 @@
import logging
import sys
from traceback import print_exception
from fastapi import FastAPI
from fastapi import FastAPI, status
from fastapi.responses import JSONResponse
# from fastapi_profiler import PyInstrumentProfilerMiddleware
from loguru import logger
from starlette.middleware import Middleware
from starlette.middleware.base import BaseHTTPMiddleware
from starlette.middleware.cors import CORSMiddleware
from starlette.requests import Request
from starlette.responses import Response
from ..core.errors import CashuError
from ..core.settings import settings
from .router import router
from .startup import start_mint_init
@@ -100,9 +105,35 @@ def create_app(config_object="core.settings") -> FastAPI:
app = create_app()
app.include_router(router=router)
@app.middleware("http")
async def catch_exceptions(request: Request, call_next):
try:
return await call_next(request)
except Exception as e:
try:
err_message = str(e)
except:
err_message = e.args[0] if e.args else "Unknown error"
if isinstance(e, CashuError):
logger.error(f"CashuError: {err_message}")
return JSONResponse(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
content={"detail": err_message, "code": e.code},
)
logger.error(f"Exception: {err_message}")
if settings.debug:
print_exception(*sys.exc_info())
return JSONResponse(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
content={"detail": err_message, "code": 0},
)
@app.on_event("startup")
async def startup_mint():
await start_mint_init()
app.include_router(router=router)

View File

@@ -21,6 +21,17 @@ from ..core.crypto import b_dhke
from ..core.crypto.keys import derive_pubkey, random_hash
from ..core.crypto.secp import PublicKey
from ..core.db import Connection, Database
from ..core.errors import (
InvoiceNotPaidError,
KeysetError,
KeysetNotFoundError,
LightningError,
NoSecretInProofsError,
NotAllowedError,
SecretTooLongError,
TokenAlreadySpentError,
TransactionError,
)
from ..core.helpers import fee_reserve, sum_proofs
from ..core.p2pk import verify_p2pk_signature
from ..core.script import verify_bitcoin_script
@@ -202,15 +213,15 @@ class Ledger:
def _verify_secret_criteria(self, proof: Proof) -> Literal[True]:
"""Verifies that a secret is present and is not too long (DOS prevention)."""
if proof.secret is None or proof.secret == "":
raise Exception("no secret in proof.")
raise NoSecretInProofsError()
if len(proof.secret) > 512:
raise Exception("secret too long.")
raise SecretTooLongError()
return True
def _verify_proof_bdhke(self, proof: Proof):
"""Verifies that the proof of promise was issued by this ledger."""
if not self._check_spendable(proof):
raise Exception(f"tokens already spent. Secret: {proof.secret}")
raise TokenAlreadySpentError()
# if no keyset id is given in proof, assume the current one
if not proof.id:
private_key_amount = self.keyset.private_keys[proof.amount]
@@ -256,14 +267,14 @@ class Ledger:
or proof.p2shscript.signature is None
):
# no script present although secret indicates one
raise Exception("no script in proof.")
raise TransactionError("no script in proof.")
# execute and verify P2SH
txin_p2sh_address, valid = verify_bitcoin_script(
proof.p2shscript.script, proof.p2shscript.signature
)
if not valid:
raise Exception("script invalid.")
raise TransactionError("script invalid.")
# check if secret commits to script address
assert secret.data == str(
txin_p2sh_address
@@ -284,11 +295,11 @@ class Ledger:
if not proof.p2pksigs:
# no signature present although secret indicates one
logger.error(f"no p2pk signatures in proof: {proof.p2pksigs}")
raise Exception("no p2pk signatures in proof.")
raise TransactionError("no p2pk signatures in proof.")
# we make sure that there are no duplicate signatures
if len(set(proof.p2pksigs)) != len(proof.p2pksigs):
raise Exception("p2pk signatures must be unique.")
raise TransactionError("p2pk signatures must be unique.")
# we parse the secret as a P2PK commitment
# assert len(proof.secret.split(":")) == 5, "p2pk secret format invalid."
@@ -451,7 +462,7 @@ class Ledger:
)
logger.trace(f"Verifying amount {amount} is valid: {valid}")
if not valid:
raise Exception("invalid amount: " + str(amount))
raise NotAllowedError("invalid amount: " + str(amount))
return amount
def _verify_equation_balanced(
@@ -486,7 +497,7 @@ class Ledger:
error, balance = await self.lightning.status()
logger.trace(f"_request_lightning_invoice: Lightning wallet balance: {balance}")
if error:
raise Exception(f"Lightning wallet not responding: {error}")
raise LightningError(f"Lightning wallet not responding: {error}")
(
ok,
checking_id,
@@ -523,9 +534,9 @@ class Ledger:
)
logger.trace(f"crud: _check_lightning_invoice: invoice: {invoice}")
if invoice is None:
raise Exception("invoice not found.")
raise LightningError("invoice not found.")
if invoice.issued:
raise Exception("tokens already issued for this invoice.")
raise LightningError("tokens already issued for this invoice.")
assert invoice.payment_hash, "invoice has no payment hash."
# set this invoice as issued
@@ -537,7 +548,7 @@ class Ledger:
try:
if amount > invoice.amount:
raise Exception(
raise LightningError(
f"requested amount too high: {amount}. Invoice amount: {invoice.amount}"
)
logger.trace(
@@ -550,7 +561,7 @@ class Ledger:
if status.paid:
return status.paid
else:
raise Exception("Lightning invoice not paid yet.")
raise InvoiceNotPaidError()
except Exception as e:
# unset issued
logger.trace(f"crud: unsetting invoice {invoice.payment_hash} as issued")
@@ -577,7 +588,7 @@ class Ledger:
error, balance = await self.lightning.status()
logger.trace(f"_pay_lightning_invoice: Lightning wallet balance: {balance}")
if error:
raise Exception(f"Lightning wallet not responding: {error}")
raise LightningError(f"Lightning wallet not responding: {error}")
(
ok,
checking_id,
@@ -631,7 +642,7 @@ class Ledger:
f"crud: _set_proofs_pending proof {p.secret} set as pending"
)
except:
raise Exception("proofs already pending.")
raise TransactionError("proofs already pending.")
async def _unset_proofs_pending(
self, proofs: List[Proof], conn: Optional[Connection] = None
@@ -674,7 +685,7 @@ class Ledger:
for p in proofs:
for pp in proofs_pending:
if p.secret == pp.secret:
raise Exception("proofs are pending.")
raise TransactionError("proofs are pending.")
async def _verify_proofs_and_outputs(
self, proofs: List[Proof], outputs: Optional[List[BlindedMessage]] = None
@@ -695,16 +706,16 @@ class Ledger:
# Verify secret criteria
if not all([self._verify_secret_criteria(p) for p in proofs]):
raise Exception("secrets do not match criteria.")
raise TransactionError("secrets do not match criteria.")
# verify that only unique proofs were used
if not self._verify_no_duplicate_proofs(proofs):
raise Exception("duplicate proofs.")
raise TransactionError("duplicate proofs.")
# Verify input spending conditions
if not all([self._verify_input_spending_conditions(p) for p in proofs]):
raise Exception("validation of input spending conditions failed.")
raise TransactionError("validation of input spending conditions failed.")
# Verify ecash signatures
if not all([self._verify_proof_bdhke(p) for p in proofs]):
raise Exception("could not verify proofs.")
raise TransactionError("could not verify proofs.")
if not outputs:
return
@@ -713,12 +724,12 @@ class Ledger:
# verify that only unique outputs were used
if not self._verify_no_duplicate_outputs(outputs):
raise Exception("duplicate promises.")
raise TransactionError("duplicate promises.")
if not self._verify_input_output_amounts(proofs, outputs):
raise Exception("input amounts less than output.")
raise TransactionError("input amounts less than output.")
# Verify output spending conditions
if outputs and not self._verify_output_spending_conditions(proofs, outputs):
raise Exception("validation of output spending conditions failed.")
raise TransactionError("validation of output spending conditions failed.")
async def _generate_change_promises(
self,
@@ -776,7 +787,7 @@ class Ledger:
for i in range(len(outputs)):
outputs[i].amount = return_amounts_sorted[i]
if not self._verify_no_duplicate_outputs(outputs):
raise Exception("duplicate promises.")
raise TransactionError("duplicate promises.")
return_promises = await self._generate_promises(outputs, keyset)
return return_promises
else:
@@ -786,9 +797,9 @@ class Ledger:
def get_keyset(self, keyset_id: Optional[str] = None):
"""Returns a dictionary of hex public keys of a specific keyset for each supported amount"""
if keyset_id and keyset_id not in self.keysets.keysets:
raise Exception("keyset does not exist")
raise KeysetNotFoundError()
keyset = self.keysets.keysets[keyset_id] if keyset_id else self.keyset
assert keyset.public_keys, Exception("no public keys for this keyset")
assert keyset.public_keys, KeysetError("no public keys for this keyset")
return {a: p.serialize().hex() for a, p in keyset.public_keys.items()}
async def request_mint(self, amount: int):
@@ -805,14 +816,16 @@ class Ledger:
"""
logger.trace(f"called request_mint")
if settings.mint_max_peg_in and amount > settings.mint_max_peg_in:
raise Exception(f"Maximum mint amount is {settings.mint_max_peg_in} sat.")
raise NotAllowedError(
f"Maximum mint amount is {settings.mint_max_peg_in} sat."
)
if settings.mint_peg_out_only:
raise Exception("Mint does not allow minting new tokens.")
raise NotAllowedError("Mint does not allow minting new tokens.")
logger.trace(f"requesting invoice for {amount} satoshis")
payment_request, payment_hash = await self._request_lightning_invoice(amount)
logger.trace(f"got invoice {payment_request} with hash {payment_hash}")
assert payment_request and payment_hash, Exception(
assert payment_request and payment_hash, LightningError(
"could not fetch invoice from Lightning backend"
)
@@ -856,7 +869,7 @@ class Ledger:
if settings.lightning:
if not hash:
raise Exception("no hash provided.")
raise NotAllowedError("no hash provided.")
self.locks[hash] = (
self.locks.get(hash) or asyncio.Lock()
) # create a new lock if it doesn't exist
@@ -867,7 +880,7 @@ class Ledger:
for amount in amounts:
if amount not in [2**i for i in range(settings.max_order)]:
raise Exception(
raise NotAllowedError(
f"Can only mint amounts with 2^n up to {2**settings.max_order}."
)
@@ -904,13 +917,13 @@ class Ledger:
invoice_obj = bolt11.decode(invoice)
invoice_amount = math.ceil(invoice_obj.amount_msat / 1000)
if settings.mint_max_peg_out and invoice_amount > settings.mint_max_peg_out:
raise Exception(
raise NotAllowedError(
f"Maximum melt amount is {settings.mint_max_peg_out} sat."
)
fees_msat = await self.check_fees(invoice)
assert total_provided >= invoice_amount + fees_msat / 1000, Exception(
"provided proofs not enough for Lightning payment."
)
assert (
total_provided >= invoice_amount + fees_msat / 1000
), TransactionError("provided proofs not enough for Lightning payment.")
# promises to return for overpaid fees
return_promises: List[BlindedSignature] = []
@@ -933,7 +946,7 @@ class Ledger:
await self._invalidate_proofs(proofs)
logger.trace("invalidated proofs")
# prepare change to compensate wallet for overpaid fees
assert fee_msat is not None, Exception("fees not valid")
assert fee_msat is not None, TransactionError("fees not valid")
if outputs:
return_promises = await self._generate_change_promises(
total_provided=total_provided,
@@ -943,7 +956,7 @@ class Ledger:
)
else:
logger.trace("lightning payment unsuccessful")
raise Exception("Lightning payment unsuccessful.")
raise LightningError("Lightning payment unsuccessful.")
except Exception as e:
logger.trace(f"exception: {e}")

View File

@@ -38,7 +38,7 @@ router: APIRouter = APIRouter()
response_model=GetInfoResponse,
response_model_exclude_none=True,
)
async def info():
async def info() -> GetInfoResponse:
logger.trace(f"> GET /info")
return GetInfoResponse(
name=settings.mint_info_name,
@@ -61,38 +61,43 @@ async def info():
"/keys",
name="Mint public keys",
summary="Get the public keys of the newest mint keyset",
response_description="A dictionary of all supported token values of the mint and their associated public key of the current keyset.",
response_model=KeysResponse,
)
async def keys() -> KeysResponse:
async def keys():
"""This endpoint returns a dictionary of all supported token values of the mint and their associated public key."""
logger.trace(f"> GET /keys")
keyset = ledger.get_keyset()
keys = KeysResponse.parse_obj(keyset)
return keys
return keys.__root__
@router.get(
"/keys/{idBase64Urlsafe}",
name="Keyset public keys",
summary="Public keys of a specific keyset",
response_description="A dictionary of all supported token values of the mint and their associated public key for a specific keyset.",
response_model=KeysResponse,
)
async def keyset_keys(idBase64Urlsafe: str) -> Union[KeysResponse, CashuError]:
async def keyset_keys(idBase64Urlsafe: str):
"""
Get the public keys of the mint from a specific keyset id.
The id is encoded in idBase64Urlsafe (by a wallet) and is converted back to
normal base64 before it can be processed (by the mint).
"""
logger.trace(f"> GET /keys/{idBase64Urlsafe}")
try:
id = idBase64Urlsafe.replace("-", "+").replace("_", "/")
keyset = ledger.get_keyset(keyset_id=id)
keys = KeysResponse.parse_obj(keyset)
return keys
except Exception as exc:
return CashuError(code=0, error=str(exc))
id = idBase64Urlsafe.replace("-", "+").replace("_", "/")
keyset = ledger.get_keyset(keyset_id=id)
keys = KeysResponse.parse_obj(keyset)
return keys.__root__
@router.get(
"/keysets", name="Active keysets", summary="Get all active keyset id of the mind"
"/keysets",
name="Active keysets",
summary="Get all active keyset id of the mind",
response_model=KeysetsResponse,
response_description="A list of all active keyset ids of the mint.",
)
async def keysets() -> KeysetsResponse:
"""This endpoint returns a list of keysets that the mint currently supports and will accept tokens from."""
@@ -101,8 +106,14 @@ async def keysets() -> KeysetsResponse:
return keysets
@router.get("/mint", name="Request mint", summary="Request minting of new tokens")
async def request_mint(amount: int = 0) -> Union[GetMintResponse, CashuError]:
@router.get(
"/mint",
name="Request mint",
summary="Request minting of new tokens",
response_model=GetMintResponse,
response_description="A Lightning invoice to be paid and a hash to request minting of new tokens after payment.",
)
async def request_mint(amount: int = 0) -> GetMintResponse:
"""
Request minting of new tokens. The mint responds with a Lightning invoice.
This endpoint can be used for a Lightning invoice UX flow.
@@ -111,72 +122,72 @@ async def request_mint(amount: int = 0) -> Union[GetMintResponse, CashuError]:
"""
logger.trace(f"> GET /mint: amount={amount}")
if amount > 21_000_000 * 100_000_000 or amount <= 0:
return CashuError(code=0, error="Amount must be a valid amount of sat.")
raise CashuError(code=0, detail="Amount must be a valid amount of sat.")
if settings.mint_peg_out_only:
return CashuError(code=0, error="Mint does not allow minting new tokens.")
try:
payment_request, hash = await ledger.request_mint(amount)
resp = GetMintResponse(pr=payment_request, hash=hash)
logger.trace(f"< GET /mint: {resp}")
return resp
except Exception as exc:
return CashuError(code=0, error=str(exc))
raise CashuError(code=0, detail="Mint does not allow minting new tokens.")
payment_request, hash = await ledger.request_mint(amount)
resp = GetMintResponse(pr=payment_request, hash=hash)
logger.trace(f"< GET /mint: {resp}")
return resp
@router.post(
"/mint",
name="Mint tokens",
summary="Mint tokens in exchange for a Bitcoin paymemt that the user has made",
response_model=PostMintResponse,
response_description="A list of blinded signatures that can be used to create proofs.",
)
async def mint(
payload: PostMintRequest,
hash: Optional[str] = None,
payment_hash: Optional[str] = None,
) -> Union[PostMintResponse, CashuError]:
) -> PostMintResponse:
"""
Requests the minting of tokens belonging to a paid payment request.
Call this endpoint after `GET /mint`.
"""
logger.trace(f"> POST /mint: {payload}")
try:
# BEGIN: backwards compatibility < 0.12 where we used to lookup payments with payment_hash
# We use the payment_hash to lookup the hash from the database and pass that one along.
hash = payment_hash or hash
# END: backwards compatibility < 0.12
promises = await ledger.mint(payload.outputs, hash=hash)
blinded_signatures = PostMintResponse(promises=promises)
logger.trace(f"< POST /mint: {blinded_signatures}")
return blinded_signatures
except Exception as exc:
return CashuError(code=0, error=str(exc))
# BEGIN: backwards compatibility < 0.12 where we used to lookup payments with payment_hash
# We use the payment_hash to lookup the hash from the database and pass that one along.
hash = payment_hash or hash
# END: backwards compatibility < 0.12
promises = await ledger.mint(payload.outputs, hash=hash)
blinded_signatures = PostMintResponse(promises=promises)
logger.trace(f"< POST /mint: {blinded_signatures}")
return blinded_signatures
@router.post(
"/melt",
name="Melt tokens",
summary="Melt tokens for a Bitcoin payment that the mint will make for the user in exchange",
response_model=GetMeltResponse,
response_description="The state of the payment, a preimage as proof of payment, and a list of promises for change.",
)
async def melt(payload: PostMeltRequest) -> Union[CashuError, GetMeltResponse]:
async def melt(payload: PostMeltRequest) -> GetMeltResponse:
"""
Requests tokens to be destroyed and sent out via Lightning.
"""
logger.trace(f"> POST /melt: {payload}")
try:
ok, preimage, change_promises = await ledger.melt(
payload.proofs, payload.pr, payload.outputs
)
resp = GetMeltResponse(paid=ok, preimage=preimage, change=change_promises)
logger.trace(f"< POST /melt: {resp}")
return resp
except Exception as exc:
return CashuError(code=0, error=str(exc))
ok, preimage, change_promises = await ledger.melt(
payload.proofs, payload.pr, payload.outputs
)
resp = GetMeltResponse(paid=ok, preimage=preimage, change=change_promises)
logger.trace(f"< POST /melt: {resp}")
return resp
@router.post(
"/check",
name="Check proof state",
summary="Check whether a proof is spent already or is pending in a transaction",
response_model=CheckSpendableResponse,
response_description="Two lists of booleans indicating whether the provided proofs are spendable or pending in a transaction respectively.",
)
async def check_spendable(
payload: CheckSpendableRequest,
@@ -193,6 +204,8 @@ async def check_spendable(
"/checkfees",
name="Check fees",
summary="Check fee reserve for a Lightning payment",
response_model=CheckFeesResponse,
response_description="The fees necessary to pay a Lightning invoice.",
)
async def check_fees(payload: CheckFeesRequest) -> CheckFeesResponse:
"""
@@ -206,27 +219,28 @@ async def check_fees(payload: CheckFeesRequest) -> CheckFeesResponse:
return CheckFeesResponse(fee=fees_sat)
@router.post("/split", name="Split", summary="Split proofs at a specified amount")
@router.post(
"/split",
name="Split",
summary="Split proofs at a specified amount",
response_model=PostSplitResponse,
response_description="A list of blinded signatures that can be used to create proofs.",
)
async def split(
payload: PostSplitRequest,
) -> Union[CashuError, PostSplitResponse, PostSplitResponse_Deprecated]:
) -> Union[PostSplitResponse, PostSplitResponse_Deprecated]:
"""
Requetst a set of tokens with amount "total" to be split into two
newly minted sets with amount "split" and "total-split".
Requests a set of Proofs to be split into two a new set of BlindedSignatures.
This endpoint is used by Alice to split a set of proofs before making a payment to Carol.
It is then used by Carol (by setting split=total) to redeem the tokens.
"""
logger.trace(f"> POST /split: {payload}")
assert payload.outputs, Exception("no outputs provided.")
try:
promises = await ledger.split(
proofs=payload.proofs, outputs=payload.outputs, amount=payload.amount
)
except Exception as exc:
return CashuError(code=0, error=str(exc))
if not promises:
return CashuError(code=0, error="there was an error with the split")
promises = await ledger.split(
proofs=payload.proofs, outputs=payload.outputs, amount=payload.amount
)
if payload.amount:
# BEGIN backwards compatibility < 0.13
@@ -253,12 +267,13 @@ async def split(
@router.post(
"/restore", name="Restore", summary="Restores a blinded signature from a secret"
"/restore",
name="Restore",
summary="Restores a blinded signature from a secret",
response_model=PostRestoreResponse,
response_description="Two lists with the first being the list of the provided outputs that have an associated blinded signature which is given in the second list.",
)
async def restore(payload: PostMintRequest) -> Union[CashuError, PostRestoreResponse]:
async def restore(payload: PostMintRequest) -> PostRestoreResponse:
assert payload.outputs, Exception("no outputs provided.")
try:
outputs, promises = await ledger.restore(payload.outputs)
except Exception as exc:
return CashuError(code=0, error=str(exc))
outputs, promises = await ledger.restore(payload.outputs)
return PostRestoreResponse(outputs=outputs, promises=promises)

View File

@@ -1,5 +1,6 @@
from fastapi import FastAPI, Request, status
from fastapi.responses import JSONResponse
from loguru import logger
from ...core.settings import settings
from .router import router
@@ -28,9 +29,9 @@ app = create_app()
@app.middleware("http")
async def catch_exceptions(request: Request, call_next):
try:
response = await call_next(request)
return response
return await call_next(request)
except Exception as e:
logger.error(f"Exception: {e}")
return JSONResponse(
status_code=status.HTTP_400_BAD_REQUEST, content={"detail": str(e)}
)

View File

@@ -15,6 +15,7 @@ import requests
from bip32 import BIP32
from loguru import logger
from mnemonic import Mnemonic
from requests import Response
from ..core import bolt11 as bolt11
from ..core.base import (
@@ -225,17 +226,24 @@ class LedgerAPI(object):
return outputs, rs_return
@staticmethod
def raise_on_error(resp_dict) -> None:
def raise_on_error(resp: Response) -> None:
"""Raises an exception if the response from the mint contains an error.
Args:
resp_dict (_type_): Response dict (previously JSON) from mint
resp_dict (Response): Response dict (previously JSON) from mint
Raises:
Exception: if the response contains an error
"""
if "error" in resp_dict:
raise Exception("Mint Error: {}".format(resp_dict["error"]))
resp_dict = resp.json()
if "detail" in resp_dict:
logger.error(f"Error from mint: {resp_dict}")
error_message = f"Mint Error: {resp_dict['detail']}"
if "code" in resp_dict:
error_message += f" (Code: {resp_dict['code']})"
raise Exception(error_message)
# raise for status if no error
resp.raise_for_status()
async def _load_mint_keys(self, keyset_id: str = "") -> WalletKeyset:
"""Loads keys from mint and stores them in the database.
@@ -347,7 +355,7 @@ class LedgerAPI(object):
resp = self.s.get(
url + "/keys",
)
resp.raise_for_status()
self.raise_on_error(resp)
keys: dict = resp.json()
assert len(keys), Exception("did not receive any keys")
keyset_keys = {
@@ -376,9 +384,8 @@ class LedgerAPI(object):
resp = self.s.get(
url + f"/keys/{keyset_id_urlsafe}",
)
resp.raise_for_status()
self.raise_on_error(resp)
keys = resp.json()
self.raise_on_error(keys)
assert len(keys), Exception("did not receive any keys")
keyset_keys = {
int(amt): PublicKey(bytes.fromhex(val), raw=True)
@@ -403,7 +410,7 @@ class LedgerAPI(object):
resp = self.s.get(
url + "/keysets",
)
resp.raise_for_status()
self.raise_on_error(resp)
keysets_dict = resp.json()
keysets = KeysetsResponse.parse_obj(keysets_dict)
assert len(keysets.keysets), Exception("did not receive any keysets")
@@ -425,7 +432,7 @@ class LedgerAPI(object):
resp = self.s.get(
url + "/info",
)
resp.raise_for_status()
self.raise_on_error(resp)
data: dict = resp.json()
mint_info: GetInfoResponse = GetInfoResponse.parse_obj(data)
return mint_info
@@ -445,9 +452,8 @@ class LedgerAPI(object):
"""
logger.trace("Requesting mint: GET /mint")
resp = self.s.get(self.url + "/mint", params={"amount": amount})
resp.raise_for_status()
self.raise_on_error(resp)
return_dict = resp.json()
self.raise_on_error(return_dict)
mint_response = GetMintResponse.parse_obj(return_dict)
return Invoice(amount=amount, pr=mint_response.pr, hash=mint_response.hash)
@@ -483,9 +489,8 @@ class LedgerAPI(object):
"payment_hash": hash, # backwards compatibility pre 0.12.0
},
)
resp.raise_for_status()
self.raise_on_error(resp)
reponse_dict = resp.json()
self.raise_on_error(reponse_dict)
logger.trace("Lightning invoice checked. POST /mint")
promises = PostMintResponse.parse_obj(reponse_dict).promises
@@ -518,10 +523,8 @@ class LedgerAPI(object):
self.url + "/split",
json=split_payload.dict(include=_splitrequest_include_fields(proofs)), # type: ignore
)
resp.raise_for_status()
self.raise_on_error(resp)
promises_dict = resp.json()
self.raise_on_error(promises_dict)
mint_response = PostMintResponse.parse_obj(promises_dict)
promises = [BlindedSignature(**p.dict()) for p in mint_response.promises]
@@ -547,9 +550,9 @@ class LedgerAPI(object):
self.url + "/check",
json=payload.dict(include=_check_proof_state_include_fields(proofs)), # type: ignore
)
resp.raise_for_status()
self.raise_on_error(resp)
return_dict = resp.json()
self.raise_on_error(return_dict)
states = CheckSpendableResponse.parse_obj(return_dict)
return states
@@ -561,9 +564,9 @@ class LedgerAPI(object):
self.url + "/checkfees",
json=payload.dict(),
)
resp.raise_for_status()
self.raise_on_error(resp)
return_dict = resp.json()
self.raise_on_error(return_dict)
return return_dict
@async_set_requests
@@ -589,9 +592,9 @@ class LedgerAPI(object):
self.url + "/melt",
json=payload.dict(include=_meltrequest_include_fields(proofs)), # type: ignore
)
resp.raise_for_status()
self.raise_on_error(resp)
return_dict = resp.json()
self.raise_on_error(return_dict)
return GetMeltResponse.parse_obj(return_dict)
@async_set_requests
@@ -603,9 +606,8 @@ class LedgerAPI(object):
"""
payload = PostMintRequest(outputs=outputs)
resp = self.s.post(self.url + "/restore", json=payload.dict())
resp.raise_for_status()
self.raise_on_error(resp)
reponse_dict = resp.json()
self.raise_on_error(reponse_dict)
returnObj = PostRestoreResponse.parse_obj(reponse_dict)
return returnObj.outputs, returnObj.promises

View File

@@ -49,13 +49,13 @@ async def test_api_keyset_keys(ledger):
@pytest.mark.asyncio
async def test_api_mint_validation(ledger):
response = requests.get(f"{BASE_URL}/mint?amount=-21")
assert "error" in response.json()
assert "detail" in response.json()
response = requests.get(f"{BASE_URL}/mint?amount=0")
assert "error" in response.json()
assert "detail" in response.json()
response = requests.get(f"{BASE_URL}/mint?amount=2100000000000001")
assert "error" in response.json()
assert "detail" in response.json()
response = requests.get(f"{BASE_URL}/mint?amount=1")
assert "error" not in response.json()
assert "detail" not in response.json()
@pytest.mark.asyncio

View File

@@ -2,7 +2,7 @@ import asyncio
import shutil
import time
from pathlib import Path
from typing import Dict, List
from typing import Dict, List, Union
import pytest
import pytest_asyncio
@@ -10,6 +10,18 @@ from mnemonic import Mnemonic
from cashu.core.base import Proof, Secret, SecretKind, Tags
from cashu.core.crypto.secp import PrivateKey, PublicKey
from cashu.core.errors import (
CashuError,
InvoiceNotPaidError,
KeysetError,
KeysetNotFoundError,
LightningError,
NoSecretInProofsError,
NotAllowedError,
SecretTooLongError,
TokenAlreadySpentError,
TransactionError,
)
from cashu.core.helpers import async_unwrap, sum_proofs
from cashu.core.migrations import migrate_databases
from cashu.core.settings import settings
@@ -20,13 +32,20 @@ from cashu.wallet.wallet import Wallet as Wallet2
from tests.conftest import SERVER_ENDPOINT, mint
async def assert_err(f, msg):
async def assert_err(f, msg: Union[str, CashuError]):
"""Compute f() and expect an error message 'msg'."""
try:
await f
except Exception as exc:
if str(exc.args[0]) != msg:
raise Exception(f"Expected error: {msg}, got: {exc.args[0]}")
error_message: str = str(exc.args[0])
if isinstance(msg, CashuError):
if msg.detail not in error_message:
raise Exception(
f"CashuError. Expected error: {msg.detail}, got: {error_message}"
)
return
if msg not in error_message:
raise Exception(f"Expected error: {msg}, got: {error_message}")
return
raise Exception(f"Expected error: {msg}, got no error")
@@ -119,7 +138,7 @@ async def test_get_info(wallet1: Wallet):
async def test_get_nonexistent_keyset(wallet1: Wallet):
await assert_err(
wallet1._get_keys_of_keyset(wallet1.url, "nonexistent"),
"Mint Error: keyset does not exist",
KeysetNotFoundError(),
)
@@ -221,7 +240,7 @@ async def test_double_spend(wallet1: Wallet):
await wallet1.split(wallet1.proofs, 20)
await assert_err(
wallet1.split(doublespend, 20),
f"Mint Error: tokens already spent. Secret: {doublespend[0]['secret']}",
f"Mint Error: Token already spent.",
)
assert wallet1.balance == 64
assert wallet1.available_balance == 64

View File

@@ -23,7 +23,7 @@ async def assert_err(f, msg):
try:
await f
except Exception as exc:
if str(exc.args[0]) != msg:
if msg not in str(exc.args[0]):
raise Exception(f"Expected error: {msg}, got: {exc.args[0]}")
return
raise Exception(f"Expected error: {msg}, got no error")