Files
nutshell/cashu/core/models.py
callebtc a0ef44dba0 Blind authentication (#675)
* auth server

* cleaning up

* auth ledger class

* class variables -> instance variables

* annotations

* add models and api route

* custom amount and api prefix

* add auth db

* blind auth token working

* jwt working

* clean up

* JWT works

* using openid connect server

* use oauth server with password flow

* new realm

* add keycloak docker

* hopefully not garbage

* auth works

* auth kinda working

* fix cli

* auth works for send and receive

* pass auth_db to Wallet

* auth in info

* refactor

* fix supported

* cache mint info

* fix settings and endpoints

* add description to .env.example

* track changes for openid connect client

* store mint in db

* store credentials

* clean up v1_api.py

* load mint info into auth wallet

* fix first login

* authenticate if refresh token fails

* clear auth also middleware

* use regex

* add cli command

* pw works

* persist keyset amounts

* add errors.py

* do not start auth server if disabled in config

* upadte poetry

* disvoery url

* fix test

* support device code flow

* adopt latest spec changes

* fix code flow

* mint max bat dynamic

* mypy ignore

* fix test

* do not serialize amount in authproof

* all auth flows working

* fix tests

* submodule

* refactor

* test

* dont sleep

* test

* add wallet auth tests

* test differently

* test only keycloak for now

* fix creds

* daemon

* fix test

* install everything

* install jinja

* delete wallet for every test

* auth: use global rate limiter

* test auth rate limit

* keycloak hostname

* move keycloak test data

* reactivate all tests

* add readme

* load proofs

* remove unused code

* remove unused code

* implement change suggestions by ok300

* add error codes

* test errors
2025-01-29 22:48:51 -06:00

367 lines
9.9 KiB
Python

from typing import Any, Dict, List, Optional, Union
from pydantic import BaseModel, Field, root_validator
from .base import (
BlindedMessage,
BlindedMessage_Deprecated,
BlindedSignature,
MeltQuote,
MintQuote,
Proof,
ProofState,
)
from .settings import settings
# ------- API -------
# ------- API: INFO -------
class MintMethodSetting(BaseModel):
method: str
unit: str
min_amount: Optional[int] = None
max_amount: Optional[int] = None
description: Optional[bool] = None
class MeltMethodSetting(BaseModel):
method: str
unit: str
min_amount: Optional[int] = None
max_amount: Optional[int] = None
class MintInfoContact(BaseModel):
method: str
info: str
class MintInfoProtectedEndpoint(BaseModel):
method: str
path: str
class GetInfoResponse(BaseModel):
name: Optional[str] = None
pubkey: Optional[str] = None
version: Optional[str] = None
description: Optional[str] = None
description_long: Optional[str] = None
contact: Optional[List[MintInfoContact]] = None
motd: Optional[str] = None
icon_url: Optional[str] = None
urls: Optional[List[str]] = None
time: Optional[int] = None
nuts: Optional[Dict[int, Any]] = None
def supports(self, nut: int) -> Optional[bool]:
return nut in self.nuts if self.nuts else None
# BEGIN DEPRECATED: NUT-06 contact field change
# NUT-06 PR: https://github.com/cashubtc/nuts/pull/117
@root_validator(pre=True)
def preprocess_deprecated_contact_field(cls, values: dict):
if "contact" in values and values["contact"]:
if isinstance(values["contact"][0], list):
values["contact"] = [
MintInfoContact(method=method, info=info)
for method, info in values["contact"]
if method and info
]
return values
# END DEPRECATED: NUT-06 contact field change
class Nut15MppSupport(BaseModel):
method: str
unit: str
class GetInfoResponse_deprecated(BaseModel):
name: Optional[str] = None
pubkey: Optional[str] = None
version: Optional[str] = None
description: Optional[str] = None
description_long: Optional[str] = None
contact: Optional[List[List[str]]] = None
nuts: Optional[List[str]] = None
motd: Optional[str] = None
parameter: Optional[dict] = None
# ------- API: KEYS -------
class KeysResponseKeyset(BaseModel):
id: str
unit: str
keys: Dict[int, str]
class KeysResponse(BaseModel):
keysets: List[KeysResponseKeyset]
class KeysetsResponseKeyset(BaseModel):
id: str
unit: str
active: bool
input_fee_ppk: Optional[int] = None
class KeysetsResponse(BaseModel):
keysets: list[KeysetsResponseKeyset]
class KeysResponse_deprecated(BaseModel):
__root__: Dict[str, str]
class KeysetsResponse_deprecated(BaseModel):
keysets: list[str]
# ------- API: MINT QUOTE -------
class PostMintQuoteRequest(BaseModel):
unit: str = Field(..., max_length=settings.mint_max_request_length) # output unit
amount: int = Field(..., gt=0) # output amount
description: Optional[str] = Field(
default=None, max_length=settings.mint_max_request_length
) # invoice description
pubkey: Optional[str] = Field(
default=None, max_length=settings.mint_max_request_length
) # NUT-20 quote lock pubkey
class PostMintQuoteResponse(BaseModel):
quote: str # quote id
request: str # input payment request
state: Optional[str] # state of the quote (optional for backwards compat)
expiry: Optional[int] # expiry of the quote
pubkey: Optional[str] = None # NUT-20 quote lock pubkey
paid: Optional[bool] = None # DEPRECATED as per NUT-04 PR #141
@classmethod
def from_mint_quote(cls, mint_quote: MintQuote) -> "PostMintQuoteResponse":
to_dict = mint_quote.dict()
# turn state into string
to_dict["state"] = mint_quote.state.value
return cls.parse_obj(to_dict)
# ------- API: MINT -------
class PostMintRequest(BaseModel):
quote: str = Field(..., max_length=settings.mint_max_request_length) # quote id
outputs: List[BlindedMessage] = Field(
..., max_items=settings.mint_max_request_length
)
signature: Optional[str] = Field(
default=None, max_length=settings.mint_max_request_length
) # NUT-20 quote signature
class PostMintResponse(BaseModel):
signatures: List[BlindedSignature] = []
class GetMintResponse_deprecated(BaseModel):
pr: str
hash: str
class PostMintRequest_deprecated(BaseModel):
outputs: List[BlindedMessage_Deprecated] = Field(
..., max_items=settings.mint_max_request_length
)
class PostMintResponse_deprecated(BaseModel):
promises: List[BlindedSignature] = []
# ------- API: MELT QUOTE -------
class PostMeltRequestOptionMpp(BaseModel):
amount: int = Field(gt=0) # input amount
class PostMeltRequestOptions(BaseModel):
mpp: Optional[PostMeltRequestOptionMpp]
class PostMeltQuoteRequest(BaseModel):
unit: str = Field(..., max_length=settings.mint_max_request_length) # input unit
request: str = Field(
..., max_length=settings.mint_max_request_length
) # output payment request
options: Optional[PostMeltRequestOptions] = None
@property
def is_mpp(self) -> bool:
if self.options and self.options.mpp:
return True
else:
return False
@property
def mpp_amount(self) -> int:
if self.is_mpp and self.options and self.options.mpp:
return self.options.mpp.amount
else:
raise Exception("quote request is not mpp.")
class PostMeltQuoteResponse(BaseModel):
quote: str # quote id
amount: int # input amount
fee_reserve: int # input fee reserve
paid: Optional[bool] = (
None # whether the request has been paid # DEPRECATED as per NUT PR #136
)
state: Optional[str] # state of the quote
expiry: Optional[int] # expiry of the quote
payment_preimage: Optional[str] = None # payment preimage
change: Union[List[BlindedSignature], None] = None # NUT-08 change
@classmethod
def from_melt_quote(self, melt_quote: MeltQuote) -> "PostMeltQuoteResponse":
to_dict = melt_quote.dict()
# turn state into string
to_dict["state"] = melt_quote.state.value
# add deprecated "paid" field
to_dict["paid"] = melt_quote.paid
return PostMeltQuoteResponse.parse_obj(to_dict)
# ------- API: MELT -------
class PostMeltRequest(BaseModel):
quote: str = Field(..., max_length=settings.mint_max_request_length) # quote id
inputs: List[Proof] = Field(..., max_items=settings.mint_max_request_length)
outputs: Union[List[BlindedMessage], None] = Field(
None, max_items=settings.mint_max_request_length
)
class PostMeltResponse_deprecated(BaseModel):
paid: Union[bool, None]
preimage: Union[str, None]
change: Union[List[BlindedSignature], None] = None
class PostMeltRequest_deprecated(BaseModel):
proofs: List[Proof] = Field(..., max_items=settings.mint_max_request_length)
pr: str = Field(..., max_length=settings.mint_max_request_length)
outputs: Union[List[BlindedMessage_Deprecated], None] = Field(
None, max_items=settings.mint_max_request_length
)
# ------- API: SPLIT -------
class PostSwapRequest(BaseModel):
inputs: List[Proof] = Field(..., max_items=settings.mint_max_request_length)
outputs: List[BlindedMessage] = Field(
..., max_items=settings.mint_max_request_length
)
class PostSwapResponse(BaseModel):
signatures: List[BlindedSignature]
# deprecated since 0.13.0
class PostSwapRequest_Deprecated(BaseModel):
proofs: List[Proof] = Field(..., max_items=settings.mint_max_request_length)
amount: Optional[int] = None
outputs: List[BlindedMessage_Deprecated] = Field(
..., max_items=settings.mint_max_request_length
)
class PostSwapResponse_Deprecated(BaseModel):
promises: List[BlindedSignature] = []
class PostSwapResponse_Very_Deprecated(BaseModel):
fst: List[BlindedSignature] = []
snd: List[BlindedSignature] = []
deprecated: str = "The amount field is deprecated since 0.13.0"
# ------- API: CHECK -------
class PostCheckStateRequest(BaseModel):
Ys: List[str] = Field(..., max_items=settings.mint_max_request_length)
class PostCheckStateResponse(BaseModel):
states: List[ProofState] = []
class CheckSpendableRequest_deprecated(BaseModel):
proofs: List[Proof] = Field(..., max_items=settings.mint_max_request_length)
class CheckSpendableResponse_deprecated(BaseModel):
spendable: List[bool]
pending: List[bool]
class CheckFeesRequest_deprecated(BaseModel):
pr: str = Field(..., max_length=settings.mint_max_request_length)
class CheckFeesResponse_deprecated(BaseModel):
fee: Union[int, None]
# ------- API: RESTORE -------
class PostRestoreRequest(BaseModel):
outputs: List[BlindedMessage] = Field(
..., max_items=settings.mint_max_request_length
)
class PostRestoreRequest_Deprecated(BaseModel):
outputs: List[BlindedMessage_Deprecated] = Field(
..., max_items=settings.mint_max_request_length
)
class PostRestoreResponse(BaseModel):
outputs: List[BlindedMessage] = []
signatures: List[BlindedSignature] = []
promises: Optional[List[BlindedSignature]] = [] # deprecated since 0.15.1
# duplicate value of "signatures" for backwards compatibility with old clients < 0.15.1
def __init__(self, **data):
super().__init__(**data)
self.promises = self.signatures
# ------- API: BLIND AUTH -------
class PostAuthBlindMintRequest(BaseModel):
outputs: List[BlindedMessage] = Field(
...,
max_items=settings.mint_max_request_length,
description="Blinded messages for creating blind auth tokens.",
)
class PostAuthBlindMintResponse(BaseModel):
signatures: List[BlindedSignature] = []