mirror of
https://github.com/aljazceru/nutshell.git
synced 2025-12-20 18:44:20 +01:00
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:
@@ -1,21 +1,83 @@
|
|||||||
from pydantic import BaseModel
|
from typing import Optional
|
||||||
|
|
||||||
|
|
||||||
class CashuError(BaseModel):
|
class CashuError(Exception):
|
||||||
code: int
|
code: int
|
||||||
error: str
|
detail: str
|
||||||
|
|
||||||
|
def __init__(self, detail, code=0):
|
||||||
|
super().__init__(detail)
|
||||||
|
self.code = code
|
||||||
|
self.detail = detail
|
||||||
|
|
||||||
|
|
||||||
class MintException(CashuError):
|
class NotAllowedError(CashuError):
|
||||||
code = 100
|
detail = "Not allowed."
|
||||||
error = "Mint"
|
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):
|
class TransactionError(CashuError):
|
||||||
code = 200
|
detail = "Transaction error."
|
||||||
error = "Lightning"
|
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):
|
class TokenAlreadySpentError(TransactionError):
|
||||||
code = 201
|
detail = "Token already spent."
|
||||||
error = "invoice not paid."
|
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)
|
||||||
|
|||||||
@@ -1,14 +1,19 @@
|
|||||||
import logging
|
import logging
|
||||||
import sys
|
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 fastapi_profiler import PyInstrumentProfilerMiddleware
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
from starlette.middleware import Middleware
|
from starlette.middleware import Middleware
|
||||||
from starlette.middleware.base import BaseHTTPMiddleware
|
from starlette.middleware.base import BaseHTTPMiddleware
|
||||||
from starlette.middleware.cors import CORSMiddleware
|
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 ..core.settings import settings
|
||||||
from .router import router
|
from .router import router
|
||||||
from .startup import start_mint_init
|
from .startup import start_mint_init
|
||||||
@@ -100,9 +105,35 @@ def create_app(config_object="core.settings") -> FastAPI:
|
|||||||
|
|
||||||
app = create_app()
|
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")
|
@app.on_event("startup")
|
||||||
async def startup_mint():
|
async def startup_mint():
|
||||||
await start_mint_init()
|
await start_mint_init()
|
||||||
|
|
||||||
|
|
||||||
|
app.include_router(router=router)
|
||||||
|
|||||||
@@ -21,6 +21,17 @@ from ..core.crypto import b_dhke
|
|||||||
from ..core.crypto.keys import derive_pubkey, random_hash
|
from ..core.crypto.keys import derive_pubkey, random_hash
|
||||||
from ..core.crypto.secp import PublicKey
|
from ..core.crypto.secp import PublicKey
|
||||||
from ..core.db import Connection, Database
|
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.helpers import fee_reserve, sum_proofs
|
||||||
from ..core.p2pk import verify_p2pk_signature
|
from ..core.p2pk import verify_p2pk_signature
|
||||||
from ..core.script import verify_bitcoin_script
|
from ..core.script import verify_bitcoin_script
|
||||||
@@ -202,15 +213,15 @@ class Ledger:
|
|||||||
def _verify_secret_criteria(self, proof: Proof) -> Literal[True]:
|
def _verify_secret_criteria(self, proof: Proof) -> Literal[True]:
|
||||||
"""Verifies that a secret is present and is not too long (DOS prevention)."""
|
"""Verifies that a secret is present and is not too long (DOS prevention)."""
|
||||||
if proof.secret is None or proof.secret == "":
|
if proof.secret is None or proof.secret == "":
|
||||||
raise Exception("no secret in proof.")
|
raise NoSecretInProofsError()
|
||||||
if len(proof.secret) > 512:
|
if len(proof.secret) > 512:
|
||||||
raise Exception("secret too long.")
|
raise SecretTooLongError()
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def _verify_proof_bdhke(self, proof: Proof):
|
def _verify_proof_bdhke(self, proof: Proof):
|
||||||
"""Verifies that the proof of promise was issued by this ledger."""
|
"""Verifies that the proof of promise was issued by this ledger."""
|
||||||
if not self._check_spendable(proof):
|
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 no keyset id is given in proof, assume the current one
|
||||||
if not proof.id:
|
if not proof.id:
|
||||||
private_key_amount = self.keyset.private_keys[proof.amount]
|
private_key_amount = self.keyset.private_keys[proof.amount]
|
||||||
@@ -256,14 +267,14 @@ class Ledger:
|
|||||||
or proof.p2shscript.signature is None
|
or proof.p2shscript.signature is None
|
||||||
):
|
):
|
||||||
# no script present although secret indicates one
|
# no script present although secret indicates one
|
||||||
raise Exception("no script in proof.")
|
raise TransactionError("no script in proof.")
|
||||||
|
|
||||||
# execute and verify P2SH
|
# execute and verify P2SH
|
||||||
txin_p2sh_address, valid = verify_bitcoin_script(
|
txin_p2sh_address, valid = verify_bitcoin_script(
|
||||||
proof.p2shscript.script, proof.p2shscript.signature
|
proof.p2shscript.script, proof.p2shscript.signature
|
||||||
)
|
)
|
||||||
if not valid:
|
if not valid:
|
||||||
raise Exception("script invalid.")
|
raise TransactionError("script invalid.")
|
||||||
# check if secret commits to script address
|
# check if secret commits to script address
|
||||||
assert secret.data == str(
|
assert secret.data == str(
|
||||||
txin_p2sh_address
|
txin_p2sh_address
|
||||||
@@ -284,11 +295,11 @@ class Ledger:
|
|||||||
if not proof.p2pksigs:
|
if not proof.p2pksigs:
|
||||||
# no signature present although secret indicates one
|
# no signature present although secret indicates one
|
||||||
logger.error(f"no p2pk signatures in proof: {proof.p2pksigs}")
|
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
|
# we make sure that there are no duplicate signatures
|
||||||
if len(set(proof.p2pksigs)) != len(proof.p2pksigs):
|
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
|
# we parse the secret as a P2PK commitment
|
||||||
# assert len(proof.secret.split(":")) == 5, "p2pk secret format invalid."
|
# 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}")
|
logger.trace(f"Verifying amount {amount} is valid: {valid}")
|
||||||
if not valid:
|
if not valid:
|
||||||
raise Exception("invalid amount: " + str(amount))
|
raise NotAllowedError("invalid amount: " + str(amount))
|
||||||
return amount
|
return amount
|
||||||
|
|
||||||
def _verify_equation_balanced(
|
def _verify_equation_balanced(
|
||||||
@@ -486,7 +497,7 @@ class Ledger:
|
|||||||
error, balance = await self.lightning.status()
|
error, balance = await self.lightning.status()
|
||||||
logger.trace(f"_request_lightning_invoice: Lightning wallet balance: {balance}")
|
logger.trace(f"_request_lightning_invoice: Lightning wallet balance: {balance}")
|
||||||
if error:
|
if error:
|
||||||
raise Exception(f"Lightning wallet not responding: {error}")
|
raise LightningError(f"Lightning wallet not responding: {error}")
|
||||||
(
|
(
|
||||||
ok,
|
ok,
|
||||||
checking_id,
|
checking_id,
|
||||||
@@ -523,9 +534,9 @@ class Ledger:
|
|||||||
)
|
)
|
||||||
logger.trace(f"crud: _check_lightning_invoice: invoice: {invoice}")
|
logger.trace(f"crud: _check_lightning_invoice: invoice: {invoice}")
|
||||||
if invoice is None:
|
if invoice is None:
|
||||||
raise Exception("invoice not found.")
|
raise LightningError("invoice not found.")
|
||||||
if invoice.issued:
|
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."
|
assert invoice.payment_hash, "invoice has no payment hash."
|
||||||
|
|
||||||
# set this invoice as issued
|
# set this invoice as issued
|
||||||
@@ -537,7 +548,7 @@ class Ledger:
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
if amount > invoice.amount:
|
if amount > invoice.amount:
|
||||||
raise Exception(
|
raise LightningError(
|
||||||
f"requested amount too high: {amount}. Invoice amount: {invoice.amount}"
|
f"requested amount too high: {amount}. Invoice amount: {invoice.amount}"
|
||||||
)
|
)
|
||||||
logger.trace(
|
logger.trace(
|
||||||
@@ -550,7 +561,7 @@ class Ledger:
|
|||||||
if status.paid:
|
if status.paid:
|
||||||
return status.paid
|
return status.paid
|
||||||
else:
|
else:
|
||||||
raise Exception("Lightning invoice not paid yet.")
|
raise InvoiceNotPaidError()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
# unset issued
|
# unset issued
|
||||||
logger.trace(f"crud: unsetting invoice {invoice.payment_hash} as issued")
|
logger.trace(f"crud: unsetting invoice {invoice.payment_hash} as issued")
|
||||||
@@ -577,7 +588,7 @@ class Ledger:
|
|||||||
error, balance = await self.lightning.status()
|
error, balance = await self.lightning.status()
|
||||||
logger.trace(f"_pay_lightning_invoice: Lightning wallet balance: {balance}")
|
logger.trace(f"_pay_lightning_invoice: Lightning wallet balance: {balance}")
|
||||||
if error:
|
if error:
|
||||||
raise Exception(f"Lightning wallet not responding: {error}")
|
raise LightningError(f"Lightning wallet not responding: {error}")
|
||||||
(
|
(
|
||||||
ok,
|
ok,
|
||||||
checking_id,
|
checking_id,
|
||||||
@@ -631,7 +642,7 @@ class Ledger:
|
|||||||
f"crud: _set_proofs_pending proof {p.secret} set as pending"
|
f"crud: _set_proofs_pending proof {p.secret} set as pending"
|
||||||
)
|
)
|
||||||
except:
|
except:
|
||||||
raise Exception("proofs already pending.")
|
raise TransactionError("proofs already pending.")
|
||||||
|
|
||||||
async def _unset_proofs_pending(
|
async def _unset_proofs_pending(
|
||||||
self, proofs: List[Proof], conn: Optional[Connection] = None
|
self, proofs: List[Proof], conn: Optional[Connection] = None
|
||||||
@@ -674,7 +685,7 @@ class Ledger:
|
|||||||
for p in proofs:
|
for p in proofs:
|
||||||
for pp in proofs_pending:
|
for pp in proofs_pending:
|
||||||
if p.secret == pp.secret:
|
if p.secret == pp.secret:
|
||||||
raise Exception("proofs are pending.")
|
raise TransactionError("proofs are pending.")
|
||||||
|
|
||||||
async def _verify_proofs_and_outputs(
|
async def _verify_proofs_and_outputs(
|
||||||
self, proofs: List[Proof], outputs: Optional[List[BlindedMessage]] = None
|
self, proofs: List[Proof], outputs: Optional[List[BlindedMessage]] = None
|
||||||
@@ -695,16 +706,16 @@ class Ledger:
|
|||||||
|
|
||||||
# Verify secret criteria
|
# Verify secret criteria
|
||||||
if not all([self._verify_secret_criteria(p) for p in proofs]):
|
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
|
# verify that only unique proofs were used
|
||||||
if not self._verify_no_duplicate_proofs(proofs):
|
if not self._verify_no_duplicate_proofs(proofs):
|
||||||
raise Exception("duplicate proofs.")
|
raise TransactionError("duplicate proofs.")
|
||||||
# Verify input spending conditions
|
# Verify input spending conditions
|
||||||
if not all([self._verify_input_spending_conditions(p) for p in proofs]):
|
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
|
# Verify ecash signatures
|
||||||
if not all([self._verify_proof_bdhke(p) for p in proofs]):
|
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:
|
if not outputs:
|
||||||
return
|
return
|
||||||
@@ -713,12 +724,12 @@ class Ledger:
|
|||||||
|
|
||||||
# verify that only unique outputs were used
|
# verify that only unique outputs were used
|
||||||
if not self._verify_no_duplicate_outputs(outputs):
|
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):
|
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
|
# Verify output spending conditions
|
||||||
if outputs and not self._verify_output_spending_conditions(proofs, outputs):
|
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(
|
async def _generate_change_promises(
|
||||||
self,
|
self,
|
||||||
@@ -776,7 +787,7 @@ class Ledger:
|
|||||||
for i in range(len(outputs)):
|
for i in range(len(outputs)):
|
||||||
outputs[i].amount = return_amounts_sorted[i]
|
outputs[i].amount = return_amounts_sorted[i]
|
||||||
if not self._verify_no_duplicate_outputs(outputs):
|
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_promises = await self._generate_promises(outputs, keyset)
|
||||||
return return_promises
|
return return_promises
|
||||||
else:
|
else:
|
||||||
@@ -786,9 +797,9 @@ class Ledger:
|
|||||||
def get_keyset(self, keyset_id: Optional[str] = None):
|
def get_keyset(self, keyset_id: Optional[str] = None):
|
||||||
"""Returns a dictionary of hex public keys of a specific keyset for each supported amount"""
|
"""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:
|
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
|
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()}
|
return {a: p.serialize().hex() for a, p in keyset.public_keys.items()}
|
||||||
|
|
||||||
async def request_mint(self, amount: int):
|
async def request_mint(self, amount: int):
|
||||||
@@ -805,14 +816,16 @@ class Ledger:
|
|||||||
"""
|
"""
|
||||||
logger.trace(f"called request_mint")
|
logger.trace(f"called request_mint")
|
||||||
if settings.mint_max_peg_in and amount > settings.mint_max_peg_in:
|
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:
|
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")
|
logger.trace(f"requesting invoice for {amount} satoshis")
|
||||||
payment_request, payment_hash = await self._request_lightning_invoice(amount)
|
payment_request, payment_hash = await self._request_lightning_invoice(amount)
|
||||||
logger.trace(f"got invoice {payment_request} with hash {payment_hash}")
|
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"
|
"could not fetch invoice from Lightning backend"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -856,7 +869,7 @@ class Ledger:
|
|||||||
|
|
||||||
if settings.lightning:
|
if settings.lightning:
|
||||||
if not hash:
|
if not hash:
|
||||||
raise Exception("no hash provided.")
|
raise NotAllowedError("no hash provided.")
|
||||||
self.locks[hash] = (
|
self.locks[hash] = (
|
||||||
self.locks.get(hash) or asyncio.Lock()
|
self.locks.get(hash) or asyncio.Lock()
|
||||||
) # create a new lock if it doesn't exist
|
) # create a new lock if it doesn't exist
|
||||||
@@ -867,7 +880,7 @@ class Ledger:
|
|||||||
|
|
||||||
for amount in amounts:
|
for amount in amounts:
|
||||||
if amount not in [2**i for i in range(settings.max_order)]:
|
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}."
|
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_obj = bolt11.decode(invoice)
|
||||||
invoice_amount = math.ceil(invoice_obj.amount_msat / 1000)
|
invoice_amount = math.ceil(invoice_obj.amount_msat / 1000)
|
||||||
if settings.mint_max_peg_out and invoice_amount > settings.mint_max_peg_out:
|
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."
|
f"Maximum melt amount is {settings.mint_max_peg_out} sat."
|
||||||
)
|
)
|
||||||
fees_msat = await self.check_fees(invoice)
|
fees_msat = await self.check_fees(invoice)
|
||||||
assert total_provided >= invoice_amount + fees_msat / 1000, Exception(
|
assert (
|
||||||
"provided proofs not enough for Lightning payment."
|
total_provided >= invoice_amount + fees_msat / 1000
|
||||||
)
|
), TransactionError("provided proofs not enough for Lightning payment.")
|
||||||
|
|
||||||
# promises to return for overpaid fees
|
# promises to return for overpaid fees
|
||||||
return_promises: List[BlindedSignature] = []
|
return_promises: List[BlindedSignature] = []
|
||||||
@@ -933,7 +946,7 @@ class Ledger:
|
|||||||
await self._invalidate_proofs(proofs)
|
await self._invalidate_proofs(proofs)
|
||||||
logger.trace("invalidated proofs")
|
logger.trace("invalidated proofs")
|
||||||
# prepare change to compensate wallet for overpaid fees
|
# 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:
|
if outputs:
|
||||||
return_promises = await self._generate_change_promises(
|
return_promises = await self._generate_change_promises(
|
||||||
total_provided=total_provided,
|
total_provided=total_provided,
|
||||||
@@ -943,7 +956,7 @@ class Ledger:
|
|||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
logger.trace("lightning payment unsuccessful")
|
logger.trace("lightning payment unsuccessful")
|
||||||
raise Exception("Lightning payment unsuccessful.")
|
raise LightningError("Lightning payment unsuccessful.")
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.trace(f"exception: {e}")
|
logger.trace(f"exception: {e}")
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ router: APIRouter = APIRouter()
|
|||||||
response_model=GetInfoResponse,
|
response_model=GetInfoResponse,
|
||||||
response_model_exclude_none=True,
|
response_model_exclude_none=True,
|
||||||
)
|
)
|
||||||
async def info():
|
async def info() -> GetInfoResponse:
|
||||||
logger.trace(f"> GET /info")
|
logger.trace(f"> GET /info")
|
||||||
return GetInfoResponse(
|
return GetInfoResponse(
|
||||||
name=settings.mint_info_name,
|
name=settings.mint_info_name,
|
||||||
@@ -61,38 +61,43 @@ async def info():
|
|||||||
"/keys",
|
"/keys",
|
||||||
name="Mint public keys",
|
name="Mint public keys",
|
||||||
summary="Get the public keys of the newest mint keyset",
|
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."""
|
"""This endpoint returns a dictionary of all supported token values of the mint and their associated public key."""
|
||||||
logger.trace(f"> GET /keys")
|
logger.trace(f"> GET /keys")
|
||||||
keyset = ledger.get_keyset()
|
keyset = ledger.get_keyset()
|
||||||
keys = KeysResponse.parse_obj(keyset)
|
keys = KeysResponse.parse_obj(keyset)
|
||||||
return keys
|
return keys.__root__
|
||||||
|
|
||||||
|
|
||||||
@router.get(
|
@router.get(
|
||||||
"/keys/{idBase64Urlsafe}",
|
"/keys/{idBase64Urlsafe}",
|
||||||
name="Keyset public keys",
|
name="Keyset public keys",
|
||||||
summary="Public keys of a specific keyset",
|
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.
|
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
|
The id is encoded in idBase64Urlsafe (by a wallet) and is converted back to
|
||||||
normal base64 before it can be processed (by the mint).
|
normal base64 before it can be processed (by the mint).
|
||||||
"""
|
"""
|
||||||
logger.trace(f"> GET /keys/{idBase64Urlsafe}")
|
logger.trace(f"> GET /keys/{idBase64Urlsafe}")
|
||||||
try:
|
id = idBase64Urlsafe.replace("-", "+").replace("_", "/")
|
||||||
id = idBase64Urlsafe.replace("-", "+").replace("_", "/")
|
keyset = ledger.get_keyset(keyset_id=id)
|
||||||
keyset = ledger.get_keyset(keyset_id=id)
|
keys = KeysResponse.parse_obj(keyset)
|
||||||
keys = KeysResponse.parse_obj(keyset)
|
return keys.__root__
|
||||||
return keys
|
|
||||||
except Exception as exc:
|
|
||||||
return CashuError(code=0, error=str(exc))
|
|
||||||
|
|
||||||
|
|
||||||
@router.get(
|
@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:
|
async def keysets() -> KeysetsResponse:
|
||||||
"""This endpoint returns a list of keysets that the mint currently supports and will accept tokens from."""
|
"""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
|
return keysets
|
||||||
|
|
||||||
|
|
||||||
@router.get("/mint", name="Request mint", summary="Request minting of new tokens")
|
@router.get(
|
||||||
async def request_mint(amount: int = 0) -> Union[GetMintResponse, CashuError]:
|
"/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.
|
Request minting of new tokens. The mint responds with a Lightning invoice.
|
||||||
This endpoint can be used for a Lightning invoice UX flow.
|
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}")
|
logger.trace(f"> GET /mint: amount={amount}")
|
||||||
if amount > 21_000_000 * 100_000_000 or amount <= 0:
|
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:
|
if settings.mint_peg_out_only:
|
||||||
return CashuError(code=0, error="Mint does not allow minting new tokens.")
|
raise CashuError(code=0, detail="Mint does not allow minting new tokens.")
|
||||||
try:
|
|
||||||
payment_request, hash = await ledger.request_mint(amount)
|
payment_request, hash = await ledger.request_mint(amount)
|
||||||
resp = GetMintResponse(pr=payment_request, hash=hash)
|
resp = GetMintResponse(pr=payment_request, hash=hash)
|
||||||
logger.trace(f"< GET /mint: {resp}")
|
logger.trace(f"< GET /mint: {resp}")
|
||||||
return resp
|
return resp
|
||||||
except Exception as exc:
|
|
||||||
return CashuError(code=0, error=str(exc))
|
|
||||||
|
|
||||||
|
|
||||||
@router.post(
|
@router.post(
|
||||||
"/mint",
|
"/mint",
|
||||||
name="Mint tokens",
|
name="Mint tokens",
|
||||||
summary="Mint tokens in exchange for a Bitcoin paymemt that the user has made",
|
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(
|
async def mint(
|
||||||
payload: PostMintRequest,
|
payload: PostMintRequest,
|
||||||
hash: Optional[str] = None,
|
hash: Optional[str] = None,
|
||||||
payment_hash: Optional[str] = None,
|
payment_hash: Optional[str] = None,
|
||||||
) -> Union[PostMintResponse, CashuError]:
|
) -> PostMintResponse:
|
||||||
"""
|
"""
|
||||||
Requests the minting of tokens belonging to a paid payment request.
|
Requests the minting of tokens belonging to a paid payment request.
|
||||||
|
|
||||||
Call this endpoint after `GET /mint`.
|
Call this endpoint after `GET /mint`.
|
||||||
"""
|
"""
|
||||||
logger.trace(f"> POST /mint: {payload}")
|
logger.trace(f"> POST /mint: {payload}")
|
||||||
try:
|
|
||||||
# BEGIN: backwards compatibility < 0.12 where we used to lookup payments with payment_hash
|
# 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.
|
# We use the payment_hash to lookup the hash from the database and pass that one along.
|
||||||
hash = payment_hash or hash
|
hash = payment_hash or hash
|
||||||
# END: backwards compatibility < 0.12
|
# END: backwards compatibility < 0.12
|
||||||
promises = await ledger.mint(payload.outputs, hash=hash)
|
|
||||||
blinded_signatures = PostMintResponse(promises=promises)
|
promises = await ledger.mint(payload.outputs, hash=hash)
|
||||||
logger.trace(f"< POST /mint: {blinded_signatures}")
|
blinded_signatures = PostMintResponse(promises=promises)
|
||||||
return blinded_signatures
|
logger.trace(f"< POST /mint: {blinded_signatures}")
|
||||||
except Exception as exc:
|
return blinded_signatures
|
||||||
return CashuError(code=0, error=str(exc))
|
|
||||||
|
|
||||||
|
|
||||||
@router.post(
|
@router.post(
|
||||||
"/melt",
|
"/melt",
|
||||||
name="Melt tokens",
|
name="Melt tokens",
|
||||||
summary="Melt tokens for a Bitcoin payment that the mint will make for the user in exchange",
|
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.
|
Requests tokens to be destroyed and sent out via Lightning.
|
||||||
"""
|
"""
|
||||||
logger.trace(f"> POST /melt: {payload}")
|
logger.trace(f"> POST /melt: {payload}")
|
||||||
try:
|
ok, preimage, change_promises = await ledger.melt(
|
||||||
ok, preimage, change_promises = await ledger.melt(
|
payload.proofs, payload.pr, payload.outputs
|
||||||
payload.proofs, payload.pr, payload.outputs
|
)
|
||||||
)
|
resp = GetMeltResponse(paid=ok, preimage=preimage, change=change_promises)
|
||||||
resp = GetMeltResponse(paid=ok, preimage=preimage, change=change_promises)
|
logger.trace(f"< POST /melt: {resp}")
|
||||||
logger.trace(f"< POST /melt: {resp}")
|
return resp
|
||||||
return resp
|
|
||||||
except Exception as exc:
|
|
||||||
return CashuError(code=0, error=str(exc))
|
|
||||||
|
|
||||||
|
|
||||||
@router.post(
|
@router.post(
|
||||||
"/check",
|
"/check",
|
||||||
name="Check proof state",
|
name="Check proof state",
|
||||||
summary="Check whether a proof is spent already or is pending in a transaction",
|
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(
|
async def check_spendable(
|
||||||
payload: CheckSpendableRequest,
|
payload: CheckSpendableRequest,
|
||||||
@@ -193,6 +204,8 @@ async def check_spendable(
|
|||||||
"/checkfees",
|
"/checkfees",
|
||||||
name="Check fees",
|
name="Check fees",
|
||||||
summary="Check fee reserve for a Lightning payment",
|
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:
|
async def check_fees(payload: CheckFeesRequest) -> CheckFeesResponse:
|
||||||
"""
|
"""
|
||||||
@@ -206,27 +219,28 @@ async def check_fees(payload: CheckFeesRequest) -> CheckFeesResponse:
|
|||||||
return CheckFeesResponse(fee=fees_sat)
|
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(
|
async def split(
|
||||||
payload: PostSplitRequest,
|
payload: PostSplitRequest,
|
||||||
) -> Union[CashuError, PostSplitResponse, PostSplitResponse_Deprecated]:
|
) -> Union[PostSplitResponse, PostSplitResponse_Deprecated]:
|
||||||
"""
|
"""
|
||||||
Requetst a set of tokens with amount "total" to be split into two
|
Requests a set of Proofs to be split into two a new set of BlindedSignatures.
|
||||||
newly minted sets with amount "split" and "total-split".
|
|
||||||
|
|
||||||
This endpoint is used by Alice to split a set of proofs before making a payment to Carol.
|
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.
|
It is then used by Carol (by setting split=total) to redeem the tokens.
|
||||||
"""
|
"""
|
||||||
logger.trace(f"> POST /split: {payload}")
|
logger.trace(f"> POST /split: {payload}")
|
||||||
assert payload.outputs, Exception("no outputs provided.")
|
assert payload.outputs, Exception("no outputs provided.")
|
||||||
try:
|
|
||||||
promises = await ledger.split(
|
promises = await ledger.split(
|
||||||
proofs=payload.proofs, outputs=payload.outputs, amount=payload.amount
|
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")
|
|
||||||
|
|
||||||
if payload.amount:
|
if payload.amount:
|
||||||
# BEGIN backwards compatibility < 0.13
|
# BEGIN backwards compatibility < 0.13
|
||||||
@@ -253,12 +267,13 @@ async def split(
|
|||||||
|
|
||||||
|
|
||||||
@router.post(
|
@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.")
|
assert payload.outputs, Exception("no outputs provided.")
|
||||||
try:
|
outputs, promises = await ledger.restore(payload.outputs)
|
||||||
outputs, promises = await ledger.restore(payload.outputs)
|
|
||||||
except Exception as exc:
|
|
||||||
return CashuError(code=0, error=str(exc))
|
|
||||||
return PostRestoreResponse(outputs=outputs, promises=promises)
|
return PostRestoreResponse(outputs=outputs, promises=promises)
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
from fastapi import FastAPI, Request, status
|
from fastapi import FastAPI, Request, status
|
||||||
from fastapi.responses import JSONResponse
|
from fastapi.responses import JSONResponse
|
||||||
|
from loguru import logger
|
||||||
|
|
||||||
from ...core.settings import settings
|
from ...core.settings import settings
|
||||||
from .router import router
|
from .router import router
|
||||||
@@ -28,9 +29,9 @@ app = create_app()
|
|||||||
@app.middleware("http")
|
@app.middleware("http")
|
||||||
async def catch_exceptions(request: Request, call_next):
|
async def catch_exceptions(request: Request, call_next):
|
||||||
try:
|
try:
|
||||||
response = await call_next(request)
|
return await call_next(request)
|
||||||
return response
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
logger.error(f"Exception: {e}")
|
||||||
return JSONResponse(
|
return JSONResponse(
|
||||||
status_code=status.HTTP_400_BAD_REQUEST, content={"detail": str(e)}
|
status_code=status.HTTP_400_BAD_REQUEST, content={"detail": str(e)}
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import requests
|
|||||||
from bip32 import BIP32
|
from bip32 import BIP32
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
from mnemonic import Mnemonic
|
from mnemonic import Mnemonic
|
||||||
|
from requests import Response
|
||||||
|
|
||||||
from ..core import bolt11 as bolt11
|
from ..core import bolt11 as bolt11
|
||||||
from ..core.base import (
|
from ..core.base import (
|
||||||
@@ -225,17 +226,24 @@ class LedgerAPI(object):
|
|||||||
return outputs, rs_return
|
return outputs, rs_return
|
||||||
|
|
||||||
@staticmethod
|
@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.
|
"""Raises an exception if the response from the mint contains an error.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
resp_dict (_type_): Response dict (previously JSON) from mint
|
resp_dict (Response): Response dict (previously JSON) from mint
|
||||||
|
|
||||||
Raises:
|
Raises:
|
||||||
Exception: if the response contains an error
|
Exception: if the response contains an error
|
||||||
"""
|
"""
|
||||||
if "error" in resp_dict:
|
resp_dict = resp.json()
|
||||||
raise Exception("Mint Error: {}".format(resp_dict["error"]))
|
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:
|
async def _load_mint_keys(self, keyset_id: str = "") -> WalletKeyset:
|
||||||
"""Loads keys from mint and stores them in the database.
|
"""Loads keys from mint and stores them in the database.
|
||||||
@@ -347,7 +355,7 @@ class LedgerAPI(object):
|
|||||||
resp = self.s.get(
|
resp = self.s.get(
|
||||||
url + "/keys",
|
url + "/keys",
|
||||||
)
|
)
|
||||||
resp.raise_for_status()
|
self.raise_on_error(resp)
|
||||||
keys: dict = resp.json()
|
keys: dict = resp.json()
|
||||||
assert len(keys), Exception("did not receive any keys")
|
assert len(keys), Exception("did not receive any keys")
|
||||||
keyset_keys = {
|
keyset_keys = {
|
||||||
@@ -376,9 +384,8 @@ class LedgerAPI(object):
|
|||||||
resp = self.s.get(
|
resp = self.s.get(
|
||||||
url + f"/keys/{keyset_id_urlsafe}",
|
url + f"/keys/{keyset_id_urlsafe}",
|
||||||
)
|
)
|
||||||
resp.raise_for_status()
|
self.raise_on_error(resp)
|
||||||
keys = resp.json()
|
keys = resp.json()
|
||||||
self.raise_on_error(keys)
|
|
||||||
assert len(keys), Exception("did not receive any keys")
|
assert len(keys), Exception("did not receive any keys")
|
||||||
keyset_keys = {
|
keyset_keys = {
|
||||||
int(amt): PublicKey(bytes.fromhex(val), raw=True)
|
int(amt): PublicKey(bytes.fromhex(val), raw=True)
|
||||||
@@ -403,7 +410,7 @@ class LedgerAPI(object):
|
|||||||
resp = self.s.get(
|
resp = self.s.get(
|
||||||
url + "/keysets",
|
url + "/keysets",
|
||||||
)
|
)
|
||||||
resp.raise_for_status()
|
self.raise_on_error(resp)
|
||||||
keysets_dict = resp.json()
|
keysets_dict = resp.json()
|
||||||
keysets = KeysetsResponse.parse_obj(keysets_dict)
|
keysets = KeysetsResponse.parse_obj(keysets_dict)
|
||||||
assert len(keysets.keysets), Exception("did not receive any keysets")
|
assert len(keysets.keysets), Exception("did not receive any keysets")
|
||||||
@@ -425,7 +432,7 @@ class LedgerAPI(object):
|
|||||||
resp = self.s.get(
|
resp = self.s.get(
|
||||||
url + "/info",
|
url + "/info",
|
||||||
)
|
)
|
||||||
resp.raise_for_status()
|
self.raise_on_error(resp)
|
||||||
data: dict = resp.json()
|
data: dict = resp.json()
|
||||||
mint_info: GetInfoResponse = GetInfoResponse.parse_obj(data)
|
mint_info: GetInfoResponse = GetInfoResponse.parse_obj(data)
|
||||||
return mint_info
|
return mint_info
|
||||||
@@ -445,9 +452,8 @@ class LedgerAPI(object):
|
|||||||
"""
|
"""
|
||||||
logger.trace("Requesting mint: GET /mint")
|
logger.trace("Requesting mint: GET /mint")
|
||||||
resp = self.s.get(self.url + "/mint", params={"amount": amount})
|
resp = self.s.get(self.url + "/mint", params={"amount": amount})
|
||||||
resp.raise_for_status()
|
self.raise_on_error(resp)
|
||||||
return_dict = resp.json()
|
return_dict = resp.json()
|
||||||
self.raise_on_error(return_dict)
|
|
||||||
mint_response = GetMintResponse.parse_obj(return_dict)
|
mint_response = GetMintResponse.parse_obj(return_dict)
|
||||||
return Invoice(amount=amount, pr=mint_response.pr, hash=mint_response.hash)
|
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
|
"payment_hash": hash, # backwards compatibility pre 0.12.0
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
resp.raise_for_status()
|
self.raise_on_error(resp)
|
||||||
reponse_dict = resp.json()
|
reponse_dict = resp.json()
|
||||||
self.raise_on_error(reponse_dict)
|
|
||||||
logger.trace("Lightning invoice checked. POST /mint")
|
logger.trace("Lightning invoice checked. POST /mint")
|
||||||
promises = PostMintResponse.parse_obj(reponse_dict).promises
|
promises = PostMintResponse.parse_obj(reponse_dict).promises
|
||||||
|
|
||||||
@@ -518,10 +523,8 @@ class LedgerAPI(object):
|
|||||||
self.url + "/split",
|
self.url + "/split",
|
||||||
json=split_payload.dict(include=_splitrequest_include_fields(proofs)), # type: ignore
|
json=split_payload.dict(include=_splitrequest_include_fields(proofs)), # type: ignore
|
||||||
)
|
)
|
||||||
resp.raise_for_status()
|
self.raise_on_error(resp)
|
||||||
promises_dict = resp.json()
|
promises_dict = resp.json()
|
||||||
self.raise_on_error(promises_dict)
|
|
||||||
|
|
||||||
mint_response = PostMintResponse.parse_obj(promises_dict)
|
mint_response = PostMintResponse.parse_obj(promises_dict)
|
||||||
promises = [BlindedSignature(**p.dict()) for p in mint_response.promises]
|
promises = [BlindedSignature(**p.dict()) for p in mint_response.promises]
|
||||||
|
|
||||||
@@ -547,9 +550,9 @@ class LedgerAPI(object):
|
|||||||
self.url + "/check",
|
self.url + "/check",
|
||||||
json=payload.dict(include=_check_proof_state_include_fields(proofs)), # type: ignore
|
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()
|
return_dict = resp.json()
|
||||||
self.raise_on_error(return_dict)
|
|
||||||
states = CheckSpendableResponse.parse_obj(return_dict)
|
states = CheckSpendableResponse.parse_obj(return_dict)
|
||||||
return states
|
return states
|
||||||
|
|
||||||
@@ -561,9 +564,9 @@ class LedgerAPI(object):
|
|||||||
self.url + "/checkfees",
|
self.url + "/checkfees",
|
||||||
json=payload.dict(),
|
json=payload.dict(),
|
||||||
)
|
)
|
||||||
resp.raise_for_status()
|
self.raise_on_error(resp)
|
||||||
|
|
||||||
return_dict = resp.json()
|
return_dict = resp.json()
|
||||||
self.raise_on_error(return_dict)
|
|
||||||
return return_dict
|
return return_dict
|
||||||
|
|
||||||
@async_set_requests
|
@async_set_requests
|
||||||
@@ -589,9 +592,9 @@ class LedgerAPI(object):
|
|||||||
self.url + "/melt",
|
self.url + "/melt",
|
||||||
json=payload.dict(include=_meltrequest_include_fields(proofs)), # type: ignore
|
json=payload.dict(include=_meltrequest_include_fields(proofs)), # type: ignore
|
||||||
)
|
)
|
||||||
resp.raise_for_status()
|
self.raise_on_error(resp)
|
||||||
return_dict = resp.json()
|
return_dict = resp.json()
|
||||||
self.raise_on_error(return_dict)
|
|
||||||
return GetMeltResponse.parse_obj(return_dict)
|
return GetMeltResponse.parse_obj(return_dict)
|
||||||
|
|
||||||
@async_set_requests
|
@async_set_requests
|
||||||
@@ -603,9 +606,8 @@ class LedgerAPI(object):
|
|||||||
"""
|
"""
|
||||||
payload = PostMintRequest(outputs=outputs)
|
payload = PostMintRequest(outputs=outputs)
|
||||||
resp = self.s.post(self.url + "/restore", json=payload.dict())
|
resp = self.s.post(self.url + "/restore", json=payload.dict())
|
||||||
resp.raise_for_status()
|
self.raise_on_error(resp)
|
||||||
reponse_dict = resp.json()
|
reponse_dict = resp.json()
|
||||||
self.raise_on_error(reponse_dict)
|
|
||||||
returnObj = PostRestoreResponse.parse_obj(reponse_dict)
|
returnObj = PostRestoreResponse.parse_obj(reponse_dict)
|
||||||
return returnObj.outputs, returnObj.promises
|
return returnObj.outputs, returnObj.promises
|
||||||
|
|
||||||
|
|||||||
@@ -49,13 +49,13 @@ async def test_api_keyset_keys(ledger):
|
|||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_api_mint_validation(ledger):
|
async def test_api_mint_validation(ledger):
|
||||||
response = requests.get(f"{BASE_URL}/mint?amount=-21")
|
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")
|
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")
|
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")
|
response = requests.get(f"{BASE_URL}/mint?amount=1")
|
||||||
assert "error" not in response.json()
|
assert "detail" not in response.json()
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import asyncio
|
|||||||
import shutil
|
import shutil
|
||||||
import time
|
import time
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Dict, List
|
from typing import Dict, List, Union
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
import pytest_asyncio
|
import pytest_asyncio
|
||||||
@@ -10,6 +10,18 @@ from mnemonic import Mnemonic
|
|||||||
|
|
||||||
from cashu.core.base import Proof, Secret, SecretKind, Tags
|
from cashu.core.base import Proof, Secret, SecretKind, Tags
|
||||||
from cashu.core.crypto.secp import PrivateKey, PublicKey
|
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.helpers import async_unwrap, sum_proofs
|
||||||
from cashu.core.migrations import migrate_databases
|
from cashu.core.migrations import migrate_databases
|
||||||
from cashu.core.settings import settings
|
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
|
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'."""
|
"""Compute f() and expect an error message 'msg'."""
|
||||||
try:
|
try:
|
||||||
await f
|
await f
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
if str(exc.args[0]) != msg:
|
error_message: str = str(exc.args[0])
|
||||||
raise Exception(f"Expected error: {msg}, got: {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
|
return
|
||||||
raise Exception(f"Expected error: {msg}, got no error")
|
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):
|
async def test_get_nonexistent_keyset(wallet1: Wallet):
|
||||||
await assert_err(
|
await assert_err(
|
||||||
wallet1._get_keys_of_keyset(wallet1.url, "nonexistent"),
|
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 wallet1.split(wallet1.proofs, 20)
|
||||||
await assert_err(
|
await assert_err(
|
||||||
wallet1.split(doublespend, 20),
|
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.balance == 64
|
||||||
assert wallet1.available_balance == 64
|
assert wallet1.available_balance == 64
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ async def assert_err(f, msg):
|
|||||||
try:
|
try:
|
||||||
await f
|
await f
|
||||||
except Exception as exc:
|
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]}")
|
raise Exception(f"Expected error: {msg}, got: {exc.args[0]}")
|
||||||
return
|
return
|
||||||
raise Exception(f"Expected error: {msg}, got no error")
|
raise Exception(f"Expected error: {msg}, got no error")
|
||||||
|
|||||||
Reference in New Issue
Block a user