Files
nutshell/cashu/wallet/wallet_deprecated.py
callebtc a518274f7e Nutshell cleanup wishlist (#332)
* fix keys

* fix tests

* backwards compatible api upgrade

* upgrade seems to work

* fix tests

* add deprecated api functions

* add more tests of backwards compat

* add test serialization for nut00

* remove a redundant test

* move mint and melt to new api

* mypy works

* CI: mypy --check-untyped-defs

* add deprecated router

* add hints and remove logs

* fix tests

* cleanup

* use new mint and melt endpoints

* tests passing?

* fix mypy

* make format

* make format

* make format

* commit

* errors gone

* save

* adjust the API

* store quotes in db

* make mypy happy

* add fakewallet settings

* remove LIGHTNING=True and pass quote id for melt

* format

* tests passing

* add CoreLightningRestWallet

* add macaroon loader

* add correct config

* preimage -> proof

* move wallet.status() to cli.helpers.print_status()

* remove statuses from tests

* remove

* make format

* Use httpx in deprecated wallet

* fix cln interface

* create invoice before quote

* internal transactions and deprecated api testing

* fix tests

* add deprecated API tests

* fastapi type hints break things

* fix duplicate wallet error

* make format

* update poetry in CI to 1.7.1

* precommit restore

* remove bolt11

* oops

* default poetry

* store fee reserve for melt quotes and refactor melt()

* works?

* make format

* test

* finally

* fix deprecated models

* rename v1 endpoints to bolt11

* raise restore and check to v1, bump version to 0.15.0

* add version byte to keyset id

* remove redundant fields in json

* checks

* generate bip32 keyset wip

* migrate old keysets

* load duplicate keys

* duplicate old keysets

* revert router changes

* add deprecated /check and /restore endpoints

* try except invalidate

* parse unit from derivation path, adjust keyset id calculation with bytes

* remove keyest id from functions again and rely on self.keyset_id

* mosts tests work

* mint loads multiple derivation paths

* make format

* properly print units

* fix tests

* wallet works with multiple units

* add strike wallet and choose backend dynamically

* fix mypy

* add get_payment_quote to lightning backends

* make format

* fix startup

* fix lnbitswallet

* fix tests

* LightningWallet -> LightningBackend

* remove comments

* make format

* remove msat conversion

* add Amount type

* fix regtest

* use melt_quote as argument for pay_invoice

* test old api

* fees in sats

* fix deprecated fees

* fixes

* print balance correctly

* internally index keyset response by int

* add pydantic validation to input models

* add timestamps to mint db

* store timestamps for invoices, promises, proofs_used

* fix wallet migration

* rotate keys correctly for testing

* remove print

* update latest keyset

* fix tests

* fix test

* make format

* make format with correct black version

* remove nsat and cheese

* test against deprecated mint

* fix tests?

* actually use env var

* mint run with env vars

* moar test

* cleanup

* simplify tests, load all keys

* try out testing with internal invoices

* fix internal melt test

* fix test

* deprecated checkfees expects appropriate fees

* adjust comment

* drop lightning table

* split migration for testing for now, remove it later

* remove unused lightning table

* skip_private_key -> skip_db_read

* throw error on migration error

* reorder

* fix migrations

* fix lnbits fee return value negative

* fix typo

* comments

* add type

* make format

* split must use correct amount

* fix tests

* test deprecated api with internal/external melts

* do not split if not necessary

* refactor

* fix test

* make format with new black

* cleanup and add comments

* add quote state check endpoints

* fix deprecated wallet response

* split -> swap endpoint

* make format

* add expiry to quotes, get quote endpoints, and adjust to nut review comments

* allow overpayment of melt

* add lightning wallet tests

* commiting to save

* fix tests a bit

* make format

* remove comments

* get mint info

* check_spendable default False, and return payment quote checking id

* make format

* bump version in pyproject

* update to /v1/checkstate

* make format

* fix mint api checks

* return witness on /v1/checkstate

* no failfast

* try fail-fast: false in ci.yaml

* fix db lookup

* clean up literals
2024-01-08 00:57:15 +01:00

426 lines
14 KiB
Python

from posixpath import join
from typing import List, Optional, Tuple, Union
import bolt11
import httpx
from httpx import Response
from loguru import logger
from ..core.base import (
BlindedMessage,
BlindedSignature,
CheckFeesRequest_deprecated,
CheckFeesResponse_deprecated,
CheckSpendableRequest_deprecated,
CheckSpendableResponse_deprecated,
GetInfoResponse,
GetInfoResponse_deprecated,
GetMintResponse_deprecated,
Invoice,
KeysetsResponse_deprecated,
PostMeltRequest_deprecated,
PostMeltResponse_deprecated,
PostMintRequest_deprecated,
PostMintResponse_deprecated,
PostRestoreResponse,
PostSplitRequest_Deprecated,
PostSplitResponse_Deprecated,
Proof,
WalletKeyset,
)
from ..core.crypto.secp import PublicKey
from ..core.settings import settings
from ..tor.tor import TorProxy
from .protocols import SupportsHttpxClient, SupportsMintURL
def async_set_httpx_client(func):
"""
Decorator that wraps around any async class method of LedgerAPI that makes
API calls. Sets some HTTP headers and starts a Tor instance if none is
already running and and sets local proxy to use it.
"""
async def wrapper(self, *args, **kwargs):
# set proxy
proxies_dict = {}
proxy_url: Union[str, None] = None
if settings.tor and TorProxy().check_platform():
self.tor = TorProxy(timeout=True)
self.tor.run_daemon(verbose=True)
proxy_url = "socks5://localhost:9050"
elif settings.socks_proxy:
proxy_url = f"socks5://{settings.socks_proxy}"
elif settings.http_proxy:
proxy_url = settings.http_proxy
if proxy_url:
proxies_dict.update({"all://": proxy_url})
headers_dict = {"Client-version": settings.version}
self.httpx = httpx.AsyncClient(
verify=not settings.debug,
proxies=proxies_dict, # type: ignore
headers=headers_dict,
base_url=self.url,
timeout=None if settings.debug else 60,
)
return await func(self, *args, **kwargs)
return wrapper
def async_ensure_mint_loaded_deprecated(func):
"""Decorator that ensures that the mint is loaded before calling the wrapped
function. If the mint is not loaded, it will be loaded first.
"""
async def wrapper(self, *args, **kwargs):
if not self.keysets:
await self._load_mint()
return await func(self, *args, **kwargs)
return wrapper
class LedgerAPIDeprecated(SupportsHttpxClient, SupportsMintURL):
"""Deprecated wallet class, will be removed in the future."""
httpx: httpx.AsyncClient
url: str
@staticmethod
def raise_on_error(
resp: Response,
) -> None:
"""Raises an exception if the response from the mint contains an error.
Args:
resp_dict (Response): Response dict (previously JSON) from mint
Raises:
Exception: if the response contains an error
"""
resp_dict = resp.json()
if "detail" in resp_dict:
logger.trace(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_set_httpx_client
async def _get_info_deprecated(self) -> GetInfoResponse:
"""API that gets the mint info.
Returns:
GetInfoResponse: Current mint info
Raises:
Exception: If the mint info request fails
"""
logger.warning(f"Using deprecated API call: {self.url}/info")
resp = await self.httpx.get(
join(self.url, "/info"),
)
self.raise_on_error(resp)
data: dict = resp.json()
mint_info_deprecated: GetInfoResponse_deprecated = (
GetInfoResponse_deprecated.parse_obj(data)
)
mint_info = GetInfoResponse(
**mint_info_deprecated.dict(exclude={"parameter", "nuts"})
)
return mint_info
@async_set_httpx_client
async def _get_keys_deprecated(self, url: str) -> WalletKeyset:
"""API that gets the current keys of the mint
Args:
url (str): Mint URL
Returns:
WalletKeyset: Current mint keyset
Raises:
Exception: If no keys are received from the mint
"""
logger.warning(f"Using deprecated API call: {url}/keys")
resp = await self.httpx.get(
url + "/keys",
)
self.raise_on_error(resp)
keys: dict = resp.json()
assert len(keys), Exception("did not receive any keys")
keyset_keys = {
int(amt): PublicKey(bytes.fromhex(val), raw=True)
for amt, val in keys.items()
}
keyset = WalletKeyset(
unit="sat", public_keys=keyset_keys, mint_url=url, use_deprecated_id=True
)
return keyset
@async_set_httpx_client
async def _get_keys_of_keyset_deprecated(
self, url: str, keyset_id: str
) -> WalletKeyset:
"""API that gets the keys of a specific keyset from the mint.
Args:
url (str): Mint URL
keyset_id (str): base64 keyset ID, needs to be urlsafe-encoded before sending to mint (done in this method)
Returns:
WalletKeyset: Keyset with ID keyset_id
Raises:
Exception: If no keys are received from the mint
"""
logger.warning(f"Using deprecated API call: {url}/keys/{keyset_id}")
keyset_id_urlsafe = keyset_id.replace("+", "-").replace("/", "_")
resp = await self.httpx.get(
url + f"/keys/{keyset_id_urlsafe}",
)
self.raise_on_error(resp)
keys = resp.json()
assert len(keys), Exception("did not receive any keys")
keyset_keys = {
int(amt): PublicKey(bytes.fromhex(val), raw=True)
for amt, val in keys.items()
}
keyset = WalletKeyset(
unit="sat",
id=keyset_id,
public_keys=keyset_keys,
mint_url=url,
use_deprecated_id=True,
)
return keyset
@async_set_httpx_client
@async_ensure_mint_loaded_deprecated
async def _get_keyset_ids_deprecated(self, url: str) -> List[str]:
"""API that gets a list of all active keysets of the mint.
Args:
url (str): Mint URL
Returns:
KeysetsResponse (List[str]): List of all active keyset IDs of the mint
Raises:
Exception: If no keysets are received from the mint
"""
logger.warning(f"Using deprecated API call: {url}/keysets")
resp = await self.httpx.get(
url + "/keysets",
)
self.raise_on_error(resp)
keysets_dict = resp.json()
keysets = KeysetsResponse_deprecated.parse_obj(keysets_dict)
assert len(keysets.keysets), Exception("did not receive any keysets")
return keysets.keysets
@async_set_httpx_client
@async_ensure_mint_loaded_deprecated
async def request_mint_deprecated(self, amount) -> Invoice:
"""Requests a mint from the server and returns Lightning invoice.
Args:
amount (int): Amount of tokens to mint
Returns:
Invoice: Lightning invoice
Raises:
Exception: If the mint request fails
"""
logger.warning("Using deprecated API call: Requesting mint: GET /mint")
resp = await self.httpx.get(self.url + "/mint", params={"amount": amount})
self.raise_on_error(resp)
return_dict = resp.json()
mint_response = GetMintResponse_deprecated.parse_obj(return_dict)
decoded_invoice = bolt11.decode(mint_response.pr)
return Invoice(
amount=amount,
bolt11=mint_response.pr,
id=mint_response.hash,
payment_hash=decoded_invoice.payment_hash,
out=False,
)
@async_set_httpx_client
@async_ensure_mint_loaded_deprecated
async def mint_deprecated(
self, outputs: List[BlindedMessage], hash: Optional[str] = None
) -> List[BlindedSignature]:
"""Mints new coins and returns a proof of promise.
Args:
outputs (List[BlindedMessage]): Outputs to mint new tokens with
hash (str, optional): Hash of the paid invoice. Defaults to None.
Returns:
list[Proof]: List of proofs.
Raises:
Exception: If the minting fails
"""
outputs_payload = PostMintRequest_deprecated(outputs=outputs)
def _mintrequest_include_fields(outputs: List[BlindedMessage]):
"""strips away fields from the model that aren't necessary for the /mint"""
outputs_include = {"amount", "B_"}
return {
"outputs": {i: outputs_include for i in range(len(outputs))},
}
payload = outputs_payload.dict(include=_mintrequest_include_fields(outputs)) # type: ignore
logger.warning(
"Using deprecated API call:Checking Lightning invoice. POST /mint"
)
resp = await self.httpx.post(
self.url + "/mint",
json=payload,
params={
"hash": hash,
"payment_hash": hash, # backwards compatibility pre 0.12.0
},
)
self.raise_on_error(resp)
response_dict = resp.json()
logger.trace("Lightning invoice checked. POST /mint")
promises = PostMintResponse_deprecated.parse_obj(response_dict).promises
return promises
@async_set_httpx_client
@async_ensure_mint_loaded_deprecated
async def pay_lightning_deprecated(
self, proofs: List[Proof], invoice: str, outputs: Optional[List[BlindedMessage]]
):
"""
Accepts proofs and a lightning invoice to pay in exchange.
"""
logger.warning("Using deprecated API call: POST /melt")
payload = PostMeltRequest_deprecated(proofs=proofs, pr=invoice, outputs=outputs)
def _meltrequest_include_fields(proofs: List[Proof]):
"""strips away fields from the model that aren't necessary for the /melt"""
proofs_include = {"id", "amount", "secret", "C", "script"}
return {
"proofs": {i: proofs_include for i in range(len(proofs))},
"pr": ...,
"outputs": ...,
}
resp = await self.httpx.post(
self.url + "/melt",
json=payload.dict(include=_meltrequest_include_fields(proofs)), # type: ignore
)
self.raise_on_error(resp)
return_dict = resp.json()
return PostMeltResponse_deprecated.parse_obj(return_dict)
@async_set_httpx_client
@async_ensure_mint_loaded_deprecated
async def split_deprecated(
self,
proofs: List[Proof],
outputs: List[BlindedMessage],
) -> List[BlindedSignature]:
"""Consume proofs and create new promises based on amount split."""
logger.warning("Using deprecated API call: Calling split. POST /split")
split_payload = PostSplitRequest_Deprecated(proofs=proofs, outputs=outputs)
# construct payload
def _splitrequest_include_fields(proofs: List[Proof]):
"""strips away fields from the model that aren't necessary for the /split"""
proofs_include = {
"id",
"amount",
"secret",
"C",
"witness",
}
return {
"outputs": ...,
"proofs": {i: proofs_include for i in range(len(proofs))},
}
resp = await self.httpx.post(
join(self.url, "/split"),
json=split_payload.dict(include=_splitrequest_include_fields(proofs)), # type: ignore
)
self.raise_on_error(resp)
promises_dict = resp.json()
mint_response = PostSplitResponse_Deprecated.parse_obj(promises_dict)
promises = [BlindedSignature(**p.dict()) for p in mint_response.promises]
if len(promises) == 0:
raise Exception("received no splits.")
return promises
@async_set_httpx_client
@async_ensure_mint_loaded_deprecated
async def check_proof_state_deprecated(
self, proofs: List[Proof]
) -> CheckSpendableResponse_deprecated:
"""
Checks whether the secrets in proofs are already spent or not and returns a list of booleans.
"""
logger.warning("Using deprecated API call: POST /check")
payload = CheckSpendableRequest_deprecated(proofs=proofs)
def _check_proof_state_include_fields(proofs):
"""strips away fields from the model that aren't necessary for the /split"""
return {
"proofs": {i: {"secret"} for i in range(len(proofs))},
}
resp = await self.httpx.post(
join(self.url, "/check"),
json=payload.dict(include=_check_proof_state_include_fields(proofs)), # type: ignore
)
self.raise_on_error(resp)
return_dict = resp.json()
states = CheckSpendableResponse_deprecated.parse_obj(return_dict)
return states
@async_set_httpx_client
@async_ensure_mint_loaded_deprecated
async def restore_promises_deprecated(
self, outputs: List[BlindedMessage]
) -> Tuple[List[BlindedMessage], List[BlindedSignature]]:
"""
Asks the mint to restore promises corresponding to outputs.
"""
logger.warning("Using deprecated API call: POST /restore")
payload = PostMintRequest_deprecated(outputs=outputs)
resp = await self.httpx.post(join(self.url, "/restore"), json=payload.dict())
self.raise_on_error(resp)
response_dict = resp.json()
returnObj = PostRestoreResponse.parse_obj(response_dict)
return returnObj.outputs, returnObj.promises
@async_set_httpx_client
@async_ensure_mint_loaded_deprecated
async def check_fees_deprecated(self, payment_request: str):
"""Checks whether the Lightning payment is internal."""
payload = CheckFeesRequest_deprecated(pr=payment_request)
resp = await self.httpx.post(
join(self.url, "/checkfees"),
json=payload.dict(),
)
self.raise_on_error(resp)
return_dict = resp.json()
return CheckFeesResponse_deprecated.parse_obj(return_dict)