mirror of
https://github.com/aljazceru/nutshell.git
synced 2025-12-20 02:24:20 +01:00
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
This commit is contained in:
2
.github/actions/prepare/action.yml
vendored
2
.github/actions/prepare/action.yml
vendored
@@ -7,7 +7,7 @@ inputs:
|
||||
default: "3.10"
|
||||
poetry-version:
|
||||
description: "Poetry Version"
|
||||
default: "1.5.1"
|
||||
default: "1.7.1"
|
||||
|
||||
runs:
|
||||
using: "composite"
|
||||
|
||||
4
.github/workflows/checks.yml
vendored
4
.github/workflows/checks.yml
vendored
@@ -7,7 +7,7 @@ on:
|
||||
default: "3.10.4"
|
||||
type: string
|
||||
poetry-version:
|
||||
default: "1.5.1"
|
||||
default: "1.7.1"
|
||||
type: string
|
||||
|
||||
jobs:
|
||||
@@ -46,7 +46,7 @@ jobs:
|
||||
- name: Setup mypy
|
||||
run: yes | poetry run mypy cashu --install-types || true
|
||||
- name: Run mypy
|
||||
run: poetry run mypy cashu --ignore-missing
|
||||
run: poetry run mypy cashu --ignore-missing --check-untyped-defs
|
||||
ruff:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
|
||||
10
.github/workflows/ci.yml
vendored
10
.github/workflows/ci.yml
vendored
@@ -10,25 +10,29 @@ jobs:
|
||||
uses: ./.github/workflows/checks.yml
|
||||
tests:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: [ubuntu-latest]
|
||||
python-version: ["3.10"]
|
||||
poetry-version: ["1.5.1"]
|
||||
mint-cache-secrets: ["true", "false"]
|
||||
poetry-version: ["1.7.1"]
|
||||
mint-cache-secrets: ["false", "true"]
|
||||
mint-only-deprecated: ["false", "true"]
|
||||
# db-url: ["", "postgres://cashu:cashu@localhost:5432/test"] # TODO: Postgres test not working
|
||||
db-url: [""]
|
||||
backend-wallet-class: ["FakeWallet"]
|
||||
uses: ./.github/workflows/tests.yml
|
||||
with:
|
||||
os: ${{ matrix.os }}
|
||||
python-version: ${{ matrix.python-version }}
|
||||
poetry-version: ${{ matrix.poetry-version }}
|
||||
mint-cache-secrets: ${{ matrix.mint-cache-secrets }}
|
||||
mint-only-deprecated: ${{ matrix.mint-only-deprecated }}
|
||||
regtest:
|
||||
uses: ./.github/workflows/regtest.yml
|
||||
strategy:
|
||||
matrix:
|
||||
python-version: ["3.10"]
|
||||
poetry-version: ["1.5.1"]
|
||||
poetry-version: ["1.7.1"]
|
||||
backend-wallet-class:
|
||||
["LndRestWallet", "CoreLightningRestWallet", "LNbitsWallet"]
|
||||
with:
|
||||
|
||||
7
.github/workflows/tests.yml
vendored
7
.github/workflows/tests.yml
vendored
@@ -7,7 +7,7 @@ on:
|
||||
default: "3.10.4"
|
||||
type: string
|
||||
poetry-version:
|
||||
default: "1.5.1"
|
||||
default: "1.7.1"
|
||||
type: string
|
||||
db-url:
|
||||
default: ""
|
||||
@@ -18,9 +18,13 @@ on:
|
||||
mint-cache-secrets:
|
||||
default: "false"
|
||||
type: string
|
||||
mint-only-deprecated:
|
||||
default: "false"
|
||||
type: string
|
||||
|
||||
jobs:
|
||||
poetry:
|
||||
name: Run (mint-cache-secrets ${{ inputs.mint-cache-secrets }}, mint-only-deprecated ${{ inputs.mint-only-deprecated }})
|
||||
runs-on: ${{ inputs.os }}
|
||||
services:
|
||||
postgres:
|
||||
@@ -51,6 +55,7 @@ jobs:
|
||||
MINT_PORT: 3337
|
||||
MINT_DATABASE: ${{ inputs.db-url }}
|
||||
MINT_CACHE_SECRETS: ${{ inputs.mint-cache-secrets }}
|
||||
DEBUG_MINT_ONLY_DEPRECATED: ${{ inputs.mint-only-deprecated }}
|
||||
TOR: false
|
||||
run: |
|
||||
make test
|
||||
|
||||
@@ -12,17 +12,18 @@ repos:
|
||||
- id: debug-statements
|
||||
- id: mixed-line-ending
|
||||
- id: check-case-conflict
|
||||
- repo: https://github.com/psf/black
|
||||
rev: 23.7.0
|
||||
hooks:
|
||||
- id: black
|
||||
# - repo: https://github.com/psf/black
|
||||
# rev: 23.11.0
|
||||
# hooks:
|
||||
# - id: black
|
||||
# args: [--line-length=150]
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
rev: v0.0.283
|
||||
hooks:
|
||||
- id: ruff
|
||||
args: [--fix, --exit-non-zero-on-fix]
|
||||
# - repo: https://github.com/pre-commit/mirrors-mypy
|
||||
# rev: v1.6.0
|
||||
# hooks:
|
||||
# - id: mypy
|
||||
# args: [--ignore-missing]
|
||||
- repo: https://github.com/pre-commit/mirrors-mypy
|
||||
rev: v1.6.0
|
||||
hooks:
|
||||
- id: mypy
|
||||
args: [--ignore-missing, --check-untyped-defs]
|
||||
|
||||
2
Makefile
2
Makefile
@@ -11,7 +11,7 @@ black-check:
|
||||
poetry run black . --check
|
||||
|
||||
mypy:
|
||||
poetry run mypy cashu --ignore-missing --check-untyped-defs
|
||||
poetry run mypy cashu --check-untyped-defs
|
||||
|
||||
format: black ruff
|
||||
|
||||
|
||||
@@ -182,9 +182,9 @@ To run the tests in this repository, first install the dev dependencies with
|
||||
poetry install --with dev
|
||||
```
|
||||
|
||||
Then, make sure to set up your `.env` file to use your local mint and disable Lightning and Tor:
|
||||
Then, make sure to set up your mint's `.env` file to use a fake Lightning backend and disable Tor:
|
||||
```bash
|
||||
LIGHTNING=FALSE
|
||||
MINT_LIGHTNING_BACKEND=FakeWallet
|
||||
TOR=FALSE
|
||||
```
|
||||
You can run the tests with
|
||||
|
||||
@@ -1,14 +1,24 @@
|
||||
import base64
|
||||
import json
|
||||
import math
|
||||
from dataclasses import dataclass
|
||||
from enum import Enum
|
||||
from sqlite3 import Row
|
||||
from typing import Dict, List, Optional, Union
|
||||
from typing import Any, Dict, List, Optional, Union
|
||||
|
||||
from loguru import logger
|
||||
from pydantic import BaseModel
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from .crypto.keys import derive_keys, derive_keyset_id, derive_pubkeys
|
||||
from .crypto.keys import (
|
||||
derive_keys,
|
||||
derive_keys_sha256,
|
||||
derive_keyset_id,
|
||||
derive_keyset_id_deprecated,
|
||||
derive_pubkeys,
|
||||
)
|
||||
from .crypto.secp import PrivateKey, PublicKey
|
||||
from .legacy import derive_keys_backwards_compatible_insecure_pre_0_12
|
||||
from .settings import settings
|
||||
|
||||
|
||||
class DLEQ(BaseModel):
|
||||
@@ -155,6 +165,9 @@ class BlindedMessage(BaseModel):
|
||||
"""
|
||||
|
||||
amount: int
|
||||
id: Optional[
|
||||
str
|
||||
] # DEPRECATION: Only Optional for backwards compatibility with old clients < 0.15 for deprecated API route.
|
||||
B_: str # Hex-encoded blinded message
|
||||
witness: Union[str, None] = None # witnesses (used for P2PK with SIG_ALL)
|
||||
|
||||
@@ -196,12 +209,52 @@ class Invoice(BaseModel):
|
||||
time_paid: Union[None, str, int, float] = ""
|
||||
|
||||
|
||||
class MeltQuote(BaseModel):
|
||||
quote: str
|
||||
method: str
|
||||
request: str
|
||||
checking_id: str
|
||||
unit: str
|
||||
amount: int
|
||||
fee_reserve: int
|
||||
paid: bool
|
||||
created_time: int = 0
|
||||
paid_time: int = 0
|
||||
fee_paid: int = 0
|
||||
proof: str = ""
|
||||
|
||||
|
||||
class MintQuote(BaseModel):
|
||||
quote: str
|
||||
method: str
|
||||
request: str
|
||||
checking_id: str
|
||||
unit: str
|
||||
amount: int
|
||||
paid: bool
|
||||
issued: bool
|
||||
created_time: int = 0
|
||||
paid_time: int = 0
|
||||
expiry: int = 0
|
||||
|
||||
|
||||
# ------- API -------
|
||||
|
||||
# ------- API: INFO -------
|
||||
|
||||
|
||||
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[List[str]]] = None
|
||||
motd: Optional[str] = None
|
||||
nuts: Optional[Dict[int, Dict[str, Any]]] = None
|
||||
|
||||
|
||||
class GetInfoResponse_deprecated(BaseModel):
|
||||
name: Optional[str] = None
|
||||
pubkey: Optional[str] = None
|
||||
version: Optional[str] = None
|
||||
@@ -216,40 +269,121 @@ class GetInfoResponse(BaseModel):
|
||||
# ------- API: KEYS -------
|
||||
|
||||
|
||||
class KeysResponseKeyset(BaseModel):
|
||||
id: str
|
||||
unit: str
|
||||
keys: Dict[int, str]
|
||||
|
||||
|
||||
class KeysResponse(BaseModel):
|
||||
__root__: Dict[str, str]
|
||||
keysets: List[KeysResponseKeyset]
|
||||
|
||||
|
||||
class KeysetsResponseKeyset(BaseModel):
|
||||
id: str
|
||||
unit: str
|
||||
active: bool
|
||||
|
||||
|
||||
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
|
||||
|
||||
|
||||
class PostMintQuoteResponse(BaseModel):
|
||||
quote: str # quote id
|
||||
request: str # input payment request
|
||||
paid: bool # whether the request has been paid
|
||||
expiry: int # expiry of the quote
|
||||
|
||||
|
||||
# ------- API: MINT -------
|
||||
|
||||
|
||||
class PostMintRequest(BaseModel):
|
||||
outputs: List[BlindedMessage]
|
||||
quote: str = Field(..., max_length=settings.mint_max_request_length) # quote id
|
||||
outputs: List[BlindedMessage] = Field(
|
||||
..., max_items=settings.mint_max_request_length
|
||||
)
|
||||
|
||||
|
||||
class PostMintResponse(BaseModel):
|
||||
signatures: List[BlindedSignature] = []
|
||||
|
||||
|
||||
class GetMintResponse_deprecated(BaseModel):
|
||||
pr: str
|
||||
hash: str
|
||||
|
||||
|
||||
class PostMintRequest_deprecated(BaseModel):
|
||||
outputs: List[BlindedMessage] = Field(
|
||||
..., max_items=settings.mint_max_request_length
|
||||
)
|
||||
|
||||
|
||||
class PostMintResponse_deprecated(BaseModel):
|
||||
promises: List[BlindedSignature] = []
|
||||
|
||||
|
||||
class GetMintResponse(BaseModel):
|
||||
pr: str
|
||||
hash: str
|
||||
# ------- API: MELT QUOTE -------
|
||||
|
||||
|
||||
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
|
||||
|
||||
|
||||
class PostMeltQuoteResponse(BaseModel):
|
||||
quote: str # quote id
|
||||
amount: int # input amount
|
||||
fee_reserve: int # input fee reserve
|
||||
paid: bool # whether the request has been paid
|
||||
|
||||
|
||||
# ------- API: MELT -------
|
||||
|
||||
|
||||
class PostMeltRequest(BaseModel):
|
||||
proofs: List[Proof]
|
||||
pr: str
|
||||
outputs: Union[List[BlindedMessage], None]
|
||||
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(
|
||||
..., max_items=settings.mint_max_request_length
|
||||
)
|
||||
|
||||
|
||||
class GetMeltResponse(BaseModel):
|
||||
class PostMeltResponse(BaseModel):
|
||||
paid: Union[bool, None]
|
||||
payment_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], None] = Field(
|
||||
..., 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
|
||||
@@ -259,17 +393,30 @@ class GetMeltResponse(BaseModel):
|
||||
|
||||
|
||||
class PostSplitRequest(BaseModel):
|
||||
proofs: List[Proof]
|
||||
amount: Optional[int] = None # deprecated since 0.13.0
|
||||
outputs: List[BlindedMessage]
|
||||
inputs: List[Proof] = Field(..., max_items=settings.mint_max_request_length)
|
||||
outputs: List[BlindedMessage] = Field(
|
||||
..., max_items=settings.mint_max_request_length
|
||||
)
|
||||
|
||||
|
||||
class PostSplitResponse(BaseModel):
|
||||
promises: List[BlindedSignature]
|
||||
signatures: List[BlindedSignature]
|
||||
|
||||
|
||||
# deprecated since 0.13.0
|
||||
class PostSplitRequest_Deprecated(BaseModel):
|
||||
proofs: List[Proof] = Field(..., max_items=settings.mint_max_request_length)
|
||||
amount: Optional[int] = None
|
||||
outputs: List[BlindedMessage] = Field(
|
||||
..., max_items=settings.mint_max_request_length
|
||||
)
|
||||
|
||||
|
||||
class PostSplitResponse_Deprecated(BaseModel):
|
||||
promises: List[BlindedSignature] = []
|
||||
|
||||
|
||||
class PostSplitResponse_Very_Deprecated(BaseModel):
|
||||
fst: List[BlindedSignature] = []
|
||||
snd: List[BlindedSignature] = []
|
||||
deprecated: str = "The amount field is deprecated since 0.13.0"
|
||||
@@ -278,23 +425,43 @@ class PostSplitResponse_Deprecated(BaseModel):
|
||||
# ------- API: CHECK -------
|
||||
|
||||
|
||||
class CheckSpendableRequest(BaseModel):
|
||||
proofs: List[Proof]
|
||||
class PostCheckStateRequest(BaseModel):
|
||||
secrets: List[str] = Field(..., max_items=settings.mint_max_request_length)
|
||||
|
||||
|
||||
class CheckSpendableResponse(BaseModel):
|
||||
class SpentState(Enum):
|
||||
unspent = "UNSPENT"
|
||||
spent = "SPENT"
|
||||
pending = "PENDING"
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
|
||||
class ProofState(BaseModel):
|
||||
secret: str
|
||||
state: SpentState
|
||||
witness: Optional[str] = None
|
||||
|
||||
|
||||
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: Optional[List[bool]] = (
|
||||
None # TODO: Uncomment when all mints are updated to 0.12.3 and support /check
|
||||
)
|
||||
# with pending tokens (kept for backwards compatibility of new wallets with old mints)
|
||||
pending: List[bool]
|
||||
|
||||
|
||||
class CheckFeesRequest(BaseModel):
|
||||
pr: str
|
||||
class CheckFeesRequest_deprecated(BaseModel):
|
||||
pr: str = Field(..., max_length=settings.mint_max_request_length)
|
||||
|
||||
|
||||
class CheckFeesResponse(BaseModel):
|
||||
class CheckFeesResponse_deprecated(BaseModel):
|
||||
fee: Union[int, None]
|
||||
|
||||
|
||||
@@ -319,12 +486,70 @@ class KeyBase(BaseModel):
|
||||
pubkey: str
|
||||
|
||||
|
||||
class Unit(Enum):
|
||||
sat = 0
|
||||
msat = 1
|
||||
usd = 2
|
||||
|
||||
def str(self, amount: int) -> str:
|
||||
if self == Unit.sat:
|
||||
return f"{amount} sat"
|
||||
elif self == Unit.msat:
|
||||
return f"{amount} msat"
|
||||
elif self == Unit.usd:
|
||||
return f"${amount/100:.2f} USD"
|
||||
else:
|
||||
raise Exception("Invalid unit")
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
|
||||
@dataclass
|
||||
class Amount:
|
||||
unit: Unit
|
||||
amount: int
|
||||
|
||||
def to(self, to_unit: Unit, round: Optional[str] = None):
|
||||
if self.unit == to_unit:
|
||||
return self
|
||||
|
||||
if self.unit == Unit.sat:
|
||||
if to_unit == Unit.msat:
|
||||
return Amount(to_unit, self.amount * 1000)
|
||||
else:
|
||||
raise Exception(f"Cannot convert {self.unit.name} to {to_unit.name}")
|
||||
elif self.unit == Unit.msat:
|
||||
if to_unit == Unit.sat:
|
||||
if round == "up":
|
||||
return Amount(to_unit, math.ceil(self.amount / 1000))
|
||||
elif round == "down":
|
||||
return Amount(to_unit, math.floor(self.amount / 1000))
|
||||
else:
|
||||
return Amount(to_unit, self.amount // 1000)
|
||||
else:
|
||||
raise Exception(f"Cannot convert {self.unit.name} to {to_unit.name}")
|
||||
else:
|
||||
return self
|
||||
|
||||
def str(self) -> str:
|
||||
return self.unit.str(self.amount)
|
||||
|
||||
def __repr__(self):
|
||||
return self.unit.str(self.amount)
|
||||
|
||||
|
||||
class Method(Enum):
|
||||
bolt11 = 0
|
||||
|
||||
|
||||
class WalletKeyset:
|
||||
"""
|
||||
Contains the keyset from the wallets's perspective.
|
||||
"""
|
||||
|
||||
id: str
|
||||
unit: Unit
|
||||
public_keys: Dict[int, PublicKey]
|
||||
mint_url: Union[str, None] = None
|
||||
valid_from: Union[str, None] = None
|
||||
@@ -335,12 +560,14 @@ class WalletKeyset:
|
||||
def __init__(
|
||||
self,
|
||||
public_keys: Dict[int, PublicKey],
|
||||
id=None,
|
||||
unit: str,
|
||||
id: Optional[str] = None,
|
||||
mint_url=None,
|
||||
valid_from=None,
|
||||
valid_to=None,
|
||||
first_seen=None,
|
||||
active=None,
|
||||
active=True,
|
||||
use_deprecated_id=False, # BACKWARDS COMPATIBILITY < 0.15.0
|
||||
):
|
||||
self.valid_from = valid_from
|
||||
self.valid_to = valid_to
|
||||
@@ -350,17 +577,34 @@ class WalletKeyset:
|
||||
|
||||
self.public_keys = public_keys
|
||||
# overwrite id by deriving it from the public keys
|
||||
if not id:
|
||||
self.id = derive_keyset_id(self.public_keys)
|
||||
else:
|
||||
self.id = id
|
||||
|
||||
# BEGIN BACKWARDS COMPATIBILITY < 0.15.0
|
||||
if use_deprecated_id:
|
||||
logger.warning(
|
||||
"Using deprecated keyset id derivation for backwards compatibility <"
|
||||
" 0.15.0"
|
||||
)
|
||||
self.id = derive_keyset_id_deprecated(self.public_keys)
|
||||
# END BACKWARDS COMPATIBILITY < 0.15.0
|
||||
|
||||
self.unit = Unit[unit]
|
||||
|
||||
logger.trace(f"Derived keyset id {self.id} from public keys.")
|
||||
if id and id != self.id:
|
||||
if id and id != self.id and use_deprecated_id:
|
||||
logger.warning(
|
||||
f"WARNING: Keyset id {self.id} does not match the given id {id}."
|
||||
" Overwriting."
|
||||
)
|
||||
self.id = id
|
||||
|
||||
def serialize(self):
|
||||
return json.dumps(
|
||||
{amount: key.serialize().hex() for amount, key in self.public_keys.items()}
|
||||
)
|
||||
return json.dumps({
|
||||
amount: key.serialize().hex() for amount, key in self.public_keys.items()
|
||||
})
|
||||
|
||||
@classmethod
|
||||
def from_row(cls, row: Row):
|
||||
@@ -372,6 +616,7 @@ class WalletKeyset:
|
||||
|
||||
return cls(
|
||||
id=row["id"],
|
||||
unit=row["unit"],
|
||||
public_keys=(
|
||||
deserialize(str(row["public_keys"]))
|
||||
if dict(row).get("public_keys")
|
||||
@@ -391,74 +636,107 @@ class MintKeyset:
|
||||
"""
|
||||
|
||||
id: str
|
||||
derivation_path: str
|
||||
private_keys: Dict[int, PrivateKey]
|
||||
active: bool
|
||||
unit: Unit
|
||||
derivation_path: str
|
||||
seed: Optional[str] = None
|
||||
public_keys: Union[Dict[int, PublicKey], None] = None
|
||||
valid_from: Union[str, None] = None
|
||||
valid_to: Union[str, None] = None
|
||||
first_seen: Union[str, None] = None
|
||||
active: Union[bool, None] = True
|
||||
version: Union[str, None] = None
|
||||
|
||||
duplicate_keyset_id: Optional[str] = None # BACKWARDS COMPATIBILITY < 0.15.0
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
id="",
|
||||
valid_from=None,
|
||||
valid_to=None,
|
||||
first_seen=None,
|
||||
active=None,
|
||||
seed: str = "",
|
||||
derivation_path: str = "",
|
||||
version: str = "1",
|
||||
seed: Optional[str] = None,
|
||||
derivation_path: Optional[str] = None,
|
||||
unit: Optional[str] = None,
|
||||
version: str = "0",
|
||||
):
|
||||
self.derivation_path = derivation_path
|
||||
self.derivation_path = derivation_path or ""
|
||||
self.seed = seed
|
||||
self.id = id
|
||||
self.valid_from = valid_from
|
||||
self.valid_to = valid_to
|
||||
self.first_seen = first_seen
|
||||
self.active = active
|
||||
self.active = bool(active) if active is not None else False
|
||||
self.version = version
|
||||
# generate keys from seed
|
||||
if seed:
|
||||
self.generate_keys(seed)
|
||||
|
||||
def generate_keys(self, seed):
|
||||
self.version_tuple = tuple(
|
||||
[int(i) for i in self.version.split(".")] if self.version else []
|
||||
)
|
||||
|
||||
# infer unit from derivation path
|
||||
if not unit:
|
||||
logger.warning(
|
||||
f"Unit for keyset {self.derivation_path} not set – attempting to parse"
|
||||
" from derivation path"
|
||||
)
|
||||
try:
|
||||
self.unit = Unit(
|
||||
int(self.derivation_path.split("/")[2].replace("'", ""))
|
||||
)
|
||||
logger.warning(f"Inferred unit: {self.unit.name}")
|
||||
except Exception:
|
||||
logger.warning(
|
||||
"Could not infer unit from derivation path"
|
||||
f" {self.derivation_path} – assuming 'sat'"
|
||||
)
|
||||
self.unit = Unit.sat
|
||||
else:
|
||||
self.unit = Unit[unit]
|
||||
|
||||
# generate keys from seed
|
||||
if self.seed and self.derivation_path:
|
||||
self.generate_keys()
|
||||
|
||||
logger.debug(f"Keyset id: {self.id} ({self.unit.name})")
|
||||
|
||||
@property
|
||||
def public_keys_hex(self) -> Dict[int, str]:
|
||||
assert self.public_keys, "public keys not set"
|
||||
return {
|
||||
int(amount): key.serialize().hex()
|
||||
for amount, key in self.public_keys.items()
|
||||
}
|
||||
|
||||
def generate_keys(self):
|
||||
"""Generates keys of a keyset from a seed."""
|
||||
backwards_compatibility_pre_0_12 = False
|
||||
if (
|
||||
self.version
|
||||
and len(self.version.split(".")) > 1
|
||||
and int(self.version.split(".")[0]) == 0
|
||||
and int(self.version.split(".")[1]) <= 11
|
||||
):
|
||||
backwards_compatibility_pre_0_12 = True
|
||||
assert self.seed, "seed not set"
|
||||
assert self.derivation_path, "derivation path not set"
|
||||
|
||||
if self.version_tuple < (0, 12):
|
||||
# WARNING: Broken key derivation for backwards compatibility with < 0.12
|
||||
self.private_keys = derive_keys_backwards_compatible_insecure_pre_0_12(
|
||||
seed, self.derivation_path
|
||||
self.seed, self.derivation_path
|
||||
)
|
||||
else:
|
||||
self.private_keys = derive_keys(seed, self.derivation_path)
|
||||
self.public_keys = derive_pubkeys(self.private_keys) # type: ignore
|
||||
self.id = derive_keyset_id(self.public_keys) # type: ignore
|
||||
if backwards_compatibility_pre_0_12:
|
||||
logger.warning(
|
||||
f"WARNING: Using weak key derivation for keyset {self.id} (backwards"
|
||||
" compatibility < 0.12)"
|
||||
)
|
||||
|
||||
|
||||
class MintKeysets:
|
||||
"""
|
||||
Collection of keyset IDs and the corresponding keyset of the mint.
|
||||
"""
|
||||
|
||||
keysets: Dict[str, MintKeyset]
|
||||
|
||||
def __init__(self, keysets: List[MintKeyset]):
|
||||
self.keysets = {k.id: k for k in keysets} # type: ignore
|
||||
|
||||
def get_ids(self):
|
||||
return [k for k, _ in self.keysets.items()]
|
||||
self.id = derive_keyset_id_deprecated(self.public_keys) # type: ignore
|
||||
elif self.version_tuple < (0, 15):
|
||||
self.private_keys = derive_keys_sha256(self.seed, self.derivation_path)
|
||||
logger.warning(
|
||||
f"WARNING: Using non-bip32 derivation for keyset {self.id} (backwards"
|
||||
" compatibility < 0.15)"
|
||||
)
|
||||
self.public_keys = derive_pubkeys(self.private_keys) # type: ignore
|
||||
self.id = derive_keyset_id_deprecated(self.public_keys) # type: ignore
|
||||
else:
|
||||
self.private_keys = derive_keys(self.seed, self.derivation_path)
|
||||
self.public_keys = derive_pubkeys(self.private_keys) # type: ignore
|
||||
self.id = derive_keyset_id(self.public_keys) # type: ignore
|
||||
|
||||
|
||||
# ------- TOKEN -------
|
||||
@@ -541,7 +819,7 @@ class TokenV3(BaseModel):
|
||||
@classmethod
|
||||
def deserialize(cls, tokenv3_serialized: str) -> "TokenV3":
|
||||
"""
|
||||
Takes a TokenV3 and serializes it as "cashuA<json_urlsafe_base64>.
|
||||
Ingesta a serialized "cashuA<json_urlsafe_base64>" token and returns a TokenV3.
|
||||
"""
|
||||
prefix = "cashuA"
|
||||
assert tokenv3_serialized.startswith(prefix), Exception(
|
||||
|
||||
@@ -3,19 +3,29 @@ import hashlib
|
||||
import random
|
||||
from typing import Dict
|
||||
|
||||
from bip32 import BIP32
|
||||
|
||||
from ..settings import settings
|
||||
from .secp import PrivateKey, PublicKey
|
||||
|
||||
# entropy = bytes([random.getrandbits(8) for i in range(16)])
|
||||
# mnemonic = bip39.mnemonic_from_bytes(entropy)
|
||||
# seed = bip39.mnemonic_to_seed(mnemonic)
|
||||
# root = bip32.HDKey.from_seed(seed, version=NETWORKS["main"]["xprv"])
|
||||
|
||||
# bip44_xprv = root.derive("m/44h/1h/0h")
|
||||
# bip44_xpub = bip44_xprv.to_public()
|
||||
def derive_keys(mnemonic: str, derivation_path: str):
|
||||
"""
|
||||
Deterministic derivation of keys for 2^n values.
|
||||
"""
|
||||
bip32 = BIP32.from_seed(mnemonic.encode())
|
||||
orders_str = [f"/{i}'" for i in range(settings.max_order)]
|
||||
return {
|
||||
2
|
||||
** i: PrivateKey(
|
||||
bip32.get_privkey_from_path(derivation_path + orders_str[i]),
|
||||
raw=True,
|
||||
)
|
||||
for i in range(settings.max_order)
|
||||
}
|
||||
|
||||
|
||||
def derive_keys(master_key: str, derivation_path: str = ""):
|
||||
def derive_keys_sha256(master_key: str, derivation_path: str = ""):
|
||||
"""
|
||||
Deterministic derivation of keys for 2^n values.
|
||||
TODO: Implement BIP32.
|
||||
@@ -40,15 +50,23 @@ def derive_pubkey(master_key: str):
|
||||
|
||||
|
||||
def derive_pubkeys(keys: Dict[int, PrivateKey]):
|
||||
return {
|
||||
amt: keys[amt].pubkey for amt in [2**i for i in range(settings.max_order)]
|
||||
}
|
||||
return {amt: keys[amt].pubkey for amt in [2**i for i in range(settings.max_order)]}
|
||||
|
||||
|
||||
def derive_keyset_id(keys: Dict[int, PublicKey]):
|
||||
"""Deterministic derivation keyset_id from set of public keys."""
|
||||
# sort public keys by amount
|
||||
sorted_keys = dict(sorted(keys.items()))
|
||||
pubkeys_concat = b"".join([p.serialize() for _, p in sorted_keys.items()])
|
||||
return "00" + hashlib.sha256(pubkeys_concat).hexdigest()[:14]
|
||||
|
||||
|
||||
def derive_keyset_id_deprecated(keys: Dict[int, PublicKey]):
|
||||
"""DEPRECATED 0.15.0: Deterministic derivation keyset_id from set of public keys.
|
||||
DEPRECATION: This method produces base64 keyset ids. Use `derive_keyset_id` instead.
|
||||
"""
|
||||
# sort public keys by amount
|
||||
sorted_keys = dict(sorted(keys.items()))
|
||||
pubkeys_concat = "".join([p.serialize().hex() for _, p in sorted_keys.items()])
|
||||
return base64.b64encode(
|
||||
hashlib.sha256((pubkeys_concat).encode("utf-8")).digest()
|
||||
|
||||
@@ -39,10 +39,8 @@ def async_unwrap(to_await):
|
||||
return async_response[0]
|
||||
|
||||
|
||||
def fee_reserve(amount_msat: int, internal=False) -> int:
|
||||
def fee_reserve(amount_msat: int) -> int:
|
||||
"""Function for calculating the Lightning fee reserve"""
|
||||
if internal:
|
||||
return 0
|
||||
return max(
|
||||
int(settings.lightning_reserve_fee_min),
|
||||
int(amount_msat * settings.lightning_fee_percent / 100.0),
|
||||
|
||||
@@ -1,38 +1,10 @@
|
||||
import hashlib
|
||||
|
||||
from secp256k1 import PrivateKey, PublicKey
|
||||
from secp256k1 import PrivateKey
|
||||
|
||||
from ..core.settings import settings
|
||||
|
||||
|
||||
def hash_to_point_pre_0_3_3(secret_msg):
|
||||
"""
|
||||
NOTE: Clients pre 0.3.3 used a different hash_to_curve
|
||||
|
||||
Generates x coordinate from the message hash and checks if the point lies on the curve.
|
||||
If it does not, it tries computing again a new x coordinate from the hash of the coordinate.
|
||||
"""
|
||||
point = None
|
||||
msg = secret_msg
|
||||
while point is None:
|
||||
_hash = hashlib.sha256(msg).hexdigest().encode("utf-8") # type: ignore
|
||||
try:
|
||||
# We construct compressed pub which has x coordinate encoded with even y
|
||||
_hash_list = list(_hash[:33]) # take the 33 bytes and get a list of bytes
|
||||
_hash_list[0] = 0x02 # set first byte to represent even y coord
|
||||
_hash = bytes(_hash_list)
|
||||
point = PublicKey(_hash, raw=True)
|
||||
except Exception:
|
||||
msg = _hash
|
||||
|
||||
return point
|
||||
|
||||
|
||||
def verify_pre_0_3_3(a, C, secret_msg):
|
||||
Y = hash_to_point_pre_0_3_3(secret_msg.encode("utf-8"))
|
||||
return C == Y.mult(a) # type: ignore
|
||||
|
||||
|
||||
def derive_keys_backwards_compatible_insecure_pre_0_12(
|
||||
master_key: str, derivation_path: str = ""
|
||||
):
|
||||
|
||||
49
cashu/core/logging.py
Normal file
49
cashu/core/logging.py
Normal file
@@ -0,0 +1,49 @@
|
||||
import logging
|
||||
import sys
|
||||
|
||||
from loguru import logger
|
||||
|
||||
from ..core.settings import settings
|
||||
|
||||
|
||||
def configure_logger() -> None:
|
||||
class Formatter:
|
||||
def __init__(self):
|
||||
self.padding = 0
|
||||
self.minimal_fmt = (
|
||||
"<green>{time:YYYY-MM-DD HH:mm:ss.SS}</green> |"
|
||||
" <level>{level}</level> | <level>{message}</level>\n"
|
||||
)
|
||||
if settings.debug:
|
||||
self.fmt = (
|
||||
"<green>{time:YYYY-MM-DD HH:mm:ss.SS}</green> | <level>{level:"
|
||||
" <4}</level> |"
|
||||
" <cyan>{name}</cyan>:<cyan>{function}</cyan>:<cyan>{line}</cyan>"
|
||||
" | <level>{message}</level>\n"
|
||||
)
|
||||
else:
|
||||
self.fmt = self.minimal_fmt
|
||||
|
||||
def format(self, record):
|
||||
function = "{function}".format(**record)
|
||||
if function == "emit": # uvicorn logs
|
||||
return self.minimal_fmt
|
||||
return self.fmt
|
||||
|
||||
class InterceptHandler(logging.Handler):
|
||||
def emit(self, record):
|
||||
try:
|
||||
level = logger.level(record.levelname).name
|
||||
except ValueError:
|
||||
level = record.levelno
|
||||
logger.log(level, record.getMessage())
|
||||
|
||||
logger.remove()
|
||||
log_level = settings.log_level
|
||||
if settings.debug and log_level == "INFO":
|
||||
log_level = "DEBUG"
|
||||
formatter = Formatter()
|
||||
logger.add(sys.stderr, level=log_level, format=formatter.format)
|
||||
|
||||
logging.getLogger("uvicorn").handlers = [InterceptHandler()]
|
||||
logging.getLogger("uvicorn.access").handlers = [InterceptHandler()]
|
||||
@@ -8,7 +8,7 @@ from pydantic import BaseSettings, Extra, Field
|
||||
|
||||
env = Env()
|
||||
|
||||
VERSION = "0.14.1"
|
||||
VERSION = "0.15.0"
|
||||
|
||||
|
||||
def find_env_file():
|
||||
@@ -25,7 +25,6 @@ def find_env_file():
|
||||
|
||||
class CashuSettings(BaseSettings):
|
||||
env_file: str = Field(default=None)
|
||||
lightning: bool = Field(default=True)
|
||||
lightning_fee_percent: float = Field(default=1.0)
|
||||
lightning_reserve_fee_min: int = Field(default=2000)
|
||||
max_order: int = Field(default=64)
|
||||
@@ -45,11 +44,13 @@ class EnvSettings(CashuSettings):
|
||||
log_level: str = Field(default="INFO")
|
||||
cashu_dir: str = Field(default=os.path.join(str(Path.home()), ".cashu"))
|
||||
debug_profiling: bool = Field(default=False)
|
||||
debug_mint_only_deprecated: bool = Field(default=False)
|
||||
|
||||
|
||||
class MintSettings(CashuSettings):
|
||||
mint_private_key: str = Field(default=None)
|
||||
mint_derivation_path: str = Field(default="0/0/0/0")
|
||||
mint_derivation_path: str = Field(default="m/0'/0'/0'")
|
||||
mint_derivation_path_list: List[str] = Field(default=[])
|
||||
mint_listen_host: str = Field(default="127.0.0.1")
|
||||
mint_listen_port: int = Field(default=3338)
|
||||
mint_lightning_backend: str = Field(default="LNbitsWallet")
|
||||
@@ -57,11 +58,19 @@ class MintSettings(CashuSettings):
|
||||
mint_peg_out_only: bool = Field(default=False)
|
||||
mint_max_peg_in: int = Field(default=None)
|
||||
mint_max_peg_out: int = Field(default=None)
|
||||
mint_max_request_length: int = Field(default=1000)
|
||||
mint_max_balance: int = Field(default=None)
|
||||
|
||||
mint_lnbits_endpoint: str = Field(default=None)
|
||||
mint_lnbits_key: str = Field(default=None)
|
||||
|
||||
mint_strike_key: str = Field(default=None)
|
||||
|
||||
|
||||
class FakeWalletSettings(MintSettings):
|
||||
fakewallet_brr: bool = Field(default=True)
|
||||
fakewallet_delay_payment: bool = Field(default=False)
|
||||
fakewallet_stochastic_invoice: bool = Field(default=False)
|
||||
mint_cache_secrets: bool = Field(default=True)
|
||||
|
||||
|
||||
@@ -75,7 +84,6 @@ class MintInformation(CashuSettings):
|
||||
|
||||
|
||||
class WalletSettings(CashuSettings):
|
||||
lightning: bool = Field(default=True)
|
||||
tor: bool = Field(default=True)
|
||||
socks_host: str = Field(default=None) # deprecated
|
||||
socks_port: int = Field(default=9050) # deprecated
|
||||
@@ -85,6 +93,7 @@ class WalletSettings(CashuSettings):
|
||||
mint_host: str = Field(default="8333.space")
|
||||
mint_port: int = Field(default=3338)
|
||||
wallet_name: str = Field(default="wallet")
|
||||
wallet_unit: str = Field(default="sat")
|
||||
|
||||
api_port: int = Field(default=4448)
|
||||
api_host: str = Field(default="127.0.0.1")
|
||||
@@ -121,6 +130,7 @@ class Settings(
|
||||
EnvSettings,
|
||||
LndRestFundingSource,
|
||||
CoreLightningRestFundingSource,
|
||||
FakeWalletSettings,
|
||||
MintSettings,
|
||||
MintInformation,
|
||||
WalletSettings,
|
||||
|
||||
@@ -4,6 +4,7 @@ from .corelightningrest import CoreLightningRestWallet # noqa: F401
|
||||
from .fake import FakeWallet # noqa: F401
|
||||
from .lnbits import LNbitsWallet # noqa: F401
|
||||
from .lndrest import LndRestWallet # noqa: F401
|
||||
from .strike import StrikeUSDWallet # noqa: F401
|
||||
|
||||
if settings.mint_lightning_backend is None:
|
||||
raise Exception("MINT_LIGHTNING_BACKEND not configured")
|
||||
|
||||
@@ -1,12 +1,25 @@
|
||||
from abc import ABC, abstractmethod
|
||||
from typing import Coroutine, Optional
|
||||
from typing import Coroutine, Optional, Union
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
from ..core.base import Amount, MeltQuote, Unit
|
||||
|
||||
|
||||
class StatusResponse(BaseModel):
|
||||
error_message: Optional[str]
|
||||
balance_msat: int
|
||||
balance: Union[int, float]
|
||||
|
||||
|
||||
class InvoiceQuoteResponse(BaseModel):
|
||||
checking_id: str
|
||||
amount: int
|
||||
|
||||
|
||||
class PaymentQuoteResponse(BaseModel):
|
||||
checking_id: str
|
||||
amount: Amount
|
||||
fee: Amount
|
||||
|
||||
|
||||
class InvoiceResponse(BaseModel):
|
||||
@@ -19,14 +32,14 @@ class InvoiceResponse(BaseModel):
|
||||
class PaymentResponse(BaseModel):
|
||||
ok: Optional[bool] = None # True: paid, False: failed, None: pending or unknown
|
||||
checking_id: Optional[str] = None
|
||||
fee_msat: Optional[int] = None
|
||||
fee: Optional[Amount] = None
|
||||
preimage: Optional[str] = None
|
||||
error_message: Optional[str] = None
|
||||
|
||||
|
||||
class PaymentStatus(BaseModel):
|
||||
paid: Optional[bool] = None
|
||||
fee_msat: Optional[int] = None
|
||||
fee: Optional[Amount] = None
|
||||
preimage: Optional[str] = None
|
||||
|
||||
@property
|
||||
@@ -48,7 +61,13 @@ class PaymentStatus(BaseModel):
|
||||
return "unknown (should never happen)"
|
||||
|
||||
|
||||
class Wallet(ABC):
|
||||
class LightningBackend(ABC):
|
||||
units: set[Unit]
|
||||
|
||||
def assert_unit_supported(self, unit: Unit):
|
||||
if unit not in self.units:
|
||||
raise Unsupported(f"Unit {unit} is not supported")
|
||||
|
||||
@abstractmethod
|
||||
def status(self) -> Coroutine[None, None, StatusResponse]:
|
||||
pass
|
||||
@@ -56,7 +75,7 @@ class Wallet(ABC):
|
||||
@abstractmethod
|
||||
def create_invoice(
|
||||
self,
|
||||
amount: int,
|
||||
amount: Amount,
|
||||
memo: Optional[str] = None,
|
||||
description_hash: Optional[bytes] = None,
|
||||
) -> Coroutine[None, None, InvoiceResponse]:
|
||||
@@ -64,7 +83,7 @@ class Wallet(ABC):
|
||||
|
||||
@abstractmethod
|
||||
def pay_invoice(
|
||||
self, bolt11: str, fee_limit_msat: int
|
||||
self, quote: MeltQuote, fee_limit_msat: int
|
||||
) -> Coroutine[None, None, PaymentResponse]:
|
||||
pass
|
||||
|
||||
@@ -80,6 +99,20 @@ class Wallet(ABC):
|
||||
) -> Coroutine[None, None, PaymentStatus]:
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def get_payment_quote(
|
||||
self,
|
||||
bolt11: str,
|
||||
) -> PaymentQuoteResponse:
|
||||
pass
|
||||
|
||||
# @abstractmethod
|
||||
# async def get_invoice_quote(
|
||||
# self,
|
||||
# bolt11: str,
|
||||
# ) -> InvoiceQuoteResponse:
|
||||
# pass
|
||||
|
||||
# @abstractmethod
|
||||
# def paid_invoices_stream(self) -> AsyncGenerator[str, None]:
|
||||
# pass
|
||||
|
||||
@@ -4,23 +4,30 @@ import random
|
||||
from typing import AsyncGenerator, Dict, Optional
|
||||
|
||||
import httpx
|
||||
from bolt11 import Bolt11Exception
|
||||
from bolt11.decode import decode
|
||||
from bolt11 import (
|
||||
Bolt11Exception,
|
||||
decode,
|
||||
)
|
||||
from loguru import logger
|
||||
|
||||
from ..core.base import Amount, MeltQuote, Unit
|
||||
from ..core.helpers import fee_reserve
|
||||
from ..core.settings import settings
|
||||
from .base import (
|
||||
InvoiceResponse,
|
||||
LightningBackend,
|
||||
PaymentQuoteResponse,
|
||||
PaymentResponse,
|
||||
PaymentStatus,
|
||||
StatusResponse,
|
||||
Unsupported,
|
||||
Wallet,
|
||||
)
|
||||
from .macaroon import load_macaroon
|
||||
|
||||
|
||||
class CoreLightningRestWallet(Wallet):
|
||||
class CoreLightningRestWallet(LightningBackend):
|
||||
units = set([Unit.sat, Unit.msat])
|
||||
|
||||
def __init__(self):
|
||||
macaroon = settings.mint_corelightning_rest_macaroon
|
||||
assert macaroon, "missing cln-rest macaroon"
|
||||
@@ -72,26 +79,27 @@ class CoreLightningRestWallet(Wallet):
|
||||
error_message=(
|
||||
f"Failed to connect to {self.url}, got: '{error_message}...'"
|
||||
),
|
||||
balance_msat=0,
|
||||
balance=0,
|
||||
)
|
||||
|
||||
data = r.json()
|
||||
if len(data) == 0:
|
||||
return StatusResponse(error_message="no data", balance_msat=0)
|
||||
return StatusResponse(error_message="no data", balance=0)
|
||||
balance_msat = int(data.get("localBalance") * 1000)
|
||||
return StatusResponse(error_message=None, balance_msat=balance_msat)
|
||||
return StatusResponse(error_message=None, balance=balance_msat)
|
||||
|
||||
async def create_invoice(
|
||||
self,
|
||||
amount: int,
|
||||
amount: Amount,
|
||||
memo: Optional[str] = None,
|
||||
description_hash: Optional[bytes] = None,
|
||||
unhashed_description: Optional[bytes] = None,
|
||||
**kwargs,
|
||||
) -> InvoiceResponse:
|
||||
self.assert_unit_supported(amount.unit)
|
||||
label = f"lbl{random.random()}"
|
||||
data: Dict = {
|
||||
"amount": amount * 1000,
|
||||
"amount": amount.to(Unit.msat, round="up").amount,
|
||||
"description": memo,
|
||||
"label": label,
|
||||
}
|
||||
@@ -139,14 +147,16 @@ class CoreLightningRestWallet(Wallet):
|
||||
error_message=None,
|
||||
)
|
||||
|
||||
async def pay_invoice(self, bolt11: str, fee_limit_msat: int) -> PaymentResponse:
|
||||
async def pay_invoice(
|
||||
self, quote: MeltQuote, fee_limit_msat: int
|
||||
) -> PaymentResponse:
|
||||
try:
|
||||
invoice = decode(bolt11)
|
||||
invoice = decode(quote.request)
|
||||
except Bolt11Exception as exc:
|
||||
return PaymentResponse(
|
||||
ok=False,
|
||||
checking_id=None,
|
||||
fee_msat=None,
|
||||
fee=None,
|
||||
preimage=None,
|
||||
error_message=str(exc),
|
||||
)
|
||||
@@ -156,7 +166,7 @@ class CoreLightningRestWallet(Wallet):
|
||||
return PaymentResponse(
|
||||
ok=False,
|
||||
checking_id=None,
|
||||
fee_msat=None,
|
||||
fee=None,
|
||||
preimage=None,
|
||||
error_message=error_message,
|
||||
)
|
||||
@@ -164,7 +174,7 @@ class CoreLightningRestWallet(Wallet):
|
||||
r = await self.client.post(
|
||||
f"{self.url}/v1/pay",
|
||||
data={
|
||||
"invoice": bolt11,
|
||||
"invoice": quote.request,
|
||||
"maxfeepercent": f"{fee_limit_percent:.11}",
|
||||
"exemptfee": 0, # so fee_limit_percent is applied even on payments
|
||||
# with fee < 5000 millisatoshi (which is default value of exemptfee)
|
||||
@@ -181,7 +191,7 @@ class CoreLightningRestWallet(Wallet):
|
||||
return PaymentResponse(
|
||||
ok=False,
|
||||
checking_id=None,
|
||||
fee_msat=None,
|
||||
fee=None,
|
||||
preimage=None,
|
||||
error_message=error_message,
|
||||
)
|
||||
@@ -192,7 +202,7 @@ class CoreLightningRestWallet(Wallet):
|
||||
return PaymentResponse(
|
||||
ok=False,
|
||||
checking_id=None,
|
||||
fee_msat=None,
|
||||
fee=None,
|
||||
preimage=None,
|
||||
error_message="payment failed",
|
||||
)
|
||||
@@ -204,7 +214,7 @@ class CoreLightningRestWallet(Wallet):
|
||||
return PaymentResponse(
|
||||
ok=self.statuses.get(data["status"]),
|
||||
checking_id=checking_id,
|
||||
fee_msat=fee_msat,
|
||||
fee=Amount(unit=Unit.msat, amount=fee_msat) if fee_msat else None,
|
||||
preimage=preimage,
|
||||
error_message=None,
|
||||
)
|
||||
@@ -249,7 +259,7 @@ class CoreLightningRestWallet(Wallet):
|
||||
|
||||
return PaymentStatus(
|
||||
paid=self.statuses.get(pay["status"]),
|
||||
fee_msat=fee_msat,
|
||||
fee=Amount(unit=Unit.msat, amount=fee_msat) if fee_msat else None,
|
||||
preimage=preimage,
|
||||
)
|
||||
except Exception as e:
|
||||
@@ -274,7 +284,6 @@ class CoreLightningRestWallet(Wallet):
|
||||
except Exception:
|
||||
continue
|
||||
logger.trace(f"paid invoice: {inv}")
|
||||
yield inv["label"]
|
||||
# NOTE: use payment_hash when corelightning-rest returns it
|
||||
# when using waitAnyInvoice
|
||||
# payment_hash = inv["payment_hash"]
|
||||
@@ -299,3 +308,14 @@ class CoreLightningRestWallet(Wallet):
|
||||
"reconnecting..."
|
||||
)
|
||||
await asyncio.sleep(0.02)
|
||||
|
||||
async def get_payment_quote(self, bolt11: str) -> PaymentQuoteResponse:
|
||||
invoice_obj = decode(bolt11)
|
||||
assert invoice_obj.amount_msat, "invoice has no amount."
|
||||
amount_msat = int(invoice_obj.amount_msat)
|
||||
fees_msat = fee_reserve(amount_msat)
|
||||
fees = Amount(unit=Unit.msat, amount=fees_msat)
|
||||
amount = Amount(unit=Unit.msat, amount=amount_msat)
|
||||
return PaymentQuoteResponse(
|
||||
checking_id=invoice_obj.payment_hash, fee=fees, amount=amount
|
||||
)
|
||||
|
||||
@@ -14,22 +14,21 @@ from bolt11 import (
|
||||
encode,
|
||||
)
|
||||
|
||||
from ..core.base import Amount, MeltQuote, Unit
|
||||
from ..core.helpers import fee_reserve
|
||||
from ..core.settings import settings
|
||||
from .base import (
|
||||
InvoiceResponse,
|
||||
LightningBackend,
|
||||
PaymentQuoteResponse,
|
||||
PaymentResponse,
|
||||
PaymentStatus,
|
||||
StatusResponse,
|
||||
Wallet,
|
||||
)
|
||||
|
||||
BRR = True
|
||||
DELAY_PAYMENT = False
|
||||
STOCHASTIC_INVOICE = False
|
||||
|
||||
|
||||
class FakeWallet(Wallet):
|
||||
"""https://github.com/lnbits/lnbits"""
|
||||
|
||||
class FakeWallet(LightningBackend):
|
||||
units = set([Unit.sat, Unit.msat])
|
||||
queue: asyncio.Queue[Bolt11] = asyncio.Queue(0)
|
||||
payment_secrets: Dict[str, str] = dict()
|
||||
paid_invoices: Set[str] = set()
|
||||
@@ -43,17 +42,18 @@ class FakeWallet(Wallet):
|
||||
).hex()
|
||||
|
||||
async def status(self) -> StatusResponse:
|
||||
return StatusResponse(error_message=None, balance_msat=1337)
|
||||
return StatusResponse(error_message=None, balance=1337)
|
||||
|
||||
async def create_invoice(
|
||||
self,
|
||||
amount: int,
|
||||
amount: Amount,
|
||||
memo: Optional[str] = None,
|
||||
description_hash: Optional[bytes] = None,
|
||||
unhashed_description: Optional[bytes] = None,
|
||||
expiry: Optional[int] = None,
|
||||
payment_secret: Optional[bytes] = None,
|
||||
) -> InvoiceResponse:
|
||||
self.assert_unit_supported(amount.unit)
|
||||
tags = Tags()
|
||||
|
||||
if description_hash:
|
||||
@@ -83,7 +83,7 @@ class FakeWallet(Wallet):
|
||||
|
||||
bolt11 = Bolt11(
|
||||
currency="bc",
|
||||
amount_msat=MilliSatoshi(amount * 1000),
|
||||
amount_msat=MilliSatoshi(amount.to(Unit.msat, round="up").amount),
|
||||
date=int(datetime.now().timestamp()),
|
||||
tags=tags,
|
||||
)
|
||||
@@ -94,19 +94,19 @@ class FakeWallet(Wallet):
|
||||
ok=True, checking_id=payment_hash, payment_request=payment_request
|
||||
)
|
||||
|
||||
async def pay_invoice(self, bolt11: str, fee_limit_msat: int) -> PaymentResponse:
|
||||
invoice = decode(bolt11)
|
||||
async def pay_invoice(self, quote: MeltQuote, fee_limit: int) -> PaymentResponse:
|
||||
invoice = decode(quote.request)
|
||||
|
||||
if DELAY_PAYMENT:
|
||||
if settings.fakewallet_delay_payment:
|
||||
await asyncio.sleep(5)
|
||||
|
||||
if invoice.payment_hash in self.payment_secrets or BRR:
|
||||
if invoice.payment_hash in self.payment_secrets or settings.fakewallet_brr:
|
||||
await self.queue.put(invoice)
|
||||
self.paid_invoices.add(invoice.payment_hash)
|
||||
return PaymentResponse(
|
||||
ok=True,
|
||||
checking_id=invoice.payment_hash,
|
||||
fee_msat=0,
|
||||
fee=Amount(unit=Unit.msat, amount=0),
|
||||
preimage=self.payment_secrets.get(invoice.payment_hash) or "0" * 64,
|
||||
)
|
||||
else:
|
||||
@@ -115,10 +115,10 @@ class FakeWallet(Wallet):
|
||||
)
|
||||
|
||||
async def get_invoice_status(self, checking_id: str) -> PaymentStatus:
|
||||
if STOCHASTIC_INVOICE:
|
||||
if settings.fakewallet_stochastic_invoice:
|
||||
paid = random.random() > 0.7
|
||||
return PaymentStatus(paid=paid)
|
||||
paid = checking_id in self.paid_invoices or BRR
|
||||
paid = checking_id in self.paid_invoices or settings.fakewallet_brr
|
||||
return PaymentStatus(paid=paid or None)
|
||||
|
||||
async def get_payment_status(self, _: str) -> PaymentStatus:
|
||||
@@ -128,3 +128,20 @@ class FakeWallet(Wallet):
|
||||
while True:
|
||||
value: Bolt11 = await self.queue.get()
|
||||
yield value.payment_hash
|
||||
|
||||
# async def get_invoice_quote(self, bolt11: str) -> InvoiceQuoteResponse:
|
||||
# invoice_obj = decode(bolt11)
|
||||
# assert invoice_obj.amount_msat, "invoice has no amount."
|
||||
# amount = invoice_obj.amount_msat
|
||||
# return InvoiceQuoteResponse(checking_id="", amount=amount)
|
||||
|
||||
async def get_payment_quote(self, bolt11: str) -> PaymentQuoteResponse:
|
||||
invoice_obj = decode(bolt11)
|
||||
assert invoice_obj.amount_msat, "invoice has no amount."
|
||||
amount_msat = int(invoice_obj.amount_msat)
|
||||
fees_msat = fee_reserve(amount_msat)
|
||||
fees = Amount(unit=Unit.msat, amount=fees_msat)
|
||||
amount = Amount(unit=Unit.msat, amount=amount_msat)
|
||||
return PaymentQuoteResponse(
|
||||
checking_id=invoice_obj.payment_hash, fee=fees, amount=amount
|
||||
)
|
||||
|
||||
@@ -2,20 +2,28 @@
|
||||
from typing import Optional
|
||||
|
||||
import httpx
|
||||
from bolt11 import (
|
||||
decode,
|
||||
)
|
||||
|
||||
from ..core.base import Amount, MeltQuote, Unit
|
||||
from ..core.helpers import fee_reserve
|
||||
from ..core.settings import settings
|
||||
from .base import (
|
||||
InvoiceResponse,
|
||||
LightningBackend,
|
||||
PaymentQuoteResponse,
|
||||
PaymentResponse,
|
||||
PaymentStatus,
|
||||
StatusResponse,
|
||||
Wallet,
|
||||
)
|
||||
|
||||
|
||||
class LNbitsWallet(Wallet):
|
||||
class LNbitsWallet(LightningBackend):
|
||||
"""https://github.com/lnbits/lnbits"""
|
||||
|
||||
units = set([Unit.sat])
|
||||
|
||||
def __init__(self):
|
||||
self.endpoint = settings.mint_lnbits_endpoint
|
||||
self.client = httpx.AsyncClient(
|
||||
@@ -30,7 +38,7 @@ class LNbitsWallet(Wallet):
|
||||
except Exception as exc:
|
||||
return StatusResponse(
|
||||
error_message=f"Failed to connect to {self.endpoint} due to: {exc}",
|
||||
balance_msat=0,
|
||||
balance=0,
|
||||
)
|
||||
|
||||
try:
|
||||
@@ -40,23 +48,25 @@ class LNbitsWallet(Wallet):
|
||||
error_message=(
|
||||
f"Failed to connect to {self.endpoint}, got: '{r.text[:200]}...'"
|
||||
),
|
||||
balance_msat=0,
|
||||
balance=0,
|
||||
)
|
||||
if "detail" in data:
|
||||
return StatusResponse(
|
||||
error_message=f"LNbits error: {data['detail']}", balance_msat=0
|
||||
error_message=f"LNbits error: {data['detail']}", balance=0
|
||||
)
|
||||
|
||||
return StatusResponse(error_message=None, balance_msat=data["balance"])
|
||||
return StatusResponse(error_message=None, balance=data["balance"])
|
||||
|
||||
async def create_invoice(
|
||||
self,
|
||||
amount: int,
|
||||
amount: Amount,
|
||||
memo: Optional[str] = None,
|
||||
description_hash: Optional[bytes] = None,
|
||||
unhashed_description: Optional[bytes] = None,
|
||||
) -> InvoiceResponse:
|
||||
data = {"out": False, "amount": amount}
|
||||
self.assert_unit_supported(amount.unit)
|
||||
|
||||
data = {"out": False, "amount": amount.to(Unit.sat).amount}
|
||||
if description_hash:
|
||||
data["description_hash"] = description_hash.hex()
|
||||
if unhashed_description:
|
||||
@@ -83,11 +93,13 @@ class LNbitsWallet(Wallet):
|
||||
payment_request=payment_request,
|
||||
)
|
||||
|
||||
async def pay_invoice(self, bolt11: str, fee_limit_msat: int) -> PaymentResponse:
|
||||
async def pay_invoice(
|
||||
self, quote: MeltQuote, fee_limit_msat: int
|
||||
) -> PaymentResponse:
|
||||
try:
|
||||
r = await self.client.post(
|
||||
url=f"{self.endpoint}/api/v1/payments",
|
||||
json={"out": True, "bolt11": bolt11},
|
||||
json={"out": True, "bolt11": quote.request},
|
||||
timeout=None,
|
||||
)
|
||||
r.raise_for_status()
|
||||
@@ -107,7 +119,7 @@ class LNbitsWallet(Wallet):
|
||||
return PaymentResponse(
|
||||
ok=True,
|
||||
checking_id=checking_id,
|
||||
fee_msat=payment.fee_msat,
|
||||
fee=payment.fee,
|
||||
preimage=payment.preimage,
|
||||
)
|
||||
|
||||
@@ -138,6 +150,17 @@ class LNbitsWallet(Wallet):
|
||||
|
||||
return PaymentStatus(
|
||||
paid=data["paid"],
|
||||
fee_msat=data["details"]["fee"],
|
||||
fee=Amount(unit=Unit.msat, amount=abs(data["details"]["fee"])),
|
||||
preimage=data["preimage"],
|
||||
)
|
||||
|
||||
async def get_payment_quote(self, bolt11: str) -> PaymentQuoteResponse:
|
||||
invoice_obj = decode(bolt11)
|
||||
assert invoice_obj.amount_msat, "invoice has no amount."
|
||||
amount_msat = int(invoice_obj.amount_msat)
|
||||
fees_msat = fee_reserve(amount_msat)
|
||||
fees = Amount(unit=Unit.msat, amount=fees_msat)
|
||||
amount = Amount(unit=Unit.msat, amount=amount_msat)
|
||||
return PaymentQuoteResponse(
|
||||
checking_id=invoice_obj.payment_hash, fee=fees, amount=amount
|
||||
)
|
||||
|
||||
@@ -5,22 +5,30 @@ import json
|
||||
from typing import AsyncGenerator, Dict, Optional
|
||||
|
||||
import httpx
|
||||
from bolt11 import (
|
||||
decode,
|
||||
)
|
||||
from loguru import logger
|
||||
|
||||
from ..core.base import Amount, MeltQuote, Unit
|
||||
from ..core.helpers import fee_reserve
|
||||
from ..core.settings import settings
|
||||
from .base import (
|
||||
InvoiceResponse,
|
||||
LightningBackend,
|
||||
PaymentQuoteResponse,
|
||||
PaymentResponse,
|
||||
PaymentStatus,
|
||||
StatusResponse,
|
||||
Wallet,
|
||||
)
|
||||
from .macaroon import load_macaroon
|
||||
|
||||
|
||||
class LndRestWallet(Wallet):
|
||||
class LndRestWallet(LightningBackend):
|
||||
"""https://api.lightning.community/rest/index.html#lnd-rest-api-reference"""
|
||||
|
||||
units = set([Unit.sat, Unit.msat])
|
||||
|
||||
def __init__(self):
|
||||
endpoint = settings.mint_lnd_rest_endpoint
|
||||
cert = settings.mint_lnd_rest_cert
|
||||
@@ -67,7 +75,7 @@ class LndRestWallet(Wallet):
|
||||
except (httpx.ConnectError, httpx.RequestError) as exc:
|
||||
return StatusResponse(
|
||||
error_message=f"Unable to connect to {self.endpoint}. {exc}",
|
||||
balance_msat=0,
|
||||
balance=0,
|
||||
)
|
||||
|
||||
try:
|
||||
@@ -75,21 +83,24 @@ class LndRestWallet(Wallet):
|
||||
if r.is_error:
|
||||
raise Exception
|
||||
except Exception:
|
||||
return StatusResponse(error_message=r.text[:200], balance_msat=0)
|
||||
return StatusResponse(error_message=r.text[:200], balance=0)
|
||||
|
||||
return StatusResponse(
|
||||
error_message=None, balance_msat=int(data["balance"]) * 1000
|
||||
)
|
||||
return StatusResponse(error_message=None, balance=int(data["balance"]) * 1000)
|
||||
|
||||
async def create_invoice(
|
||||
self,
|
||||
amount: int,
|
||||
amount: Amount,
|
||||
memo: Optional[str] = None,
|
||||
description_hash: Optional[bytes] = None,
|
||||
unhashed_description: Optional[bytes] = None,
|
||||
**kwargs,
|
||||
) -> InvoiceResponse:
|
||||
data: Dict = {"value": amount, "private": True, "memo": memo or ""}
|
||||
self.assert_unit_supported(amount.unit)
|
||||
data: Dict = {
|
||||
"value": amount.to(Unit.sat).amount,
|
||||
"private": True,
|
||||
"memo": memo or "",
|
||||
}
|
||||
if kwargs.get("expiry"):
|
||||
data["expiry"] = kwargs["expiry"]
|
||||
if description_hash:
|
||||
@@ -101,7 +112,10 @@ class LndRestWallet(Wallet):
|
||||
hashlib.sha256(unhashed_description).digest()
|
||||
).decode("ascii")
|
||||
|
||||
try:
|
||||
r = await self.client.post(url="/v1/invoices", json=data)
|
||||
except Exception as e:
|
||||
raise Exception(f"failed to create invoice: {e}")
|
||||
|
||||
if r.is_error:
|
||||
error_message = r.text
|
||||
@@ -128,14 +142,16 @@ class LndRestWallet(Wallet):
|
||||
error_message=None,
|
||||
)
|
||||
|
||||
async def pay_invoice(self, bolt11: str, fee_limit_msat: int) -> PaymentResponse:
|
||||
async def pay_invoice(
|
||||
self, quote: MeltQuote, fee_limit_msat: int
|
||||
) -> PaymentResponse:
|
||||
# set the fee limit for the payment
|
||||
lnrpcFeeLimit = dict()
|
||||
lnrpcFeeLimit["fixed_msat"] = f"{fee_limit_msat}"
|
||||
|
||||
r = await self.client.post(
|
||||
url="/v1/channels/transactions",
|
||||
json={"payment_request": bolt11, "fee_limit": lnrpcFeeLimit},
|
||||
json={"payment_request": quote.request, "fee_limit": lnrpcFeeLimit},
|
||||
timeout=None,
|
||||
)
|
||||
|
||||
@@ -144,7 +160,7 @@ class LndRestWallet(Wallet):
|
||||
return PaymentResponse(
|
||||
ok=False,
|
||||
checking_id=None,
|
||||
fee_msat=None,
|
||||
fee=None,
|
||||
preimage=None,
|
||||
error_message=error_message,
|
||||
)
|
||||
@@ -156,7 +172,7 @@ class LndRestWallet(Wallet):
|
||||
return PaymentResponse(
|
||||
ok=True,
|
||||
checking_id=checking_id,
|
||||
fee_msat=fee_msat,
|
||||
fee=Amount(unit=Unit.msat, amount=fee_msat) if fee_msat else None,
|
||||
preimage=preimage,
|
||||
error_message=None,
|
||||
)
|
||||
@@ -209,7 +225,11 @@ class LndRestWallet(Wallet):
|
||||
if payment is not None and payment.get("status"):
|
||||
return PaymentStatus(
|
||||
paid=statuses[payment["status"]],
|
||||
fee_msat=payment.get("fee_msat"),
|
||||
fee=(
|
||||
Amount(unit=Unit.msat, amount=payment.get("fee_msat"))
|
||||
if payment.get("fee_msat")
|
||||
else None
|
||||
),
|
||||
preimage=payment.get("payment_preimage"),
|
||||
)
|
||||
else:
|
||||
@@ -240,3 +260,14 @@ class LndRestWallet(Wallet):
|
||||
" seconds"
|
||||
)
|
||||
await asyncio.sleep(5)
|
||||
|
||||
async def get_payment_quote(self, bolt11: str) -> PaymentQuoteResponse:
|
||||
invoice_obj = decode(bolt11)
|
||||
assert invoice_obj.amount_msat, "invoice has no amount."
|
||||
amount_msat = int(invoice_obj.amount_msat)
|
||||
fees_msat = fee_reserve(amount_msat)
|
||||
fees = Amount(unit=Unit.msat, amount=fees_msat)
|
||||
amount = Amount(unit=Unit.msat, amount=amount_msat)
|
||||
return PaymentQuoteResponse(
|
||||
checking_id=invoice_obj.payment_hash, fee=fees, amount=amount
|
||||
)
|
||||
|
||||
219
cashu/lightning/strike.py
Normal file
219
cashu/lightning/strike.py
Normal file
@@ -0,0 +1,219 @@
|
||||
# type: ignore
|
||||
import secrets
|
||||
from typing import Dict, Optional
|
||||
|
||||
import httpx
|
||||
|
||||
from ..core.base import Amount, MeltQuote, Unit
|
||||
from ..core.settings import settings
|
||||
from .base import (
|
||||
InvoiceResponse,
|
||||
LightningBackend,
|
||||
PaymentQuoteResponse,
|
||||
PaymentResponse,
|
||||
PaymentStatus,
|
||||
StatusResponse,
|
||||
)
|
||||
|
||||
|
||||
class StrikeUSDWallet(LightningBackend):
|
||||
"""https://github.com/lnbits/lnbits"""
|
||||
|
||||
units = [Unit.usd]
|
||||
|
||||
def __init__(self):
|
||||
self.endpoint = "https://api.strike.me"
|
||||
|
||||
# bearer auth with settings.mint_strike_key
|
||||
bearer_auth = {
|
||||
"Authorization": f"Bearer {settings.mint_strike_key}",
|
||||
}
|
||||
self.client = httpx.AsyncClient(
|
||||
verify=not settings.debug,
|
||||
headers=bearer_auth,
|
||||
)
|
||||
|
||||
async def status(self) -> StatusResponse:
|
||||
try:
|
||||
r = await self.client.get(url=f"{self.endpoint}/v1/balances", timeout=15)
|
||||
r.raise_for_status()
|
||||
except Exception as exc:
|
||||
return StatusResponse(
|
||||
error_message=f"Failed to connect to {self.endpoint} due to: {exc}",
|
||||
balance=0,
|
||||
)
|
||||
|
||||
try:
|
||||
data = r.json()
|
||||
except Exception:
|
||||
return StatusResponse(
|
||||
error_message=(
|
||||
f"Failed to connect to {self.endpoint}, got: '{r.text[:200]}...'"
|
||||
),
|
||||
balance=0,
|
||||
)
|
||||
|
||||
for balance in data:
|
||||
if balance["currency"] == "USD":
|
||||
return StatusResponse(error_message=None, balance=balance["total"])
|
||||
|
||||
async def create_invoice(
|
||||
self,
|
||||
amount: Amount,
|
||||
memo: Optional[str] = None,
|
||||
description_hash: Optional[bytes] = None,
|
||||
unhashed_description: Optional[bytes] = None,
|
||||
) -> InvoiceResponse:
|
||||
self.assert_unit_supported(amount.unit)
|
||||
|
||||
data: Dict = {"out": False, "amount": amount}
|
||||
if description_hash:
|
||||
data["description_hash"] = description_hash.hex()
|
||||
if unhashed_description:
|
||||
data["unhashed_description"] = unhashed_description.hex()
|
||||
|
||||
data["memo"] = memo or ""
|
||||
payload = {
|
||||
"correlationId": secrets.token_hex(16),
|
||||
"description": "Invoice for order 123",
|
||||
"amount": {"amount": str(amount.amount / 100), "currency": "USD"},
|
||||
}
|
||||
try:
|
||||
r = await self.client.post(url=f"{self.endpoint}/v1/invoices", json=payload)
|
||||
r.raise_for_status()
|
||||
except Exception:
|
||||
return InvoiceResponse(
|
||||
paid=False,
|
||||
checking_id=None,
|
||||
payment_request=None,
|
||||
error_message=r.json()["detail"],
|
||||
)
|
||||
|
||||
quote = r.json()
|
||||
invoice_id = quote.get("invoiceId")
|
||||
|
||||
try:
|
||||
payload = {"descriptionHash": secrets.token_hex(32)}
|
||||
r2 = await self.client.post(
|
||||
f"{self.endpoint}/v1/invoices/{invoice_id}/quote", json=payload
|
||||
)
|
||||
except Exception:
|
||||
return InvoiceResponse(
|
||||
paid=False,
|
||||
checking_id=None,
|
||||
payment_request=None,
|
||||
error_message=r.json()["detail"],
|
||||
)
|
||||
|
||||
data2 = r2.json()
|
||||
payment_request = data2.get("lnInvoice")
|
||||
assert payment_request, "Did not receive an invoice"
|
||||
checking_id = invoice_id
|
||||
return InvoiceResponse(
|
||||
ok=True,
|
||||
checking_id=checking_id,
|
||||
payment_request=payment_request,
|
||||
error_message=None,
|
||||
)
|
||||
|
||||
async def get_payment_quote(self, bolt11: str) -> PaymentQuoteResponse:
|
||||
try:
|
||||
r = await self.client.post(
|
||||
url=f"{self.endpoint}/v1/payment-quotes/lightning",
|
||||
json={"sourceCurrency": "USD", "lnInvoice": bolt11},
|
||||
timeout=None,
|
||||
)
|
||||
r.raise_for_status()
|
||||
except Exception:
|
||||
error_message = r.json()["data"]["message"]
|
||||
raise Exception(error_message)
|
||||
data = r.json()
|
||||
|
||||
amount_cent = int(float(data.get("amount").get("amount")) * 100)
|
||||
quote = PaymentQuoteResponse(
|
||||
amount=Amount(Unit.usd, amount=amount_cent),
|
||||
checking_id=data.get("paymentQuoteId"),
|
||||
fee=Amount(Unit.usd, 0),
|
||||
)
|
||||
return quote
|
||||
|
||||
async def pay_invoice(
|
||||
self, quote: MeltQuote, fee_limit_msat: int
|
||||
) -> PaymentResponse:
|
||||
# we need to get the checking_id of this quote
|
||||
try:
|
||||
r = await self.client.patch(
|
||||
url=f"{self.endpoint}/v1/payment-quotes/{quote.checking_id}/execute",
|
||||
timeout=None,
|
||||
)
|
||||
r.raise_for_status()
|
||||
except Exception:
|
||||
error_message = r.json()["data"]["message"]
|
||||
return PaymentResponse(
|
||||
ok=None,
|
||||
checking_id=None,
|
||||
fee=None,
|
||||
preimage=None,
|
||||
error_message=error_message,
|
||||
)
|
||||
|
||||
data = r.json()
|
||||
states = {"PENDING": None, "COMPLETED": True, "FAILED": False}
|
||||
if states[data.get("state")]:
|
||||
return PaymentResponse(
|
||||
ok=True, checking_id=None, fee=None, preimage=None, error_message=None
|
||||
)
|
||||
else:
|
||||
return PaymentResponse(
|
||||
ok=False, checking_id=None, fee=None, preimage=None, error_message=None
|
||||
)
|
||||
|
||||
async def get_invoice_status(self, checking_id: str) -> PaymentStatus:
|
||||
try:
|
||||
r = await self.client.get(url=f"{self.endpoint}/v1/invoices/{checking_id}")
|
||||
r.raise_for_status()
|
||||
except Exception:
|
||||
return PaymentStatus(paid=None)
|
||||
data = r.json()
|
||||
states = {"PENDING": None, "UNPAID": None, "PAID": True, "CANCELLED": False}
|
||||
return PaymentStatus(paid=states[data["state"]])
|
||||
|
||||
async def get_payment_status(self, checking_id: str) -> PaymentStatus:
|
||||
try:
|
||||
r = await self.client.get(url=f"{self.endpoint}/v1/payments/{checking_id}")
|
||||
r.raise_for_status()
|
||||
except Exception:
|
||||
return PaymentStatus(paid=None)
|
||||
data = r.json()
|
||||
if "paid" not in data and "details" not in data:
|
||||
return PaymentStatus(paid=None)
|
||||
|
||||
return PaymentStatus(
|
||||
paid=data["paid"],
|
||||
fee_msat=data["details"]["fee"],
|
||||
preimage=data["preimage"],
|
||||
)
|
||||
|
||||
# async def paid_invoices_stream(self) -> AsyncGenerator[str, None]:
|
||||
# url = f"{self.endpoint}/api/v1/payments/sse"
|
||||
|
||||
# while True:
|
||||
# try:
|
||||
# async with requests.stream("GET", url) as r:
|
||||
# async for line in r.aiter_lines():
|
||||
# if line.startswith("data:"):
|
||||
# try:
|
||||
# data = json.loads(line[5:])
|
||||
# except json.decoder.JSONDecodeError:
|
||||
# continue
|
||||
|
||||
# if type(data) is not dict:
|
||||
# continue
|
||||
|
||||
# yield data["payment_hash"] # payment_hash
|
||||
|
||||
# except:
|
||||
# pass
|
||||
|
||||
# print("lost connection to lnbits /payments/sse, retrying in 5 seconds")
|
||||
# await asyncio.sleep(5)
|
||||
@@ -1,4 +1,3 @@
|
||||
import logging
|
||||
import sys
|
||||
from traceback import print_exception
|
||||
|
||||
@@ -14,8 +13,10 @@ from starlette.middleware.cors import CORSMiddleware
|
||||
from starlette.requests import Request
|
||||
|
||||
from ..core.errors import CashuError
|
||||
from ..core.logging import configure_logger
|
||||
from ..core.settings import settings
|
||||
from .router import router
|
||||
from .router_deprecated import router_deprecated
|
||||
from .startup import start_mint_init
|
||||
|
||||
if settings.debug_profiling:
|
||||
@@ -37,48 +38,6 @@ if settings.debug_profiling:
|
||||
|
||||
|
||||
def create_app(config_object="core.settings") -> FastAPI:
|
||||
def configure_logger() -> None:
|
||||
class Formatter:
|
||||
def __init__(self):
|
||||
self.padding = 0
|
||||
self.minimal_fmt = (
|
||||
"<green>{time:YYYY-MM-DD HH:mm:ss.SS}</green> |"
|
||||
" <level>{level}</level> | <level>{message}</level>\n"
|
||||
)
|
||||
if settings.debug:
|
||||
self.fmt = (
|
||||
"<green>{time:YYYY-MM-DD HH:mm:ss.SS}</green> | <level>{level:"
|
||||
" <4}</level> |"
|
||||
" <cyan>{name}</cyan>:<cyan>{function}</cyan>:<cyan>{line}</cyan>"
|
||||
" | <level>{message}</level>\n"
|
||||
)
|
||||
else:
|
||||
self.fmt = self.minimal_fmt
|
||||
|
||||
def format(self, record):
|
||||
function = "{function}".format(**record)
|
||||
if function == "emit": # uvicorn logs
|
||||
return self.minimal_fmt
|
||||
return self.fmt
|
||||
|
||||
class InterceptHandler(logging.Handler):
|
||||
def emit(self, record):
|
||||
try:
|
||||
level = logger.level(record.levelname).name
|
||||
except ValueError:
|
||||
level = record.levelno
|
||||
logger.log(level, record.getMessage())
|
||||
|
||||
logger.remove()
|
||||
log_level = settings.log_level
|
||||
if settings.debug and log_level == "INFO":
|
||||
log_level = "DEBUG"
|
||||
formatter = Formatter()
|
||||
logger.add(sys.stderr, level=log_level, format=formatter.format)
|
||||
|
||||
logging.getLogger("uvicorn").handlers = [InterceptHandler()]
|
||||
logging.getLogger("uvicorn.access").handlers = [InterceptHandler()]
|
||||
|
||||
configure_logger()
|
||||
|
||||
# middleware = [
|
||||
@@ -99,8 +58,8 @@ def create_app(config_object="core.settings") -> FastAPI:
|
||||
]
|
||||
|
||||
app = FastAPI(
|
||||
title="Cashu Python Mint",
|
||||
description="Ecash wallet and mint for Bitcoin",
|
||||
title="Nutshell Cashu Mint",
|
||||
description="Ecash wallet and mint based on the Cashu protocol.",
|
||||
version=settings.version,
|
||||
license_info={
|
||||
"name": "MIT License",
|
||||
@@ -176,5 +135,10 @@ async def startup_mint():
|
||||
await start_mint_init()
|
||||
|
||||
|
||||
app.include_router(router=router)
|
||||
if settings.debug_mint_only_deprecated:
|
||||
app.include_router(router=router_deprecated, tags=["Deprecated"], deprecated=True)
|
||||
else:
|
||||
app.include_router(router=router, tags=["Mint"])
|
||||
app.include_router(router=router_deprecated, tags=["Deprecated"], deprecated=True)
|
||||
|
||||
app.add_exception_handler(RequestValidationError, request_validation_exception_handler)
|
||||
|
||||
@@ -255,9 +255,9 @@ class LedgerSpendingConditions:
|
||||
# check if all secrets are P2PK
|
||||
# NOTE: This is redundant, because P2PKSecret.from_secret() already checks for the kind
|
||||
# Leaving it in for explicitness
|
||||
if not all(
|
||||
[SecretKind(secret.kind) == SecretKind.P2PK for secret in p2pk_secrets]
|
||||
):
|
||||
if not all([
|
||||
SecretKind(secret.kind) == SecretKind.P2PK for secret in p2pk_secrets
|
||||
]):
|
||||
# not all secrets are P2PK
|
||||
return True
|
||||
|
||||
|
||||
@@ -1,10 +1,18 @@
|
||||
import time
|
||||
from abc import ABC, abstractmethod
|
||||
from typing import Any, List, Optional
|
||||
|
||||
from ..core.base import BlindedSignature, Invoice, MintKeyset, Proof
|
||||
from ..core.base import (
|
||||
BlindedSignature,
|
||||
MeltQuote,
|
||||
MintKeyset,
|
||||
MintQuote,
|
||||
Proof,
|
||||
)
|
||||
from ..core.db import Connection, Database, table_with_schema
|
||||
|
||||
|
||||
class LedgerCrud:
|
||||
class LedgerCrud(ABC):
|
||||
"""
|
||||
Database interface for Cashu mint.
|
||||
|
||||
@@ -12,179 +20,82 @@ class LedgerCrud:
|
||||
to use their own database.
|
||||
"""
|
||||
|
||||
@abstractmethod
|
||||
async def get_keyset(
|
||||
self,
|
||||
*,
|
||||
db: Database,
|
||||
id: str = "",
|
||||
derivation_path: str = "",
|
||||
conn: Optional[Connection] = None,
|
||||
):
|
||||
return await get_keyset(
|
||||
db=db,
|
||||
id=id,
|
||||
derivation_path=derivation_path,
|
||||
conn=conn,
|
||||
)
|
||||
) -> List[MintKeyset]: ...
|
||||
|
||||
async def get_lightning_invoice(
|
||||
self,
|
||||
db: Database,
|
||||
id: str,
|
||||
conn: Optional[Connection] = None,
|
||||
) -> Optional[Invoice]:
|
||||
return await get_lightning_invoice(
|
||||
db=db,
|
||||
id=id,
|
||||
conn=conn,
|
||||
)
|
||||
|
||||
async def get_secrets_used(
|
||||
self,
|
||||
db: Database,
|
||||
conn: Optional[Connection] = None,
|
||||
) -> List[str]:
|
||||
return await get_secrets_used(
|
||||
db=db,
|
||||
conn=conn,
|
||||
)
|
||||
|
||||
async def get_proof_used(
|
||||
self,
|
||||
db: Database,
|
||||
proof: Proof,
|
||||
conn: Optional[Connection] = None,
|
||||
) -> Optional[Proof]:
|
||||
return await get_proof_used(
|
||||
db=db,
|
||||
proof=proof,
|
||||
conn=conn,
|
||||
)
|
||||
|
||||
async def invalidate_proof(
|
||||
self,
|
||||
db: Database,
|
||||
proof: Proof,
|
||||
conn: Optional[Connection] = None,
|
||||
):
|
||||
return await invalidate_proof(
|
||||
db=db,
|
||||
proof=proof,
|
||||
conn=conn,
|
||||
)
|
||||
|
||||
async def get_proofs_pending(
|
||||
self,
|
||||
db: Database,
|
||||
conn: Optional[Connection] = None,
|
||||
):
|
||||
return await get_proofs_pending(db=db, conn=conn)
|
||||
|
||||
async def set_proof_pending(
|
||||
self,
|
||||
db: Database,
|
||||
proof: Proof,
|
||||
conn: Optional[Connection] = None,
|
||||
):
|
||||
return await set_proof_pending(
|
||||
db=db,
|
||||
proof=proof,
|
||||
conn=conn,
|
||||
)
|
||||
|
||||
async def unset_proof_pending(
|
||||
self, proof: Proof, db: Database, conn: Optional[Connection] = None
|
||||
):
|
||||
return await unset_proof_pending(
|
||||
proof=proof,
|
||||
db=db,
|
||||
conn=conn,
|
||||
)
|
||||
|
||||
async def store_keyset(
|
||||
self,
|
||||
db: Database,
|
||||
keyset: MintKeyset,
|
||||
conn: Optional[Connection] = None,
|
||||
):
|
||||
return await store_keyset(
|
||||
db=db,
|
||||
keyset=keyset,
|
||||
conn=conn,
|
||||
)
|
||||
|
||||
async def store_lightning_invoice(
|
||||
self,
|
||||
db: Database,
|
||||
invoice: Invoice,
|
||||
conn: Optional[Connection] = None,
|
||||
):
|
||||
return await store_lightning_invoice(
|
||||
db=db,
|
||||
invoice=invoice,
|
||||
conn=conn,
|
||||
)
|
||||
|
||||
async def store_promise(
|
||||
@abstractmethod
|
||||
async def get_spent_proofs(
|
||||
self,
|
||||
*,
|
||||
db: Database,
|
||||
amount: int,
|
||||
B_: str,
|
||||
C_: str,
|
||||
id: str,
|
||||
e: str = "",
|
||||
s: str = "",
|
||||
conn: Optional[Connection] = None,
|
||||
):
|
||||
return await store_promise(
|
||||
db=db,
|
||||
amount=amount,
|
||||
B_=B_,
|
||||
C_=C_,
|
||||
id=id,
|
||||
e=e,
|
||||
s=s,
|
||||
conn=conn,
|
||||
)
|
||||
) -> List[Proof]: ...
|
||||
|
||||
async def get_promise(
|
||||
async def get_proof_used(
|
||||
self,
|
||||
*,
|
||||
db: Database,
|
||||
B_: str,
|
||||
secret: str,
|
||||
conn: Optional[Connection] = None,
|
||||
):
|
||||
return await get_promise(
|
||||
db=db,
|
||||
B_=B_,
|
||||
conn=conn,
|
||||
)
|
||||
) -> Optional[Proof]: ...
|
||||
|
||||
async def update_lightning_invoice(
|
||||
@abstractmethod
|
||||
async def invalidate_proof(
|
||||
self,
|
||||
*,
|
||||
db: Database,
|
||||
id: str,
|
||||
issued: bool,
|
||||
proof: Proof,
|
||||
conn: Optional[Connection] = None,
|
||||
):
|
||||
return await update_lightning_invoice(
|
||||
db=db,
|
||||
id=id,
|
||||
issued=issued,
|
||||
conn=conn,
|
||||
)
|
||||
) -> None: ...
|
||||
|
||||
@abstractmethod
|
||||
async def get_proofs_pending(
|
||||
self,
|
||||
*,
|
||||
db: Database,
|
||||
conn: Optional[Connection] = None,
|
||||
) -> List[Proof]: ...
|
||||
|
||||
@abstractmethod
|
||||
async def set_proof_pending(
|
||||
self,
|
||||
*,
|
||||
db: Database,
|
||||
proof: Proof,
|
||||
conn: Optional[Connection] = None,
|
||||
) -> None: ...
|
||||
|
||||
@abstractmethod
|
||||
async def unset_proof_pending(
|
||||
self, *, proof: Proof, db: Database, conn: Optional[Connection] = None
|
||||
) -> None: ...
|
||||
|
||||
@abstractmethod
|
||||
async def store_keyset(
|
||||
self,
|
||||
*,
|
||||
db: Database,
|
||||
keyset: MintKeyset,
|
||||
conn: Optional[Connection] = None,
|
||||
) -> None: ...
|
||||
|
||||
@abstractmethod
|
||||
async def get_balance(
|
||||
self,
|
||||
db: Database,
|
||||
conn: Optional[Connection] = None,
|
||||
) -> int:
|
||||
return await get_balance(
|
||||
db=db,
|
||||
conn=conn,
|
||||
)
|
||||
|
||||
) -> int: ...
|
||||
|
||||
@abstractmethod
|
||||
async def store_promise(
|
||||
self,
|
||||
*,
|
||||
db: Database,
|
||||
amount: int,
|
||||
@@ -194,12 +105,116 @@ async def store_promise(
|
||||
e: str = "",
|
||||
s: str = "",
|
||||
conn: Optional[Connection] = None,
|
||||
):
|
||||
) -> None: ...
|
||||
|
||||
@abstractmethod
|
||||
async def get_promise(
|
||||
self,
|
||||
*,
|
||||
db: Database,
|
||||
B_: str,
|
||||
conn: Optional[Connection] = None,
|
||||
) -> Optional[BlindedSignature]: ...
|
||||
|
||||
@abstractmethod
|
||||
async def store_mint_quote(
|
||||
self,
|
||||
*,
|
||||
quote: MintQuote,
|
||||
db: Database,
|
||||
conn: Optional[Connection] = None,
|
||||
) -> None: ...
|
||||
|
||||
@abstractmethod
|
||||
async def get_mint_quote(
|
||||
self,
|
||||
*,
|
||||
quote_id: str,
|
||||
db: Database,
|
||||
conn: Optional[Connection] = None,
|
||||
) -> Optional[MintQuote]: ...
|
||||
|
||||
@abstractmethod
|
||||
async def get_mint_quote_by_checking_id(
|
||||
self,
|
||||
*,
|
||||
checking_id: str,
|
||||
db: Database,
|
||||
conn: Optional[Connection] = None,
|
||||
) -> Optional[MintQuote]: ...
|
||||
|
||||
@abstractmethod
|
||||
async def update_mint_quote(
|
||||
self,
|
||||
*,
|
||||
quote: MintQuote,
|
||||
db: Database,
|
||||
conn: Optional[Connection] = None,
|
||||
) -> None: ...
|
||||
|
||||
# @abstractmethod
|
||||
# async def update_mint_quote_paid(
|
||||
# self,
|
||||
# *,
|
||||
# quote_id: str,
|
||||
# paid: bool,
|
||||
# db: Database,
|
||||
# conn: Optional[Connection] = None,
|
||||
# ) -> None: ...
|
||||
|
||||
@abstractmethod
|
||||
async def store_melt_quote(
|
||||
self,
|
||||
*,
|
||||
quote: MeltQuote,
|
||||
db: Database,
|
||||
conn: Optional[Connection] = None,
|
||||
) -> None: ...
|
||||
|
||||
@abstractmethod
|
||||
async def get_melt_quote(
|
||||
self,
|
||||
*,
|
||||
quote_id: str,
|
||||
db: Database,
|
||||
checking_id: Optional[str] = None,
|
||||
conn: Optional[Connection] = None,
|
||||
) -> Optional[MeltQuote]: ...
|
||||
|
||||
@abstractmethod
|
||||
async def update_melt_quote(
|
||||
self,
|
||||
*,
|
||||
quote: MeltQuote,
|
||||
db: Database,
|
||||
conn: Optional[Connection] = None,
|
||||
) -> None: ...
|
||||
|
||||
|
||||
class LedgerCrudSqlite(LedgerCrud):
|
||||
"""Implementation of LedgerCrud for sqlite.
|
||||
|
||||
Args:
|
||||
LedgerCrud (ABC): Abstract base class for LedgerCrud.
|
||||
"""
|
||||
|
||||
async def store_promise(
|
||||
self,
|
||||
*,
|
||||
db: Database,
|
||||
amount: int,
|
||||
B_: str,
|
||||
C_: str,
|
||||
id: str,
|
||||
e: str = "",
|
||||
s: str = "",
|
||||
conn: Optional[Connection] = None,
|
||||
) -> None:
|
||||
await (conn or db).execute(
|
||||
f"""
|
||||
INSERT INTO {table_with_schema(db, 'promises')}
|
||||
(amount, B_b, C_b, e, s, id)
|
||||
VALUES (?, ?, ?, ?, ?, ?)
|
||||
(amount, B_b, C_b, e, s, id, created)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?)
|
||||
""",
|
||||
(
|
||||
amount,
|
||||
@@ -208,15 +223,17 @@ async def store_promise(
|
||||
e,
|
||||
s,
|
||||
id,
|
||||
int(time.time()),
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
async def get_promise(
|
||||
self,
|
||||
*,
|
||||
db: Database,
|
||||
B_: str,
|
||||
conn: Optional[Connection] = None,
|
||||
):
|
||||
) -> Optional[BlindedSignature]:
|
||||
row = await (conn or db).fetchone(
|
||||
f"""
|
||||
SELECT * from {table_with_schema(db, 'promises')}
|
||||
@@ -226,202 +243,326 @@ async def get_promise(
|
||||
)
|
||||
return BlindedSignature(amount=row[0], C_=row[2], id=row[3]) if row else None
|
||||
|
||||
|
||||
async def get_secrets_used(
|
||||
async def get_spent_proofs(
|
||||
self,
|
||||
*,
|
||||
db: Database,
|
||||
conn: Optional[Connection] = None,
|
||||
) -> List[str]:
|
||||
) -> List[Proof]:
|
||||
rows = await (conn or db).fetchall(f"""
|
||||
SELECT secret from {table_with_schema(db, 'proofs_used')}
|
||||
SELECT * from {table_with_schema(db, 'proofs_used')}
|
||||
""")
|
||||
return [row[0] for row in rows]
|
||||
|
||||
return [Proof(**r) for r in rows] if rows else []
|
||||
|
||||
async def invalidate_proof(
|
||||
self,
|
||||
*,
|
||||
db: Database,
|
||||
proof: Proof,
|
||||
conn: Optional[Connection] = None,
|
||||
):
|
||||
) -> None:
|
||||
# we add the proof and secret to the used list
|
||||
await (conn or db).execute(
|
||||
f"""
|
||||
INSERT INTO {table_with_schema(db, 'proofs_used')}
|
||||
(amount, C, secret, id)
|
||||
(amount, C, secret, id, witness, created)
|
||||
VALUES (?, ?, ?, ?, ?, ?)
|
||||
""",
|
||||
(
|
||||
proof.amount,
|
||||
proof.C,
|
||||
proof.secret,
|
||||
proof.id,
|
||||
proof.witness,
|
||||
int(time.time()),
|
||||
),
|
||||
)
|
||||
|
||||
async def get_proofs_pending(
|
||||
self,
|
||||
*,
|
||||
db: Database,
|
||||
conn: Optional[Connection] = None,
|
||||
) -> List[Proof]:
|
||||
rows = await (conn or db).fetchall(f"""
|
||||
SELECT * from {table_with_schema(db, 'proofs_pending')}
|
||||
""")
|
||||
return [Proof(**r) for r in rows]
|
||||
|
||||
async def set_proof_pending(
|
||||
self,
|
||||
*,
|
||||
db: Database,
|
||||
proof: Proof,
|
||||
conn: Optional[Connection] = None,
|
||||
) -> None:
|
||||
# we add the proof and secret to the used list
|
||||
await (conn or db).execute(
|
||||
f"""
|
||||
INSERT INTO {table_with_schema(db, 'proofs_pending')}
|
||||
(amount, C, secret, created)
|
||||
VALUES (?, ?, ?, ?)
|
||||
""",
|
||||
(
|
||||
proof.amount,
|
||||
str(proof.C),
|
||||
str(proof.secret),
|
||||
str(proof.id),
|
||||
int(time.time()),
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
async def get_proofs_pending(
|
||||
db: Database,
|
||||
conn: Optional[Connection] = None,
|
||||
):
|
||||
rows = await (conn or db).fetchall(f"""
|
||||
SELECT * from {table_with_schema(db, 'proofs_pending')}
|
||||
""")
|
||||
return [Proof(**r) for r in rows]
|
||||
|
||||
|
||||
async def get_proof_used(
|
||||
db: Database,
|
||||
proof: Proof,
|
||||
conn: Optional[Connection] = None,
|
||||
) -> Optional[Proof]:
|
||||
row = await (conn or db).fetchone(
|
||||
f"""
|
||||
SELECT 1 from {table_with_schema(db, 'proofs_used')}
|
||||
WHERE secret = ?
|
||||
""",
|
||||
(str(proof.secret),),
|
||||
)
|
||||
return Proof(**row) if row else None
|
||||
|
||||
|
||||
async def set_proof_pending(
|
||||
db: Database,
|
||||
proof: Proof,
|
||||
conn: Optional[Connection] = None,
|
||||
):
|
||||
# we add the proof and secret to the used list
|
||||
await (conn or db).execute(
|
||||
f"""
|
||||
INSERT INTO {table_with_schema(db, 'proofs_pending')}
|
||||
(amount, C, secret)
|
||||
VALUES (?, ?, ?)
|
||||
""",
|
||||
(
|
||||
proof.amount,
|
||||
str(proof.C),
|
||||
str(proof.secret),
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
async def unset_proof_pending(
|
||||
self,
|
||||
*,
|
||||
proof: Proof,
|
||||
db: Database,
|
||||
conn: Optional[Connection] = None,
|
||||
):
|
||||
) -> None:
|
||||
await (conn or db).execute(
|
||||
f"""
|
||||
DELETE FROM {table_with_schema(db, 'proofs_pending')}
|
||||
WHERE secret = ?
|
||||
""",
|
||||
(str(proof["secret"]),),
|
||||
(proof.secret,),
|
||||
)
|
||||
|
||||
|
||||
async def store_lightning_invoice(
|
||||
async def store_mint_quote(
|
||||
self,
|
||||
*,
|
||||
quote: MintQuote,
|
||||
db: Database,
|
||||
invoice: Invoice,
|
||||
conn: Optional[Connection] = None,
|
||||
):
|
||||
) -> None:
|
||||
await (conn or db).execute(
|
||||
f"""
|
||||
INSERT INTO {table_with_schema(db, 'invoices')}
|
||||
(amount, bolt11, id, issued, payment_hash, out)
|
||||
VALUES (?, ?, ?, ?, ?, ?)
|
||||
INSERT INTO {table_with_schema(db, 'mint_quotes')}
|
||||
(quote, method, request, checking_id, unit, amount, issued, paid, created_time, paid_time)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
""",
|
||||
(
|
||||
invoice.amount,
|
||||
invoice.bolt11,
|
||||
invoice.id,
|
||||
invoice.issued,
|
||||
invoice.payment_hash,
|
||||
invoice.out,
|
||||
quote.quote,
|
||||
quote.method,
|
||||
quote.request,
|
||||
quote.checking_id,
|
||||
quote.unit,
|
||||
quote.amount,
|
||||
quote.issued,
|
||||
quote.paid,
|
||||
quote.created_time,
|
||||
quote.paid_time,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
async def get_lightning_invoice(
|
||||
db: Database,
|
||||
async def get_mint_quote(
|
||||
self,
|
||||
*,
|
||||
id: Optional[str] = None,
|
||||
payment_hash: Optional[str] = None,
|
||||
quote_id: str,
|
||||
db: Database,
|
||||
conn: Optional[Connection] = None,
|
||||
):
|
||||
) -> Optional[MintQuote]:
|
||||
row = await (conn or db).fetchone(
|
||||
f"""
|
||||
SELECT * from {table_with_schema(db, 'mint_quotes')}
|
||||
WHERE quote = ?
|
||||
""",
|
||||
(quote_id,),
|
||||
)
|
||||
return MintQuote(**dict(row)) if row else None
|
||||
|
||||
async def get_mint_quote_by_checking_id(
|
||||
self,
|
||||
*,
|
||||
checking_id: str,
|
||||
db: Database,
|
||||
conn: Optional[Connection] = None,
|
||||
) -> Optional[MintQuote]:
|
||||
row = await (conn or db).fetchone(
|
||||
f"""
|
||||
SELECT * from {table_with_schema(db, 'mint_quotes')}
|
||||
WHERE checking_id = ?
|
||||
""",
|
||||
(checking_id,),
|
||||
)
|
||||
return MintQuote(**dict(row)) if row else None
|
||||
|
||||
async def update_mint_quote(
|
||||
self,
|
||||
*,
|
||||
quote: MintQuote,
|
||||
db: Database,
|
||||
conn: Optional[Connection] = None,
|
||||
) -> None:
|
||||
await (conn or db).execute(
|
||||
f"UPDATE {table_with_schema(db, 'mint_quotes')} SET issued = ?, paid = ?,"
|
||||
" paid_time = ? WHERE quote = ?",
|
||||
(
|
||||
quote.issued,
|
||||
quote.paid,
|
||||
quote.paid_time,
|
||||
quote.quote,
|
||||
),
|
||||
)
|
||||
|
||||
# async def update_mint_quote_paid(
|
||||
# self,
|
||||
# *,
|
||||
# quote_id: str,
|
||||
# paid: bool,
|
||||
# db: Database,
|
||||
# conn: Optional[Connection] = None,
|
||||
# ) -> None:
|
||||
# await (conn or db).execute(
|
||||
# f"UPDATE {table_with_schema(db, 'mint_quotes')} SET paid = ? WHERE"
|
||||
# " quote = ?",
|
||||
# (
|
||||
# paid,
|
||||
# quote_id,
|
||||
# ),
|
||||
# )
|
||||
|
||||
async def store_melt_quote(
|
||||
self,
|
||||
*,
|
||||
quote: MeltQuote,
|
||||
db: Database,
|
||||
conn: Optional[Connection] = None,
|
||||
) -> None:
|
||||
await (conn or db).execute(
|
||||
f"""
|
||||
INSERT INTO {table_with_schema(db, 'melt_quotes')}
|
||||
(quote, method, request, checking_id, unit, amount, fee_reserve, paid, created_time, paid_time, fee_paid, proof)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
""",
|
||||
(
|
||||
quote.quote,
|
||||
quote.method,
|
||||
quote.request,
|
||||
quote.checking_id,
|
||||
quote.unit,
|
||||
quote.amount,
|
||||
quote.fee_reserve or 0,
|
||||
quote.paid,
|
||||
quote.created_time,
|
||||
quote.paid_time,
|
||||
quote.fee_paid,
|
||||
quote.proof,
|
||||
),
|
||||
)
|
||||
|
||||
async def get_melt_quote(
|
||||
self,
|
||||
*,
|
||||
quote_id: str,
|
||||
db: Database,
|
||||
checking_id: Optional[str] = None,
|
||||
request: Optional[str] = None,
|
||||
conn: Optional[Connection] = None,
|
||||
) -> Optional[MeltQuote]:
|
||||
clauses = []
|
||||
values: List[Any] = []
|
||||
if id:
|
||||
clauses.append("id = ?")
|
||||
values.append(id)
|
||||
if payment_hash:
|
||||
clauses.append("payment_hash = ?")
|
||||
values.append(payment_hash)
|
||||
if quote_id:
|
||||
clauses.append("quote = ?")
|
||||
values.append(quote_id)
|
||||
if checking_id:
|
||||
clauses.append("checking_id = ?")
|
||||
values.append(checking_id)
|
||||
if request:
|
||||
clauses.append("request = ?")
|
||||
values.append(request)
|
||||
where = ""
|
||||
if clauses:
|
||||
where = f"WHERE {' AND '.join(clauses)}"
|
||||
row = await (conn or db).fetchone(
|
||||
f"""
|
||||
SELECT * from {table_with_schema(db, 'invoices')}
|
||||
SELECT * from {table_with_schema(db, 'melt_quotes')}
|
||||
{where}
|
||||
""",
|
||||
tuple(values),
|
||||
)
|
||||
row_dict = dict(row)
|
||||
return Invoice(**row_dict) if row_dict else None
|
||||
if row is None:
|
||||
return None
|
||||
return MeltQuote(**dict(row)) if row else None
|
||||
|
||||
|
||||
async def update_lightning_invoice(
|
||||
async def update_melt_quote(
|
||||
self,
|
||||
*,
|
||||
quote: MeltQuote,
|
||||
db: Database,
|
||||
id: str,
|
||||
issued: bool,
|
||||
conn: Optional[Connection] = None,
|
||||
):
|
||||
) -> None:
|
||||
await (conn or db).execute(
|
||||
f"UPDATE {table_with_schema(db, 'invoices')} SET issued = ? WHERE id = ?",
|
||||
f"UPDATE {table_with_schema(db, 'melt_quotes')} SET paid = ?, fee_paid = ?,"
|
||||
" paid_time = ?, proof = ? WHERE quote = ?",
|
||||
(
|
||||
issued,
|
||||
id,
|
||||
quote.paid,
|
||||
quote.fee_paid,
|
||||
quote.paid_time,
|
||||
quote.proof,
|
||||
quote.quote,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
async def store_keyset(
|
||||
self,
|
||||
*,
|
||||
db: Database,
|
||||
keyset: MintKeyset,
|
||||
conn: Optional[Connection] = None,
|
||||
):
|
||||
) -> None:
|
||||
await (conn or db).execute( # type: ignore
|
||||
f"""
|
||||
INSERT INTO {table_with_schema(db, 'keysets')}
|
||||
(id, derivation_path, valid_from, valid_to, first_seen, active, version)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?)
|
||||
(id, seed, derivation_path, valid_from, valid_to, first_seen, active, version, unit)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
""",
|
||||
(
|
||||
keyset.id,
|
||||
keyset.seed,
|
||||
keyset.derivation_path,
|
||||
keyset.valid_from or db.timestamp_now,
|
||||
keyset.valid_to or db.timestamp_now,
|
||||
keyset.first_seen or db.timestamp_now,
|
||||
keyset.valid_from or int(time.time()),
|
||||
keyset.valid_to or int(time.time()),
|
||||
keyset.first_seen or int(time.time()),
|
||||
True,
|
||||
keyset.version,
|
||||
keyset.unit.name,
|
||||
),
|
||||
)
|
||||
|
||||
async def get_balance(
|
||||
self,
|
||||
db: Database,
|
||||
conn: Optional[Connection] = None,
|
||||
) -> int:
|
||||
row = await (conn or db).fetchone(f"""
|
||||
SELECT * from {table_with_schema(db, 'balance')}
|
||||
""")
|
||||
assert row, "Balance not found"
|
||||
return int(row[0])
|
||||
|
||||
async def get_keyset(
|
||||
self,
|
||||
*,
|
||||
db: Database,
|
||||
id: str = "",
|
||||
derivation_path: str = "",
|
||||
id: Optional[str] = None,
|
||||
derivation_path: Optional[str] = None,
|
||||
unit: Optional[str] = None,
|
||||
active: Optional[bool] = None,
|
||||
conn: Optional[Connection] = None,
|
||||
):
|
||||
) -> List[MintKeyset]:
|
||||
clauses = []
|
||||
values: List[Any] = []
|
||||
if active is not None:
|
||||
clauses.append("active = ?")
|
||||
values.append(True)
|
||||
if id:
|
||||
values.append(active)
|
||||
if id is not None:
|
||||
clauses.append("id = ?")
|
||||
values.append(id)
|
||||
if derivation_path:
|
||||
if derivation_path is not None:
|
||||
clauses.append("derivation_path = ?")
|
||||
values.append(derivation_path)
|
||||
if unit is not None:
|
||||
clauses.append("unit = ?")
|
||||
values.append(unit)
|
||||
where = ""
|
||||
if clauses:
|
||||
where = f"WHERE {' AND '.join(clauses)}"
|
||||
@@ -435,13 +576,17 @@ async def get_keyset(
|
||||
)
|
||||
return [MintKeyset(**row) for row in rows]
|
||||
|
||||
|
||||
async def get_balance(
|
||||
async def get_proof_used(
|
||||
self,
|
||||
db: Database,
|
||||
secret: str,
|
||||
conn: Optional[Connection] = None,
|
||||
) -> int:
|
||||
row = await (conn or db).fetchone(f"""
|
||||
SELECT * from {table_with_schema(db, 'balance')}
|
||||
""")
|
||||
assert row, "Balance not found"
|
||||
return int(row[0])
|
||||
) -> Optional[Proof]:
|
||||
row = await (conn or db).fetchone(
|
||||
f"""
|
||||
SELECT * from {table_with_schema(db, 'proofs_used')}
|
||||
WHERE secret = ?
|
||||
""",
|
||||
(secret,),
|
||||
)
|
||||
return Proof(**row) if row else None
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,137 +0,0 @@
|
||||
from typing import Optional, Union
|
||||
|
||||
from loguru import logger
|
||||
|
||||
from ..core.base import (
|
||||
Invoice,
|
||||
)
|
||||
from ..core.db import Connection, Database
|
||||
from ..core.errors import (
|
||||
InvoiceNotPaidError,
|
||||
LightningError,
|
||||
)
|
||||
from ..lightning.base import InvoiceResponse, PaymentResponse, PaymentStatus, Wallet
|
||||
from ..mint.crud import LedgerCrud
|
||||
from .protocols import SupportLightning, SupportsDb
|
||||
|
||||
|
||||
class LedgerLightning(SupportLightning, SupportsDb):
|
||||
"""Lightning functions for the ledger."""
|
||||
|
||||
lightning: Wallet
|
||||
crud: LedgerCrud
|
||||
db: Database
|
||||
|
||||
async def _request_lightning_invoice(self, amount: int) -> InvoiceResponse:
|
||||
"""Generate a Lightning invoice using the funding source backend.
|
||||
|
||||
Args:
|
||||
amount (int): Amount of invoice (in Satoshis)
|
||||
|
||||
Raises:
|
||||
Exception: Error with funding source.
|
||||
|
||||
Returns:
|
||||
Tuple[str, str]: Bolt11 invoice and payment id (for lookup)
|
||||
"""
|
||||
logger.trace(
|
||||
"_request_lightning_invoice: Requesting Lightning invoice for"
|
||||
f" {amount} satoshis."
|
||||
)
|
||||
status = await self.lightning.status()
|
||||
logger.trace(
|
||||
"_request_lightning_invoice: Lightning wallet balance:"
|
||||
f" {status.balance_msat}"
|
||||
)
|
||||
if status.error_message:
|
||||
raise LightningError(
|
||||
f"Lightning wallet not responding: {status.error_message}"
|
||||
)
|
||||
payment = await self.lightning.create_invoice(amount, "Cashu deposit")
|
||||
logger.trace(
|
||||
f"_request_lightning_invoice: Lightning invoice: {payment.payment_request}"
|
||||
)
|
||||
|
||||
if not payment.ok:
|
||||
raise LightningError(f"Lightning wallet error: {payment.error_message}")
|
||||
assert payment.payment_request and payment.checking_id, LightningError(
|
||||
"could not fetch invoice from Lightning backend"
|
||||
)
|
||||
return payment
|
||||
|
||||
async def _check_lightning_invoice(
|
||||
self, *, amount: int, id: str, conn: Optional[Connection] = None
|
||||
) -> PaymentStatus:
|
||||
"""Checks with the Lightning backend whether an invoice with `id` was paid.
|
||||
|
||||
Args:
|
||||
amount (int): Amount of the outputs the wallet wants in return (in Satoshis).
|
||||
id (str): Id to look up Lightning invoice by.
|
||||
|
||||
Raises:
|
||||
Exception: Invoice not found.
|
||||
Exception: Tokens for invoice already issued.
|
||||
Exception: Amount larger than invoice amount.
|
||||
Exception: Invoice not paid yet
|
||||
e: Update database and pass through error.
|
||||
|
||||
Returns:
|
||||
bool: True if invoice has been paid, else False
|
||||
"""
|
||||
invoice: Union[Invoice, None] = await self.crud.get_lightning_invoice(
|
||||
id=id, db=self.db, conn=conn
|
||||
)
|
||||
if invoice is None:
|
||||
raise LightningError("invoice not found.")
|
||||
if invoice.issued:
|
||||
raise LightningError("tokens already issued for this invoice.")
|
||||
if amount > invoice.amount:
|
||||
raise LightningError(
|
||||
f"requested amount too high: {amount}. Invoice amount: {invoice.amount}"
|
||||
)
|
||||
assert invoice.payment_hash, "invoice has no payment hash."
|
||||
# set this invoice as issued
|
||||
await self.crud.update_lightning_invoice(
|
||||
id=id, issued=True, db=self.db, conn=conn
|
||||
)
|
||||
|
||||
try:
|
||||
status = await self.lightning.get_invoice_status(invoice.payment_hash)
|
||||
if status.paid:
|
||||
return status
|
||||
else:
|
||||
raise InvoiceNotPaidError()
|
||||
except Exception as e:
|
||||
# unset issued
|
||||
await self.crud.update_lightning_invoice(
|
||||
id=id, issued=False, db=self.db, conn=conn
|
||||
)
|
||||
raise e
|
||||
|
||||
async def _pay_lightning_invoice(
|
||||
self, invoice: str, fee_limit_msat: int
|
||||
) -> PaymentResponse:
|
||||
"""Pays a Lightning invoice via the funding source backend.
|
||||
|
||||
Args:
|
||||
invoice (str): Bolt11 Lightning invoice
|
||||
fee_limit_msat (int): Maximum fee reserve for payment (in Millisatoshi)
|
||||
|
||||
Raises:
|
||||
Exception: Funding source error.
|
||||
|
||||
Returns:
|
||||
Tuple[bool, string, int]: Returns payment status, preimage of invoice, paid fees (in Millisatoshi)
|
||||
"""
|
||||
status = await self.lightning.status()
|
||||
if status.error_message:
|
||||
raise LightningError(
|
||||
f"Lightning wallet not responding: {status.error_message}"
|
||||
)
|
||||
payment = await self.lightning.pay_invoice(
|
||||
invoice, fee_limit_msat=fee_limit_msat
|
||||
)
|
||||
logger.trace(f"_pay_lightning_invoice: Lightning payment status: {payment.ok}")
|
||||
# make sure that fee is positive and not None
|
||||
payment.fee_msat = abs(payment.fee_msat) if payment.fee_msat else 0
|
||||
return payment
|
||||
@@ -32,7 +32,6 @@ def main(
|
||||
for a in ctx.args:
|
||||
item = a.split("=")
|
||||
if len(item) > 1: # argument like --key=value
|
||||
print(a, item)
|
||||
d[item[0].strip("--").replace("-", "_")] = (
|
||||
int(item[1]) # need to convert to int if it's a number
|
||||
if item[1].isdigit()
|
||||
@@ -49,5 +48,6 @@ def main(
|
||||
ssl_certfile=ssl_certfile,
|
||||
**d, # type: ignore
|
||||
)
|
||||
|
||||
server = uvicorn.Server(config)
|
||||
server.run()
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
import time
|
||||
|
||||
from ..core.db import Connection, Database, table_with_schema
|
||||
from ..core.settings import settings
|
||||
|
||||
|
||||
async def m000_create_migrations_table(conn: Connection):
|
||||
@@ -218,3 +221,86 @@ async def m010_add_index_to_proofs_used(db: Database):
|
||||
" proofs_used_secret_idx ON"
|
||||
f" {table_with_schema(db, 'proofs_used')} (secret)"
|
||||
)
|
||||
|
||||
|
||||
async def m011_add_quote_tables(db: Database):
|
||||
async with db.connect() as conn:
|
||||
# add column "created" to tables invoices, promises, proofs_used, proofs_pending
|
||||
tables = ["invoices", "promises", "proofs_used", "proofs_pending"]
|
||||
for table in tables:
|
||||
await conn.execute(
|
||||
f"ALTER TABLE {table_with_schema(db, table)} ADD COLUMN created"
|
||||
" TIMESTAMP"
|
||||
)
|
||||
await conn.execute(
|
||||
f"UPDATE {table_with_schema(db, table)} SET created ="
|
||||
f" '{int(time.time())}'"
|
||||
)
|
||||
|
||||
# add column "witness" to table proofs_used
|
||||
await conn.execute(
|
||||
f"ALTER TABLE {table_with_schema(db, 'proofs_used')} ADD COLUMN witness"
|
||||
" TEXT"
|
||||
)
|
||||
|
||||
# add columns "seed" and "unit" to table keysets
|
||||
await conn.execute(
|
||||
f"ALTER TABLE {table_with_schema(db, 'keysets')} ADD COLUMN seed TEXT"
|
||||
)
|
||||
await conn.execute(
|
||||
f"ALTER TABLE {table_with_schema(db, 'keysets')} ADD COLUMN unit TEXT"
|
||||
)
|
||||
|
||||
# fill columns "seed" and "unit" in table keysets
|
||||
await conn.execute(
|
||||
f"UPDATE {table_with_schema(db, 'keysets')} SET seed ="
|
||||
f" '{settings.mint_private_key}', unit = 'sat'"
|
||||
)
|
||||
|
||||
await conn.execute(f"""
|
||||
CREATE TABLE IF NOT EXISTS {table_with_schema(db, 'mint_quotes')} (
|
||||
quote TEXT NOT NULL,
|
||||
method TEXT NOT NULL,
|
||||
request TEXT NOT NULL,
|
||||
checking_id TEXT NOT NULL,
|
||||
unit TEXT NOT NULL,
|
||||
amount INTEGER NOT NULL,
|
||||
paid BOOL NOT NULL,
|
||||
issued BOOL NOT NULL,
|
||||
created_time TIMESTAMP,
|
||||
paid_time TIMESTAMP,
|
||||
|
||||
UNIQUE (quote)
|
||||
|
||||
);
|
||||
""")
|
||||
|
||||
await conn.execute(f"""
|
||||
CREATE TABLE IF NOT EXISTS {table_with_schema(db, 'melt_quotes')} (
|
||||
quote TEXT NOT NULL,
|
||||
method TEXT NOT NULL,
|
||||
request TEXT NOT NULL,
|
||||
checking_id TEXT NOT NULL,
|
||||
unit TEXT NOT NULL,
|
||||
amount INTEGER NOT NULL,
|
||||
fee_reserve INTEGER,
|
||||
paid BOOL NOT NULL,
|
||||
created_time TIMESTAMP,
|
||||
paid_time TIMESTAMP,
|
||||
fee_paid INTEGER,
|
||||
proof TEXT,
|
||||
|
||||
UNIQUE (quote)
|
||||
|
||||
);
|
||||
""")
|
||||
|
||||
await conn.execute(
|
||||
f"INSERT INTO {table_with_schema(db, 'mint_quotes')} (quote, method,"
|
||||
" request, checking_id, unit, amount, paid, issued, created_time,"
|
||||
" paid_time) SELECT id, 'bolt11', bolt11, payment_hash, 'sat', amount,"
|
||||
f" False, issued, created, 0 FROM {table_with_schema(db, 'invoices')} "
|
||||
)
|
||||
|
||||
# drop table invoices
|
||||
await conn.execute(f"DROP TABLE {table_with_schema(db, 'invoices')}")
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
from typing import Protocol
|
||||
from typing import Dict, Protocol
|
||||
|
||||
from ..core.base import MintKeyset, MintKeysets
|
||||
from ..core.base import MintKeyset, Unit
|
||||
from ..core.db import Database
|
||||
from ..lightning.base import Wallet
|
||||
from ..lightning.base import LightningBackend
|
||||
from ..mint.crud import LedgerCrud
|
||||
|
||||
|
||||
class SupportsKeysets(Protocol):
|
||||
keyset: MintKeyset
|
||||
keysets: MintKeysets
|
||||
keysets: Dict[str, MintKeyset]
|
||||
|
||||
|
||||
class SupportLightning(Protocol):
|
||||
lightning: Wallet
|
||||
lightning: Dict[Unit, LightningBackend]
|
||||
|
||||
|
||||
class SupportsDb(Protocol):
|
||||
|
||||
@@ -1,26 +1,27 @@
|
||||
from typing import List, Optional, Union
|
||||
from typing import Any, Dict, List
|
||||
|
||||
from fastapi import APIRouter
|
||||
from fastapi import APIRouter, Request
|
||||
from loguru import logger
|
||||
|
||||
from ..core.base import (
|
||||
BlindedSignature,
|
||||
CheckFeesRequest,
|
||||
CheckFeesResponse,
|
||||
CheckSpendableRequest,
|
||||
CheckSpendableResponse,
|
||||
GetInfoResponse,
|
||||
GetMeltResponse,
|
||||
GetMintResponse,
|
||||
KeysetsResponse,
|
||||
KeysetsResponseKeyset,
|
||||
KeysResponse,
|
||||
KeysResponseKeyset,
|
||||
PostCheckStateRequest,
|
||||
PostCheckStateResponse,
|
||||
PostMeltQuoteRequest,
|
||||
PostMeltQuoteResponse,
|
||||
PostMeltRequest,
|
||||
PostMeltResponse,
|
||||
PostMintQuoteRequest,
|
||||
PostMintQuoteResponse,
|
||||
PostMintRequest,
|
||||
PostMintResponse,
|
||||
PostRestoreResponse,
|
||||
PostSplitRequest,
|
||||
PostSplitResponse,
|
||||
PostSplitResponse_Deprecated,
|
||||
)
|
||||
from ..core.errors import CashuError
|
||||
from ..core.settings import settings
|
||||
@@ -30,14 +31,38 @@ router: APIRouter = APIRouter()
|
||||
|
||||
|
||||
@router.get(
|
||||
"/info",
|
||||
"/v1/info",
|
||||
name="Mint information",
|
||||
summary="Mint information, operator contact information, and other info.",
|
||||
response_model=GetInfoResponse,
|
||||
response_model_exclude_none=True,
|
||||
)
|
||||
async def info() -> GetInfoResponse:
|
||||
logger.trace("> GET /info")
|
||||
logger.trace("> GET /v1/info")
|
||||
|
||||
# determine all method-unit pairs
|
||||
method_unit_pairs: List[List[str]] = []
|
||||
for method, unit_dict in ledger.backends.items():
|
||||
for unit in unit_dict.keys():
|
||||
method_unit_pairs.append([method.name, unit.name])
|
||||
supported_dict = dict(supported=True)
|
||||
|
||||
mint_features: Dict[int, Dict[str, Any]] = {
|
||||
4: dict(
|
||||
methods=method_unit_pairs,
|
||||
),
|
||||
5: dict(
|
||||
methods=method_unit_pairs,
|
||||
disabled=False,
|
||||
),
|
||||
7: supported_dict,
|
||||
8: supported_dict,
|
||||
9: supported_dict,
|
||||
10: supported_dict,
|
||||
11: supported_dict,
|
||||
12: supported_dict,
|
||||
}
|
||||
|
||||
return GetInfoResponse(
|
||||
name=settings.mint_info_name,
|
||||
pubkey=ledger.pubkey.serialize().hex() if ledger.pubkey else None,
|
||||
@@ -45,18 +70,13 @@ async def info() -> GetInfoResponse:
|
||||
description=settings.mint_info_description,
|
||||
description_long=settings.mint_info_description_long,
|
||||
contact=settings.mint_info_contact,
|
||||
nuts=settings.mint_info_nuts,
|
||||
nuts=mint_features,
|
||||
motd=settings.mint_info_motd,
|
||||
parameter={
|
||||
"max_peg_in": settings.mint_max_peg_in,
|
||||
"max_peg_out": settings.mint_max_peg_out,
|
||||
"peg_out_only": settings.mint_peg_out_only,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@router.get(
|
||||
"/keys",
|
||||
"/v1/keys",
|
||||
name="Mint public keys",
|
||||
summary="Get the public keys of the newest mint keyset",
|
||||
response_description=(
|
||||
@@ -67,14 +87,23 @@ async def info() -> GetInfoResponse:
|
||||
)
|
||||
async def keys():
|
||||
"""This endpoint returns a dictionary of all supported token values of the mint and their associated public key."""
|
||||
logger.trace("> GET /keys")
|
||||
keyset = ledger.get_keyset()
|
||||
keys = KeysResponse.parse_obj(keyset)
|
||||
return keys.__root__
|
||||
logger.trace("> GET /v1/keys")
|
||||
keyset = ledger.keyset
|
||||
keyset_for_response = []
|
||||
for keyset in ledger.keysets.values():
|
||||
if keyset.active:
|
||||
keyset_for_response.append(
|
||||
KeysResponseKeyset(
|
||||
id=keyset.id,
|
||||
unit=keyset.unit.name,
|
||||
keys={k: v for k, v in keyset.public_keys_hex.items()},
|
||||
)
|
||||
)
|
||||
return KeysResponse(keysets=keyset_for_response)
|
||||
|
||||
|
||||
@router.get(
|
||||
"/keys/{idBase64Urlsafe}",
|
||||
"/v1/keys/{keyset_id}",
|
||||
name="Keyset public keys",
|
||||
summary="Public keys of a specific keyset",
|
||||
response_description=(
|
||||
@@ -83,21 +112,33 @@ async def keys():
|
||||
),
|
||||
response_model=KeysResponse,
|
||||
)
|
||||
async def keyset_keys(idBase64Urlsafe: str):
|
||||
async def keyset_keys(keyset_id: str, request: Request) -> KeysResponse:
|
||||
"""
|
||||
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}")
|
||||
id = idBase64Urlsafe.replace("-", "+").replace("_", "/")
|
||||
keyset = ledger.get_keyset(keyset_id=id)
|
||||
keys = KeysResponse.parse_obj(keyset)
|
||||
return keys.__root__
|
||||
logger.trace(f"> GET /v1/keys/{keyset_id}")
|
||||
# BEGIN BACKWARDS COMPATIBILITY < 0.15.0
|
||||
# if keyset_id is not hex, we assume it is base64 and sanitize it
|
||||
try:
|
||||
int(keyset_id, 16)
|
||||
except ValueError:
|
||||
keyset_id = keyset_id.replace("-", "+").replace("_", "/")
|
||||
# END BACKWARDS COMPATIBILITY < 0.15.0
|
||||
|
||||
keyset = ledger.keysets.get(keyset_id)
|
||||
if keyset is None:
|
||||
raise CashuError(code=0, detail="Keyset not found.")
|
||||
|
||||
keyset_for_response = KeysResponseKeyset(
|
||||
id=keyset.id,
|
||||
unit=keyset.unit.name,
|
||||
keys={k: v for k, v in keyset.public_keys_hex.items()},
|
||||
)
|
||||
return KeysResponse(keysets=[keyset_for_response])
|
||||
|
||||
|
||||
@router.get(
|
||||
"/keysets",
|
||||
"/v1/keysets",
|
||||
name="Active keysets",
|
||||
summary="Get all active keyset id of the mind",
|
||||
response_model=KeysetsResponse,
|
||||
@@ -105,44 +146,75 @@ async def keyset_keys(idBase64Urlsafe: str):
|
||||
)
|
||||
async def keysets() -> KeysetsResponse:
|
||||
"""This endpoint returns a list of keysets that the mint currently supports and will accept tokens from."""
|
||||
logger.trace("> GET /keysets")
|
||||
keysets = KeysetsResponse(keysets=ledger.keysets.get_ids())
|
||||
return keysets
|
||||
|
||||
|
||||
@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."
|
||||
),
|
||||
logger.trace("> GET /v1/keysets")
|
||||
keysets = []
|
||||
for id, keyset in ledger.keysets.items():
|
||||
keysets.append(
|
||||
KeysetsResponseKeyset(
|
||||
id=id, unit=keyset.unit.name, active=keyset.active or False
|
||||
)
|
||||
async def request_mint(amount: int = 0) -> GetMintResponse:
|
||||
)
|
||||
return KeysetsResponse(keysets=keysets)
|
||||
|
||||
|
||||
@router.post(
|
||||
"/v1/mint/quote/bolt11",
|
||||
name="Request mint quote",
|
||||
summary="Request a quote for minting of new tokens",
|
||||
response_model=PostMintQuoteResponse,
|
||||
response_description="A payment request to mint tokens of a denomination",
|
||||
)
|
||||
async def mint_quote(payload: PostMintQuoteRequest) -> PostMintQuoteResponse:
|
||||
"""
|
||||
Request minting of new tokens. The mint responds with a Lightning invoice.
|
||||
This endpoint can be used for a Lightning invoice UX flow.
|
||||
|
||||
Call `POST /mint` after paying the invoice.
|
||||
Call `POST /v1/mint/bolt11` after paying the invoice.
|
||||
"""
|
||||
logger.trace(f"> GET /mint: amount={amount}")
|
||||
logger.trace(f"> POST /v1/mint/quote/bolt11: payload={payload}")
|
||||
amount = payload.amount
|
||||
if amount > 21_000_000 * 100_000_000 or amount <= 0:
|
||||
raise CashuError(code=0, detail="Amount must be a valid amount of sat.")
|
||||
if settings.mint_peg_out_only:
|
||||
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}")
|
||||
quote = await ledger.mint_quote(payload)
|
||||
resp = PostMintQuoteResponse(
|
||||
request=quote.request,
|
||||
quote=quote.quote,
|
||||
paid=quote.paid,
|
||||
expiry=quote.expiry,
|
||||
)
|
||||
logger.trace(f"< POST /v1/mint/quote/bolt11: {resp}")
|
||||
return resp
|
||||
|
||||
|
||||
@router.get(
|
||||
"/v1/mint/quote/{quote}",
|
||||
summary="Get mint quote",
|
||||
response_model=PostMintQuoteResponse,
|
||||
response_description="Get an existing mint quote to check its status.",
|
||||
)
|
||||
async def get_mint_quote(quote: str) -> PostMintQuoteResponse:
|
||||
"""
|
||||
Get mint quote state.
|
||||
"""
|
||||
logger.trace(f"> POST /v1/mint/quote/{quote}")
|
||||
mint_quote = await ledger.get_mint_quote(quote)
|
||||
resp = PostMintQuoteResponse(
|
||||
quote=mint_quote.quote,
|
||||
request=mint_quote.request,
|
||||
paid=mint_quote.paid,
|
||||
expiry=mint_quote.expiry,
|
||||
)
|
||||
logger.trace(f"< POST /v1/mint/quote/{quote}")
|
||||
return resp
|
||||
|
||||
|
||||
@router.post(
|
||||
"/mint",
|
||||
"/v1/mint/bolt11",
|
||||
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 payment that the user has made",
|
||||
response_model=PostMintResponse,
|
||||
response_description=(
|
||||
"A list of blinded signatures that can be used to create proofs."
|
||||
@@ -150,144 +222,133 @@ async def request_mint(amount: int = 0) -> GetMintResponse:
|
||||
)
|
||||
async def mint(
|
||||
payload: PostMintRequest,
|
||||
hash: Optional[str] = None,
|
||||
payment_hash: Optional[str] = None,
|
||||
) -> PostMintResponse:
|
||||
"""
|
||||
Requests the minting of tokens belonging to a paid payment request.
|
||||
|
||||
Call this endpoint after `GET /mint`.
|
||||
Call this endpoint after `POST /v1/mint/quote`.
|
||||
"""
|
||||
logger.trace(f"> POST /mint: {payload}")
|
||||
logger.trace(f"> POST /v1/mint/bolt11: {payload}")
|
||||
|
||||
# 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.
|
||||
id = payment_hash or hash
|
||||
# END: backwards compatibility < 0.12
|
||||
|
||||
promises = await ledger.mint(payload.outputs, id=id)
|
||||
blinded_signatures = PostMintResponse(promises=promises)
|
||||
logger.trace(f"< POST /mint: {blinded_signatures}")
|
||||
promises = await ledger.mint(outputs=payload.outputs, quote_id=payload.quote)
|
||||
blinded_signatures = PostMintResponse(signatures=promises)
|
||||
logger.trace(f"< POST /v1/mint/bolt11: {blinded_signatures}")
|
||||
return blinded_signatures
|
||||
|
||||
|
||||
@router.post(
|
||||
"/melt",
|
||||
"/v1/melt/quote/bolt11",
|
||||
summary="Request a quote for melting tokens",
|
||||
response_model=PostMeltQuoteResponse,
|
||||
response_description="Melt tokens for a payment on a supported payment method.",
|
||||
)
|
||||
async def get_melt_quote(payload: PostMeltQuoteRequest) -> PostMeltQuoteResponse:
|
||||
"""
|
||||
Request a quote for melting tokens.
|
||||
"""
|
||||
logger.trace(f"> POST /v1/melt/quote/bolt11: {payload}")
|
||||
quote = await ledger.melt_quote(payload) # TODO
|
||||
logger.trace(f"< POST /v1/melt/quote/bolt11: {quote}")
|
||||
return quote
|
||||
|
||||
|
||||
@router.get(
|
||||
"/v1/melt/quote/{quote}",
|
||||
summary="Get melt quote",
|
||||
response_model=PostMeltQuoteResponse,
|
||||
response_description="Get an existing melt quote to check its status.",
|
||||
)
|
||||
async def melt_quote(quote: str) -> PostMeltQuoteResponse:
|
||||
"""
|
||||
Get melt quote state.
|
||||
"""
|
||||
logger.trace(f"> POST /v1/melt/quote/{quote}")
|
||||
melt_quote = await ledger.get_melt_quote(quote)
|
||||
resp = PostMeltQuoteResponse(
|
||||
quote=melt_quote.quote,
|
||||
amount=melt_quote.amount,
|
||||
fee_reserve=melt_quote.fee_reserve,
|
||||
paid=melt_quote.paid,
|
||||
)
|
||||
logger.trace(f"< POST /v1/melt/quote/{quote}")
|
||||
return resp
|
||||
|
||||
|
||||
@router.post(
|
||||
"/v1/melt/bolt11",
|
||||
name="Melt tokens",
|
||||
summary=(
|
||||
"Melt tokens for a Bitcoin payment that the mint will make for the user in"
|
||||
" exchange"
|
||||
),
|
||||
response_model=GetMeltResponse,
|
||||
response_model=PostMeltResponse,
|
||||
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) -> GetMeltResponse:
|
||||
async def melt(payload: PostMeltRequest) -> PostMeltResponse:
|
||||
"""
|
||||
Requests tokens to be destroyed and sent out via Lightning.
|
||||
"""
|
||||
logger.trace(f"> POST /melt: {payload}")
|
||||
ok, preimage, change_promises = await ledger.melt(
|
||||
payload.proofs, payload.pr, payload.outputs
|
||||
logger.trace(f"> POST /v1/melt/bolt11: {payload}")
|
||||
preimage, change_promises = await ledger.melt(
|
||||
proofs=payload.inputs, quote=payload.quote, outputs=payload.outputs
|
||||
)
|
||||
resp = GetMeltResponse(paid=ok, preimage=preimage, change=change_promises)
|
||||
logger.trace(f"< POST /melt: {resp}")
|
||||
resp = PostMeltResponse(
|
||||
paid=True, payment_preimage=preimage, change=change_promises
|
||||
)
|
||||
logger.trace(f"< POST /v1/melt/bolt11: {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,
|
||||
"/v1/swap",
|
||||
name="Swap tokens",
|
||||
summary="Swap inputs for outputs of the same value",
|
||||
response_model=PostSplitResponse,
|
||||
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,
|
||||
) -> CheckSpendableResponse:
|
||||
"""Check whether a secret has been spent already or not."""
|
||||
logger.trace(f"> POST /check: {payload}")
|
||||
spendableList, pendingList = await ledger.check_proof_state(payload.proofs)
|
||||
logger.trace(f"< POST /check <spendable>: {spendableList}")
|
||||
logger.trace(f"< POST /check <pending>: {pendingList}")
|
||||
return CheckSpendableResponse(spendable=spendableList, pending=pendingList)
|
||||
|
||||
|
||||
@router.post(
|
||||
"/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:
|
||||
"""
|
||||
Responds with the fees necessary to pay a Lightning invoice.
|
||||
Used by wallets for figuring out the fees they need to supply together with the payment amount.
|
||||
This is can be useful for checking whether an invoice is internal (Cashu-to-Cashu).
|
||||
"""
|
||||
logger.trace(f"> POST /checkfees: {payload}")
|
||||
fees_sat = await ledger.get_melt_fees(payload.pr)
|
||||
logger.trace(f"< POST /checkfees: {fees_sat}")
|
||||
return CheckFeesResponse(fee=fees_sat)
|
||||
|
||||
|
||||
@router.post(
|
||||
"/split",
|
||||
name="Split",
|
||||
summary="Split proofs at a specified amount",
|
||||
response_model=Union[PostSplitResponse, PostSplitResponse_Deprecated],
|
||||
response_description=(
|
||||
"A list of blinded signatures that can be used to create proofs."
|
||||
"An array of blinded signatures that can be used to create proofs."
|
||||
),
|
||||
)
|
||||
async def split(
|
||||
payload: PostSplitRequest,
|
||||
) -> Union[PostSplitResponse, PostSplitResponse_Deprecated]:
|
||||
) -> PostSplitResponse:
|
||||
"""
|
||||
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}")
|
||||
logger.trace(f"> POST /v1/swap: {payload}")
|
||||
assert payload.outputs, Exception("no outputs provided.")
|
||||
|
||||
promises = await ledger.split(proofs=payload.proofs, outputs=payload.outputs)
|
||||
signatures = await ledger.split(proofs=payload.inputs, outputs=payload.outputs)
|
||||
|
||||
if payload.amount:
|
||||
# BEGIN backwards compatibility < 0.13
|
||||
# old clients expect two lists of promises where the second one's amounts
|
||||
# sum up to `amount`. The first one is the rest.
|
||||
# The returned value `promises` has the form [keep1, keep2, ..., send1, send2, ...]
|
||||
# The sum of the sendx is `amount`. We need to split this into two lists and keep the order of the elements.
|
||||
frst_promises: List[BlindedSignature] = []
|
||||
scnd_promises: List[BlindedSignature] = []
|
||||
scnd_amount = 0
|
||||
for promise in promises[::-1]: # we iterate backwards
|
||||
if scnd_amount < payload.amount:
|
||||
scnd_promises.insert(0, promise) # and insert at the beginning
|
||||
scnd_amount += promise.amount
|
||||
else:
|
||||
frst_promises.insert(0, promise) # and insert at the beginning
|
||||
logger.trace(
|
||||
f"Split into keep: {len(frst_promises)}:"
|
||||
f" {sum([p.amount for p in frst_promises])} sat and send:"
|
||||
f" {len(scnd_promises)}: {sum([p.amount for p in scnd_promises])} sat"
|
||||
)
|
||||
return PostSplitResponse_Deprecated(fst=frst_promises, snd=scnd_promises)
|
||||
# END backwards compatibility < 0.13
|
||||
else:
|
||||
return PostSplitResponse(promises=promises)
|
||||
return PostSplitResponse(signatures=signatures)
|
||||
|
||||
|
||||
@router.post(
|
||||
"/restore",
|
||||
"/v1/checkstate",
|
||||
name="Check proof state",
|
||||
summary="Check whether a proof is spent already or is pending in a transaction",
|
||||
response_model=PostCheckStateResponse,
|
||||
response_description=(
|
||||
"Two lists of booleans indicating whether the provided proofs "
|
||||
"are spendable or pending in a transaction respectively."
|
||||
),
|
||||
)
|
||||
async def check_state(
|
||||
payload: PostCheckStateRequest,
|
||||
) -> PostCheckStateResponse:
|
||||
"""Check whether a secret has been spent already or not."""
|
||||
logger.trace(f"> POST /v1/checkstate: {payload}")
|
||||
proof_states = await ledger.check_proofs_state(payload.secrets)
|
||||
return PostCheckStateResponse(states=proof_states)
|
||||
|
||||
|
||||
@router.post(
|
||||
"/v1/restore",
|
||||
name="Restore",
|
||||
summary="Restores a blinded signature from a secret",
|
||||
response_model=PostRestoreResponse,
|
||||
|
||||
363
cashu/mint/router_deprecated.py
Normal file
363
cashu/mint/router_deprecated.py
Normal file
@@ -0,0 +1,363 @@
|
||||
from typing import List, Optional
|
||||
|
||||
from fastapi import APIRouter
|
||||
from loguru import logger
|
||||
|
||||
from ..core.base import (
|
||||
BlindedSignature,
|
||||
CheckFeesRequest_deprecated,
|
||||
CheckFeesResponse_deprecated,
|
||||
CheckSpendableRequest_deprecated,
|
||||
CheckSpendableResponse_deprecated,
|
||||
GetInfoResponse_deprecated,
|
||||
GetMintResponse_deprecated,
|
||||
KeysetsResponse_deprecated,
|
||||
KeysResponse_deprecated,
|
||||
PostMeltQuoteRequest,
|
||||
PostMeltRequest_deprecated,
|
||||
PostMeltResponse_deprecated,
|
||||
PostMintQuoteRequest,
|
||||
PostMintRequest_deprecated,
|
||||
PostMintResponse_deprecated,
|
||||
PostRestoreResponse,
|
||||
PostSplitRequest_Deprecated,
|
||||
PostSplitResponse_Deprecated,
|
||||
PostSplitResponse_Very_Deprecated,
|
||||
SpentState,
|
||||
)
|
||||
from ..core.errors import CashuError
|
||||
from ..core.settings import settings
|
||||
from .startup import ledger
|
||||
|
||||
router_deprecated: APIRouter = APIRouter()
|
||||
|
||||
|
||||
@router_deprecated.get(
|
||||
"/info",
|
||||
name="Mint information",
|
||||
summary="Mint information, operator contact information, and other info.",
|
||||
response_model=GetInfoResponse_deprecated,
|
||||
response_model_exclude_none=True,
|
||||
deprecated=True,
|
||||
)
|
||||
async def info() -> GetInfoResponse_deprecated:
|
||||
logger.trace("> GET /info")
|
||||
return GetInfoResponse_deprecated(
|
||||
name=settings.mint_info_name,
|
||||
pubkey=ledger.pubkey.serialize().hex() if ledger.pubkey else None,
|
||||
version=f"Nutshell/{settings.version}",
|
||||
description=settings.mint_info_description,
|
||||
description_long=settings.mint_info_description_long,
|
||||
contact=settings.mint_info_contact,
|
||||
nuts=settings.mint_info_nuts,
|
||||
motd=settings.mint_info_motd,
|
||||
parameter={
|
||||
"max_peg_in": settings.mint_max_peg_in,
|
||||
"max_peg_out": settings.mint_max_peg_out,
|
||||
"peg_out_only": settings.mint_peg_out_only,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@router_deprecated.get(
|
||||
"/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_deprecated,
|
||||
deprecated=True,
|
||||
)
|
||||
async def keys_deprecated():
|
||||
"""This endpoint returns a dictionary of all supported token values of the mint and their associated public key."""
|
||||
logger.trace("> GET /keys")
|
||||
keyset = ledger.get_keyset()
|
||||
keys = KeysResponse_deprecated.parse_obj(keyset)
|
||||
return keys.__root__
|
||||
|
||||
|
||||
@router_deprecated.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_deprecated,
|
||||
deprecated=True,
|
||||
)
|
||||
async def keyset_deprecated(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}")
|
||||
id = idBase64Urlsafe.replace("-", "+").replace("_", "/")
|
||||
keyset = ledger.get_keyset(keyset_id=id)
|
||||
keys = KeysResponse_deprecated.parse_obj(keyset)
|
||||
return keys.__root__
|
||||
|
||||
|
||||
@router_deprecated.get(
|
||||
"/keysets",
|
||||
name="Active keysets",
|
||||
summary="Get all active keyset id of the mind",
|
||||
response_model=KeysetsResponse_deprecated,
|
||||
response_description="A list of all active keyset ids of the mint.",
|
||||
deprecated=True,
|
||||
)
|
||||
async def keysets_deprecated() -> KeysetsResponse_deprecated:
|
||||
"""This endpoint returns a list of keysets that the mint currently supports and will accept tokens from."""
|
||||
logger.trace("> GET /keysets")
|
||||
keysets = KeysetsResponse_deprecated(keysets=list(ledger.keysets.keys()))
|
||||
return keysets
|
||||
|
||||
|
||||
@router_deprecated.get(
|
||||
"/mint",
|
||||
name="Request mint",
|
||||
summary="Request minting of new tokens",
|
||||
response_model=GetMintResponse_deprecated,
|
||||
response_description=(
|
||||
"A Lightning invoice to be paid and a hash to request minting of new tokens"
|
||||
" after payment."
|
||||
),
|
||||
deprecated=True,
|
||||
)
|
||||
async def request_mint_deprecated(amount: int = 0) -> GetMintResponse_deprecated:
|
||||
"""
|
||||
Request minting of new tokens. The mint responds with a Lightning invoice.
|
||||
This endpoint can be used for a Lightning invoice UX flow.
|
||||
|
||||
Call `POST /mint` after paying the invoice.
|
||||
"""
|
||||
logger.trace(f"> GET /mint: amount={amount}")
|
||||
if amount > 21_000_000 * 100_000_000 or amount <= 0:
|
||||
raise CashuError(code=0, detail="Amount must be a valid amount of sat.")
|
||||
if settings.mint_peg_out_only:
|
||||
raise CashuError(code=0, detail="Mint does not allow minting new tokens.")
|
||||
quote = await ledger.mint_quote(PostMintQuoteRequest(amount=amount, unit="sat"))
|
||||
resp = GetMintResponse_deprecated(pr=quote.request, hash=quote.quote)
|
||||
logger.trace(f"< GET /mint: {resp}")
|
||||
return resp
|
||||
|
||||
|
||||
@router_deprecated.post(
|
||||
"/mint",
|
||||
name="Mint tokens",
|
||||
summary="Mint tokens in exchange for a Bitcoin payment that the user has made",
|
||||
response_model=PostMintResponse_deprecated,
|
||||
response_description=(
|
||||
"A list of blinded signatures that can be used to create proofs."
|
||||
),
|
||||
deprecated=True,
|
||||
)
|
||||
async def mint_deprecated(
|
||||
payload: PostMintRequest_deprecated,
|
||||
hash: Optional[str] = None,
|
||||
payment_hash: Optional[str] = None,
|
||||
) -> PostMintResponse_deprecated:
|
||||
"""
|
||||
Requests the minting of tokens belonging to a paid payment request.
|
||||
|
||||
Call this endpoint after `GET /mint`.
|
||||
"""
|
||||
logger.trace(f"> POST /mint: {payload}")
|
||||
|
||||
# BEGIN BACKWARDS COMPATIBILITY < 0.15
|
||||
# Mint expects "id" in outputs to know which keyset to use to sign them.
|
||||
for output in payload.outputs:
|
||||
if not output.id:
|
||||
# use the deprecated version of the current keyset
|
||||
output.id = ledger.keyset.duplicate_keyset_id
|
||||
# END BACKWARDS COMPATIBILITY < 0.15
|
||||
|
||||
# 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
|
||||
assert hash, "hash must be set."
|
||||
# END: backwards compatibility < 0.12
|
||||
|
||||
promises = await ledger.mint(outputs=payload.outputs, quote_id=hash)
|
||||
blinded_signatures = PostMintResponse_deprecated(promises=promises)
|
||||
|
||||
logger.trace(f"< POST /mint: {blinded_signatures}")
|
||||
return blinded_signatures
|
||||
|
||||
|
||||
@router_deprecated.post(
|
||||
"/melt",
|
||||
name="Melt tokens",
|
||||
summary=(
|
||||
"Melt tokens for a Bitcoin payment that the mint will make for the user in"
|
||||
" exchange"
|
||||
),
|
||||
response_model=PostMeltResponse_deprecated,
|
||||
response_description=(
|
||||
"The state of the payment, a preimage as proof of payment, and a list of"
|
||||
" promises for change."
|
||||
),
|
||||
deprecated=True,
|
||||
)
|
||||
async def melt_deprecated(
|
||||
payload: PostMeltRequest_deprecated,
|
||||
) -> PostMeltResponse_deprecated:
|
||||
"""
|
||||
Requests tokens to be destroyed and sent out via Lightning.
|
||||
"""
|
||||
logger.trace(f"> POST /melt: {payload}")
|
||||
# BEGIN BACKWARDS COMPATIBILITY < 0.14: add "id" to outputs
|
||||
if payload.outputs:
|
||||
for output in payload.outputs:
|
||||
if not output.id:
|
||||
output.id = ledger.keyset.id
|
||||
# END BACKWARDS COMPATIBILITY < 0.14
|
||||
quote = await ledger.melt_quote(
|
||||
PostMeltQuoteRequest(request=payload.pr, unit="sat")
|
||||
)
|
||||
preimage, change_promises = await ledger.melt(
|
||||
proofs=payload.proofs, quote=quote.quote, outputs=payload.outputs
|
||||
)
|
||||
resp = PostMeltResponse_deprecated(
|
||||
paid=True, preimage=preimage, change=change_promises
|
||||
)
|
||||
logger.trace(f"< POST /melt: {resp}")
|
||||
return resp
|
||||
|
||||
|
||||
@router_deprecated.post(
|
||||
"/checkfees",
|
||||
name="Check fees",
|
||||
summary="Check fee reserve for a Lightning payment",
|
||||
response_model=CheckFeesResponse_deprecated,
|
||||
response_description="The fees necessary to pay a Lightning invoice.",
|
||||
deprecated=True,
|
||||
)
|
||||
async def check_fees(
|
||||
payload: CheckFeesRequest_deprecated,
|
||||
) -> CheckFeesResponse_deprecated:
|
||||
"""
|
||||
Responds with the fees necessary to pay a Lightning invoice.
|
||||
Used by wallets for figuring out the fees they need to supply together with the payment amount.
|
||||
This is can be useful for checking whether an invoice is internal (Cashu-to-Cashu).
|
||||
"""
|
||||
logger.trace(f"> POST /checkfees: {payload}")
|
||||
quote = await ledger.melt_quote(
|
||||
PostMeltQuoteRequest(request=payload.pr, unit="sat")
|
||||
)
|
||||
fees_sat = quote.fee_reserve
|
||||
logger.trace(f"< POST /checkfees: {fees_sat}")
|
||||
return CheckFeesResponse_deprecated(fee=fees_sat)
|
||||
|
||||
|
||||
@router_deprecated.post(
|
||||
"/split",
|
||||
name="Split",
|
||||
summary="Split proofs at a specified amount",
|
||||
# response_model=Union[
|
||||
# PostSplitResponse_Very_Deprecated, PostSplitResponse_Deprecated
|
||||
# ],
|
||||
response_description=(
|
||||
"A list of blinded signatures that can be used to create proofs."
|
||||
),
|
||||
deprecated=True,
|
||||
)
|
||||
async def split_deprecated(
|
||||
payload: PostSplitRequest_Deprecated,
|
||||
# ) -> Union[PostSplitResponse_Very_Deprecated, PostSplitResponse_Deprecated]:
|
||||
):
|
||||
"""
|
||||
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.")
|
||||
# BEGIN BACKWARDS COMPATIBILITY < 0.14: add "id" to outputs
|
||||
if payload.outputs:
|
||||
for output in payload.outputs:
|
||||
if not output.id:
|
||||
output.id = ledger.keyset.id
|
||||
# END BACKWARDS COMPATIBILITY < 0.14
|
||||
promises = await ledger.split(proofs=payload.proofs, outputs=payload.outputs)
|
||||
|
||||
if payload.amount:
|
||||
# BEGIN backwards compatibility < 0.13
|
||||
# old clients expect two lists of promises where the second one's amounts
|
||||
# sum up to `amount`. The first one is the rest.
|
||||
# The returned value `promises` has the form [keep1, keep2, ..., send1, send2, ...]
|
||||
# The sum of the sendx is `amount`. We need to split this into two lists and keep the order of the elements.
|
||||
frst_promises: List[BlindedSignature] = []
|
||||
scnd_promises: List[BlindedSignature] = []
|
||||
scnd_amount = 0
|
||||
for promise in promises[::-1]: # we iterate backwards
|
||||
if scnd_amount < payload.amount:
|
||||
scnd_promises.insert(0, promise) # and insert at the beginning
|
||||
scnd_amount += promise.amount
|
||||
else:
|
||||
frst_promises.insert(0, promise) # and insert at the beginning
|
||||
logger.trace(
|
||||
f"Split into keep: {len(frst_promises)}:"
|
||||
f" {sum([p.amount for p in frst_promises])} sat and send:"
|
||||
f" {len(scnd_promises)}: {sum([p.amount for p in scnd_promises])} sat"
|
||||
)
|
||||
return PostSplitResponse_Very_Deprecated(fst=frst_promises, snd=scnd_promises)
|
||||
# END backwards compatibility < 0.13
|
||||
else:
|
||||
return PostSplitResponse_Deprecated(promises=promises)
|
||||
|
||||
|
||||
@router_deprecated.post(
|
||||
"/check",
|
||||
name="Check proof state",
|
||||
summary="Check whether a proof is spent already or is pending in a transaction",
|
||||
response_model=CheckSpendableResponse_deprecated,
|
||||
response_description=(
|
||||
"Two lists of booleans indicating whether the provided proofs "
|
||||
"are spendable or pending in a transaction respectively."
|
||||
),
|
||||
deprecated=True,
|
||||
)
|
||||
async def check_spendable(
|
||||
payload: CheckSpendableRequest_deprecated,
|
||||
) -> CheckSpendableResponse_deprecated:
|
||||
"""Check whether a secret has been spent already or not."""
|
||||
logger.trace(f"> POST /check: {payload}")
|
||||
proofs_state = await ledger.check_proofs_state([p.secret for p in payload.proofs])
|
||||
spendableList: List[bool] = []
|
||||
pendingList: List[bool] = []
|
||||
for proof_state in proofs_state:
|
||||
if proof_state.state == SpentState.unspent:
|
||||
spendableList.append(True)
|
||||
pendingList.append(False)
|
||||
elif proof_state.state == SpentState.spent:
|
||||
spendableList.append(False)
|
||||
pendingList.append(False)
|
||||
elif proof_state.state == SpentState.pending:
|
||||
spendableList.append(True)
|
||||
pendingList.append(True)
|
||||
return CheckSpendableResponse_deprecated(
|
||||
spendable=spendableList, pending=pendingList
|
||||
)
|
||||
|
||||
|
||||
@router_deprecated.post(
|
||||
"/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."
|
||||
),
|
||||
deprecated=True,
|
||||
)
|
||||
async def restore(payload: PostMintRequest_deprecated) -> PostRestoreResponse:
|
||||
assert payload.outputs, Exception("no outputs provided.")
|
||||
outputs, promises = await ledger.restore(payload.outputs)
|
||||
return PostRestoreResponse(outputs=outputs, promises=promises)
|
||||
@@ -6,11 +6,12 @@ import importlib
|
||||
|
||||
from loguru import logger
|
||||
|
||||
from ..core.base import Method, Unit
|
||||
from ..core.db import Database
|
||||
from ..core.migrations import migrate_databases
|
||||
from ..core.settings import settings
|
||||
from ..mint import migrations
|
||||
from ..mint.crud import LedgerCrud
|
||||
from ..mint.crud import LedgerCrudSqlite
|
||||
from ..mint.ledger import Ledger
|
||||
|
||||
logger.debug("Enviroment Settings:")
|
||||
@@ -22,16 +23,29 @@ lightning_backend = getattr(wallets_module, settings.mint_lightning_backend)()
|
||||
|
||||
assert settings.mint_private_key is not None, "No mint private key set."
|
||||
|
||||
# strike_backend = getattr(wallets_module, "StrikeUSDWallet")()
|
||||
# backends = {
|
||||
# Method.bolt11: {Unit.sat: lightning_backend, Unit.usd: strike_backend},
|
||||
# }
|
||||
# backends = {
|
||||
# Method.bolt11: {Unit.sat: lightning_backend, Unit.msat: lightning_backend},
|
||||
# }
|
||||
# backends = {
|
||||
# Method.bolt11: {Unit.sat: lightning_backend, Unit.msat: lightning_backend,
|
||||
# }
|
||||
backends = {
|
||||
Method.bolt11: {Unit.sat: lightning_backend},
|
||||
}
|
||||
ledger = Ledger(
|
||||
db=Database("mint", settings.mint_database),
|
||||
seed=settings.mint_private_key,
|
||||
derivation_path=settings.mint_derivation_path,
|
||||
lightning=lightning_backend,
|
||||
crud=LedgerCrud(),
|
||||
backends=backends,
|
||||
crud=LedgerCrudSqlite(),
|
||||
)
|
||||
|
||||
|
||||
async def rotate_keys(n_seconds=10):
|
||||
async def rotate_keys(n_seconds=60):
|
||||
"""Rotate keyset epoch every n_seconds.
|
||||
Note: This is just a helper function for testing purposes.
|
||||
"""
|
||||
@@ -39,8 +53,10 @@ async def rotate_keys(n_seconds=10):
|
||||
while True:
|
||||
i += 1
|
||||
logger.info("Rotating keys.")
|
||||
ledger.derivation_path = f"0/0/0/{i}"
|
||||
await ledger.init_keysets()
|
||||
incremented_derivation_path = (
|
||||
"/".join(ledger.derivation_path.split("/")[:-1]) + f"/{i}"
|
||||
)
|
||||
await ledger.activate_keyset(incremented_derivation_path)
|
||||
logger.info(f"Current keyset: {ledger.keyset.id}")
|
||||
await asyncio.sleep(n_seconds)
|
||||
|
||||
@@ -51,16 +67,24 @@ async def start_mint_init():
|
||||
await ledger.load_used_proofs()
|
||||
await ledger.init_keysets()
|
||||
|
||||
if settings.lightning:
|
||||
logger.info(f"Using backend: {settings.mint_lightning_backend}")
|
||||
status = await ledger.lightning.status()
|
||||
for derivation_path in settings.mint_derivation_path_list:
|
||||
await ledger.activate_keyset(derivation_path)
|
||||
|
||||
for method in ledger.backends:
|
||||
for unit in ledger.backends[method]:
|
||||
logger.info(
|
||||
f"Using {ledger.backends[method][unit].__class__.__name__} backend for"
|
||||
f" method: '{method.name}' and unit: '{unit.name}'"
|
||||
)
|
||||
status = await ledger.backends[method][unit].status()
|
||||
if status.error_message:
|
||||
logger.warning(
|
||||
f"The backend for {ledger.lightning.__class__.__name__} isn't"
|
||||
"The backend for"
|
||||
f" {ledger.backends[method][unit].__class__.__name__} isn't"
|
||||
f" working properly: '{status.error_message}'",
|
||||
RuntimeWarning,
|
||||
)
|
||||
logger.info(f"Lightning balance: {status.balance_msat} msat")
|
||||
logger.info(f"Backend balance: {status.balance} {unit.name}")
|
||||
|
||||
logger.info(f"Data dir: {settings.cashu_dir}")
|
||||
logger.info("Mint started.")
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
from typing import List, Literal, Optional, Set, Union
|
||||
from typing import Dict, List, Literal, Optional, Union
|
||||
|
||||
from loguru import logger
|
||||
|
||||
@@ -6,7 +6,6 @@ from ..core.base import (
|
||||
BlindedMessage,
|
||||
BlindedSignature,
|
||||
MintKeyset,
|
||||
MintKeysets,
|
||||
Proof,
|
||||
)
|
||||
from ..core.crypto import b_dhke
|
||||
@@ -29,20 +28,20 @@ class LedgerVerification(LedgerSpendingConditions, SupportsKeysets, SupportsDb):
|
||||
"""Verification functions for the ledger."""
|
||||
|
||||
keyset: MintKeyset
|
||||
keysets: MintKeysets
|
||||
secrets_used: Set[str] = set()
|
||||
keysets: Dict[str, MintKeyset]
|
||||
spent_proofs: Dict[str, Proof]
|
||||
crud: LedgerCrud
|
||||
db: Database
|
||||
|
||||
async def verify_inputs_and_outputs(
|
||||
self, proofs: List[Proof], outputs: Optional[List[BlindedMessage]] = None
|
||||
self, *, proofs: List[Proof], outputs: Optional[List[BlindedMessage]] = None
|
||||
):
|
||||
"""Checks all proofs and outputs for validity.
|
||||
|
||||
Args:
|
||||
proofs (List[Proof]): List of proofs to check.
|
||||
outputs (Optional[List[BlindedMessage]], optional): List of outputs to check.
|
||||
Must be provided for /split but not for /melt. Defaults to None.
|
||||
Must be provided for a swap but not for a melt. Defaults to None.
|
||||
|
||||
Raises:
|
||||
Exception: Scripts did not validate.
|
||||
@@ -52,8 +51,8 @@ class LedgerVerification(LedgerSpendingConditions, SupportsKeysets, SupportsDb):
|
||||
"""
|
||||
# Verify inputs
|
||||
# Verify proofs are spendable
|
||||
spendable = await self._check_proofs_spendable(proofs)
|
||||
if not all(spendable):
|
||||
spent_proofs = await self._get_proofs_spent([p.secret for p in proofs])
|
||||
if not len(spent_proofs) == 0:
|
||||
raise TokenAlreadySpentError()
|
||||
# Verify amounts of inputs
|
||||
if not all([self._verify_amount(p.amount) for p in proofs]):
|
||||
@@ -83,12 +82,31 @@ class LedgerVerification(LedgerSpendingConditions, SupportsKeysets, SupportsDb):
|
||||
# Verify inputs and outputs together
|
||||
if not self._verify_input_output_amounts(proofs, outputs):
|
||||
raise TransactionError("input amounts less than output.")
|
||||
# Verify that input keyset units are the same as output keyset unit
|
||||
# We have previously verified that all outputs have the same keyset id in `_verify_outputs`
|
||||
assert outputs[0].id, "output id not set"
|
||||
if not all([
|
||||
self.keysets[p.id].unit == self.keysets[outputs[0].id].unit
|
||||
for p in proofs
|
||||
if p.id
|
||||
]):
|
||||
raise TransactionError("input and output keysets have different units.")
|
||||
|
||||
# Verify output spending conditions
|
||||
if outputs and not self._verify_output_spending_conditions(proofs, outputs):
|
||||
raise TransactionError("validation of output spending conditions failed.")
|
||||
|
||||
async def _verify_outputs(self, outputs: List[BlindedMessage]):
|
||||
"""Verify that the outputs are valid."""
|
||||
logger.trace(f"Verifying {len(outputs)} outputs.")
|
||||
# Verify all outputs have the same keyset id
|
||||
if not all([o.id == outputs[0].id for o in outputs]):
|
||||
raise TransactionError("outputs have different keyset ids.")
|
||||
# Verify that the keyset id is known and active
|
||||
if outputs[0].id not in self.keysets:
|
||||
raise TransactionError("keyset id unknown.")
|
||||
if not self.keysets[outputs[0].id].active:
|
||||
raise TransactionError("keyset id inactive.")
|
||||
# Verify amounts of outputs
|
||||
if not all([self._verify_amount(o.amount) for o in outputs]):
|
||||
raise TransactionError("invalid amount.")
|
||||
@@ -98,6 +116,7 @@ class LedgerVerification(LedgerSpendingConditions, SupportsKeysets, SupportsDb):
|
||||
# verify that outputs have not been signed previously
|
||||
if any(await self._check_outputs_issued_before(outputs)):
|
||||
raise TransactionError("outputs have already been signed before.")
|
||||
logger.trace(f"Verified {len(outputs)} outputs.")
|
||||
|
||||
async def _check_outputs_issued_before(self, outputs: List[BlindedMessage]):
|
||||
"""Checks whether the provided outputs have previously been signed by the mint
|
||||
@@ -118,24 +137,32 @@ class LedgerVerification(LedgerSpendingConditions, SupportsKeysets, SupportsDb):
|
||||
result.append(False if promise is None else True)
|
||||
return result
|
||||
|
||||
async def _check_proofs_spendable(self, proofs: List[Proof]) -> List[bool]:
|
||||
"""Checks whether the proof was already spent."""
|
||||
spendable_states = []
|
||||
async def _get_proofs_pending(self, secrets: List[str]) -> Dict[str, Proof]:
|
||||
"""Returns only those proofs that are pending."""
|
||||
all_proofs_pending = await self.crud.get_proofs_pending(db=self.db)
|
||||
proofs_pending = list(filter(lambda p: p.secret in secrets, all_proofs_pending))
|
||||
proofs_pending_dict = {p.secret: p for p in proofs_pending}
|
||||
return proofs_pending_dict
|
||||
|
||||
async def _get_proofs_spent(self, secrets: List[str]) -> Dict[str, Proof]:
|
||||
"""Returns all proofs that are spent."""
|
||||
proofs_spent: List[Proof] = []
|
||||
if settings.mint_cache_secrets:
|
||||
# check used secrets in memory
|
||||
for p in proofs:
|
||||
spendable_state = p.secret not in self.secrets_used
|
||||
spendable_states.append(spendable_state)
|
||||
for secret in secrets:
|
||||
if secret in self.spent_proofs:
|
||||
proofs_spent.append(self.spent_proofs[secret])
|
||||
else:
|
||||
# check used secrets in database
|
||||
async with self.db.connect() as conn:
|
||||
for p in proofs:
|
||||
spendable_state = (
|
||||
await self.crud.get_proof_used(db=self.db, proof=p, conn=conn)
|
||||
is None
|
||||
for secret in secrets:
|
||||
spent_proof = await self.crud.get_proof_used(
|
||||
db=self.db, secret=secret, conn=conn
|
||||
)
|
||||
spendable_states.append(spendable_state)
|
||||
return spendable_states
|
||||
if spent_proof:
|
||||
proofs_spent.append(spent_proof)
|
||||
proofs_spent_dict = {p.secret: p for p in proofs_spent}
|
||||
return proofs_spent_dict
|
||||
|
||||
def _verify_secret_criteria(self, proof: Proof) -> Literal[True]:
|
||||
"""Verifies that a secret is present and is not too long (DOS prevention)."""
|
||||
@@ -145,23 +172,27 @@ class LedgerVerification(LedgerSpendingConditions, SupportsKeysets, SupportsDb):
|
||||
raise SecretTooLongError()
|
||||
return True
|
||||
|
||||
def _verify_proof_bdhke(self, proof: Proof):
|
||||
def _verify_proof_bdhke(self, proof: Proof) -> bool:
|
||||
"""Verifies that the proof of promise was issued by this ledger."""
|
||||
# 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]
|
||||
else:
|
||||
assert proof.id in self.keysets.keysets, f"keyset {proof.id} unknown"
|
||||
assert proof.id in self.keysets, f"keyset {proof.id} unknown"
|
||||
logger.trace(
|
||||
f"Validating proof with keyset {self.keysets.keysets[proof.id].id}."
|
||||
f"Validating proof {proof.secret} with keyset"
|
||||
f" {self.keysets[proof.id].id}."
|
||||
)
|
||||
# use the appropriate active keyset for this proof.id
|
||||
private_key_amount = self.keysets.keysets[proof.id].private_keys[
|
||||
proof.amount
|
||||
]
|
||||
private_key_amount = self.keysets[proof.id].private_keys[proof.amount]
|
||||
|
||||
C = PublicKey(bytes.fromhex(proof.C), raw=True)
|
||||
return b_dhke.verify(private_key_amount, C, proof.secret)
|
||||
valid = b_dhke.verify(private_key_amount, C, proof.secret)
|
||||
if valid:
|
||||
logger.trace("Proof verified.")
|
||||
else:
|
||||
logger.trace(f"Proof verification failed for {proof.secret} – {proof.C}.")
|
||||
return valid
|
||||
|
||||
def _verify_input_output_amounts(
|
||||
self, inputs: List[Proof], outputs: List[BlindedMessage]
|
||||
|
||||
@@ -45,9 +45,9 @@ class NostrClient:
|
||||
def connect(self):
|
||||
for relay in self.relays:
|
||||
self.relay_manager.add_relay(relay)
|
||||
self.relay_manager.open_connections(
|
||||
{"cert_reqs": ssl.CERT_NONE}
|
||||
) # NOTE: This disables ssl certificate verification
|
||||
self.relay_manager.open_connections({
|
||||
"cert_reqs": ssl.CERT_NONE
|
||||
}) # NOTE: This disables ssl certificate verification
|
||||
|
||||
def close(self):
|
||||
self.relay_manager.close_connections()
|
||||
@@ -105,15 +105,13 @@ class NostrClient:
|
||||
self.relay_manager.publish_event(dm)
|
||||
|
||||
def get_dm(self, sender_publickey: PublicKey, callback_func=None, filter_kwargs={}):
|
||||
filters = Filters(
|
||||
[
|
||||
filters = Filters([
|
||||
Filter(
|
||||
kinds=[EventKind.ENCRYPTED_DIRECT_MESSAGE],
|
||||
pubkey_refs=[sender_publickey.hex()],
|
||||
**filter_kwargs,
|
||||
)
|
||||
]
|
||||
)
|
||||
])
|
||||
subscription_id = os.urandom(4).hex()
|
||||
self.relay_manager.add_subscription(subscription_id, filters)
|
||||
|
||||
|
||||
@@ -77,8 +77,7 @@ class Event:
|
||||
)
|
||||
|
||||
def to_message(self) -> str:
|
||||
return json.dumps(
|
||||
[
|
||||
return json.dumps([
|
||||
ClientMessageType.EVENT,
|
||||
{
|
||||
"id": self.id,
|
||||
@@ -89,8 +88,7 @@ class Event:
|
||||
"content": self.content,
|
||||
"sig": self.signature,
|
||||
},
|
||||
]
|
||||
)
|
||||
])
|
||||
|
||||
|
||||
@dataclass
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
from ...core.base import TokenV3
|
||||
from ...wallet.crud import get_keyset
|
||||
from ...wallet.crud import get_keysets
|
||||
|
||||
|
||||
async def verify_mints(wallet, tokenObj: TokenV3):
|
||||
@@ -9,5 +9,5 @@ async def verify_mints(wallet, tokenObj: TokenV3):
|
||||
raise Exception("Token has missing mint information.")
|
||||
for mint in mints:
|
||||
assert mint
|
||||
mint_keysets = await get_keyset(mint_url=mint, db=wallet.db)
|
||||
assert mint_keysets, "We don't know this mint."
|
||||
mint_keysets = await get_keysets(mint_url=mint, db=wallet.db)
|
||||
assert len(mint_keysets), "We don't know this mint."
|
||||
|
||||
@@ -92,7 +92,9 @@ async def pay(
|
||||
if mint:
|
||||
wallet = await mint_wallet(mint)
|
||||
payment_response = await wallet.pay_invoice(bolt11)
|
||||
return payment_response
|
||||
ret = PaymentResponse(**payment_response.dict())
|
||||
ret.fee = None # TODO: we can't return an Amount object, overwriting
|
||||
return ret
|
||||
|
||||
|
||||
@router.get(
|
||||
@@ -162,10 +164,8 @@ async def lightning_balance() -> StatusResponse:
|
||||
try:
|
||||
await wallet.load_proofs(reload=True)
|
||||
except Exception as exc:
|
||||
return StatusResponse(error_message=str(exc), balance_msat=0)
|
||||
return StatusResponse(
|
||||
error_message=None, balance_msat=wallet.available_balance * 1000
|
||||
)
|
||||
return StatusResponse(error_message=str(exc), balance=0)
|
||||
return StatusResponse(error_message=None, balance=wallet.available_balance * 1000)
|
||||
|
||||
|
||||
@router.post(
|
||||
@@ -179,8 +179,6 @@ async def swap(
|
||||
outgoing_mint: str = Query(default=..., description="URL of outgoing mint"),
|
||||
incoming_mint: str = Query(default=..., description="URL of incoming mint"),
|
||||
):
|
||||
if not settings.lightning:
|
||||
raise Exception("lightning not supported")
|
||||
incoming_wallet = await mint_wallet(incoming_mint)
|
||||
outgoing_wallet = await mint_wallet(outgoing_mint)
|
||||
if incoming_wallet.url == outgoing_wallet.url:
|
||||
@@ -191,17 +189,17 @@ async def swap(
|
||||
|
||||
# pay invoice from outgoing mint
|
||||
await outgoing_wallet.load_proofs(reload=True)
|
||||
total_amount, fee_reserve_sat = await outgoing_wallet.get_pay_amount_with_fees(
|
||||
invoice.bolt11
|
||||
)
|
||||
assert total_amount > 0, "amount must be positive"
|
||||
quote = await outgoing_wallet.get_pay_amount_with_fees(invoice.bolt11)
|
||||
total_amount = quote.amount + quote.fee_reserve
|
||||
if outgoing_wallet.available_balance < total_amount:
|
||||
raise Exception("balance too low")
|
||||
|
||||
_, send_proofs = await outgoing_wallet.split_to_send(
|
||||
outgoing_wallet.proofs, total_amount, set_reserved=True
|
||||
)
|
||||
await outgoing_wallet.pay_lightning(send_proofs, invoice.bolt11, fee_reserve_sat)
|
||||
await outgoing_wallet.pay_lightning(
|
||||
send_proofs, invoice.bolt11, quote.fee_reserve, quote.quote
|
||||
)
|
||||
|
||||
# mint token in incoming mint
|
||||
await incoming_wallet.mint(amount, id=invoice.id)
|
||||
@@ -325,9 +323,9 @@ async def burn(
|
||||
proofs = tokenObj.get_proofs()
|
||||
|
||||
if delete:
|
||||
await wallet.invalidate(proofs, check_spendable=False)
|
||||
else:
|
||||
await wallet.invalidate(proofs)
|
||||
else:
|
||||
await wallet.invalidate(proofs, check_spendable=True)
|
||||
return BurnResponse(balance=wallet.available_balance)
|
||||
|
||||
|
||||
@@ -361,8 +359,7 @@ async def pending(
|
||||
reserved_date = datetime.utcfromtimestamp(
|
||||
int(grouped_proofs[0].time_reserved) # type: ignore
|
||||
).strftime("%Y-%m-%d %H:%M:%S")
|
||||
result.update(
|
||||
{
|
||||
result.update({
|
||||
f"{i}": {
|
||||
"amount": sum_proofs(grouped_proofs),
|
||||
"time": reserved_date,
|
||||
@@ -370,8 +367,7 @@ async def pending(
|
||||
"token": token,
|
||||
"mint": mint,
|
||||
}
|
||||
}
|
||||
)
|
||||
})
|
||||
return PendingResponse(pending_token=result)
|
||||
|
||||
|
||||
@@ -416,22 +412,20 @@ async def wallets():
|
||||
if w == wallet.name:
|
||||
active_wallet = True
|
||||
if active_wallet:
|
||||
result.update(
|
||||
{
|
||||
result.update({
|
||||
f"{w}": {
|
||||
"balance": sum_proofs(wallet.proofs),
|
||||
"available": sum_proofs(
|
||||
[p for p in wallet.proofs if not p.reserved]
|
||||
),
|
||||
"available": sum_proofs([
|
||||
p for p in wallet.proofs if not p.reserved
|
||||
]),
|
||||
}
|
||||
}
|
||||
)
|
||||
})
|
||||
except Exception:
|
||||
pass
|
||||
return WalletsResponse(wallets=result)
|
||||
|
||||
|
||||
@router.post("/restore", name="Restore wallet", response_model=RestoreResponse)
|
||||
@router.post("/v1/restore", name="Restore wallet", response_model=RestoreResponse)
|
||||
async def restore(
|
||||
to: int = Query(default=..., description="Counter to which restore the wallet"),
|
||||
):
|
||||
@@ -439,8 +433,7 @@ async def restore(
|
||||
raise Exception("Counter must be positive")
|
||||
await wallet.load_mint()
|
||||
await wallet.restore_promises_from_to(0, to)
|
||||
await wallet.invalidate(wallet.proofs)
|
||||
wallet.status()
|
||||
await wallet.invalidate(wallet.proofs, check_spendable=True)
|
||||
return RestoreResponse(balance=wallet.available_balance)
|
||||
|
||||
|
||||
|
||||
@@ -14,7 +14,9 @@ import click
|
||||
from click import Context
|
||||
from loguru import logger
|
||||
|
||||
from ...core.base import TokenV3
|
||||
from cashu.core.logging import configure_logger
|
||||
|
||||
from ...core.base import TokenV3, Unit
|
||||
from ...core.helpers import sum_proofs
|
||||
from ...core.settings import settings
|
||||
from ...nostr.client.client import NostrClient
|
||||
@@ -26,7 +28,13 @@ from ...wallet.crud import (
|
||||
)
|
||||
from ...wallet.wallet import Wallet as Wallet
|
||||
from ..api.api_server import start_api_server
|
||||
from ..cli.cli_helpers import get_mint_wallet, print_mint_balances, verify_mint
|
||||
from ..cli.cli_helpers import (
|
||||
get_mint_wallet,
|
||||
get_unit_wallet,
|
||||
print_balance,
|
||||
print_mint_balances,
|
||||
verify_mint,
|
||||
)
|
||||
from ..helpers import (
|
||||
deserialize_token_from_string,
|
||||
init_wallet,
|
||||
@@ -74,6 +82,13 @@ def coro(f):
|
||||
default=settings.wallet_name,
|
||||
help=f"Wallet name (default: {settings.wallet_name}).",
|
||||
)
|
||||
@click.option(
|
||||
"--unit",
|
||||
"-u",
|
||||
"unit",
|
||||
default=None,
|
||||
help=f"Wallet unit (default: {settings.wallet_unit}).",
|
||||
)
|
||||
@click.option(
|
||||
"--daemon",
|
||||
"-d",
|
||||
@@ -92,7 +107,9 @@ def coro(f):
|
||||
)
|
||||
@click.pass_context
|
||||
@coro
|
||||
async def cli(ctx: Context, host: str, walletname: str, tests: bool):
|
||||
async def cli(ctx: Context, host: str, walletname: str, unit: str, tests: bool):
|
||||
if settings.debug:
|
||||
configure_logger()
|
||||
if settings.tor and not TorProxy().check_platform():
|
||||
error_str = (
|
||||
"Your settings say TOR=true but the built-in Tor bundle is not supported on"
|
||||
@@ -120,6 +137,7 @@ async def cli(ctx: Context, host: str, walletname: str, tests: bool):
|
||||
|
||||
ctx.ensure_object(dict)
|
||||
ctx.obj["HOST"] = host or settings.mint_url
|
||||
ctx.obj["UNIT"] = unit
|
||||
ctx.obj["WALLET_NAME"] = walletname
|
||||
settings.wallet_name = walletname
|
||||
|
||||
@@ -128,13 +146,13 @@ async def cli(ctx: Context, host: str, walletname: str, tests: bool):
|
||||
# otherwise it will create a mnemonic and store it in the database
|
||||
if ctx.invoked_subcommand == "restore":
|
||||
wallet = await Wallet.with_db(
|
||||
ctx.obj["HOST"], db_path, name=walletname, skip_private_key=True
|
||||
ctx.obj["HOST"], db_path, name=walletname, skip_db_read=True
|
||||
)
|
||||
else:
|
||||
# # we need to run the migrations before we load the wallet for the first time
|
||||
# # otherwise the wallet will not be able to generate a new private key and store it
|
||||
wallet = await Wallet.with_db(
|
||||
ctx.obj["HOST"], db_path, name=walletname, skip_private_key=True
|
||||
ctx.obj["HOST"], db_path, name=walletname, skip_db_read=True
|
||||
)
|
||||
# now with the migrations done, we can load the wallet and generate a new mnemonic if needed
|
||||
wallet = await Wallet.with_db(ctx.obj["HOST"], db_path, name=walletname)
|
||||
@@ -143,11 +161,13 @@ async def cli(ctx: Context, host: str, walletname: str, tests: bool):
|
||||
ctx.obj["WALLET"] = wallet
|
||||
# await init_wallet(ctx.obj["WALLET"], load_proofs=False)
|
||||
|
||||
# ------ MUTLIMINT ------- : Select a wallet
|
||||
# only if a command is one of a subset that needs to specify a mint host
|
||||
# if a mint host is already specified as an argument `host`, use it
|
||||
if ctx.invoked_subcommand not in ["send", "invoice", "pay"] or host:
|
||||
return
|
||||
# ------ MULTIUNIT ------- : Select a unit
|
||||
ctx.obj["WALLET"] = await get_unit_wallet(ctx)
|
||||
# ------ MUTLIMINT ------- : Select a wallet
|
||||
# else: we ask the user to select one
|
||||
ctx.obj["WALLET"] = await get_mint_wallet(
|
||||
ctx
|
||||
@@ -165,13 +185,17 @@ async def cli(ctx: Context, host: str, walletname: str, tests: bool):
|
||||
async def pay(ctx: Context, invoice: str, yes: bool):
|
||||
wallet: Wallet = ctx.obj["WALLET"]
|
||||
await wallet.load_mint()
|
||||
wallet.status()
|
||||
total_amount, fee_reserve_sat = await wallet.get_pay_amount_with_fees(invoice)
|
||||
print_balance(ctx)
|
||||
quote = await wallet.get_pay_amount_with_fees(invoice)
|
||||
logger.debug(f"Quote: {quote}")
|
||||
total_amount = quote.amount + quote.fee_reserve
|
||||
if not yes:
|
||||
potential = (
|
||||
f" ({total_amount} sat with potential fees)" if fee_reserve_sat else ""
|
||||
f" ({wallet.unit.str(total_amount)} with potential fees)"
|
||||
if quote.fee_reserve
|
||||
else ""
|
||||
)
|
||||
message = f"Pay {total_amount - fee_reserve_sat} sat{potential}?"
|
||||
message = f"Pay {wallet.unit.str(quote.amount)}{potential}?"
|
||||
click.confirm(
|
||||
message,
|
||||
abort=True,
|
||||
@@ -186,22 +210,21 @@ async def pay(ctx: Context, invoice: str, yes: bool):
|
||||
_, send_proofs = await wallet.split_to_send(wallet.proofs, total_amount)
|
||||
try:
|
||||
melt_response = await wallet.pay_lightning(
|
||||
send_proofs, invoice, fee_reserve_sat
|
||||
send_proofs, invoice, quote.fee_reserve, quote.quote
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
print(f"\nError paying invoice: {str(e)}")
|
||||
print(f" Error paying invoice: {str(e)}")
|
||||
return
|
||||
print(" Invoice paid", end="", flush=True)
|
||||
if melt_response.preimage and melt_response.preimage != "0" * 64:
|
||||
print(f" (Proof: {melt_response.preimage}).")
|
||||
if melt_response.payment_preimage and melt_response.payment_preimage != "0" * 64:
|
||||
print(f" (Preimage: {melt_response.payment_preimage}).")
|
||||
else:
|
||||
print(".")
|
||||
wallet.status()
|
||||
print_balance(ctx)
|
||||
|
||||
|
||||
@cli.command("invoice", help="Create Lighting invoice.")
|
||||
@click.argument("amount", type=int)
|
||||
@click.argument("amount", type=float)
|
||||
@click.option("--id", default="", help="Id of the paid invoice.", type=str)
|
||||
@click.option(
|
||||
"--split",
|
||||
@@ -223,7 +246,8 @@ async def pay(ctx: Context, invoice: str, yes: bool):
|
||||
async def invoice(ctx: Context, amount: int, id: str, split: int, no_check: bool):
|
||||
wallet: Wallet = ctx.obj["WALLET"]
|
||||
await wallet.load_mint()
|
||||
wallet.status()
|
||||
print_balance(ctx)
|
||||
amount = int(amount * 100) if wallet.unit == Unit.usd else int(amount)
|
||||
# in case the user wants a specific split, we create a list of amounts
|
||||
optional_split = None
|
||||
if split:
|
||||
@@ -231,15 +255,16 @@ async def invoice(ctx: Context, amount: int, id: str, split: int, no_check: bool
|
||||
assert amount >= split, "split must smaller or equal amount"
|
||||
n_splits = amount // split
|
||||
optional_split = [split] * n_splits
|
||||
logger.debug(f"Requesting split with {n_splits} * {split} sat tokens.")
|
||||
logger.debug(
|
||||
f"Requesting split with {n_splits} * {wallet.unit.str(split)} tokens."
|
||||
)
|
||||
|
||||
if not settings.lightning:
|
||||
await wallet.mint(amount, split=optional_split)
|
||||
# user requests an invoice
|
||||
elif amount and not id:
|
||||
if amount and not id:
|
||||
invoice = await wallet.request_mint(amount)
|
||||
if invoice.bolt11:
|
||||
print(f"Pay invoice to mint {amount} sat:")
|
||||
print("")
|
||||
print(f"Pay invoice to mint {wallet.unit.str(amount)}:")
|
||||
print("")
|
||||
print(f"Invoice: {invoice.bolt11}")
|
||||
print("")
|
||||
@@ -280,7 +305,8 @@ async def invoice(ctx: Context, amount: int, id: str, split: int, no_check: bool
|
||||
# user paid invoice and want to check it
|
||||
elif amount and id:
|
||||
await wallet.mint(amount, split=optional_split, id=id)
|
||||
wallet.status()
|
||||
print("")
|
||||
print_balance(ctx)
|
||||
return
|
||||
|
||||
|
||||
@@ -288,8 +314,6 @@ async def invoice(ctx: Context, amount: int, id: str, split: int, no_check: bool
|
||||
@click.pass_context
|
||||
@coro
|
||||
async def swap(ctx: Context):
|
||||
if not settings.lightning:
|
||||
raise Exception("lightning not supported.")
|
||||
print("Select the mint to swap from:")
|
||||
outgoing_wallet = await get_mint_wallet(ctx, force_select=True)
|
||||
|
||||
@@ -302,22 +326,23 @@ async def swap(ctx: Context):
|
||||
if incoming_wallet.url == outgoing_wallet.url:
|
||||
raise Exception("mints for swap have to be different")
|
||||
|
||||
amount = int(input("Enter amount to swap in sat: "))
|
||||
amount = int(input(f"Enter amount to swap in {incoming_wallet.unit.name}: "))
|
||||
assert amount > 0, "amount is not positive"
|
||||
|
||||
# request invoice from incoming mint
|
||||
invoice = await incoming_wallet.request_mint(amount)
|
||||
|
||||
# pay invoice from outgoing mint
|
||||
total_amount, fee_reserve_sat = await outgoing_wallet.get_pay_amount_with_fees(
|
||||
invoice.bolt11
|
||||
)
|
||||
quote = await outgoing_wallet.get_pay_amount_with_fees(invoice.bolt11)
|
||||
total_amount = quote.amount + quote.fee_reserve
|
||||
if outgoing_wallet.available_balance < total_amount:
|
||||
raise Exception("balance too low")
|
||||
_, send_proofs = await outgoing_wallet.split_to_send(
|
||||
outgoing_wallet.proofs, total_amount, set_reserved=True
|
||||
)
|
||||
await outgoing_wallet.pay_lightning(send_proofs, invoice.bolt11, fee_reserve_sat)
|
||||
await outgoing_wallet.pay_lightning(
|
||||
send_proofs, invoice.bolt11, quote.fee_reserve, quote.quote
|
||||
)
|
||||
|
||||
# mint token in incoming mint
|
||||
await incoming_wallet.mint(amount, id=invoice.id)
|
||||
@@ -339,34 +364,44 @@ async def swap(ctx: Context):
|
||||
@coro
|
||||
async def balance(ctx: Context, verbose):
|
||||
wallet: Wallet = ctx.obj["WALLET"]
|
||||
await wallet.load_proofs()
|
||||
await wallet.load_proofs(unit=False)
|
||||
unit_balances = wallet.balance_per_unit()
|
||||
if len(unit_balances) > 1 and not ctx.obj["UNIT"]:
|
||||
print(f"You have balances in {len(unit_balances)} units:")
|
||||
print("")
|
||||
for i, (k, v) in enumerate(unit_balances.items()):
|
||||
unit = k
|
||||
print(f"Unit {i+1} ({unit}) – Balance: {unit.str(int(v['available']))}")
|
||||
print("")
|
||||
if verbose:
|
||||
# show balances per keyset
|
||||
keyset_balances = wallet.balance_per_keyset()
|
||||
if len(keyset_balances) > 1:
|
||||
if len(keyset_balances):
|
||||
print(f"You have balances in {len(keyset_balances)} keysets:")
|
||||
print("")
|
||||
for k, v in keyset_balances.items():
|
||||
for k, v in keyset_balances.items(): # type: ignore
|
||||
unit = Unit[str(v["unit"])]
|
||||
print(
|
||||
f"Keyset: {k} - Balance: {v['available']} sat (pending:"
|
||||
f" {v['balance']-v['available']} sat)"
|
||||
f"Keyset: {k} - Balance: {unit.str(int(v['available']))} (pending:"
|
||||
f" {unit.str(int(v['balance'])-int(v['available']))})"
|
||||
)
|
||||
print("")
|
||||
|
||||
await print_mint_balances(wallet)
|
||||
|
||||
await wallet.load_proofs(reload=True)
|
||||
if verbose:
|
||||
print(
|
||||
f"Balance: {wallet.available_balance} sat (pending:"
|
||||
f" {wallet.balance-wallet.available_balance} sat) in"
|
||||
f"Balance: {wallet.unit.str(wallet.available_balance)} (pending:"
|
||||
f" {wallet.unit.str(wallet.balance-wallet.available_balance)}) in"
|
||||
f" {len([p for p in wallet.proofs if not p.reserved])} tokens"
|
||||
)
|
||||
else:
|
||||
print(f"Balance: {wallet.available_balance} sat")
|
||||
print(f"Balance: {wallet.unit.str(wallet.available_balance)}")
|
||||
|
||||
|
||||
@cli.command("send", help="Send tokens.")
|
||||
@click.argument("amount", type=int)
|
||||
@click.argument("amount", type=float)
|
||||
@click.argument("nostr", type=str, required=False)
|
||||
@click.option(
|
||||
"--nostr",
|
||||
@@ -426,6 +461,7 @@ async def send_command(
|
||||
nosplit: bool,
|
||||
):
|
||||
wallet: Wallet = ctx.obj["WALLET"]
|
||||
amount = int(amount * 100) if wallet.unit == Unit.usd else int(amount)
|
||||
if not nostr and not nopt:
|
||||
await send(
|
||||
wallet,
|
||||
@@ -439,6 +475,7 @@ async def send_command(
|
||||
await send_nostr(
|
||||
wallet, amount=amount, pubkey=nostr or nopt, verbose=verbose, yes=yes
|
||||
)
|
||||
print_balance(ctx)
|
||||
|
||||
|
||||
@cli.command("receive", help="Receive tokens.")
|
||||
@@ -493,6 +530,8 @@ async def receive_cli(
|
||||
await receive(wallet, tokenObj)
|
||||
else:
|
||||
print("Error: enter token or use either flag --nostr or --all.")
|
||||
return
|
||||
print_balance(ctx)
|
||||
|
||||
|
||||
@cli.command("burn", help="Burn spent tokens.")
|
||||
@@ -536,10 +575,10 @@ async def burn(ctx: Context, token: str, all: bool, force: bool, delete: str):
|
||||
proofs = tokenObj.get_proofs()
|
||||
|
||||
if delete:
|
||||
await wallet.invalidate(proofs, check_spendable=False)
|
||||
else:
|
||||
await wallet.invalidate(proofs)
|
||||
wallet.status()
|
||||
else:
|
||||
await wallet.invalidate(proofs, check_spendable=True)
|
||||
print_balance(ctx)
|
||||
|
||||
|
||||
@cli.command("pending", help="Show pending tokens.")
|
||||
@@ -591,7 +630,8 @@ async def pending(ctx: Context, legacy, number: int, offset: int):
|
||||
int(grouped_proofs[0].time_reserved)
|
||||
).strftime("%Y-%m-%d %H:%M:%S")
|
||||
print(
|
||||
f"#{i} Amount: {sum_proofs(grouped_proofs)} sat Time:"
|
||||
f"#{i} Amount:"
|
||||
f" {wallet.unit.str(sum_proofs(grouped_proofs))} Time:"
|
||||
f" {reserved_date} ID: {key} Mint: {mint}\n"
|
||||
)
|
||||
print(f"{token}\n")
|
||||
@@ -697,9 +737,10 @@ async def wallets(ctx):
|
||||
if w == ctx.obj["WALLET_NAME"]:
|
||||
active_wallet = True
|
||||
print(
|
||||
f"Wallet: {w}\tBalance: {sum_proofs(wallet.proofs)} sat"
|
||||
f"Wallet: {w}\tBalance:"
|
||||
f" {wallet.unit.str(sum_proofs(wallet.proofs))}"
|
||||
" (available: "
|
||||
f"{sum_proofs([p for p in wallet.proofs if not p.reserved])} sat){' *' if active_wallet else ''}"
|
||||
f"{wallet.unit.str(sum_proofs([p for p in wallet.proofs if not p.reserved]))}){' *' if active_wallet else ''}"
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
@@ -737,25 +778,32 @@ async def info(ctx: Context, mint: bool, mnemonic: bool):
|
||||
if mint:
|
||||
for mint_url in mint_list:
|
||||
wallet.url = mint_url
|
||||
try:
|
||||
mint_info: dict = (await wallet._load_mint_info()).dict()
|
||||
print("")
|
||||
print("Mint information:")
|
||||
print("---- Mint information ----")
|
||||
print("")
|
||||
print(f"Mint URL: {mint_url}")
|
||||
if mint_info:
|
||||
print(f"Mint name: {mint_info['name']}")
|
||||
if mint_info["description"]:
|
||||
if mint_info.get("description"):
|
||||
print(f"Description: {mint_info['description']}")
|
||||
if mint_info["description_long"]:
|
||||
if mint_info.get("description_long"):
|
||||
print(f"Long description: {mint_info['description_long']}")
|
||||
if mint_info["contact"]:
|
||||
if mint_info.get("contact"):
|
||||
print(f"Contact: {mint_info['contact']}")
|
||||
if mint_info["version"]:
|
||||
if mint_info.get("version"):
|
||||
print(f"Version: {mint_info['version']}")
|
||||
if mint_info["motd"]:
|
||||
if mint_info.get("motd"):
|
||||
print(f"Message of the day: {mint_info['motd']}")
|
||||
if mint_info["parameter"]:
|
||||
print(f"Parameter: {mint_info['parameter']}")
|
||||
if mint_info.get("nuts"):
|
||||
print(
|
||||
"Supported NUTS:"
|
||||
f" {', '.join(['NUT-'+str(k) for k in mint_info['nuts'].keys()])}"
|
||||
)
|
||||
except Exception as e:
|
||||
print("")
|
||||
print(f"Error fetching mint information for {mint_url}: {e}")
|
||||
|
||||
if mnemonic:
|
||||
assert wallet.mnemonic
|
||||
@@ -807,7 +855,7 @@ async def restore(ctx: Context, to: int, batch: int):
|
||||
|
||||
await wallet.restore_wallet_from_mnemonic(mnemonic, to=to, batch=batch)
|
||||
await wallet.load_proofs()
|
||||
wallet.status()
|
||||
print_balance(ctx)
|
||||
|
||||
|
||||
@cli.command("selfpay", help="Refresh tokens.")
|
||||
@@ -820,7 +868,7 @@ async def selfpay(ctx: Context, all: bool = False):
|
||||
|
||||
# get balance on this mint
|
||||
mint_balance_dict = await wallet.balance_per_minturl()
|
||||
mint_balance = mint_balance_dict[wallet.url]["available"]
|
||||
mint_balance = int(mint_balance_dict[wallet.url]["available"])
|
||||
# send balance once to mark as reserved
|
||||
await wallet.split_to_send(wallet.proofs, mint_balance, None, set_reserved=True)
|
||||
# load all reserved proofs (including the one we just sent)
|
||||
|
||||
@@ -4,11 +4,62 @@ import click
|
||||
from click import Context
|
||||
from loguru import logger
|
||||
|
||||
from ...core.base import Unit
|
||||
from ...core.settings import settings
|
||||
from ...wallet.crud import get_keyset
|
||||
from ...wallet.crud import get_keysets
|
||||
from ...wallet.wallet import Wallet as Wallet
|
||||
|
||||
|
||||
def print_balance(ctx: Context):
|
||||
wallet: Wallet = ctx.obj["WALLET"]
|
||||
print(f"Balance: {wallet.unit.str(wallet.available_balance)}")
|
||||
|
||||
|
||||
async def get_unit_wallet(ctx: Context, force_select: bool = False):
|
||||
"""Helper function that asks the user for an input to select which unit they want to load.
|
||||
|
||||
Args:
|
||||
ctx (Context): Context
|
||||
force_select (bool, optional): Force the user to select a unit. Defaults to False.
|
||||
"""
|
||||
wallet: Wallet = ctx.obj["WALLET"]
|
||||
await wallet.load_proofs(reload=True, unit=False)
|
||||
# show balances per unit
|
||||
unit_balances = wallet.balance_per_unit()
|
||||
if ctx.obj["UNIT"] in [u.name for u in unit_balances] and not force_select:
|
||||
wallet.unit = Unit[ctx.obj["UNIT"]]
|
||||
elif len(unit_balances) > 1 and not ctx.obj["UNIT"]:
|
||||
print(f"You have balances in {len(unit_balances)} units:")
|
||||
print("")
|
||||
for i, (k, v) in enumerate(unit_balances.items()):
|
||||
unit = k
|
||||
print(f"Unit {i+1} ({unit}) – Balance: {unit.str(int(v['available']))}")
|
||||
print("")
|
||||
unit_nr_str = input(
|
||||
f"Select unit [1-{len(unit_balances)}] or "
|
||||
f"press enter for your default '{Unit[settings.wallet_unit]}': "
|
||||
)
|
||||
if not unit_nr_str: # default unit
|
||||
unit = Unit[settings.wallet_unit]
|
||||
elif unit_nr_str.isdigit() and int(unit_nr_str) <= len(
|
||||
unit_balances
|
||||
): # specific unit
|
||||
unit = list(unit_balances.keys())[int(unit_nr_str) - 1]
|
||||
else:
|
||||
raise Exception("invalid input.")
|
||||
|
||||
print(f"Selected unit: {unit}")
|
||||
print("")
|
||||
# load this unit into a wallet
|
||||
wallet.unit = unit
|
||||
elif len(unit_balances) == 1 and not ctx.obj["UNIT"]:
|
||||
wallet.unit = list(unit_balances.keys())[0]
|
||||
elif ctx.obj["UNIT"]:
|
||||
wallet.unit = Unit[ctx.obj["UNIT"]]
|
||||
settings.wallet_unit = wallet.unit.name
|
||||
return wallet
|
||||
|
||||
|
||||
async def get_mint_wallet(ctx: Context, force_select: bool = False):
|
||||
"""
|
||||
Helper function that asks the user for an input to select which mint they want to load.
|
||||
@@ -56,18 +107,18 @@ async def get_mint_wallet(ctx: Context, force_select: bool = False):
|
||||
return mint_wallet
|
||||
|
||||
|
||||
async def print_mint_balances(wallet, show_mints=False):
|
||||
async def print_mint_balances(wallet: Wallet, show_mints: bool = False):
|
||||
"""
|
||||
Helper function that prints the balances for each mint URL that we have tokens from.
|
||||
"""
|
||||
# get balances per mint
|
||||
mint_balances = await wallet.balance_per_minturl()
|
||||
|
||||
mint_balances = await wallet.balance_per_minturl(unit=wallet.unit)
|
||||
# if we have a balance on a non-default mint, we show its URL
|
||||
keysets = [k for k, v in wallet.balance_per_keyset().items()]
|
||||
for k in keysets:
|
||||
ks = await get_keyset(id=str(k), db=wallet.db)
|
||||
if ks and ks.mint_url != wallet.url:
|
||||
keysets_local = await get_keysets(id=str(k), db=wallet.db)
|
||||
for kl in keysets_local:
|
||||
if kl and kl.mint_url != wallet.url:
|
||||
show_mints = True
|
||||
|
||||
# or we have a balance on more than one mint
|
||||
@@ -76,9 +127,10 @@ async def print_mint_balances(wallet, show_mints=False):
|
||||
print(f"You have balances in {len(mint_balances)} mints:")
|
||||
print("")
|
||||
for i, (k, v) in enumerate(mint_balances.items()):
|
||||
unit = Unit[str(v["unit"])]
|
||||
print(
|
||||
f"Mint {i+1}: Balance: {v['available']} sat (pending:"
|
||||
f" {v['balance']-v['available']} sat) URL: {k}"
|
||||
f"Mint {i+1}: Balance: {unit.str(int(v['available']))} (pending:"
|
||||
f" {unit.str(int(v['balance'])-int(v['available']))}) URL: {k}"
|
||||
)
|
||||
print("")
|
||||
|
||||
@@ -93,7 +145,7 @@ async def verify_mint(mint_wallet: Wallet, url: str):
|
||||
# dummy Wallet to check the database later
|
||||
# mint_wallet = Wallet(url, os.path.join(settings.cashu_dir, ctx.obj["WALLET_NAME"]))
|
||||
# we check the db whether we know this mint already and ask the user if not
|
||||
mint_keysets = await get_keyset(mint_url=url, db=mint_wallet.db)
|
||||
mint_keysets = await get_keysets(mint_url=url, db=mint_wallet.db)
|
||||
if mint_keysets is None:
|
||||
# we encountered a new mint and ask for a user confirmation
|
||||
print("")
|
||||
@@ -107,4 +159,4 @@ async def verify_mint(mint_wallet: Wallet, url: str):
|
||||
default=True,
|
||||
)
|
||||
else:
|
||||
logger.debug(f"We know keyset {mint_keysets.id} already")
|
||||
logger.debug(f"We know mint {url} already")
|
||||
|
||||
@@ -167,8 +167,8 @@ async def store_keyset(
|
||||
await (conn or db).execute( # type: ignore
|
||||
"""
|
||||
INSERT INTO keysets
|
||||
(id, mint_url, valid_from, valid_to, first_seen, active, public_keys)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?)
|
||||
(id, mint_url, valid_from, valid_to, first_seen, active, public_keys, unit)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
||||
""",
|
||||
(
|
||||
keyset.id,
|
||||
@@ -176,18 +176,19 @@ async def store_keyset(
|
||||
keyset.valid_from or int(time.time()),
|
||||
keyset.valid_to or int(time.time()),
|
||||
keyset.first_seen or int(time.time()),
|
||||
True,
|
||||
keyset.active,
|
||||
keyset.serialize(),
|
||||
keyset.unit.name,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
async def get_keyset(
|
||||
async def get_keysets(
|
||||
id: str = "",
|
||||
mint_url: str = "",
|
||||
db: Optional[Database] = None,
|
||||
conn: Optional[Connection] = None,
|
||||
) -> Optional[WalletKeyset]:
|
||||
) -> List[WalletKeyset]:
|
||||
clauses = []
|
||||
values: List[Any] = []
|
||||
clauses.append("active = ?")
|
||||
@@ -202,14 +203,18 @@ async def get_keyset(
|
||||
if clauses:
|
||||
where = f"WHERE {' AND '.join(clauses)}"
|
||||
|
||||
row = await (conn or db).fetchone( # type: ignore
|
||||
row = await (conn or db).fetchall( # type: ignore
|
||||
f"""
|
||||
SELECT * from keysets
|
||||
{where}
|
||||
""",
|
||||
tuple(values),
|
||||
)
|
||||
return WalletKeyset.from_row(row) if row is not None else None
|
||||
ret = []
|
||||
for r in row:
|
||||
keyset = WalletKeyset.from_row(r)
|
||||
ret.append(keyset)
|
||||
return ret
|
||||
|
||||
|
||||
async def store_lightning_invoice(
|
||||
|
||||
@@ -10,7 +10,7 @@ from ..core.helpers import sum_proofs
|
||||
from ..core.migrations import migrate_databases
|
||||
from ..core.settings import settings
|
||||
from ..wallet import migrations
|
||||
from ..wallet.crud import get_keyset
|
||||
from ..wallet.crud import get_keysets
|
||||
from ..wallet.wallet import Wallet
|
||||
|
||||
|
||||
@@ -47,15 +47,16 @@ async def redeem_TokenV3_multimint(wallet: Wallet, token: TokenV3):
|
||||
mint_wallet = await Wallet.with_db(
|
||||
t.mint, os.path.join(settings.cashu_dir, wallet.name)
|
||||
)
|
||||
keysets = mint_wallet._get_proofs_keysets(t.proofs)
|
||||
logger.debug(f"Keysets in tokens: {keysets}")
|
||||
keyset_ids = mint_wallet._get_proofs_keysets(t.proofs)
|
||||
logger.trace(f"Keysets in tokens: {keyset_ids}")
|
||||
# loop over all keysets
|
||||
for keyset in set(keysets):
|
||||
await mint_wallet.load_mint()
|
||||
for keyset_id in set(keyset_ids):
|
||||
await mint_wallet.load_mint(keyset_id)
|
||||
mint_wallet.unit = mint_wallet.keysets[keyset_id].unit
|
||||
# redeem proofs of this keyset
|
||||
redeem_proofs = [p for p in t.proofs if p.id == keyset]
|
||||
redeem_proofs = [p for p in t.proofs if p.id == keyset_id]
|
||||
_, _ = await mint_wallet.redeem(redeem_proofs)
|
||||
print(f"Received {sum_proofs(redeem_proofs)} sats")
|
||||
print(f"Received {mint_wallet.unit.str(sum_proofs(redeem_proofs))}")
|
||||
|
||||
|
||||
def serialize_TokenV2_to_TokenV3(tokenv2: TokenV2):
|
||||
@@ -138,21 +139,21 @@ async def receive(
|
||||
keyset_in_token = proofs[0].id
|
||||
assert keyset_in_token
|
||||
# we get the keyset from the db
|
||||
mint_keysets = await get_keyset(id=keyset_in_token, db=wallet.db)
|
||||
assert mint_keysets, Exception("we don't know this keyset")
|
||||
assert mint_keysets.mint_url, Exception("we don't know this mint's URL")
|
||||
mint_keysets = await get_keysets(id=keyset_in_token, db=wallet.db)
|
||||
assert mint_keysets, Exception(f"we don't know this keyset: {keyset_in_token}")
|
||||
mint_keyset = mint_keysets[0]
|
||||
assert mint_keyset.mint_url, Exception("we don't know this mint's URL")
|
||||
# now we have the URL
|
||||
mint_wallet = await Wallet.with_db(
|
||||
mint_keysets.mint_url,
|
||||
mint_keyset.mint_url,
|
||||
os.path.join(settings.cashu_dir, wallet.name),
|
||||
)
|
||||
await mint_wallet.load_mint(keyset_in_token)
|
||||
_, _ = await mint_wallet.redeem(proofs)
|
||||
print(f"Received {sum_proofs(proofs)} sats")
|
||||
print(f"Received {mint_wallet.unit.str(sum_proofs(proofs))}")
|
||||
|
||||
# reload main wallet so the balance updates
|
||||
await wallet.load_proofs(reload=True)
|
||||
wallet.status()
|
||||
return wallet.available_balance
|
||||
|
||||
|
||||
@@ -224,5 +225,4 @@ async def send(
|
||||
)
|
||||
print(token)
|
||||
|
||||
wallet.status()
|
||||
return wallet.available_balance, token
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import bolt11
|
||||
|
||||
from ...core.base import Amount, SpentState, Unit
|
||||
from ...core.helpers import sum_promises
|
||||
from ...core.settings import settings
|
||||
from ...lightning.base import (
|
||||
@@ -54,25 +55,28 @@ class LightningWallet(Wallet):
|
||||
Returns:
|
||||
bool: True if successful
|
||||
"""
|
||||
total_amount, fee_reserve_sat = await self.get_pay_amount_with_fees(pr)
|
||||
quote = await self.get_pay_amount_with_fees(pr)
|
||||
total_amount = quote.amount + quote.fee_reserve
|
||||
assert total_amount > 0, "amount is not positive"
|
||||
if self.available_balance < total_amount:
|
||||
print("Error: Balance too low.")
|
||||
return PaymentResponse(ok=False)
|
||||
_, send_proofs = await self.split_to_send(self.proofs, total_amount)
|
||||
try:
|
||||
resp = await self.pay_lightning(send_proofs, pr, fee_reserve_sat)
|
||||
resp = await self.pay_lightning(
|
||||
send_proofs, pr, quote.fee_reserve, quote.quote
|
||||
)
|
||||
if resp.change:
|
||||
fees_paid_sat = fee_reserve_sat - sum_promises(resp.change)
|
||||
fees_paid_sat = quote.fee_reserve - sum_promises(resp.change)
|
||||
else:
|
||||
fees_paid_sat = fee_reserve_sat
|
||||
fees_paid_sat = quote.fee_reserve
|
||||
|
||||
invoice_obj = bolt11.decode(pr)
|
||||
return PaymentResponse(
|
||||
ok=True,
|
||||
checking_id=invoice_obj.payment_hash,
|
||||
preimage=resp.preimage,
|
||||
fee_msat=fees_paid_sat * 1000,
|
||||
preimage=resp.payment_preimage,
|
||||
fee=Amount(Unit.msat, fees_paid_sat),
|
||||
)
|
||||
except Exception as e:
|
||||
print("Exception:", e)
|
||||
@@ -126,19 +130,15 @@ class LightningWallet(Wallet):
|
||||
if not proofs:
|
||||
return PaymentStatus(paid=False) # "proofs not fount (in db)"
|
||||
proofs_states = await self.check_proof_state(proofs)
|
||||
if (
|
||||
not proofs_states
|
||||
or not proofs_states.spendable
|
||||
or not proofs_states.pending
|
||||
):
|
||||
if not proofs_states:
|
||||
return PaymentStatus(paid=False) # "states not fount"
|
||||
|
||||
if all(proofs_states.spendable) and all(proofs_states.pending):
|
||||
if all([p.state == SpentState.pending for p in proofs_states.states]):
|
||||
return PaymentStatus(paid=None) # "pending (with check)"
|
||||
if not any(proofs_states.spendable) and not any(proofs_states.pending):
|
||||
if any([p.state == SpentState.spent for p in proofs_states.states]):
|
||||
# NOTE: consider adding this check in wallet.py and mark the invoice as paid if all proofs are spent
|
||||
return PaymentStatus(paid=True) # "paid (with check)"
|
||||
if all(proofs_states.spendable) and not any(proofs_states.pending):
|
||||
if all([p.state == SpentState.unspent for p in proofs_states.states]):
|
||||
return PaymentStatus(paid=False) # "failed (with check)"
|
||||
return PaymentStatus(paid=None) # "undefined state"
|
||||
|
||||
@@ -148,6 +148,4 @@ class LightningWallet(Wallet):
|
||||
Returns:
|
||||
int: balance in satoshis
|
||||
"""
|
||||
return StatusResponse(
|
||||
error_message=None, balance_msat=self.available_balance * 1000
|
||||
)
|
||||
return StatusResponse(error_message=None, balance=self.available_balance * 1000)
|
||||
|
||||
@@ -196,9 +196,11 @@ async def m010_add_ids_to_proofs_and_out_to_invoices(db: Database):
|
||||
Columns that store mint and melt id for proofs and invoices.
|
||||
"""
|
||||
async with db.connect() as conn:
|
||||
print("Running wallet migrations")
|
||||
await conn.execute("ALTER TABLE proofs ADD COLUMN mint_id TEXT")
|
||||
await conn.execute("ALTER TABLE proofs_used ADD COLUMN mint_id TEXT")
|
||||
await conn.execute("ALTER TABLE proofs ADD COLUMN melt_id TEXT")
|
||||
|
||||
await conn.execute("ALTER TABLE proofs_used ADD COLUMN mint_id TEXT")
|
||||
await conn.execute("ALTER TABLE proofs_used ADD COLUMN melt_id TEXT")
|
||||
|
||||
# column in invoices for marking whether the invoice is incoming (out=False) or outgoing (out=True)
|
||||
@@ -209,3 +211,10 @@ async def m010_add_ids_to_proofs_and_out_to_invoices(db: Database):
|
||||
await conn.execute("ALTER TABLE invoices RENAME COLUMN hash TO id")
|
||||
# add column payment_hash
|
||||
await conn.execute("ALTER TABLE invoices ADD COLUMN payment_hash TEXT")
|
||||
|
||||
|
||||
async def m011_keysets_add_unit(db: Database):
|
||||
async with db.connect() as conn:
|
||||
# add column for storing the unit of a keyset
|
||||
await conn.execute("ALTER TABLE keysets ADD COLUMN unit TEXT")
|
||||
await conn.execute("UPDATE keysets SET unit = 'sat'")
|
||||
|
||||
@@ -133,12 +133,9 @@ class WalletP2PK(SupportsPrivateKey, SupportsDb):
|
||||
return outputs
|
||||
|
||||
# if any of the proofs provided require SIG_ALL, we must provide it
|
||||
if any(
|
||||
[
|
||||
P2PKSecret.deserialize(p.secret).sigflag == SigFlags.SIG_ALL
|
||||
for p in proofs
|
||||
]
|
||||
):
|
||||
if any([
|
||||
P2PKSecret.deserialize(p.secret).sigflag == SigFlags.SIG_ALL for p in proofs
|
||||
]):
|
||||
outputs = await self.add_p2pk_witnesses_to_outputs(outputs)
|
||||
return outputs
|
||||
|
||||
@@ -184,9 +181,9 @@ class WalletP2PK(SupportsPrivateKey, SupportsDb):
|
||||
return proofs
|
||||
logger.debug("Spending conditions detected.")
|
||||
# P2PK signatures
|
||||
if all(
|
||||
[Secret.deserialize(p.secret).kind == SecretKind.P2PK.value for p in proofs]
|
||||
):
|
||||
if all([
|
||||
Secret.deserialize(p.secret).kind == SecretKind.P2PK.value for p in proofs
|
||||
]):
|
||||
logger.debug("P2PK redemption detected.")
|
||||
proofs = await self.add_p2pk_witnesses_to_proofs(proofs)
|
||||
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
from typing import Protocol
|
||||
|
||||
import httpx
|
||||
|
||||
from ..core.crypto.secp import PrivateKey
|
||||
from ..core.db import Database
|
||||
|
||||
@@ -14,3 +16,11 @@ class SupportsDb(Protocol):
|
||||
|
||||
class SupportsKeysets(Protocol):
|
||||
keyset_id: str
|
||||
|
||||
|
||||
class SupportsHttpxClient(Protocol):
|
||||
httpx: httpx.AsyncClient
|
||||
|
||||
|
||||
class SupportsMintURL(Protocol):
|
||||
url: str
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import base64
|
||||
import hashlib
|
||||
import os
|
||||
from typing import List, Optional, Tuple
|
||||
|
||||
from bip32 import BIP32
|
||||
@@ -73,8 +74,8 @@ class WalletSecrets(SupportsDb, SupportsKeysets):
|
||||
self.seed = mnemo.to_seed(mnemonic_str)
|
||||
self.mnemonic = mnemonic_str
|
||||
|
||||
logger.debug(f"Using seed: {self.seed.hex()}")
|
||||
logger.debug(f"Using mnemonic: {mnemonic_str}")
|
||||
# logger.debug(f"Using seed: {self.seed.hex()}")
|
||||
# logger.debug(f"Using mnemonic: {mnemonic_str}")
|
||||
|
||||
# if no mnemonic was in the database, store the new one
|
||||
if ret_db is None:
|
||||
@@ -92,20 +93,21 @@ class WalletSecrets(SupportsDb, SupportsKeysets):
|
||||
except Exception as e:
|
||||
logger.error(e)
|
||||
|
||||
async def _generate_secret(self, randombits=128) -> str:
|
||||
async def _generate_secret(self) -> str:
|
||||
"""Returns base64 encoded deterministic random string.
|
||||
|
||||
NOTE: This method should probably retire after `deterministic_secrets`. We are
|
||||
deriving secrets from a counter but don't store the respective blinding factor.
|
||||
We won't be able to restore any ecash generated with these secrets.
|
||||
"""
|
||||
secret_counter = await bump_secret_derivation(
|
||||
db=self.db, keyset_id=self.keyset_id
|
||||
)
|
||||
logger.trace(f"secret_counter: {secret_counter}")
|
||||
s, _, _ = await self.generate_determinstic_secret(secret_counter)
|
||||
# return s.decode("utf-8")
|
||||
return hashlib.sha256(s).hexdigest()
|
||||
# secret_counter = await bump_secret_derivation(db=self.db, keyset_id=keyset_id)
|
||||
# logger.trace(f"secret_counter: {secret_counter}")
|
||||
# s, _, _ = await self.generate_determinstic_secret(secret_counter, keyset_id)
|
||||
# # return s.decode("utf-8")
|
||||
# return hashlib.sha256(s).hexdigest()
|
||||
|
||||
# return random 32 byte hex string
|
||||
return hashlib.sha256(os.urandom(32)).hexdigest()
|
||||
|
||||
async def generate_determinstic_secret(
|
||||
self, counter: int
|
||||
@@ -116,11 +118,20 @@ class WalletSecrets(SupportsDb, SupportsKeysets):
|
||||
"""
|
||||
assert self.bip32, "BIP32 not initialized yet."
|
||||
# integer keyset id modulo max number of bip32 child keys
|
||||
keyest_id = int.from_bytes(base64.b64decode(self.keyset_id), "big") % (
|
||||
try:
|
||||
keyest_id_int = int.from_bytes(bytes.fromhex(self.keyset_id), "big") % (
|
||||
2**31 - 1
|
||||
)
|
||||
logger.trace(f"keyset id: {self.keyset_id} becomes {keyest_id}")
|
||||
token_derivation_path = f"m/129372'/0'/{keyest_id}'/{counter}'"
|
||||
except ValueError:
|
||||
# BEGIN: BACKWARDS COMPATIBILITY < 0.15.0 keyset id is not hex
|
||||
# calculate an integer keyset id from the base64 encoded keyset id
|
||||
keyest_id_int = int.from_bytes(base64.b64decode(self.keyset_id), "big") % (
|
||||
2**31 - 1
|
||||
)
|
||||
# END: BACKWARDS COMPATIBILITY < 0.15.0 keyset id is not hex
|
||||
|
||||
logger.trace(f"keyset id: {self.keyset_id} becomes {keyest_id_int}")
|
||||
token_derivation_path = f"m/129372'/0'/{keyest_id_int}'/{counter}'"
|
||||
# for secret
|
||||
secret_derivation_path = f"{token_derivation_path}/0"
|
||||
logger.trace(f"secret derivation path: {secret_derivation_path}")
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
425
cashu/wallet/wallet_deprecated.py
Normal file
425
cashu/wallet/wallet_deprecated.py
Normal file
@@ -0,0 +1,425 @@
|
||||
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)
|
||||
262
poetry.lock
generated
262
poetry.lock
generated
@@ -2,24 +2,24 @@
|
||||
|
||||
[[package]]
|
||||
name = "anyio"
|
||||
version = "4.0.0"
|
||||
version = "3.7.1"
|
||||
description = "High level compatibility layer for multiple asynchronous event loop implementations"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
python-versions = ">=3.7"
|
||||
files = [
|
||||
{file = "anyio-4.0.0-py3-none-any.whl", hash = "sha256:cfdb2b588b9fc25ede96d8db56ed50848b0b649dca3dd1df0b11f683bb9e0b5f"},
|
||||
{file = "anyio-4.0.0.tar.gz", hash = "sha256:f7ed51751b2c2add651e5747c891b47e26d2a21be5d32d9311dfe9692f3e5d7a"},
|
||||
{file = "anyio-3.7.1-py3-none-any.whl", hash = "sha256:91dee416e570e92c64041bd18b900d1d6fa78dff7048769ce5ac5ddad004fbb5"},
|
||||
{file = "anyio-3.7.1.tar.gz", hash = "sha256:44a3c9aba0f5defa43261a8b3efb97891f2bd7d804e0e1f56419befa1adfc780"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
exceptiongroup = {version = ">=1.0.2", markers = "python_version < \"3.11\""}
|
||||
exceptiongroup = {version = "*", markers = "python_version < \"3.11\""}
|
||||
idna = ">=2.8"
|
||||
sniffio = ">=1.1"
|
||||
|
||||
[package.extras]
|
||||
doc = ["Sphinx (>=7)", "packaging", "sphinx-autodoc-typehints (>=1.2.0)"]
|
||||
test = ["anyio[trio]", "coverage[toml] (>=7)", "hypothesis (>=4.0)", "psutil (>=5.9)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "uvloop (>=0.17)"]
|
||||
trio = ["trio (>=0.22)"]
|
||||
doc = ["Sphinx", "packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx-rtd-theme (>=1.2.2)", "sphinxcontrib-jquery"]
|
||||
test = ["anyio[trio]", "coverage[toml] (>=4.5)", "hypothesis (>=4.0)", "mock (>=4)", "psutil (>=5.9)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "uvloop (>=0.17)"]
|
||||
trio = ["trio (<0.22)"]
|
||||
|
||||
[[package]]
|
||||
name = "asn1crypto"
|
||||
@@ -104,33 +104,29 @@ files = [
|
||||
|
||||
[[package]]
|
||||
name = "black"
|
||||
version = "23.9.1"
|
||||
version = "23.11.0"
|
||||
description = "The uncompromising code formatter."
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "black-23.9.1-cp310-cp310-macosx_10_16_arm64.whl", hash = "sha256:d6bc09188020c9ac2555a498949401ab35bb6bf76d4e0f8ee251694664df6301"},
|
||||
{file = "black-23.9.1-cp310-cp310-macosx_10_16_universal2.whl", hash = "sha256:13ef033794029b85dfea8032c9d3b92b42b526f1ff4bf13b2182ce4e917f5100"},
|
||||
{file = "black-23.9.1-cp310-cp310-macosx_10_16_x86_64.whl", hash = "sha256:75a2dc41b183d4872d3a500d2b9c9016e67ed95738a3624f4751a0cb4818fe71"},
|
||||
{file = "black-23.9.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:13a2e4a93bb8ca74a749b6974925c27219bb3df4d42fc45e948a5d9feb5122b7"},
|
||||
{file = "black-23.9.1-cp310-cp310-win_amd64.whl", hash = "sha256:adc3e4442eef57f99b5590b245a328aad19c99552e0bdc7f0b04db6656debd80"},
|
||||
{file = "black-23.9.1-cp311-cp311-macosx_10_16_arm64.whl", hash = "sha256:8431445bf62d2a914b541da7ab3e2b4f3bc052d2ccbf157ebad18ea126efb91f"},
|
||||
{file = "black-23.9.1-cp311-cp311-macosx_10_16_universal2.whl", hash = "sha256:8fc1ddcf83f996247505db6b715294eba56ea9372e107fd54963c7553f2b6dfe"},
|
||||
{file = "black-23.9.1-cp311-cp311-macosx_10_16_x86_64.whl", hash = "sha256:7d30ec46de88091e4316b17ae58bbbfc12b2de05e069030f6b747dfc649ad186"},
|
||||
{file = "black-23.9.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:031e8c69f3d3b09e1aa471a926a1eeb0b9071f80b17689a655f7885ac9325a6f"},
|
||||
{file = "black-23.9.1-cp311-cp311-win_amd64.whl", hash = "sha256:538efb451cd50f43aba394e9ec7ad55a37598faae3348d723b59ea8e91616300"},
|
||||
{file = "black-23.9.1-cp38-cp38-macosx_10_16_arm64.whl", hash = "sha256:638619a559280de0c2aa4d76f504891c9860bb8fa214267358f0a20f27c12948"},
|
||||
{file = "black-23.9.1-cp38-cp38-macosx_10_16_universal2.whl", hash = "sha256:a732b82747235e0542c03bf352c126052c0fbc458d8a239a94701175b17d4855"},
|
||||
{file = "black-23.9.1-cp38-cp38-macosx_10_16_x86_64.whl", hash = "sha256:cf3a4d00e4cdb6734b64bf23cd4341421e8953615cba6b3670453737a72ec204"},
|
||||
{file = "black-23.9.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cf99f3de8b3273a8317681d8194ea222f10e0133a24a7548c73ce44ea1679377"},
|
||||
{file = "black-23.9.1-cp38-cp38-win_amd64.whl", hash = "sha256:14f04c990259576acd093871e7e9b14918eb28f1866f91968ff5524293f9c573"},
|
||||
{file = "black-23.9.1-cp39-cp39-macosx_10_16_arm64.whl", hash = "sha256:c619f063c2d68f19b2d7270f4cf3192cb81c9ec5bc5ba02df91471d0b88c4c5c"},
|
||||
{file = "black-23.9.1-cp39-cp39-macosx_10_16_universal2.whl", hash = "sha256:6a3b50e4b93f43b34a9d3ef00d9b6728b4a722c997c99ab09102fd5efdb88325"},
|
||||
{file = "black-23.9.1-cp39-cp39-macosx_10_16_x86_64.whl", hash = "sha256:c46767e8df1b7beefb0899c4a95fb43058fa8500b6db144f4ff3ca38eb2f6393"},
|
||||
{file = "black-23.9.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:50254ebfa56aa46a9fdd5d651f9637485068a1adf42270148cd101cdf56e0ad9"},
|
||||
{file = "black-23.9.1-cp39-cp39-win_amd64.whl", hash = "sha256:403397c033adbc45c2bd41747da1f7fc7eaa44efbee256b53842470d4ac5a70f"},
|
||||
{file = "black-23.9.1-py3-none-any.whl", hash = "sha256:6ccd59584cc834b6d127628713e4b6b968e5f79572da66284532525a042549f9"},
|
||||
{file = "black-23.9.1.tar.gz", hash = "sha256:24b6b3ff5c6d9ea08a8888f6977eae858e1f340d7260cf56d70a49823236b62d"},
|
||||
{file = "black-23.11.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:dbea0bb8575c6b6303cc65017b46351dc5953eea5c0a59d7b7e3a2d2f433a911"},
|
||||
{file = "black-23.11.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:412f56bab20ac85927f3a959230331de5614aecda1ede14b373083f62ec24e6f"},
|
||||
{file = "black-23.11.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d136ef5b418c81660ad847efe0e55c58c8208b77a57a28a503a5f345ccf01394"},
|
||||
{file = "black-23.11.0-cp310-cp310-win_amd64.whl", hash = "sha256:6c1cac07e64433f646a9a838cdc00c9768b3c362805afc3fce341af0e6a9ae9f"},
|
||||
{file = "black-23.11.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cf57719e581cfd48c4efe28543fea3d139c6b6f1238b3f0102a9c73992cbb479"},
|
||||
{file = "black-23.11.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:698c1e0d5c43354ec5d6f4d914d0d553a9ada56c85415700b81dc90125aac244"},
|
||||
{file = "black-23.11.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:760415ccc20f9e8747084169110ef75d545f3b0932ee21368f63ac0fee86b221"},
|
||||
{file = "black-23.11.0-cp311-cp311-win_amd64.whl", hash = "sha256:58e5f4d08a205b11800332920e285bd25e1a75c54953e05502052738fe16b3b5"},
|
||||
{file = "black-23.11.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:45aa1d4675964946e53ab81aeec7a37613c1cb71647b5394779e6efb79d6d187"},
|
||||
{file = "black-23.11.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:4c44b7211a3a0570cc097e81135faa5f261264f4dfaa22bd5ee2875a4e773bd6"},
|
||||
{file = "black-23.11.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2a9acad1451632021ee0d146c8765782a0c3846e0e0ea46659d7c4f89d9b212b"},
|
||||
{file = "black-23.11.0-cp38-cp38-win_amd64.whl", hash = "sha256:fc7f6a44d52747e65a02558e1d807c82df1d66ffa80a601862040a43ec2e3142"},
|
||||
{file = "black-23.11.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:7f622b6822f02bfaf2a5cd31fdb7cd86fcf33dab6ced5185c35f5db98260b055"},
|
||||
{file = "black-23.11.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:250d7e60f323fcfc8ea6c800d5eba12f7967400eb6c2d21ae85ad31c204fb1f4"},
|
||||
{file = "black-23.11.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5133f5507007ba08d8b7b263c7aa0f931af5ba88a29beacc4b2dc23fcefe9c06"},
|
||||
{file = "black-23.11.0-cp39-cp39-win_amd64.whl", hash = "sha256:421f3e44aa67138ab1b9bfbc22ee3780b22fa5b291e4db8ab7eee95200726b07"},
|
||||
{file = "black-23.11.0-py3-none-any.whl", hash = "sha256:54caaa703227c6e0c87b76326d0862184729a69b73d3b7305b6288e1d830067e"},
|
||||
{file = "black-23.11.0.tar.gz", hash = "sha256:4c68855825ff432d197229846f971bc4d6666ce90492e5b02013bcaca4d9ab05"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
@@ -169,13 +165,13 @@ secp256k1 = "*"
|
||||
|
||||
[[package]]
|
||||
name = "certifi"
|
||||
version = "2023.7.22"
|
||||
version = "2023.11.17"
|
||||
description = "Python package for providing Mozilla's CA Bundle."
|
||||
optional = false
|
||||
python-versions = ">=3.6"
|
||||
files = [
|
||||
{file = "certifi-2023.7.22-py3-none-any.whl", hash = "sha256:92d6037539857d8206b8f6ae472e8b77db8058fec5937a1ef3f54304089edbb9"},
|
||||
{file = "certifi-2023.7.22.tar.gz", hash = "sha256:539cc1d13202e33ca466e88b2807e29f4c13049d6d87031a3c110744495cb082"},
|
||||
{file = "certifi-2023.11.17-py3-none-any.whl", hash = "sha256:e036ab49d5b79556f99cfc2d9320b34cfbe5be05c5871b51de9329f0603b0474"},
|
||||
{file = "certifi-2023.11.17.tar.gz", hash = "sha256:9b469f3a900bf28dc19b8cfbf8019bf47f7fdd1a65a1d4ffb98fc14166beb4d1"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -402,34 +398,34 @@ toml = ["tomli"]
|
||||
|
||||
[[package]]
|
||||
name = "cryptography"
|
||||
version = "41.0.4"
|
||||
version = "41.0.5"
|
||||
description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers."
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
files = [
|
||||
{file = "cryptography-41.0.4-cp37-abi3-macosx_10_12_universal2.whl", hash = "sha256:80907d3faa55dc5434a16579952ac6da800935cd98d14dbd62f6f042c7f5e839"},
|
||||
{file = "cryptography-41.0.4-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:35c00f637cd0b9d5b6c6bd11b6c3359194a8eba9c46d4e875a3660e3b400005f"},
|
||||
{file = "cryptography-41.0.4-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cecfefa17042941f94ab54f769c8ce0fe14beff2694e9ac684176a2535bf9714"},
|
||||
{file = "cryptography-41.0.4-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e40211b4923ba5a6dc9769eab704bdb3fbb58d56c5b336d30996c24fcf12aadb"},
|
||||
{file = "cryptography-41.0.4-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:23a25c09dfd0d9f28da2352503b23e086f8e78096b9fd585d1d14eca01613e13"},
|
||||
{file = "cryptography-41.0.4-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:2ed09183922d66c4ec5fdaa59b4d14e105c084dd0febd27452de8f6f74704143"},
|
||||
{file = "cryptography-41.0.4-cp37-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:5a0f09cefded00e648a127048119f77bc2b2ec61e736660b5789e638f43cc397"},
|
||||
{file = "cryptography-41.0.4-cp37-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:9eeb77214afae972a00dee47382d2591abe77bdae166bda672fb1e24702a3860"},
|
||||
{file = "cryptography-41.0.4-cp37-abi3-win32.whl", hash = "sha256:3b224890962a2d7b57cf5eeb16ccaafba6083f7b811829f00476309bce2fe0fd"},
|
||||
{file = "cryptography-41.0.4-cp37-abi3-win_amd64.whl", hash = "sha256:c880eba5175f4307129784eca96f4e70b88e57aa3f680aeba3bab0e980b0f37d"},
|
||||
{file = "cryptography-41.0.4-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:004b6ccc95943f6a9ad3142cfabcc769d7ee38a3f60fb0dddbfb431f818c3a67"},
|
||||
{file = "cryptography-41.0.4-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:86defa8d248c3fa029da68ce61fe735432b047e32179883bdb1e79ed9bb8195e"},
|
||||
{file = "cryptography-41.0.4-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:37480760ae08065437e6573d14be973112c9e6dcaf5f11d00147ee74f37a3829"},
|
||||
{file = "cryptography-41.0.4-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:b5f4dfe950ff0479f1f00eda09c18798d4f49b98f4e2006d644b3301682ebdca"},
|
||||
{file = "cryptography-41.0.4-pp38-pypy38_pp73-macosx_10_12_x86_64.whl", hash = "sha256:7e53db173370dea832190870e975a1e09c86a879b613948f09eb49324218c14d"},
|
||||
{file = "cryptography-41.0.4-pp38-pypy38_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:5b72205a360f3b6176485a333256b9bcd48700fc755fef51c8e7e67c4b63e3ac"},
|
||||
{file = "cryptography-41.0.4-pp38-pypy38_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:93530900d14c37a46ce3d6c9e6fd35dbe5f5601bf6b3a5c325c7bffc030344d9"},
|
||||
{file = "cryptography-41.0.4-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:efc8ad4e6fc4f1752ebfb58aefece8b4e3c4cae940b0994d43649bdfce8d0d4f"},
|
||||
{file = "cryptography-41.0.4-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:c3391bd8e6de35f6f1140e50aaeb3e2b3d6a9012536ca23ab0d9c35ec18c8a91"},
|
||||
{file = "cryptography-41.0.4-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:0d9409894f495d465fe6fda92cb70e8323e9648af912d5b9141d616df40a87b8"},
|
||||
{file = "cryptography-41.0.4-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:8ac4f9ead4bbd0bc8ab2d318f97d85147167a488be0e08814a37eb2f439d5cf6"},
|
||||
{file = "cryptography-41.0.4-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:047c4603aeb4bbd8db2756e38f5b8bd7e94318c047cfe4efeb5d715e08b49311"},
|
||||
{file = "cryptography-41.0.4.tar.gz", hash = "sha256:7febc3094125fc126a7f6fb1f420d0da639f3f32cb15c8ff0dc3997c4549f51a"},
|
||||
{file = "cryptography-41.0.5-cp37-abi3-macosx_10_12_universal2.whl", hash = "sha256:da6a0ff8f1016ccc7477e6339e1d50ce5f59b88905585f77193ebd5068f1e797"},
|
||||
{file = "cryptography-41.0.5-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:b948e09fe5fb18517d99994184854ebd50b57248736fd4c720ad540560174ec5"},
|
||||
{file = "cryptography-41.0.5-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d38e6031e113b7421db1de0c1b1f7739564a88f1684c6b89234fbf6c11b75147"},
|
||||
{file = "cryptography-41.0.5-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e270c04f4d9b5671ebcc792b3ba5d4488bf7c42c3c241a3748e2599776f29696"},
|
||||
{file = "cryptography-41.0.5-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:ec3b055ff8f1dce8e6ef28f626e0972981475173d7973d63f271b29c8a2897da"},
|
||||
{file = "cryptography-41.0.5-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:7d208c21e47940369accfc9e85f0de7693d9a5d843c2509b3846b2db170dfd20"},
|
||||
{file = "cryptography-41.0.5-cp37-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:8254962e6ba1f4d2090c44daf50a547cd5f0bf446dc658a8e5f8156cae0d8548"},
|
||||
{file = "cryptography-41.0.5-cp37-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:a48e74dad1fb349f3dc1d449ed88e0017d792997a7ad2ec9587ed17405667e6d"},
|
||||
{file = "cryptography-41.0.5-cp37-abi3-win32.whl", hash = "sha256:d3977f0e276f6f5bf245c403156673db103283266601405376f075c849a0b936"},
|
||||
{file = "cryptography-41.0.5-cp37-abi3-win_amd64.whl", hash = "sha256:73801ac9736741f220e20435f84ecec75ed70eda90f781a148f1bad546963d81"},
|
||||
{file = "cryptography-41.0.5-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:3be3ca726e1572517d2bef99a818378bbcf7d7799d5372a46c79c29eb8d166c1"},
|
||||
{file = "cryptography-41.0.5-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:e886098619d3815e0ad5790c973afeee2c0e6e04b4da90b88e6bd06e2a0b1b72"},
|
||||
{file = "cryptography-41.0.5-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:573eb7128cbca75f9157dcde974781209463ce56b5804983e11a1c462f0f4e88"},
|
||||
{file = "cryptography-41.0.5-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:0c327cac00f082013c7c9fb6c46b7cc9fa3c288ca702c74773968173bda421bf"},
|
||||
{file = "cryptography-41.0.5-pp38-pypy38_pp73-macosx_10_12_x86_64.whl", hash = "sha256:227ec057cd32a41c6651701abc0328135e472ed450f47c2766f23267b792a88e"},
|
||||
{file = "cryptography-41.0.5-pp38-pypy38_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:22892cc830d8b2c89ea60148227631bb96a7da0c1b722f2aac8824b1b7c0b6b8"},
|
||||
{file = "cryptography-41.0.5-pp38-pypy38_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:5a70187954ba7292c7876734183e810b728b4f3965fbe571421cb2434d279179"},
|
||||
{file = "cryptography-41.0.5-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:88417bff20162f635f24f849ab182b092697922088b477a7abd6664ddd82291d"},
|
||||
{file = "cryptography-41.0.5-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:c707f7afd813478e2019ae32a7c49cd932dd60ab2d2a93e796f68236b7e1fbf1"},
|
||||
{file = "cryptography-41.0.5-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:580afc7b7216deeb87a098ef0674d6ee34ab55993140838b14c9b83312b37b86"},
|
||||
{file = "cryptography-41.0.5-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:fba1e91467c65fe64a82c689dc6cf58151158993b13eb7a7f3f4b7f395636723"},
|
||||
{file = "cryptography-41.0.5-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:0d2a6a598847c46e3e321a7aef8af1436f11c27f1254933746304ff014664d84"},
|
||||
{file = "cryptography-41.0.5.tar.gz", hash = "sha256:392cb88b597247177172e02da6b7a63deeff1937fa6fec3bbf902ebd75d97ec7"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
@@ -497,13 +493,13 @@ tests = ["dj-database-url", "dj-email-url", "django-cache-url", "pytest"]
|
||||
|
||||
[[package]]
|
||||
name = "exceptiongroup"
|
||||
version = "1.1.3"
|
||||
version = "1.2.0"
|
||||
description = "Backport of PEP 654 (exception groups)"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
files = [
|
||||
{file = "exceptiongroup-1.1.3-py3-none-any.whl", hash = "sha256:343280667a4585d195ca1cf9cef84a4e178c4b6cf2274caef9859782b567d5e3"},
|
||||
{file = "exceptiongroup-1.1.3.tar.gz", hash = "sha256:097acd85d473d75af5bb98e41b61ff7fe35efe6675e4f9370ec6ec5126d160e9"},
|
||||
{file = "exceptiongroup-1.2.0-py3-none-any.whl", hash = "sha256:4bfd3996ac73b41e9b9628b04e079f193850720ea5945fc96a08633c66912f14"},
|
||||
{file = "exceptiongroup-1.2.0.tar.gz", hash = "sha256:91f5c769735f051a4290d52edd0858999b57e5876e9f85937691bd4c9fa3ed68"},
|
||||
]
|
||||
|
||||
[package.extras]
|
||||
@@ -511,19 +507,20 @@ test = ["pytest (>=6)"]
|
||||
|
||||
[[package]]
|
||||
name = "fastapi"
|
||||
version = "0.103.0"
|
||||
version = "0.104.1"
|
||||
description = "FastAPI framework, high performance, easy to learn, fast to code, ready for production"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "fastapi-0.103.0-py3-none-any.whl", hash = "sha256:61ab72c6c281205dd0cbaccf503e829a37e0be108d965ac223779a8479243665"},
|
||||
{file = "fastapi-0.103.0.tar.gz", hash = "sha256:4166732f5ddf61c33e9fa4664f73780872511e0598d4d5434b1816dc1e6d9421"},
|
||||
{file = "fastapi-0.104.1-py3-none-any.whl", hash = "sha256:752dc31160cdbd0436bb93bad51560b57e525cbb1d4bbf6f4904ceee75548241"},
|
||||
{file = "fastapi-0.104.1.tar.gz", hash = "sha256:e5e4540a7c5e1dcfbbcf5b903c234feddcdcd881f191977a1c5dfd917487e7ae"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
anyio = ">=3.7.1,<4.0.0"
|
||||
pydantic = ">=1.7.4,<1.8 || >1.8,<1.8.1 || >1.8.1,<2.0.0 || >2.0.0,<2.0.1 || >2.0.1,<2.1.0 || >2.1.0,<3.0.0"
|
||||
starlette = ">=0.27.0,<0.28.0"
|
||||
typing-extensions = ">=4.5.0"
|
||||
typing-extensions = ">=4.8.0"
|
||||
|
||||
[package.extras]
|
||||
all = ["email-validator (>=2.0.0)", "httpx (>=0.23.0)", "itsdangerous (>=1.1.0)", "jinja2 (>=2.11.2)", "orjson (>=3.2.1)", "pydantic-extra-types (>=2.0.0)", "pydantic-settings (>=2.0.0)", "python-multipart (>=0.0.5)", "pyyaml (>=5.3.1)", "ujson (>=4.0.1,!=4.0.2,!=4.1.0,!=4.2.0,!=4.3.0,!=5.0.0,!=5.1.0)", "uvicorn[standard] (>=0.12.0)"]
|
||||
@@ -544,19 +541,19 @@ pyinstrument = ">=4.4.0"
|
||||
|
||||
[[package]]
|
||||
name = "filelock"
|
||||
version = "3.12.4"
|
||||
version = "3.13.1"
|
||||
description = "A platform independent file lock."
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "filelock-3.12.4-py3-none-any.whl", hash = "sha256:08c21d87ded6e2b9da6728c3dff51baf1dcecf973b768ef35bcbc3447edb9ad4"},
|
||||
{file = "filelock-3.12.4.tar.gz", hash = "sha256:2e6f249f1f3654291606e046b09f1fd5eac39b360664c27f5aad072012f8bcbd"},
|
||||
{file = "filelock-3.13.1-py3-none-any.whl", hash = "sha256:57dbda9b35157b05fb3e58ee91448612eb674172fab98ee235ccb0b5bee19a1c"},
|
||||
{file = "filelock-3.13.1.tar.gz", hash = "sha256:521f5f56c50f8426f5e03ad3b281b490a87ef15bc6c526f168290f0c7148d44e"},
|
||||
]
|
||||
|
||||
[package.extras]
|
||||
docs = ["furo (>=2023.7.26)", "sphinx (>=7.1.2)", "sphinx-autodoc-typehints (>=1.24)"]
|
||||
testing = ["covdefaults (>=2.3)", "coverage (>=7.3)", "diff-cover (>=7.7)", "pytest (>=7.4)", "pytest-cov (>=4.1)", "pytest-mock (>=3.11.1)", "pytest-timeout (>=2.1)"]
|
||||
typing = ["typing-extensions (>=4.7.1)"]
|
||||
docs = ["furo (>=2023.9.10)", "sphinx (>=7.2.6)", "sphinx-autodoc-typehints (>=1.24)"]
|
||||
testing = ["covdefaults (>=2.3)", "coverage (>=7.3.2)", "diff-cover (>=8)", "pytest (>=7.4.3)", "pytest-cov (>=4.1)", "pytest-mock (>=3.12)", "pytest-timeout (>=2.2)"]
|
||||
typing = ["typing-extensions (>=4.8)"]
|
||||
|
||||
[[package]]
|
||||
name = "h11"
|
||||
@@ -571,40 +568,40 @@ files = [
|
||||
|
||||
[[package]]
|
||||
name = "httpcore"
|
||||
version = "0.18.0"
|
||||
version = "1.0.2"
|
||||
description = "A minimal low-level HTTP client."
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "httpcore-0.18.0-py3-none-any.whl", hash = "sha256:adc5398ee0a476567bf87467063ee63584a8bce86078bf748e48754f60202ced"},
|
||||
{file = "httpcore-0.18.0.tar.gz", hash = "sha256:13b5e5cd1dca1a6636a6aaea212b19f4f85cd88c366a2b82304181b769aab3c9"},
|
||||
{file = "httpcore-1.0.2-py3-none-any.whl", hash = "sha256:096cc05bca73b8e459a1fc3dcf585148f63e534eae4339559c9b8a8d6399acc7"},
|
||||
{file = "httpcore-1.0.2.tar.gz", hash = "sha256:9fc092e4799b26174648e54b74ed5f683132a464e95643b226e00c2ed2fa6535"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
anyio = ">=3.0,<5.0"
|
||||
certifi = "*"
|
||||
h11 = ">=0.13,<0.15"
|
||||
sniffio = "==1.*"
|
||||
|
||||
[package.extras]
|
||||
asyncio = ["anyio (>=4.0,<5.0)"]
|
||||
http2 = ["h2 (>=3,<5)"]
|
||||
socks = ["socksio (==1.*)"]
|
||||
trio = ["trio (>=0.22.0,<0.23.0)"]
|
||||
|
||||
[[package]]
|
||||
name = "httpx"
|
||||
version = "0.25.1"
|
||||
version = "0.25.2"
|
||||
description = "The next generation HTTP client."
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "httpx-0.25.1-py3-none-any.whl", hash = "sha256:fec7d6cc5c27c578a391f7e87b9aa7d3d8fbcd034f6399f9f79b45bcc12a866a"},
|
||||
{file = "httpx-0.25.1.tar.gz", hash = "sha256:ffd96d5cf901e63863d9f1b4b6807861dbea4d301613415d9e6e57ead15fc5d0"},
|
||||
{file = "httpx-0.25.2-py3-none-any.whl", hash = "sha256:a05d3d052d9b2dfce0e3896636467f8a5342fb2b902c819428e1ac65413ca118"},
|
||||
{file = "httpx-0.25.2.tar.gz", hash = "sha256:8b8fcaa0c8ea7b05edd69a094e63a2094c4efcb48129fb757361bc423c0ad9e8"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
anyio = "*"
|
||||
certifi = "*"
|
||||
httpcore = "*"
|
||||
httpcore = "==1.*"
|
||||
idna = "*"
|
||||
sniffio = "*"
|
||||
socksio = {version = "==1.*", optional = true, markers = "extra == \"socks\""}
|
||||
@@ -617,13 +614,13 @@ socks = ["socksio (==1.*)"]
|
||||
|
||||
[[package]]
|
||||
name = "identify"
|
||||
version = "2.5.30"
|
||||
version = "2.5.32"
|
||||
description = "File identification library for Python"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "identify-2.5.30-py2.py3-none-any.whl", hash = "sha256:afe67f26ae29bab007ec21b03d4114f41316ab9dd15aa8736a167481e108da54"},
|
||||
{file = "identify-2.5.30.tar.gz", hash = "sha256:f302a4256a15c849b91cfcdcec052a8ce914634b2f77ae87dad29cd749f2d88d"},
|
||||
{file = "identify-2.5.32-py2.py3-none-any.whl", hash = "sha256:0b7656ef6cba81664b783352c73f8c24b39cf82f926f78f4550eda928e5e0545"},
|
||||
{file = "identify-2.5.32.tar.gz", hash = "sha256:5d9979348ec1a21c768ae07e0a652924538e8bce67313a73cb0f681cf08ba407"},
|
||||
]
|
||||
|
||||
[package.extras]
|
||||
@@ -631,13 +628,13 @@ license = ["ukkonen"]
|
||||
|
||||
[[package]]
|
||||
name = "idna"
|
||||
version = "3.4"
|
||||
version = "3.6"
|
||||
description = "Internationalized Domain Names in Applications (IDNA)"
|
||||
optional = false
|
||||
python-versions = ">=3.5"
|
||||
files = [
|
||||
{file = "idna-3.4-py3-none-any.whl", hash = "sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2"},
|
||||
{file = "idna-3.4.tar.gz", hash = "sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4"},
|
||||
{file = "idna-3.6-py3-none-any.whl", hash = "sha256:c05567e9c24a6b9faaa835c4821bad0590fbb9d5779e7caa6e1cc4978e7eb24f"},
|
||||
{file = "idna-3.6.tar.gz", hash = "sha256:9ecdbbd083b06798ae1e86adcbfe8ab1479cf864e4ee30fe4e46a003d12491ca"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -721,38 +718,38 @@ files = [
|
||||
|
||||
[[package]]
|
||||
name = "mypy"
|
||||
version = "1.6.0"
|
||||
version = "1.7.1"
|
||||
description = "Optional static typing for Python"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "mypy-1.6.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:091f53ff88cb093dcc33c29eee522c087a438df65eb92acd371161c1f4380ff0"},
|
||||
{file = "mypy-1.6.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:eb7ff4007865833c470a601498ba30462b7374342580e2346bf7884557e40531"},
|
||||
{file = "mypy-1.6.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:49499cf1e464f533fc45be54d20a6351a312f96ae7892d8e9f1708140e27ce41"},
|
||||
{file = "mypy-1.6.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:4c192445899c69f07874dabda7e931b0cc811ea055bf82c1ababf358b9b2a72c"},
|
||||
{file = "mypy-1.6.0-cp310-cp310-win_amd64.whl", hash = "sha256:3df87094028e52766b0a59a3e46481bb98b27986ed6ded6a6cc35ecc75bb9182"},
|
||||
{file = "mypy-1.6.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3c8835a07b8442da900db47ccfda76c92c69c3a575872a5b764332c4bacb5a0a"},
|
||||
{file = "mypy-1.6.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:24f3de8b9e7021cd794ad9dfbf2e9fe3f069ff5e28cb57af6f873ffec1cb0425"},
|
||||
{file = "mypy-1.6.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:856bad61ebc7d21dbc019b719e98303dc6256cec6dcc9ebb0b214b81d6901bd8"},
|
||||
{file = "mypy-1.6.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:89513ddfda06b5c8ebd64f026d20a61ef264e89125dc82633f3c34eeb50e7d60"},
|
||||
{file = "mypy-1.6.0-cp311-cp311-win_amd64.whl", hash = "sha256:9f8464ed410ada641c29f5de3e6716cbdd4f460b31cf755b2af52f2d5ea79ead"},
|
||||
{file = "mypy-1.6.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:971104bcb180e4fed0d7bd85504c9036346ab44b7416c75dd93b5c8c6bb7e28f"},
|
||||
{file = "mypy-1.6.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ab98b8f6fdf669711f3abe83a745f67f50e3cbaea3998b90e8608d2b459fd566"},
|
||||
{file = "mypy-1.6.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1a69db3018b87b3e6e9dd28970f983ea6c933800c9edf8c503c3135b3274d5ad"},
|
||||
{file = "mypy-1.6.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:dccd850a2e3863891871c9e16c54c742dba5470f5120ffed8152956e9e0a5e13"},
|
||||
{file = "mypy-1.6.0-cp312-cp312-win_amd64.whl", hash = "sha256:f8598307150b5722854f035d2e70a1ad9cc3c72d392c34fffd8c66d888c90f17"},
|
||||
{file = "mypy-1.6.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:fea451a3125bf0bfe716e5d7ad4b92033c471e4b5b3e154c67525539d14dc15a"},
|
||||
{file = "mypy-1.6.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:e28d7b221898c401494f3b77db3bac78a03ad0a0fff29a950317d87885c655d2"},
|
||||
{file = "mypy-1.6.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e4b7a99275a61aa22256bab5839c35fe8a6887781862471df82afb4b445daae6"},
|
||||
{file = "mypy-1.6.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:7469545380dddce5719e3656b80bdfbb217cfe8dbb1438532d6abc754b828fed"},
|
||||
{file = "mypy-1.6.0-cp38-cp38-win_amd64.whl", hash = "sha256:7807a2a61e636af9ca247ba8494031fb060a0a744b9fee7de3a54bed8a753323"},
|
||||
{file = "mypy-1.6.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:d2dad072e01764823d4b2f06bc7365bb1d4b6c2f38c4d42fade3c8d45b0b4b67"},
|
||||
{file = "mypy-1.6.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:b19006055dde8a5425baa5f3b57a19fa79df621606540493e5e893500148c72f"},
|
||||
{file = "mypy-1.6.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:31eba8a7a71f0071f55227a8057468b8d2eb5bf578c8502c7f01abaec8141b2f"},
|
||||
{file = "mypy-1.6.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:8e0db37ac4ebb2fee7702767dfc1b773c7365731c22787cb99f507285014fcaf"},
|
||||
{file = "mypy-1.6.0-cp39-cp39-win_amd64.whl", hash = "sha256:c69051274762cccd13498b568ed2430f8d22baa4b179911ad0c1577d336ed849"},
|
||||
{file = "mypy-1.6.0-py3-none-any.whl", hash = "sha256:9e1589ca150a51d9d00bb839bfeca2f7a04f32cd62fad87a847bc0818e15d7dc"},
|
||||
{file = "mypy-1.6.0.tar.gz", hash = "sha256:4f3d27537abde1be6d5f2c96c29a454da333a2a271ae7d5bc7110e6d4b7beb3f"},
|
||||
{file = "mypy-1.7.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:12cce78e329838d70a204293e7b29af9faa3ab14899aec397798a4b41be7f340"},
|
||||
{file = "mypy-1.7.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1484b8fa2c10adf4474f016e09d7a159602f3239075c7bf9f1627f5acf40ad49"},
|
||||
{file = "mypy-1.7.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:31902408f4bf54108bbfb2e35369877c01c95adc6192958684473658c322c8a5"},
|
||||
{file = "mypy-1.7.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:f2c2521a8e4d6d769e3234350ba7b65ff5d527137cdcde13ff4d99114b0c8e7d"},
|
||||
{file = "mypy-1.7.1-cp310-cp310-win_amd64.whl", hash = "sha256:fcd2572dd4519e8a6642b733cd3a8cfc1ef94bafd0c1ceed9c94fe736cb65b6a"},
|
||||
{file = "mypy-1.7.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4b901927f16224d0d143b925ce9a4e6b3a758010673eeded9b748f250cf4e8f7"},
|
||||
{file = "mypy-1.7.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2f7f6985d05a4e3ce8255396df363046c28bea790e40617654e91ed580ca7c51"},
|
||||
{file = "mypy-1.7.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:944bdc21ebd620eafefc090cdf83158393ec2b1391578359776c00de00e8907a"},
|
||||
{file = "mypy-1.7.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:9c7ac372232c928fff0645d85f273a726970c014749b924ce5710d7d89763a28"},
|
||||
{file = "mypy-1.7.1-cp311-cp311-win_amd64.whl", hash = "sha256:f6efc9bd72258f89a3816e3a98c09d36f079c223aa345c659622f056b760ab42"},
|
||||
{file = "mypy-1.7.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:6dbdec441c60699288adf051f51a5d512b0d818526d1dcfff5a41f8cd8b4aaf1"},
|
||||
{file = "mypy-1.7.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4fc3d14ee80cd22367caaaf6e014494415bf440980a3045bf5045b525680ac33"},
|
||||
{file = "mypy-1.7.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2c6e4464ed5f01dc44dc9821caf67b60a4e5c3b04278286a85c067010653a0eb"},
|
||||
{file = "mypy-1.7.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:d9b338c19fa2412f76e17525c1b4f2c687a55b156320acb588df79f2e6fa9fea"},
|
||||
{file = "mypy-1.7.1-cp312-cp312-win_amd64.whl", hash = "sha256:204e0d6de5fd2317394a4eff62065614c4892d5a4d1a7ee55b765d7a3d9e3f82"},
|
||||
{file = "mypy-1.7.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:84860e06ba363d9c0eeabd45ac0fde4b903ad7aa4f93cd8b648385a888e23200"},
|
||||
{file = "mypy-1.7.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:8c5091ebd294f7628eb25ea554852a52058ac81472c921150e3a61cdd68f75a7"},
|
||||
{file = "mypy-1.7.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:40716d1f821b89838589e5b3106ebbc23636ffdef5abc31f7cd0266db936067e"},
|
||||
{file = "mypy-1.7.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:5cf3f0c5ac72139797953bd50bc6c95ac13075e62dbfcc923571180bebb662e9"},
|
||||
{file = "mypy-1.7.1-cp38-cp38-win_amd64.whl", hash = "sha256:78e25b2fd6cbb55ddfb8058417df193f0129cad5f4ee75d1502248e588d9e0d7"},
|
||||
{file = "mypy-1.7.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:75c4d2a6effd015786c87774e04331b6da863fc3fc4e8adfc3b40aa55ab516fe"},
|
||||
{file = "mypy-1.7.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:2643d145af5292ee956aa0a83c2ce1038a3bdb26e033dadeb2f7066fb0c9abce"},
|
||||
{file = "mypy-1.7.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:75aa828610b67462ffe3057d4d8a4112105ed211596b750b53cbfe182f44777a"},
|
||||
{file = "mypy-1.7.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ee5d62d28b854eb61889cde4e1dbc10fbaa5560cb39780c3995f6737f7e82120"},
|
||||
{file = "mypy-1.7.1-cp39-cp39-win_amd64.whl", hash = "sha256:72cf32ce7dd3562373f78bd751f73c96cfb441de147cc2448a92c1a308bd0ca6"},
|
||||
{file = "mypy-1.7.1-py3-none-any.whl", hash = "sha256:f7c5d642db47376a0cc130f0de6d055056e010debdaf0707cd2b0fc7e7ef30ea"},
|
||||
{file = "mypy-1.7.1.tar.gz", hash = "sha256:fcb6d9afb1b6208b4c712af0dafdc650f518836065df0d4fb1d800f5d6773db2"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
@@ -763,6 +760,7 @@ typing-extensions = ">=4.1.0"
|
||||
[package.extras]
|
||||
dmypy = ["psutil (>=4.0)"]
|
||||
install-types = ["pip"]
|
||||
mypyc = ["setuptools (>=50)"]
|
||||
reports = ["lxml"]
|
||||
|
||||
[[package]]
|
||||
@@ -792,13 +790,13 @@ setuptools = "*"
|
||||
|
||||
[[package]]
|
||||
name = "outcome"
|
||||
version = "1.2.0"
|
||||
version = "1.3.0.post0"
|
||||
description = "Capture the outcome of Python function calls."
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
files = [
|
||||
{file = "outcome-1.2.0-py2.py3-none-any.whl", hash = "sha256:c4ab89a56575d6d38a05aa16daeaa333109c1f96167aba8901ab18b6b5e0f7f5"},
|
||||
{file = "outcome-1.2.0.tar.gz", hash = "sha256:6f82bd3de45da303cf1f771ecafa1633750a358436a8bb60e06a1ceb745d2672"},
|
||||
{file = "outcome-1.3.0.post0-py2.py3-none-any.whl", hash = "sha256:e771c5ce06d1415e356078d3bdd68523f284b4ce5419828922b6871e65eda82b"},
|
||||
{file = "outcome-1.3.0.post0.tar.gz", hash = "sha256:9dcf02e65f2971b80047b377468e72a268e15c0af3cf1238e6ff14f7f91143b8"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
@@ -828,13 +826,13 @@ files = [
|
||||
|
||||
[[package]]
|
||||
name = "platformdirs"
|
||||
version = "3.11.0"
|
||||
version = "4.0.0"
|
||||
description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"."
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
files = [
|
||||
{file = "platformdirs-3.11.0-py3-none-any.whl", hash = "sha256:e9d171d00af68be50e9202731309c4e658fd8bc76f55c11c7dd760d023bda68e"},
|
||||
{file = "platformdirs-3.11.0.tar.gz", hash = "sha256:cf8ee52a3afdb965072dcc652433e0c7e3e40cf5ea1477cd4b3b1d2eb75495b3"},
|
||||
{file = "platformdirs-4.0.0-py3-none-any.whl", hash = "sha256:118c954d7e949b35437270383a3f2531e99dd93cf7ce4dc8340d3356d30f173b"},
|
||||
{file = "platformdirs-4.0.0.tar.gz", hash = "sha256:cb633b2bcf10c51af60beb0ab06d2f1d69064b43abf4c185ca6b28865f3f9731"},
|
||||
]
|
||||
|
||||
[package.extras]
|
||||
@@ -1134,13 +1132,13 @@ types = ["typing-extensions"]
|
||||
|
||||
[[package]]
|
||||
name = "pytest"
|
||||
version = "7.4.2"
|
||||
version = "7.4.3"
|
||||
description = "pytest: simple powerful testing with Python"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
files = [
|
||||
{file = "pytest-7.4.2-py3-none-any.whl", hash = "sha256:1d881c6124e08ff0a1bb75ba3ec0bfd8b5354a01c194ddd5a0a870a48d99b002"},
|
||||
{file = "pytest-7.4.2.tar.gz", hash = "sha256:a766259cfab564a2ad52cb1aae1b881a75c3eb7e34ca3779697c23ed47c47069"},
|
||||
{file = "pytest-7.4.3-py3-none-any.whl", hash = "sha256:0d009c083ea859a71b76adf7c1d502e4bc170b80a8ef002da5806527b9591fac"},
|
||||
{file = "pytest-7.4.3.tar.gz", hash = "sha256:d989d136982de4e3b29dabcc838ad581c64e8ed52c11fbe86ddebd9da0818cd5"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
@@ -1517,19 +1515,19 @@ standard = ["colorama (>=0.4)", "httptools (>=0.5.0)", "python-dotenv (>=0.13)",
|
||||
|
||||
[[package]]
|
||||
name = "virtualenv"
|
||||
version = "20.24.5"
|
||||
version = "20.24.7"
|
||||
description = "Virtual Python Environment builder"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
files = [
|
||||
{file = "virtualenv-20.24.5-py3-none-any.whl", hash = "sha256:b80039f280f4919c77b30f1c23294ae357c4c8701042086e3fc005963e4e537b"},
|
||||
{file = "virtualenv-20.24.5.tar.gz", hash = "sha256:e8361967f6da6fbdf1426483bfe9fca8287c242ac0bc30429905721cefbff752"},
|
||||
{file = "virtualenv-20.24.7-py3-none-any.whl", hash = "sha256:a18b3fd0314ca59a2e9f4b556819ed07183b3e9a3702ecfe213f593d44f7b3fd"},
|
||||
{file = "virtualenv-20.24.7.tar.gz", hash = "sha256:69050ffb42419c91f6c1284a7b24e0475d793447e35929b488bf6a0aade39353"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
distlib = ">=0.3.7,<1"
|
||||
filelock = ">=3.12.2,<4"
|
||||
platformdirs = ">=3.9.1,<4"
|
||||
platformdirs = ">=3.9.1,<5"
|
||||
|
||||
[package.extras]
|
||||
docs = ["furo (>=2023.7.26)", "proselint (>=0.13)", "sphinx (>=7.1.2)", "sphinx-argparse (>=0.4)", "sphinxcontrib-towncrier (>=0.2.1a0)", "towncrier (>=23.6)"]
|
||||
@@ -1553,13 +1551,13 @@ test = ["websockets"]
|
||||
|
||||
[[package]]
|
||||
name = "wheel"
|
||||
version = "0.41.2"
|
||||
version = "0.41.3"
|
||||
description = "A built-package format for Python"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
files = [
|
||||
{file = "wheel-0.41.2-py3-none-any.whl", hash = "sha256:75909db2664838d015e3d9139004ee16711748a52c8f336b52882266540215d8"},
|
||||
{file = "wheel-0.41.2.tar.gz", hash = "sha256:0c5ac5ff2afb79ac23ab82bab027a0be7b5dbcf2e54dc50efe4bf507de1f7985"},
|
||||
{file = "wheel-0.41.3-py3-none-any.whl", hash = "sha256:488609bc63a29322326e05560731bf7bfea8e48ad646e1f5e40d366607de0942"},
|
||||
{file = "wheel-0.41.3.tar.gz", hash = "sha256:4d4987ce51a49370ea65c0bfd2234e8ce80a12780820d9dc462597a6e60d0841"},
|
||||
]
|
||||
|
||||
[package.extras]
|
||||
@@ -1600,4 +1598,4 @@ pgsql = ["psycopg2-binary"]
|
||||
[metadata]
|
||||
lock-version = "2.0"
|
||||
python-versions = "^3.8.1"
|
||||
content-hash = "b2c312fd906aa18a26712039f700322c2c20889a95e1cd9af787df54d700b2ca"
|
||||
content-hash = "f7aa2919aca77aa4d1dfcba18c6fc9694a2cc1d5cfd60e7ec991a615251fa86e"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[tool.poetry]
|
||||
name = "cashu"
|
||||
version = "0.14.0"
|
||||
version = "0.15.0"
|
||||
description = "Ecash wallet and mint"
|
||||
authors = ["calle <callebtc@protonmail.com>"]
|
||||
license = "MIT"
|
||||
@@ -11,7 +11,7 @@ SQLAlchemy = "^1.3.24"
|
||||
click = "^8.1.7"
|
||||
pydantic = "^1.10.2"
|
||||
bech32 = "^1.2.0"
|
||||
fastapi = "0.103.0"
|
||||
fastapi = "^0.104.1"
|
||||
environs = "^9.5.0"
|
||||
uvicorn = "0.23.2"
|
||||
loguru = "^0.7.0"
|
||||
@@ -31,13 +31,15 @@ httpx = {extras = ["socks"], version = "^0.25.1"}
|
||||
bip32 = "^3.4"
|
||||
mnemonic = "^0.20"
|
||||
bolt11 = "^2.0.5"
|
||||
black = "23.11.0"
|
||||
pre-commit = "^3.5.0"
|
||||
|
||||
[tool.poetry.extras]
|
||||
pgsql = ["psycopg2-binary"]
|
||||
|
||||
[tool.poetry.group.dev.dependencies]
|
||||
mypy = "^1.5.1"
|
||||
black = "^23.7.0"
|
||||
black = "^23.11.0"
|
||||
pytest-asyncio = "^0.21.1"
|
||||
pytest-cov = "^4.0.0"
|
||||
pytest = "^7.4.0"
|
||||
|
||||
2
setup.py
2
setup.py
@@ -13,7 +13,7 @@ entry_points = {"console_scripts": ["cashu = cashu.wallet.cli.cli:cli"]}
|
||||
|
||||
setuptools.setup(
|
||||
name="cashu",
|
||||
version="0.14.1",
|
||||
version="0.15.0",
|
||||
description="Ecash wallet and mint for Bitcoin Lightning",
|
||||
long_description=long_description,
|
||||
long_description_content_type="text/markdown",
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import asyncio
|
||||
import multiprocessing
|
||||
import os
|
||||
import shutil
|
||||
@@ -9,35 +10,50 @@ import pytest_asyncio
|
||||
import uvicorn
|
||||
from uvicorn import Config, Server
|
||||
|
||||
from cashu.core.base import Method, Unit
|
||||
from cashu.core.db import Database
|
||||
from cashu.core.migrations import migrate_databases
|
||||
from cashu.core.settings import settings
|
||||
from cashu.lightning.fake import FakeWallet
|
||||
from cashu.mint import migrations as migrations_mint
|
||||
from cashu.mint.crud import LedgerCrud
|
||||
from cashu.mint.crud import LedgerCrudSqlite
|
||||
from cashu.mint.ledger import Ledger
|
||||
|
||||
SERVER_PORT = 3337
|
||||
SERVER_ENDPOINT = f"http://localhost:{SERVER_PORT}"
|
||||
|
||||
settings.debug = True
|
||||
settings.debug = False
|
||||
settings.cashu_dir = "./test_data/"
|
||||
settings.mint_host = "localhost"
|
||||
settings.mint_port = SERVER_PORT
|
||||
settings.mint_host = "0.0.0.0"
|
||||
settings.mint_listen_port = SERVER_PORT
|
||||
settings.mint_url = SERVER_ENDPOINT
|
||||
settings.lightning = True
|
||||
settings.tor = False
|
||||
settings.wallet_unit = "sat"
|
||||
settings.mint_lightning_backend = settings.mint_lightning_backend or "FakeWallet"
|
||||
settings.fakewallet_brr = True
|
||||
settings.fakewallet_delay_payment = False
|
||||
settings.fakewallet_stochastic_invoice = False
|
||||
settings.mint_database = "./test_data/test_mint"
|
||||
settings.mint_derivation_path = "0/0/0/0"
|
||||
settings.mint_derivation_path = "m/0'/0'/0'"
|
||||
settings.mint_derivation_path_list = []
|
||||
settings.mint_private_key = "TEST_PRIVATE_KEY"
|
||||
settings.mint_max_balance = 0
|
||||
|
||||
assert "test" in settings.cashu_dir
|
||||
shutil.rmtree(settings.cashu_dir, ignore_errors=True)
|
||||
Path(settings.cashu_dir).mkdir(parents=True, exist_ok=True)
|
||||
|
||||
from cashu.mint.startup import lightning_backend # noqa
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def event_loop():
|
||||
policy = asyncio.get_event_loop_policy()
|
||||
loop = policy.new_event_loop()
|
||||
yield loop
|
||||
loop.close()
|
||||
|
||||
|
||||
class UvicornServer(multiprocessing.Process):
|
||||
def __init__(self, config: Config):
|
||||
@@ -52,33 +68,7 @@ class UvicornServer(multiprocessing.Process):
|
||||
self.server.run()
|
||||
|
||||
|
||||
@pytest_asyncio.fixture(scope="function")
|
||||
async def ledger():
|
||||
async def start_mint_init(ledger: Ledger):
|
||||
await migrate_databases(ledger.db, migrations_mint)
|
||||
if settings.mint_cache_secrets:
|
||||
await ledger.load_used_proofs()
|
||||
await ledger.init_keysets()
|
||||
|
||||
database_name = "mint"
|
||||
|
||||
if not settings.mint_database.startswith("postgres"):
|
||||
# clear sqlite database
|
||||
db_file = os.path.join(settings.mint_database, database_name + ".sqlite3")
|
||||
if os.path.exists(db_file):
|
||||
os.remove(db_file)
|
||||
|
||||
ledger = Ledger(
|
||||
db=Database(database_name, settings.mint_database),
|
||||
seed=settings.mint_private_key,
|
||||
derivation_path=settings.mint_derivation_path,
|
||||
lightning=FakeWallet(),
|
||||
crud=LedgerCrud(),
|
||||
)
|
||||
await start_mint_init(ledger)
|
||||
yield ledger
|
||||
|
||||
|
||||
# # This fixture is used for tests that require API access to the mint
|
||||
@pytest.fixture(autouse=True, scope="session")
|
||||
def mint():
|
||||
config = uvicorn.Config(
|
||||
@@ -92,3 +82,33 @@ def mint():
|
||||
time.sleep(1)
|
||||
yield server
|
||||
server.stop()
|
||||
|
||||
|
||||
# This fixture is used for all other tests
|
||||
@pytest_asyncio.fixture(scope="function")
|
||||
async def ledger():
|
||||
async def start_mint_init(ledger: Ledger):
|
||||
await migrate_databases(ledger.db, migrations_mint)
|
||||
if settings.mint_cache_secrets:
|
||||
await ledger.load_used_proofs()
|
||||
await ledger.init_keysets()
|
||||
|
||||
if not settings.mint_database.startswith("postgres"):
|
||||
# clear sqlite database
|
||||
db_file = os.path.join(settings.mint_database, "mint.sqlite3")
|
||||
if os.path.exists(db_file):
|
||||
os.remove(db_file)
|
||||
|
||||
backends = {
|
||||
Method.bolt11: {Unit.sat: lightning_backend},
|
||||
}
|
||||
ledger = Ledger(
|
||||
db=Database("mint", settings.mint_database),
|
||||
seed=settings.mint_private_key,
|
||||
derivation_path=settings.mint_derivation_path,
|
||||
backends=backends,
|
||||
crud=LedgerCrudSqlite(),
|
||||
)
|
||||
await start_mint_init(ledger)
|
||||
yield ledger
|
||||
print("teardown")
|
||||
|
||||
@@ -29,7 +29,7 @@ wallet_class = getattr(wallets_module, settings.mint_lightning_backend)
|
||||
WALLET = wallet_class()
|
||||
is_fake: bool = WALLET.__class__.__name__ == "FakeWallet"
|
||||
is_regtest: bool = not is_fake
|
||||
|
||||
is_deprecated_api_only = settings.debug_mint_only_deprecated
|
||||
|
||||
docker_lightning_cli = [
|
||||
"docker",
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import asyncio
|
||||
import base64
|
||||
import json
|
||||
from typing import Tuple
|
||||
|
||||
import pytest
|
||||
@@ -27,8 +29,9 @@ def get_bolt11_and_invoice_id_from_invoice_command(output: str) -> Tuple[str, st
|
||||
|
||||
|
||||
async def init_wallet():
|
||||
settings.debug = False
|
||||
wallet = await Wallet.with_db(
|
||||
url=settings.mint_host,
|
||||
url=settings.mint_url,
|
||||
db="test_data/test_cli_wallet",
|
||||
name="wallet",
|
||||
)
|
||||
@@ -56,7 +59,7 @@ def test_info_with_mint(cli_prefix):
|
||||
[*cli_prefix, "info", "--mint"],
|
||||
)
|
||||
assert result.exception is None
|
||||
print("INFO -M")
|
||||
print("INFO --MINT")
|
||||
print(result.output)
|
||||
assert "Mint name" in result.output
|
||||
assert result.exit_code == 0
|
||||
@@ -69,7 +72,7 @@ def test_info_with_mnemonic(cli_prefix):
|
||||
[*cli_prefix, "info", "--mnemonic"],
|
||||
)
|
||||
assert result.exception is None
|
||||
print("INFO -M")
|
||||
print("INFO --MNEMONIC")
|
||||
print(result.output)
|
||||
assert "Mnemonic" in result.output
|
||||
assert result.exit_code == 0
|
||||
@@ -177,7 +180,7 @@ def test_send(mint, cli_prefix):
|
||||
[*cli_prefix, "send", "10"],
|
||||
)
|
||||
assert result.exception is None
|
||||
print(result.output)
|
||||
print("test_send", result.output)
|
||||
token_str = result.output.split("\n")[0]
|
||||
assert "cashuA" in token_str, "output does not have a token"
|
||||
token = TokenV3.deserialize(token_str)
|
||||
@@ -191,7 +194,7 @@ def test_send_with_dleq(mint, cli_prefix):
|
||||
[*cli_prefix, "send", "10", "--dleq"],
|
||||
)
|
||||
assert result.exception is None
|
||||
print(result.output)
|
||||
print("test_send_with_dleq", result.output)
|
||||
token_str = result.output.split("\n")[0]
|
||||
assert "cashuA" in token_str, "output does not have a token"
|
||||
token = TokenV3.deserialize(token_str)
|
||||
@@ -205,7 +208,7 @@ def test_send_legacy(mint, cli_prefix):
|
||||
[*cli_prefix, "send", "10", "--legacy"],
|
||||
)
|
||||
assert result.exception is None
|
||||
print(result.output)
|
||||
print("test_send_legacy", result.output)
|
||||
# this is the legacy token in the output
|
||||
token_str = result.output.split("\n")[4]
|
||||
assert token_str.startswith("eyJwcm9v"), "output is not as expected"
|
||||
@@ -219,7 +222,7 @@ def test_send_without_split(mint, cli_prefix):
|
||||
)
|
||||
assert result.exception is None
|
||||
print("SEND")
|
||||
print(result.output)
|
||||
print("test_send_without_split", result.output)
|
||||
assert "cashuA" in result.output, "output does not have a token"
|
||||
|
||||
|
||||
@@ -234,12 +237,7 @@ def test_send_without_split_but_wrong_amount(mint, cli_prefix):
|
||||
|
||||
def test_receive_tokenv3(mint, cli_prefix):
|
||||
runner = CliRunner()
|
||||
token = (
|
||||
"cashuAeyJ0b2tlbiI6IFt7InByb29mcyI6IFt7ImlkIjogIjFjQ05JQVoyWC93MSIsICJhbW91bnQiOiAyLCAic2VjcmV0IjogIld6TEF2VW53SDlRaFYwQU1rMy1oYWciLC"
|
||||
"AiQyI6ICIwMmZlMzUxYjAyN2FlMGY1ZDkyN2U2ZjFjMTljMjNjNTc3NzRhZTI2M2UyOGExN2E2MTUxNjY1ZjU3NWNhNjMyNWMifSwgeyJpZCI6ICIxY0NOSUFaMlgvdzEiLCAiYW"
|
||||
"1vdW50IjogOCwgInNlY3JldCI6ICJDamFTeTcyR2dVOGwzMGV6bE5zZnVBIiwgIkMiOiAiMDNjMzM0OTJlM2ZlNjI4NzFhMWEzMDhiNWUyYjVhZjBkNWI1Mjk5YzI0YmVkNDI2Zj"
|
||||
"Q1YzZmNDg5N2QzZjc4NGQ5In1dLCAibWludCI6ICJodHRwOi8vbG9jYWxob3N0OjMzMzcifV19"
|
||||
)
|
||||
token = "cashuAeyJ0b2tlbiI6IFt7InByb29mcyI6IFt7ImlkIjogIjAwOWExZjI5MzI1M2U0MWUiLCAiYW1vdW50IjogMiwgInNlY3JldCI6ICI0NzlkY2E0MzUzNzU4MTM4N2Q1ODllMDU1MGY0Y2Q2MjFmNjE0MDM1MGY5M2Q4ZmI1OTA2YjJlMGRiNmRjYmI3IiwgIkMiOiAiMDM1MGQ0ZmI0YzdiYTMzNDRjMWRjYWU1ZDExZjNlNTIzZGVkOThmNGY4ODdkNTQwZmYyMDRmNmVlOWJjMjkyZjQ1In0sIHsiaWQiOiAiMDA5YTFmMjkzMjUzZTQxZSIsICJhbW91bnQiOiA4LCAic2VjcmV0IjogIjZjNjAzNDgwOGQyNDY5N2IyN2YxZTEyMDllNjdjNjVjNmE2MmM2Zjc3NGI4NWVjMGQ5Y2Y3MjE0M2U0NWZmMDEiLCAiQyI6ICIwMjZkNDlhYTE0MmFlNjM1NWViZTJjZGQzYjFhOTdmMjE1MDk2NTlkMDE3YWU0N2FjNDY3OGE4NWVkY2E4MGMxYmQifV0sICJtaW50IjogImh0dHA6Ly9sb2NhbGhvc3Q6MzMzNyJ9XX0=" # noqa
|
||||
result = runner.invoke(
|
||||
cli,
|
||||
[
|
||||
@@ -258,12 +256,29 @@ def test_receive_tokenv3_no_mint(mint, cli_prefix):
|
||||
# where the mint URL is not in the token therefore, we need to know the mint keyset
|
||||
# already and have the mint URL in the db
|
||||
runner = CliRunner()
|
||||
token = (
|
||||
"cashuAeyJ0b2tlbiI6IFt7InByb29mcyI6IFt7ImlkIjogIjFjQ05JQVoyWC93MSIsICJhbW91bnQiOiAyLCAic2VjcmV0IjogIi1oM0ZXMFFoX1FYLW9ac1V2c0RuNlEiLC"
|
||||
"AiQyI6ICIwMzY5Mzc4MzdlYjg5ZWI4NjMyNWYwOWUyOTIxMWQxYTI4OTRlMzQ2YmM1YzQwZTZhMThlNTk5ZmVjNjEwOGRmMGIifSwgeyJpZCI6ICIxY0NOSUFaMlgvdzEiLCAiYW"
|
||||
"1vdW50IjogOCwgInNlY3JldCI6ICI3d0VhNUgzZGhSRGRNZl94c1k3c3JnIiwgIkMiOiAiMDJiZmZkM2NlZDkxNjUyMzcxMDg2NjQxMzJiMjgxYjBhZjY1ZTNlZWVkNTY3MmFkZj"
|
||||
"M0Y2VhNzE5ODhhZWM1NWI1In1dfV19"
|
||||
)
|
||||
token_dict = {
|
||||
"token": [
|
||||
{
|
||||
"proofs": [
|
||||
{
|
||||
"id": "009a1f293253e41e",
|
||||
"amount": 2,
|
||||
"secret": "ea3420987e1ecd71de58e4ff00e8a94d1f1f9333dad98e923e3083d21bf314e2",
|
||||
"C": "0204eb99cf27105b4de4029478376d6f71e9e3d5af1cc28a652c028d1bcd6537cc",
|
||||
},
|
||||
{
|
||||
"id": "009a1f293253e41e",
|
||||
"amount": 8,
|
||||
"secret": "3447975db92f43b269290e05b91805df7aa733f622e55d885a2cab78e02d4a72",
|
||||
"C": "0286c78750d414bc067178cbac0f3551093cea47d213ebf356899c972448ee6255",
|
||||
},
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
token = "cashuA" + base64.b64encode(json.dumps(token_dict).encode()).decode()
|
||||
print("RECEIVE")
|
||||
print(token)
|
||||
result = runner.invoke(
|
||||
cli,
|
||||
[
|
||||
@@ -273,18 +288,37 @@ def test_receive_tokenv3_no_mint(mint, cli_prefix):
|
||||
],
|
||||
)
|
||||
assert result.exception is None
|
||||
print("RECEIVE")
|
||||
print(result.output)
|
||||
|
||||
|
||||
def test_receive_tokenv2(mint, cli_prefix):
|
||||
runner = CliRunner()
|
||||
token = (
|
||||
"eyJwcm9vZnMiOiBbeyJpZCI6ICIxY0NOSUFaMlgvdzEiLCAiYW1vdW50IjogMiwgInNlY3JldCI6ICJhUmREbzlFdW9yZUVfOW90enRNVVpnIiwgIkMiOiAiMDNhMzY5ZmUy"
|
||||
"N2IxYmVmOTg4MzA3NDQyN2RjMzc1NmU0NThlMmMwYjQ1NWMwYmVmZGM4ZjVmNTA3YmM5MGQxNmU3In0sIHsiaWQiOiAiMWNDTklBWjJYL3cxIiwgImFtb3VudCI6IDgsICJzZWNy"
|
||||
"ZXQiOiAiTEZQbFp6Ui1MWHFfYXFDMGhUeDQyZyIsICJDIjogIjAzNGNiYzQxYWY0ODIxMGFmNjVmYjVjOWIzOTNkMjhmMmQ5ZDZhOWE5MzI2YmI3MzQ2YzVkZmRmMTU5MDk1MzI2"
|
||||
"YyJ9XSwgIm1pbnRzIjogW3sidXJsIjogImh0dHA6Ly9sb2NhbGhvc3Q6MzMzNyIsICJpZHMiOiBbIjFjQ05JQVoyWC93MSJdfV19"
|
||||
)
|
||||
token_dict = {
|
||||
"proofs": [
|
||||
{
|
||||
"id": "009a1f293253e41e",
|
||||
"amount": 2,
|
||||
"secret": (
|
||||
"a1efb610726b342aec209375397fee86a0b88732779ce218e99132f9a975db2a"
|
||||
),
|
||||
"C": (
|
||||
"03057e5fe352bac785468ffa51a1ecf0f75af24d2d27ab1fd00164672a417d9523"
|
||||
),
|
||||
},
|
||||
{
|
||||
"id": "009a1f293253e41e",
|
||||
"amount": 8,
|
||||
"secret": (
|
||||
"b065a17938bc79d6224dc381873b8b7f3a46267e8b00d9ce59530354d9d81ae4"
|
||||
),
|
||||
"C": (
|
||||
"021e83773f5eb66f837a5721a067caaa8d7018ef0745b4302f4e2c6cac8806dc69"
|
||||
),
|
||||
},
|
||||
],
|
||||
"mints": [{"url": "http://localhost:3337", "ids": ["009a1f293253e41e"]}],
|
||||
}
|
||||
token = base64.b64encode(json.dumps(token_dict).encode()).decode()
|
||||
result = runner.invoke(
|
||||
cli,
|
||||
[*cli_prefix, "receive", token],
|
||||
@@ -296,11 +330,25 @@ def test_receive_tokenv2(mint, cli_prefix):
|
||||
|
||||
def test_receive_tokenv1(mint, cli_prefix):
|
||||
runner = CliRunner()
|
||||
token = (
|
||||
"W3siaWQiOiAiMWNDTklBWjJYL3cxIiwgImFtb3VudCI6IDIsICJzZWNyZXQiOiAiRnVsc2dzMktQV1FMcUlLX200SzgwQSIsICJDIjogIjAzNTc4OThlYzlhMjIxN2VhYWIx"
|
||||
"ZDc3YmM1Mzc2OTUwMjJlMjU2YTljMmMwNjc0ZDJlM2FiM2JiNGI0ZDMzMWZiMSJ9LCB7ImlkIjogIjFjQ05JQVoyWC93MSIsICJhbW91bnQiOiA4LCAic2VjcmV0IjogInJlRDBD"
|
||||
"azVNS2xBTUQ0dWk2OEtfbEEiLCAiQyI6ICIwMjNkODNkNDE0MDU0NWQ1NTg4NjUyMzU5YjJhMjFhODljODY1ZGIzMzAyZTkzMTZkYTM5NjA0YTA2ZDYwYWQzOGYifV0="
|
||||
)
|
||||
token_dict = [
|
||||
{
|
||||
"id": "009a1f293253e41e",
|
||||
"amount": 2,
|
||||
"secret": (
|
||||
"bc0360c041117969ef7b8add48d0981c669619aa5743cccce13d4a771c9e164d"
|
||||
),
|
||||
"C": "026fd492f933e9240f36fb2559a7327f47b3441b895a5f8f0b1d6825fee73438f0",
|
||||
},
|
||||
{
|
||||
"id": "009a1f293253e41e",
|
||||
"amount": 8,
|
||||
"secret": (
|
||||
"cf83bd8df35bb104d3818511c1653e9ebeb2b645a36fd071b2229aa2c3044acd"
|
||||
),
|
||||
"C": "0279606f3dfd7784757c6320b17e1bf2211f284318814c12bfaa40680e017abd34",
|
||||
},
|
||||
]
|
||||
token = base64.b64encode(json.dumps(token_dict).encode()).decode()
|
||||
result = runner.invoke(
|
||||
cli,
|
||||
[*cli_prefix, "receive", token],
|
||||
|
||||
@@ -9,7 +9,7 @@ def test_get_output_split():
|
||||
assert amount_split(13) == [1, 4, 8]
|
||||
|
||||
|
||||
def test_tokenv3_get_amount():
|
||||
def test_tokenv3_deserialize_get_attributes():
|
||||
token_str = (
|
||||
"cashuAeyJ0b2tlbiI6IFt7InByb29mcyI6IFt7ImlkIjogIkplaFpMVTZuQ3BSZCIsICJhbW91bnQiOiAyLCAic2VjcmV0IjogIjBFN2lDazRkVmxSZjVQRjFnNFpWMnci"
|
||||
"LCAiQyI6ICIwM2FiNTgwYWQ5NTc3OGVkNTI5NmY4YmVlNjU1ZGJkN2Q2NDJmNWQzMmRlOGUyNDg0NzdlMGI0ZDZhYTg2M2ZjZDUifSwgeyJpZCI6ICJKZWhaTFU2bkNwUmQiLCAiYW"
|
||||
@@ -18,16 +18,6 @@ def test_tokenv3_get_amount():
|
||||
)
|
||||
token = TokenV3.deserialize(token_str)
|
||||
assert token.get_amount() == 10
|
||||
|
||||
|
||||
def test_tokenv3_get_proofs():
|
||||
token_str = (
|
||||
"cashuAeyJ0b2tlbiI6IFt7InByb29mcyI6IFt7ImlkIjogIkplaFpMVTZuQ3BSZCIsICJhbW91bnQiOiAyLCAic2VjcmV0IjogIjBFN2lDazRkVmxSZjVQRjFnNFpWMnci"
|
||||
"LCAiQyI6ICIwM2FiNTgwYWQ5NTc3OGVkNTI5NmY4YmVlNjU1ZGJkN2Q2NDJmNWQzMmRlOGUyNDg0NzdlMGI0ZDZhYTg2M2ZjZDUifSwgeyJpZCI6ICJKZWhaTFU2bkNwUmQiLCAiYW"
|
||||
"1vdW50IjogOCwgInNlY3JldCI6ICJzNklwZXh3SGNxcXVLZDZYbW9qTDJnIiwgIkMiOiAiMDIyZDAwNGY5ZWMxNmE1OGFkOTAxNGMyNTliNmQ2MTRlZDM2ODgyOWYwMmMzODc3M2M0"
|
||||
"NzIyMWY0OTYxY2UzZjIzIn1dLCAibWludCI6ICJodHRwOi8vbG9jYWxob3N0OjMzMzgifV19"
|
||||
)
|
||||
token = TokenV3.deserialize(token_str)
|
||||
assert len(token.get_proofs()) == 2
|
||||
|
||||
|
||||
@@ -117,6 +107,43 @@ def test_tokenv3_deserialize_with_memo():
|
||||
assert token.memo == "Test memo"
|
||||
|
||||
|
||||
def test_serialize_example_token_nut00():
|
||||
token_dict = {
|
||||
"token": [
|
||||
{
|
||||
"mint": "https://8333.space:3338",
|
||||
"proofs": [
|
||||
{
|
||||
"id": "9bb9d58392cd823e",
|
||||
"amount": 2,
|
||||
"secret": "EhpennC9qB3iFlW8FZ_pZw",
|
||||
"C": "02c020067db727d586bc3183aecf97fcb800c3f4cc4759f69c626c9db5d8f5b5d4",
|
||||
},
|
||||
{
|
||||
"id": "9bb9d58392cd823e",
|
||||
"amount": 8,
|
||||
"secret": "TmS6Cv0YT5PU_5ATVKnukw",
|
||||
"C": "02ac910bef28cbe5d7325415d5c263026f15f9b967a079ca9779ab6e5c2db133a7",
|
||||
},
|
||||
],
|
||||
}
|
||||
],
|
||||
"memo": "Thank you.",
|
||||
}
|
||||
tokenObj = TokenV3.parse_obj(token_dict)
|
||||
assert (
|
||||
tokenObj.serialize()
|
||||
== "cashuAeyJ0b2tlbiI6IFt7InByb29mcyI6IFt7ImlkIjogIjliYjlkNTgzOTJjZDg"
|
||||
"yM2UiLCAiYW1vdW50IjogMiwgInNlY3JldCI6ICJFaHBlbm5DOXFCM2lGbFc4Rlpf"
|
||||
"cFp3IiwgIkMiOiAiMDJjMDIwMDY3ZGI3MjdkNTg2YmMzMTgzYWVjZjk3ZmNiODAwY"
|
||||
"zNmNGNjNDc1OWY2OWM2MjZjOWRiNWQ4ZjViNWQ0In0sIHsiaWQiOiAiOWJiOWQ1OD"
|
||||
"M5MmNkODIzZSIsICJhbW91bnQiOiA4LCAic2VjcmV0IjogIlRtUzZDdjBZVDVQVV8"
|
||||
"1QVRWS251a3ciLCAiQyI6ICIwMmFjOTEwYmVmMjhjYmU1ZDczMjU0MTVkNWMyNjMw"
|
||||
"MjZmMTVmOWI5NjdhMDc5Y2E5Nzc5YWI2ZTVjMmRiMTMzYTcifV0sICJtaW50IjogI"
|
||||
"mh0dHBzOi8vODMzMy5zcGFjZTozMzM4In1dLCAibWVtbyI6ICJUaGFuayB5b3UuIn0="
|
||||
)
|
||||
|
||||
|
||||
def test_calculate_number_of_blank_outputs():
|
||||
# Example from NUT-08 specification.
|
||||
fee_reserve_sat = 1000
|
||||
|
||||
@@ -2,11 +2,12 @@ from typing import List
|
||||
|
||||
import pytest
|
||||
|
||||
from cashu.core.base import BlindedMessage, Proof
|
||||
from cashu.core.base import BlindedMessage, PostMintQuoteRequest, Proof
|
||||
from cashu.core.crypto.b_dhke import step1_alice
|
||||
from cashu.core.helpers import calculate_number_of_blank_outputs
|
||||
from cashu.core.settings import settings
|
||||
from cashu.mint.ledger import Ledger
|
||||
from tests.helpers import pay_if_regtest
|
||||
|
||||
|
||||
async def assert_err(f, msg):
|
||||
@@ -29,11 +30,11 @@ async def test_pubkeys(ledger: Ledger):
|
||||
assert ledger.keyset.public_keys
|
||||
assert (
|
||||
ledger.keyset.public_keys[1].serialize().hex()
|
||||
== "03190ebc0c3e2726a5349904f572a2853ea021b0128b269b8b6906501d262edaa8"
|
||||
== "02194603ffa36356f4a56b7df9371fc3192472351453ec7398b8da8117e7c3e104"
|
||||
)
|
||||
assert (
|
||||
ledger.keyset.public_keys[2 ** (settings.max_order - 1)].serialize().hex()
|
||||
== "032dc008b88b85fdc2301a499bfaaef774c191a6307d8c9434838fc2eaa2e48d51"
|
||||
== "023c84c0895cc0e827b348ea0a62951ca489a5e436f3ea7545f3c1d5f1bea1c866"
|
||||
)
|
||||
|
||||
|
||||
@@ -42,19 +43,30 @@ async def test_privatekeys(ledger: Ledger):
|
||||
assert ledger.keyset.private_keys
|
||||
assert (
|
||||
ledger.keyset.private_keys[1].serialize()
|
||||
== "67de62e1bf8b5ccf88dbad6768b7d13fa0f41433b0a89caf915039505f2e00a7"
|
||||
== "8300050453f08e6ead1296bb864e905bd46761beed22b81110fae0751d84604d"
|
||||
)
|
||||
assert (
|
||||
ledger.keyset.private_keys[2 ** (settings.max_order - 1)].serialize()
|
||||
== "3b1340c703b02028a11025302d2d9e68d2a6dd721ab1a2770f0942d15eacb8d0"
|
||||
== "b0477644cb3d82ffcc170bc0a76e0409727232e87c5ae51d64a259936228c7be"
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_keysets(ledger: Ledger):
|
||||
assert len(ledger.keysets.keysets)
|
||||
assert len(ledger.keysets.get_ids())
|
||||
assert ledger.keyset.id == "1cCNIAZ2X/w1"
|
||||
assert len(ledger.keysets)
|
||||
assert len(list(ledger.keysets.keys()))
|
||||
assert ledger.keyset.id == "009a1f293253e41e"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_keysets_backwards_compatibility_pre_v0_15(ledger: Ledger):
|
||||
"""Backwards compatibility test for keysets pre v0.15.0
|
||||
We expect two instances of the same keyset but with different IDs.
|
||||
First one is the new hex ID, second one is the old base64 ID.
|
||||
"""
|
||||
assert len(ledger.keysets) == 2
|
||||
assert list(ledger.keysets.keys()) == ["009a1f293253e41e", "eGnEWtdJ0PIM"]
|
||||
assert ledger.keyset.id == "009a1f293253e41e"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@@ -66,33 +78,37 @@ async def test_get_keyset(ledger: Ledger):
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_mint(ledger: Ledger):
|
||||
invoice, id = await ledger.request_mint(8)
|
||||
quote = await ledger.mint_quote(PostMintQuoteRequest(amount=8, unit="sat"))
|
||||
pay_if_regtest(quote.request)
|
||||
blinded_messages_mock = [
|
||||
BlindedMessage(
|
||||
amount=8,
|
||||
B_="02634a2c2b34bec9e8a4aba4361f6bf202d7fa2365379b0840afe249a7a9d71239",
|
||||
id="009a1f293253e41e",
|
||||
)
|
||||
]
|
||||
promises = await ledger.mint(blinded_messages_mock, id=id)
|
||||
promises = await ledger.mint(outputs=blinded_messages_mock, quote_id=quote.quote)
|
||||
assert len(promises)
|
||||
assert promises[0].amount == 8
|
||||
assert (
|
||||
promises[0].C_
|
||||
== "037074c4f53e326ee14ed67125f387d160e0e729351471b69ad41f7d5d21071e15"
|
||||
== "031422eeffb25319e519c68de000effb294cb362ef713a7cf4832cea7b0452ba6e"
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_mint_invalid_blinded_message(ledger: Ledger):
|
||||
invoice, id = await ledger.request_mint(8)
|
||||
quote = await ledger.mint_quote(PostMintQuoteRequest(amount=8, unit="sat"))
|
||||
pay_if_regtest(quote.request)
|
||||
blinded_messages_mock_invalid_key = [
|
||||
BlindedMessage(
|
||||
amount=8,
|
||||
B_="02634a2c2b34bec9e8a4aba4361f6bff02d7fa2365379b0840afe249a7a9d71237",
|
||||
id="009a1f293253e41e",
|
||||
)
|
||||
]
|
||||
await assert_err(
|
||||
ledger.mint(blinded_messages_mock_invalid_key, id=id),
|
||||
ledger.mint(outputs=blinded_messages_mock_invalid_key, quote_id=quote.quote),
|
||||
"invalid public key",
|
||||
)
|
||||
|
||||
@@ -103,14 +119,16 @@ async def test_generate_promises(ledger: Ledger):
|
||||
BlindedMessage(
|
||||
amount=8,
|
||||
B_="02634a2c2b34bec9e8a4aba4361f6bf202d7fa2365379b0840afe249a7a9d71239",
|
||||
id="009a1f293253e41e",
|
||||
)
|
||||
]
|
||||
promises = await ledger._generate_promises(blinded_messages_mock)
|
||||
assert (
|
||||
promises[0].C_
|
||||
== "037074c4f53e326ee14ed67125f387d160e0e729351471b69ad41f7d5d21071e15"
|
||||
== "031422eeffb25319e519c68de000effb294cb362ef713a7cf4832cea7b0452ba6e"
|
||||
)
|
||||
assert promises[0].amount == 8
|
||||
assert promises[0].id == "009a1f293253e41e"
|
||||
|
||||
# DLEQ proof present
|
||||
assert promises[0].dleq
|
||||
@@ -118,6 +136,55 @@ async def test_generate_promises(ledger: Ledger):
|
||||
assert promises[0].dleq.e
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_generate_promises_deprecated_keyset_id(ledger: Ledger):
|
||||
blinded_messages_mock = [
|
||||
BlindedMessage(
|
||||
amount=8,
|
||||
B_="02634a2c2b34bec9e8a4aba4361f6bf202d7fa2365379b0840afe249a7a9d71239",
|
||||
id="eGnEWtdJ0PIM",
|
||||
)
|
||||
]
|
||||
promises = await ledger._generate_promises(blinded_messages_mock)
|
||||
assert (
|
||||
promises[0].C_
|
||||
== "031422eeffb25319e519c68de000effb294cb362ef713a7cf4832cea7b0452ba6e"
|
||||
)
|
||||
assert promises[0].amount == 8
|
||||
assert promises[0].id == "eGnEWtdJ0PIM"
|
||||
|
||||
# DLEQ proof present
|
||||
assert promises[0].dleq
|
||||
assert promises[0].dleq.s
|
||||
assert promises[0].dleq.e
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_generate_promises_keyset_backwards_compatibility_pre_v0_15(
|
||||
ledger: Ledger,
|
||||
):
|
||||
"""Backwards compatibility test for keysets pre v0.15.0
|
||||
We want to generate promises using the old keyset ID.
|
||||
We expect the promise to have the old base64 ID.
|
||||
"""
|
||||
blinded_messages_mock = [
|
||||
BlindedMessage(
|
||||
amount=8,
|
||||
B_="02634a2c2b34bec9e8a4aba4361f6bf202d7fa2365379b0840afe249a7a9d71239",
|
||||
id="eGnEWtdJ0PIM",
|
||||
)
|
||||
]
|
||||
promises = await ledger._generate_promises(
|
||||
blinded_messages_mock, keyset=ledger.keysets["eGnEWtdJ0PIM"]
|
||||
)
|
||||
assert (
|
||||
promises[0].C_
|
||||
== "031422eeffb25319e519c68de000effb294cb362ef713a7cf4832cea7b0452ba6e"
|
||||
)
|
||||
assert promises[0].amount == 8
|
||||
assert promises[0].id == "eGnEWtdJ0PIM"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_generate_change_promises(ledger: Ledger):
|
||||
# Example slightly adapted from NUT-08 because we want to ensure the dynamic change
|
||||
@@ -125,7 +192,7 @@ async def test_generate_change_promises(ledger: Ledger):
|
||||
invoice_amount = 100_000
|
||||
fee_reserve = 2_000
|
||||
total_provided = invoice_amount + fee_reserve
|
||||
actual_fee_msat = 100_000
|
||||
actual_fee = 100
|
||||
|
||||
expected_returned_promises = 7 # Amounts = [4, 8, 32, 64, 256, 512, 1024]
|
||||
expected_returned_fees = 1900
|
||||
@@ -133,11 +200,16 @@ async def test_generate_change_promises(ledger: Ledger):
|
||||
n_blank_outputs = calculate_number_of_blank_outputs(fee_reserve)
|
||||
blinded_msgs = [step1_alice(str(n)) for n in range(n_blank_outputs)]
|
||||
outputs = [
|
||||
BlindedMessage(amount=1, B_=b.serialize().hex()) for b, _ in blinded_msgs
|
||||
BlindedMessage(
|
||||
amount=1,
|
||||
B_=b.serialize().hex(),
|
||||
id="009a1f293253e41e",
|
||||
)
|
||||
for b, _ in blinded_msgs
|
||||
]
|
||||
|
||||
promises = await ledger._generate_change_promises(
|
||||
total_provided, invoice_amount, actual_fee_msat, outputs
|
||||
total_provided, invoice_amount, actual_fee, outputs
|
||||
)
|
||||
|
||||
assert len(promises) == expected_returned_promises
|
||||
@@ -151,7 +223,7 @@ async def test_generate_change_promises_legacy_wallet(ledger: Ledger):
|
||||
invoice_amount = 100_000
|
||||
fee_reserve = 2_000
|
||||
total_provided = invoice_amount + fee_reserve
|
||||
actual_fee_msat = 100_000
|
||||
actual_fee = 100
|
||||
|
||||
expected_returned_promises = 4 # Amounts = [64, 256, 512, 1024]
|
||||
expected_returned_fees = 1856
|
||||
@@ -159,11 +231,16 @@ async def test_generate_change_promises_legacy_wallet(ledger: Ledger):
|
||||
n_blank_outputs = 4
|
||||
blinded_msgs = [step1_alice(str(n)) for n in range(n_blank_outputs)]
|
||||
outputs = [
|
||||
BlindedMessage(amount=1, B_=b.serialize().hex()) for b, _ in blinded_msgs
|
||||
BlindedMessage(
|
||||
amount=1,
|
||||
B_=b.serialize().hex(),
|
||||
id="009a1f293253e41e",
|
||||
)
|
||||
for b, _ in blinded_msgs
|
||||
]
|
||||
|
||||
promises = await ledger._generate_change_promises(
|
||||
total_provided, invoice_amount, actual_fee_msat, outputs
|
||||
total_provided, invoice_amount, actual_fee, outputs
|
||||
)
|
||||
|
||||
assert len(promises) == expected_returned_promises
|
||||
@@ -193,9 +270,9 @@ async def test_get_balance(ledger: Ledger):
|
||||
@pytest.mark.asyncio
|
||||
async def test_maximum_balance(ledger: Ledger):
|
||||
settings.mint_max_balance = 1000
|
||||
invoice, id = await ledger.request_mint(8)
|
||||
await ledger.mint_quote(PostMintQuoteRequest(amount=8, unit="sat"))
|
||||
await assert_err(
|
||||
ledger.request_mint(8000),
|
||||
ledger.mint_quote(PostMintQuoteRequest(amount=8000, unit="sat")),
|
||||
"Mint has reached maximum balance.",
|
||||
)
|
||||
settings.mint_max_balance = 0
|
||||
|
||||
@@ -1,71 +1,371 @@
|
||||
import bolt11
|
||||
import httpx
|
||||
import pytest
|
||||
import pytest_asyncio
|
||||
|
||||
from cashu.core.base import CheckSpendableRequest, CheckSpendableResponse, Proof
|
||||
from cashu.core.base import (
|
||||
PostCheckStateRequest,
|
||||
PostCheckStateResponse,
|
||||
SpentState,
|
||||
)
|
||||
from cashu.core.settings import settings
|
||||
from cashu.mint.ledger import Ledger
|
||||
from cashu.wallet.wallet import Wallet
|
||||
from tests.helpers import get_real_invoice, is_fake, is_regtest, pay_if_regtest
|
||||
|
||||
BASE_URL = "http://localhost:3337"
|
||||
|
||||
|
||||
@pytest_asyncio.fixture(scope="function")
|
||||
async def wallet(ledger: Ledger):
|
||||
wallet1 = await Wallet.with_db(
|
||||
url=BASE_URL,
|
||||
db="test_data/wallet_mint_api",
|
||||
name="wallet_mint_api",
|
||||
)
|
||||
await wallet1.load_mint()
|
||||
yield wallet1
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_info(ledger):
|
||||
response = httpx.get(f"{BASE_URL}/info")
|
||||
@pytest.mark.skipif(
|
||||
settings.debug_mint_only_deprecated,
|
||||
reason="settings.debug_mint_only_deprecated is set",
|
||||
)
|
||||
async def test_info(ledger: Ledger):
|
||||
response = httpx.get(f"{BASE_URL}/v1/info")
|
||||
assert response.status_code == 200, f"{response.url} {response.status_code}"
|
||||
assert ledger.pubkey
|
||||
assert response.json()["pubkey"] == ledger.pubkey.serialize().hex()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_api_keys(ledger):
|
||||
response = httpx.get(f"{BASE_URL}/keys")
|
||||
@pytest.mark.skipif(
|
||||
settings.debug_mint_only_deprecated,
|
||||
reason="settings.debug_mint_only_deprecated is set",
|
||||
)
|
||||
async def test_api_keys(ledger: Ledger):
|
||||
response = httpx.get(f"{BASE_URL}/v1/keys")
|
||||
assert response.status_code == 200, f"{response.url} {response.status_code}"
|
||||
assert response.json() == {
|
||||
str(k): v.serialize().hex() for k, v in ledger.keyset.public_keys.items()
|
||||
assert ledger.keyset.public_keys
|
||||
expected = {
|
||||
"keysets": [
|
||||
{
|
||||
"id": keyset.id,
|
||||
"unit": keyset.unit.name,
|
||||
"keys": {
|
||||
str(k): v.serialize().hex() for k, v in keyset.public_keys.items() # type: ignore
|
||||
},
|
||||
}
|
||||
for keyset in ledger.keysets.values()
|
||||
]
|
||||
}
|
||||
assert response.json() == expected
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_api_keysets(ledger):
|
||||
response = httpx.get(f"{BASE_URL}/keysets")
|
||||
@pytest.mark.skipif(
|
||||
settings.debug_mint_only_deprecated,
|
||||
reason="settings.debug_mint_only_deprecated is set",
|
||||
)
|
||||
async def test_api_keysets(ledger: Ledger):
|
||||
response = httpx.get(f"{BASE_URL}/v1/keysets")
|
||||
assert response.status_code == 200, f"{response.url} {response.status_code}"
|
||||
assert response.json()["keysets"] == list(ledger.keysets.keysets.keys())
|
||||
expected = {
|
||||
"keysets": [
|
||||
{
|
||||
"id": "009a1f293253e41e",
|
||||
"unit": "sat",
|
||||
"active": True,
|
||||
},
|
||||
# for backwards compatibility of the new keyset ID format,
|
||||
# we also return the same keyset with the old base64 ID
|
||||
{
|
||||
"id": "eGnEWtdJ0PIM",
|
||||
"unit": "sat",
|
||||
"active": True,
|
||||
},
|
||||
]
|
||||
}
|
||||
assert response.json() == expected
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_api_keyset_keys(ledger):
|
||||
response = httpx.get(
|
||||
f"{BASE_URL}/keys/{'1cCNIAZ2X/w1'.replace('/', '_').replace('+', '-')}"
|
||||
@pytest.mark.skipif(
|
||||
settings.debug_mint_only_deprecated,
|
||||
reason="settings.debug_mint_only_deprecated is set",
|
||||
)
|
||||
async def test_api_keyset_keys(ledger: Ledger):
|
||||
response = httpx.get(f"{BASE_URL}/v1/keys/009a1f293253e41e")
|
||||
assert response.status_code == 200, f"{response.url} {response.status_code}"
|
||||
assert ledger.keyset.public_keys
|
||||
expected = {
|
||||
"keysets": [
|
||||
{
|
||||
"id": "009a1f293253e41e",
|
||||
"unit": "sat",
|
||||
"keys": {
|
||||
str(k): v.serialize().hex()
|
||||
for k, v in ledger.keysets["009a1f293253e41e"].public_keys.items() # type: ignore
|
||||
},
|
||||
}
|
||||
]
|
||||
}
|
||||
assert response.json() == expected
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.skipif(
|
||||
settings.debug_mint_only_deprecated,
|
||||
reason="settings.debug_mint_only_deprecated is set",
|
||||
)
|
||||
async def test_api_keyset_keys_old_keyset_id(ledger: Ledger):
|
||||
response = httpx.get(f"{BASE_URL}/v1/keys/eGnEWtdJ0PIM")
|
||||
assert response.status_code == 200, f"{response.url} {response.status_code}"
|
||||
assert ledger.keyset.public_keys
|
||||
expected = {
|
||||
"keysets": [
|
||||
{
|
||||
"id": "eGnEWtdJ0PIM",
|
||||
"unit": "sat",
|
||||
"keys": {
|
||||
str(k): v.serialize().hex()
|
||||
for k, v in ledger.keysets["eGnEWtdJ0PIM"].public_keys.items() # type: ignore
|
||||
},
|
||||
}
|
||||
]
|
||||
}
|
||||
assert response.json() == expected
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.skipif(
|
||||
settings.debug_mint_only_deprecated,
|
||||
reason="settings.debug_mint_only_deprecated is set",
|
||||
)
|
||||
async def test_split(ledger: Ledger, wallet: Wallet):
|
||||
invoice = await wallet.request_mint(64)
|
||||
pay_if_regtest(invoice.bolt11)
|
||||
await wallet.mint(64, id=invoice.id)
|
||||
assert wallet.balance == 64
|
||||
secrets, rs, derivation_paths = await wallet.generate_n_secrets(2)
|
||||
outputs, rs = wallet._construct_outputs([32, 32], secrets, rs)
|
||||
# outputs = wallet._construct_outputs([32, 32], ["a", "b"], ["c", "d"])
|
||||
inputs_payload = [p.to_dict() for p in wallet.proofs]
|
||||
outputs_payload = [o.dict() for o in outputs]
|
||||
payload = {"inputs": inputs_payload, "outputs": outputs_payload}
|
||||
response = httpx.post(f"{BASE_URL}/v1/swap", json=payload)
|
||||
assert response.status_code == 200, f"{response.url} {response.status_code}"
|
||||
result = response.json()
|
||||
assert len(result["signatures"]) == 2
|
||||
assert result["signatures"][0]["amount"] == 32
|
||||
assert result["signatures"][1]["amount"] == 32
|
||||
assert result["signatures"][0]["id"] == "009a1f293253e41e"
|
||||
assert result["signatures"][0]["dleq"]
|
||||
assert "e" in result["signatures"][0]["dleq"]
|
||||
assert "s" in result["signatures"][0]["dleq"]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.skipif(
|
||||
settings.debug_mint_only_deprecated,
|
||||
reason="settings.debug_mint_only_deprecated is set",
|
||||
)
|
||||
async def test_mint_quote(ledger: Ledger):
|
||||
response = httpx.post(
|
||||
f"{BASE_URL}/v1/mint/quote/bolt11",
|
||||
json={"unit": "sat", "amount": 100},
|
||||
)
|
||||
assert response.status_code == 200, f"{response.url} {response.status_code}"
|
||||
assert response.json() == {
|
||||
str(k): v.serialize().hex() for k, v in ledger.keyset.public_keys.items()
|
||||
}
|
||||
result = response.json()
|
||||
assert result["quote"]
|
||||
assert result["request"]
|
||||
invoice = bolt11.decode(result["request"])
|
||||
assert invoice.amount_msat == 100 * 1000
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_api_mint_validation(ledger):
|
||||
response = httpx.get(f"{BASE_URL}/mint?amount=-21")
|
||||
assert "detail" in response.json()
|
||||
response = httpx.get(f"{BASE_URL}/mint?amount=0")
|
||||
assert "detail" in response.json()
|
||||
response = httpx.get(f"{BASE_URL}/mint?amount=2100000000000001")
|
||||
assert "detail" in response.json()
|
||||
response = httpx.get(f"{BASE_URL}/mint?amount=1")
|
||||
assert "detail" not in response.json()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_api_check_state(ledger):
|
||||
proofs = [
|
||||
Proof(id="1234", amount=0, secret="asdasdasd", C="asdasdasd"),
|
||||
Proof(id="1234", amount=0, secret="asdasdasd1", C="asdasdasd1"),
|
||||
]
|
||||
payload = CheckSpendableRequest(proofs=proofs)
|
||||
@pytest.mark.skipif(
|
||||
settings.debug_mint_only_deprecated,
|
||||
reason="settings.debug_mint_only_deprecated is set",
|
||||
)
|
||||
async def test_mint(ledger: Ledger, wallet: Wallet):
|
||||
invoice = await wallet.request_mint(64)
|
||||
pay_if_regtest(invoice.bolt11)
|
||||
quote_id = invoice.id
|
||||
secrets, rs, derivation_paths = await wallet.generate_secrets_from_to(10000, 10001)
|
||||
outputs, rs = wallet._construct_outputs([32, 32], secrets, rs)
|
||||
outputs_payload = [o.dict() for o in outputs]
|
||||
response = httpx.post(
|
||||
f"{BASE_URL}/check",
|
||||
f"{BASE_URL}/v1/mint/bolt11",
|
||||
json={"quote": quote_id, "outputs": outputs_payload},
|
||||
timeout=None,
|
||||
)
|
||||
assert response.status_code == 200, f"{response.url} {response.status_code}"
|
||||
result = response.json()
|
||||
assert len(result["signatures"]) == 2
|
||||
assert result["signatures"][0]["amount"] == 32
|
||||
assert result["signatures"][1]["amount"] == 32
|
||||
assert result["signatures"][0]["id"] == "009a1f293253e41e"
|
||||
assert result["signatures"][0]["dleq"]
|
||||
assert "e" in result["signatures"][0]["dleq"]
|
||||
assert "s" in result["signatures"][0]["dleq"]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.skipif(
|
||||
settings.debug_mint_only_deprecated,
|
||||
reason="settings.debug_mint_only_deprecated is set",
|
||||
)
|
||||
@pytest.mark.skipif(
|
||||
is_regtest,
|
||||
reason="regtest",
|
||||
)
|
||||
async def test_melt_quote_internal(ledger: Ledger, wallet: Wallet):
|
||||
# internal invoice
|
||||
invoice = await wallet.request_mint(64)
|
||||
request = invoice.bolt11
|
||||
response = httpx.post(
|
||||
f"{BASE_URL}/v1/melt/quote/bolt11",
|
||||
json={"unit": "sat", "request": request},
|
||||
)
|
||||
assert response.status_code == 200, f"{response.url} {response.status_code}"
|
||||
result = response.json()
|
||||
assert result["quote"]
|
||||
assert result["amount"] == 64
|
||||
# TODO: internal invoice, fee should be 0
|
||||
assert result["fee_reserve"] == 0
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.skipif(
|
||||
settings.debug_mint_only_deprecated,
|
||||
reason="settings.debug_mint_only_deprecated is set",
|
||||
)
|
||||
@pytest.mark.skipif(
|
||||
is_fake,
|
||||
reason="only works on regtest",
|
||||
)
|
||||
async def test_melt_quote_external(ledger: Ledger, wallet: Wallet):
|
||||
# internal invoice
|
||||
invoice_dict = get_real_invoice(64)
|
||||
request = invoice_dict["payment_request"]
|
||||
response = httpx.post(
|
||||
f"{BASE_URL}/v1/melt/quote/bolt11",
|
||||
json={"unit": "sat", "request": request},
|
||||
)
|
||||
assert response.status_code == 200, f"{response.url} {response.status_code}"
|
||||
result = response.json()
|
||||
assert result["quote"]
|
||||
assert result["amount"] == 64
|
||||
# external invoice, fee should be 2
|
||||
assert result["fee_reserve"] == 2
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.skipif(
|
||||
settings.debug_mint_only_deprecated,
|
||||
reason="settings.debug_mint_only_deprecated is set",
|
||||
)
|
||||
async def test_melt_internal(ledger: Ledger, wallet: Wallet):
|
||||
# internal invoice
|
||||
invoice = await wallet.request_mint(64)
|
||||
pay_if_regtest(invoice.bolt11)
|
||||
await wallet.mint(64, id=invoice.id)
|
||||
assert wallet.balance == 64
|
||||
|
||||
# create invoice to melt to
|
||||
invoice = await wallet.request_mint(64)
|
||||
invoice_payment_request = invoice.bolt11
|
||||
|
||||
quote = await wallet.melt_quote(invoice_payment_request)
|
||||
assert quote.amount == 64
|
||||
assert quote.fee_reserve == 0
|
||||
|
||||
inputs_payload = [p.to_dict() for p in wallet.proofs]
|
||||
|
||||
# outputs for change
|
||||
secrets, rs, derivation_paths = await wallet.generate_n_secrets(1)
|
||||
outputs, rs = wallet._construct_outputs([2], secrets, rs)
|
||||
outputs_payload = [o.dict() for o in outputs]
|
||||
|
||||
response = httpx.post(
|
||||
f"{BASE_URL}/v1/melt/bolt11",
|
||||
json={
|
||||
"quote": quote.quote,
|
||||
"inputs": inputs_payload,
|
||||
"outputs": outputs_payload,
|
||||
},
|
||||
timeout=None,
|
||||
)
|
||||
assert response.status_code == 200, f"{response.url} {response.status_code}"
|
||||
result = response.json()
|
||||
assert result.get("payment_preimage") is not None
|
||||
assert result["paid"] is True
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.skipif(
|
||||
settings.debug_mint_only_deprecated,
|
||||
reason="settings.debug_mint_only_deprecated is set",
|
||||
)
|
||||
@pytest.mark.skipif(
|
||||
is_fake,
|
||||
reason="only works on regtest",
|
||||
)
|
||||
async def test_melt_external(ledger: Ledger, wallet: Wallet):
|
||||
# internal invoice
|
||||
invoice = await wallet.request_mint(64)
|
||||
pay_if_regtest(invoice.bolt11)
|
||||
await wallet.mint(64, id=invoice.id)
|
||||
assert wallet.balance == 64
|
||||
|
||||
invoice_dict = get_real_invoice(62)
|
||||
invoice_payment_request = invoice_dict["payment_request"]
|
||||
|
||||
quote = await wallet.melt_quote(invoice_payment_request)
|
||||
assert quote.amount == 62
|
||||
assert quote.fee_reserve == 2
|
||||
|
||||
keep, send = await wallet.split_to_send(wallet.proofs, 64)
|
||||
inputs_payload = [p.to_dict() for p in send]
|
||||
|
||||
# outputs for change
|
||||
secrets, rs, derivation_paths = await wallet.generate_n_secrets(1)
|
||||
outputs, rs = wallet._construct_outputs([2], secrets, rs)
|
||||
outputs_payload = [o.dict() for o in outputs]
|
||||
|
||||
response = httpx.post(
|
||||
f"{BASE_URL}/v1/melt/bolt11",
|
||||
json={
|
||||
"quote": quote.quote,
|
||||
"inputs": inputs_payload,
|
||||
"outputs": outputs_payload,
|
||||
},
|
||||
timeout=None,
|
||||
)
|
||||
assert response.status_code == 200, f"{response.url} {response.status_code}"
|
||||
result = response.json()
|
||||
assert result.get("payment_preimage") is not None
|
||||
assert result["paid"] is True
|
||||
assert result["change"]
|
||||
# we get back 2 sats because Lightning was free to pay on regtest
|
||||
assert result["change"][0]["amount"] == 2
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.skipif(
|
||||
settings.debug_mint_only_deprecated,
|
||||
reason="settings.debug_mint_only_deprecated is set",
|
||||
)
|
||||
async def test_api_check_state(ledger: Ledger):
|
||||
payload = PostCheckStateRequest(secrets=["asdasdasd", "asdasdasd1"])
|
||||
response = httpx.post(
|
||||
f"{BASE_URL}/v1/checkstate",
|
||||
json=payload.dict(),
|
||||
)
|
||||
assert response.status_code == 200, f"{response.url} {response.status_code}"
|
||||
states = CheckSpendableResponse.parse_obj(response.json())
|
||||
assert states.spendable
|
||||
assert len(states.spendable) == 2
|
||||
assert states.pending
|
||||
assert len(states.pending) == 2
|
||||
response = PostCheckStateResponse.parse_obj(response.json())
|
||||
assert response
|
||||
assert len(response.states) == 2
|
||||
assert response.states[0].state == SpentState.unspent
|
||||
|
||||
289
tests/test_mint_api_deprecated.py
Normal file
289
tests/test_mint_api_deprecated.py
Normal file
@@ -0,0 +1,289 @@
|
||||
import httpx
|
||||
import pytest
|
||||
import pytest_asyncio
|
||||
|
||||
from cashu.core.base import (
|
||||
CheckSpendableRequest_deprecated,
|
||||
CheckSpendableResponse_deprecated,
|
||||
Proof,
|
||||
)
|
||||
from cashu.core.settings import settings
|
||||
from cashu.mint.ledger import Ledger
|
||||
from cashu.wallet.wallet import Wallet
|
||||
from tests.helpers import get_real_invoice, is_fake, is_regtest, pay_if_regtest
|
||||
|
||||
BASE_URL = "http://localhost:3337"
|
||||
|
||||
|
||||
@pytest_asyncio.fixture(scope="function")
|
||||
async def wallet(ledger: Ledger):
|
||||
wallet1 = await Wallet.with_db(
|
||||
url=BASE_URL,
|
||||
db="test_data/wallet_mint_api_deprecated",
|
||||
name="wallet_mint_api_deprecated",
|
||||
)
|
||||
await wallet1.load_mint()
|
||||
yield wallet1
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_info(ledger: Ledger):
|
||||
response = httpx.get(f"{BASE_URL}/info")
|
||||
assert response.status_code == 200, f"{response.url} {response.status_code}"
|
||||
assert ledger.pubkey
|
||||
assert response.json()["pubkey"] == ledger.pubkey.serialize().hex()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_api_keys(ledger: Ledger):
|
||||
response = httpx.get(f"{BASE_URL}/keys")
|
||||
assert response.status_code == 200, f"{response.url} {response.status_code}"
|
||||
assert ledger.keyset.public_keys
|
||||
assert response.json() == {
|
||||
str(k): v.serialize().hex() for k, v in ledger.keyset.public_keys.items()
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_api_keysets(ledger: Ledger):
|
||||
response = httpx.get(f"{BASE_URL}/keysets")
|
||||
assert response.status_code == 200, f"{response.url} {response.status_code}"
|
||||
assert ledger.keyset.public_keys
|
||||
assert response.json()["keysets"] == list(ledger.keysets.keys())
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_api_keyset_keys(ledger: Ledger):
|
||||
response = httpx.get(f"{BASE_URL}/keys/009a1f293253e41e")
|
||||
assert response.status_code == 200, f"{response.url} {response.status_code}"
|
||||
assert ledger.keyset.public_keys
|
||||
assert response.json() == {
|
||||
str(k): v.serialize().hex() for k, v in ledger.keyset.public_keys.items()
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_split(ledger: Ledger, wallet: Wallet):
|
||||
invoice = await wallet.request_mint(64)
|
||||
pay_if_regtest(invoice.bolt11)
|
||||
await wallet.mint(64, id=invoice.id)
|
||||
assert wallet.balance == 64
|
||||
secrets, rs, derivation_paths = await wallet.generate_secrets_from_to(20000, 20001)
|
||||
outputs, rs = wallet._construct_outputs([32, 32], secrets, rs)
|
||||
# outputs = wallet._construct_outputs([32, 32], ["a", "b"], ["c", "d"])
|
||||
inputs_payload = [p.to_dict() for p in wallet.proofs]
|
||||
outputs_payload = [o.dict() for o in outputs]
|
||||
# strip "id" from outputs_payload, which is not used in the deprecated split endpoint
|
||||
for o in outputs_payload:
|
||||
o.pop("id")
|
||||
payload = {"proofs": inputs_payload, "outputs": outputs_payload}
|
||||
response = httpx.post(f"{BASE_URL}/split", json=payload, timeout=None)
|
||||
assert response.status_code == 200, f"{response.url} {response.status_code}"
|
||||
result = response.json()
|
||||
assert result["promises"]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_split_deprecated_with_amount(ledger: Ledger, wallet: Wallet):
|
||||
invoice = await wallet.request_mint(64)
|
||||
pay_if_regtest(invoice.bolt11)
|
||||
await wallet.mint(64, id=invoice.id)
|
||||
assert wallet.balance == 64
|
||||
secrets, rs, derivation_paths = await wallet.generate_secrets_from_to(80000, 80001)
|
||||
outputs, rs = wallet._construct_outputs([32, 32], secrets, rs)
|
||||
# outputs = wallet._construct_outputs([32, 32], ["a", "b"], ["c", "d"])
|
||||
inputs_payload = [p.to_dict() for p in wallet.proofs]
|
||||
outputs_payload = [o.dict() for o in outputs]
|
||||
# strip "id" from outputs_payload, which is not used in the deprecated split endpoint
|
||||
for o in outputs_payload:
|
||||
o.pop("id")
|
||||
# we supply an amount here, which should cause the very old deprecated split endpoint to be used
|
||||
payload = {"proofs": inputs_payload, "outputs": outputs_payload, "amount": 32}
|
||||
response = httpx.post(f"{BASE_URL}/split", json=payload, timeout=None)
|
||||
assert response.status_code == 200, f"{response.url} {response.status_code}"
|
||||
result = response.json()
|
||||
# old deprecated output format
|
||||
assert result["fst"]
|
||||
assert result["snd"]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_api_mint_validation(ledger):
|
||||
response = httpx.get(f"{BASE_URL}/mint?amount=-21")
|
||||
assert "detail" in response.json()
|
||||
response = httpx.get(f"{BASE_URL}/mint?amount=0")
|
||||
assert "detail" in response.json()
|
||||
response = httpx.get(f"{BASE_URL}/mint?amount=2100000000000001")
|
||||
assert "detail" in response.json()
|
||||
response = httpx.get(f"{BASE_URL}/mint?amount=1")
|
||||
assert "detail" not in response.json()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_mint(ledger: Ledger, wallet: Wallet):
|
||||
invoice = await wallet.request_mint(64)
|
||||
pay_if_regtest(invoice.bolt11)
|
||||
quote_id = invoice.id
|
||||
secrets, rs, derivation_paths = await wallet.generate_secrets_from_to(10000, 10001)
|
||||
outputs, rs = wallet._construct_outputs([32, 32], secrets, rs)
|
||||
outputs_payload = [o.dict() for o in outputs]
|
||||
response = httpx.post(
|
||||
f"{BASE_URL}/mint",
|
||||
json={"outputs": outputs_payload},
|
||||
params={"hash": quote_id},
|
||||
timeout=None,
|
||||
)
|
||||
assert response.status_code == 200, f"{response.url} {response.status_code}"
|
||||
result = response.json()
|
||||
assert len(result["promises"]) == 2
|
||||
assert result["promises"][0]["amount"] == 32
|
||||
assert result["promises"][1]["amount"] == 32
|
||||
if settings.debug_mint_only_deprecated:
|
||||
assert result["promises"][0]["id"] == "eGnEWtdJ0PIM"
|
||||
else:
|
||||
assert result["promises"][0]["id"] == "009a1f293253e41e"
|
||||
assert result["promises"][0]["dleq"]
|
||||
assert "e" in result["promises"][0]["dleq"]
|
||||
assert "s" in result["promises"][0]["dleq"]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_melt_internal(ledger: Ledger, wallet: Wallet):
|
||||
# internal invoice
|
||||
invoice = await wallet.request_mint(64)
|
||||
pay_if_regtest(invoice.bolt11)
|
||||
await wallet.mint(64, id=invoice.id)
|
||||
assert wallet.balance == 64
|
||||
|
||||
# create invoice to melt to
|
||||
invoice = await wallet.request_mint(64)
|
||||
|
||||
invoice_payment_request = invoice.bolt11
|
||||
|
||||
quote = await wallet.melt_quote(invoice_payment_request)
|
||||
assert quote.amount == 64
|
||||
assert quote.fee_reserve == 0
|
||||
|
||||
inputs_payload = [p.to_dict() for p in wallet.proofs]
|
||||
|
||||
# outputs for change
|
||||
secrets, rs, derivation_paths = await wallet.generate_n_secrets(1)
|
||||
outputs, rs = wallet._construct_outputs([2], secrets, rs)
|
||||
outputs_payload = [o.dict() for o in outputs]
|
||||
|
||||
response = httpx.post(
|
||||
f"{BASE_URL}/melt",
|
||||
json={
|
||||
"pr": invoice_payment_request,
|
||||
"proofs": inputs_payload,
|
||||
"outputs": outputs_payload,
|
||||
},
|
||||
timeout=None,
|
||||
)
|
||||
assert response.status_code == 200, f"{response.url} {response.status_code}"
|
||||
result = response.json()
|
||||
assert result.get("preimage") is not None
|
||||
assert result["paid"] is True
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.skipif(
|
||||
is_fake,
|
||||
reason="only works on regtest",
|
||||
)
|
||||
async def test_melt_external(ledger: Ledger, wallet: Wallet):
|
||||
# internal invoice
|
||||
invoice = await wallet.request_mint(64)
|
||||
pay_if_regtest(invoice.bolt11)
|
||||
await wallet.mint(64, id=invoice.id)
|
||||
assert wallet.balance == 64
|
||||
|
||||
# create invoice to melt to
|
||||
# use 2 sat less because we need to pay the fee
|
||||
invoice_dict = get_real_invoice(62)
|
||||
invoice_payment_request = invoice_dict["payment_request"]
|
||||
|
||||
quote = await wallet.melt_quote(invoice_payment_request)
|
||||
assert quote.amount == 62
|
||||
assert quote.fee_reserve == 2
|
||||
|
||||
inputs_payload = [p.to_dict() for p in wallet.proofs]
|
||||
|
||||
# outputs for change
|
||||
secrets, rs, derivation_paths = await wallet.generate_n_secrets(1)
|
||||
outputs, rs = wallet._construct_outputs([2], secrets, rs)
|
||||
outputs_payload = [o.dict() for o in outputs]
|
||||
|
||||
response = httpx.post(
|
||||
f"{BASE_URL}/melt",
|
||||
json={
|
||||
"pr": invoice_payment_request,
|
||||
"proofs": inputs_payload,
|
||||
"outputs": outputs_payload,
|
||||
},
|
||||
timeout=None,
|
||||
)
|
||||
assert response.status_code == 200, f"{response.url} {response.status_code}"
|
||||
result = response.json()
|
||||
assert result.get("preimage") is not None
|
||||
assert result["paid"] is True
|
||||
assert result["change"]
|
||||
# we get back 2 sats because Lightning was free to pay on regtest
|
||||
assert result["change"][0]["amount"] == 2
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_checkfees(ledger: Ledger, wallet: Wallet):
|
||||
# internal invoice
|
||||
invoice = await wallet.request_mint(64)
|
||||
response = httpx.post(
|
||||
f"{BASE_URL}/checkfees",
|
||||
json={
|
||||
"pr": invoice.bolt11,
|
||||
},
|
||||
timeout=None,
|
||||
)
|
||||
assert response.status_code == 200, f"{response.url} {response.status_code}"
|
||||
result = response.json()
|
||||
# internal invoice, so no fee
|
||||
assert result["fee"] == 0
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.skipif(not is_regtest, reason="only works on regtest")
|
||||
async def test_checkfees_external(ledger: Ledger, wallet: Wallet):
|
||||
# external invoice
|
||||
invoice_dict = get_real_invoice(62)
|
||||
invoice_payment_request = invoice_dict["payment_request"]
|
||||
response = httpx.post(
|
||||
f"{BASE_URL}/checkfees",
|
||||
json={"pr": invoice_payment_request},
|
||||
timeout=None,
|
||||
)
|
||||
assert response.status_code == 200, f"{response.url} {response.status_code}"
|
||||
result = response.json()
|
||||
# external invoice, so fee
|
||||
assert result["fee"] == 2
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.skipif(
|
||||
settings.debug_mint_only_deprecated,
|
||||
reason="settings.debug_mint_only_deprecated is set",
|
||||
)
|
||||
async def test_api_check_state(ledger: Ledger):
|
||||
proofs = [
|
||||
Proof(id="1234", amount=0, secret="asdasdasd", C="asdasdasd"),
|
||||
Proof(id="1234", amount=0, secret="asdasdasd1", C="asdasdasd1"),
|
||||
]
|
||||
payload = CheckSpendableRequest_deprecated(proofs=proofs)
|
||||
response = httpx.post(
|
||||
f"{BASE_URL}/check",
|
||||
json=payload.dict(),
|
||||
)
|
||||
assert response.status_code == 200, f"{response.url} {response.status_code}"
|
||||
states = CheckSpendableResponse_deprecated.parse_obj(response.json())
|
||||
assert states.spendable
|
||||
assert len(states.spendable) == 2
|
||||
assert states.pending
|
||||
assert len(states.pending) == 2
|
||||
@@ -1,11 +1,13 @@
|
||||
import pytest
|
||||
import pytest_asyncio
|
||||
|
||||
from cashu.core.base import PostMeltQuoteRequest, PostMintQuoteRequest
|
||||
from cashu.core.helpers import sum_proofs
|
||||
from cashu.mint.ledger import Ledger
|
||||
from cashu.wallet.wallet import Wallet
|
||||
from cashu.wallet.wallet import Wallet as Wallet1
|
||||
from tests.conftest import SERVER_ENDPOINT
|
||||
from tests.helpers import pay_if_regtest
|
||||
from tests.helpers import get_real_invoice, is_fake, is_regtest, pay_if_regtest
|
||||
|
||||
|
||||
async def assert_err(f, msg):
|
||||
@@ -20,36 +22,120 @@ async def assert_err(f, msg):
|
||||
|
||||
|
||||
@pytest_asyncio.fixture(scope="function")
|
||||
async def wallet1(mint):
|
||||
async def wallet1(ledger: Ledger):
|
||||
wallet1 = await Wallet1.with_db(
|
||||
url=SERVER_ENDPOINT,
|
||||
db="test_data/wallet1",
|
||||
name="wallet1",
|
||||
)
|
||||
await wallet1.load_mint()
|
||||
wallet1.status()
|
||||
yield wallet1
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_melt(wallet1: Wallet, ledger: Ledger):
|
||||
@pytest.mark.skipif(is_regtest, reason="only works with FakeWallet")
|
||||
async def test_melt_internal(wallet1: Wallet, ledger: Ledger):
|
||||
# mint twice so we have enough to pay the second invoice back
|
||||
invoice = await wallet1.request_mint(64)
|
||||
pay_if_regtest(invoice.bolt11)
|
||||
await wallet1.mint(64, id=invoice.id)
|
||||
invoice2 = await wallet1.request_mint(64)
|
||||
pay_if_regtest(invoice2.bolt11)
|
||||
await wallet1.mint(64, id=invoice2.id)
|
||||
invoice = await wallet1.request_mint(128)
|
||||
await wallet1.mint(128, id=invoice.id)
|
||||
assert wallet1.balance == 128
|
||||
total_amount, fee_reserve_sat = await wallet1.get_pay_amount_with_fees(
|
||||
invoice2.bolt11
|
||||
|
||||
# create a mint quote so that we can melt to it internally
|
||||
invoice_to_pay = await wallet1.request_mint(64)
|
||||
invoice_payment_request = invoice_to_pay.bolt11
|
||||
|
||||
melt_quote = await ledger.melt_quote(
|
||||
PostMeltQuoteRequest(request=invoice_payment_request, unit="sat")
|
||||
)
|
||||
melt_fees = await ledger.get_melt_fees(invoice2.bolt11)
|
||||
assert melt_fees == fee_reserve_sat
|
||||
assert not melt_quote.paid
|
||||
assert melt_quote.amount == 64
|
||||
assert melt_quote.fee_reserve == 0
|
||||
|
||||
melt_quote_pre_payment = await ledger.get_melt_quote(melt_quote.quote)
|
||||
assert not melt_quote_pre_payment.paid, "melt quote should not be paid"
|
||||
|
||||
keep_proofs, send_proofs = await wallet1.split_to_send(wallet1.proofs, 64)
|
||||
await ledger.melt(proofs=send_proofs, quote=melt_quote.quote)
|
||||
|
||||
melt_quote_post_payment = await ledger.get_melt_quote(melt_quote.quote)
|
||||
assert melt_quote_post_payment.paid, "melt quote should be paid"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.skipif(is_fake, reason="only works with Regtest")
|
||||
async def test_melt_external(wallet1: Wallet, ledger: Ledger):
|
||||
# mint twice so we have enough to pay the second invoice back
|
||||
invoice = await wallet1.request_mint(128)
|
||||
pay_if_regtest(invoice.bolt11)
|
||||
await wallet1.mint(128, id=invoice.id)
|
||||
assert wallet1.balance == 128
|
||||
|
||||
invoice_dict = get_real_invoice(64)
|
||||
invoice_payment_request = invoice_dict["payment_request"]
|
||||
|
||||
mint_quote = await wallet1.get_pay_amount_with_fees(invoice_payment_request)
|
||||
total_amount = mint_quote.amount + mint_quote.fee_reserve
|
||||
keep_proofs, send_proofs = await wallet1.split_to_send(wallet1.proofs, total_amount)
|
||||
melt_quote = await ledger.melt_quote(
|
||||
PostMeltQuoteRequest(request=invoice_payment_request, unit="sat")
|
||||
)
|
||||
|
||||
await ledger.melt(send_proofs, invoice2.bolt11, outputs=None)
|
||||
melt_quote_pre_payment = await ledger.get_melt_quote(melt_quote.quote)
|
||||
assert not melt_quote_pre_payment.paid, "melt quote should not be paid"
|
||||
|
||||
assert not melt_quote.paid, "melt quote should not be paid"
|
||||
await ledger.melt(proofs=send_proofs, quote=melt_quote.quote)
|
||||
|
||||
melt_quote_post_payment = await ledger.get_melt_quote(melt_quote.quote)
|
||||
assert melt_quote_post_payment.paid, "melt quote should be paid"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.skipif(is_regtest, reason="only works with FakeWallet")
|
||||
async def test_mint_internal(wallet1: Wallet, ledger: Ledger):
|
||||
invoice = await wallet1.request_mint(128)
|
||||
|
||||
mint_quote = await ledger.get_mint_quote(invoice.id)
|
||||
|
||||
assert mint_quote.paid, "mint quote should be paid"
|
||||
|
||||
output_amounts = [128]
|
||||
secrets, rs, derivation_paths = await wallet1.generate_n_secrets(
|
||||
len(output_amounts)
|
||||
)
|
||||
outputs, rs = wallet1._construct_outputs(output_amounts, secrets, rs)
|
||||
await ledger.mint(outputs=outputs, quote_id=invoice.id)
|
||||
|
||||
await assert_err(
|
||||
ledger.mint(outputs=outputs, quote_id=invoice.id),
|
||||
"outputs have already been signed before.",
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.skipif(is_fake, reason="only works with Regtest")
|
||||
async def test_mint_external(wallet1: Wallet, ledger: Ledger):
|
||||
quote = await ledger.mint_quote(PostMintQuoteRequest(amount=128, unit="sat"))
|
||||
|
||||
mint_quote = await ledger.get_mint_quote(quote.quote)
|
||||
assert not mint_quote.paid, "mint quote not should be paid"
|
||||
|
||||
await assert_err(
|
||||
wallet1.mint(128, id=quote.quote),
|
||||
"quote not paid",
|
||||
)
|
||||
|
||||
pay_if_regtest(quote.request)
|
||||
|
||||
mint_quote = await ledger.get_mint_quote(quote.quote)
|
||||
assert mint_quote.paid, "mint quote should be paid"
|
||||
|
||||
output_amounts = [128]
|
||||
secrets, rs, derivation_paths = await wallet1.generate_n_secrets(
|
||||
len(output_amounts)
|
||||
)
|
||||
outputs, rs = wallet1._construct_outputs(output_amounts, secrets, rs)
|
||||
await ledger.mint(outputs=outputs, quote_id=quote.quote)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@@ -120,14 +206,13 @@ async def test_split_with_input_more_than_outputs(wallet1: Wallet, ledger: Ledge
|
||||
|
||||
# make sure we can still spend our tokens
|
||||
keep_proofs, send_proofs = await wallet1.split(inputs, 10)
|
||||
print(keep_proofs, send_proofs)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_split_twice_with_same_outputs(wallet1: Wallet, ledger: Ledger):
|
||||
invoice = await wallet1.request_mint(128)
|
||||
pay_if_regtest(invoice.bolt11)
|
||||
await wallet1.mint(128, [64, 64], id=invoice.id)
|
||||
await wallet1.mint(128, split=[64, 64], id=invoice.id)
|
||||
inputs1 = wallet1.proofs[:1]
|
||||
inputs2 = wallet1.proofs[1:]
|
||||
|
||||
@@ -164,14 +249,14 @@ async def test_mint_with_same_outputs_twice(wallet1: Wallet, ledger: Ledger):
|
||||
len(output_amounts)
|
||||
)
|
||||
outputs, rs = wallet1._construct_outputs(output_amounts, secrets, rs)
|
||||
await ledger.mint(outputs, id=invoice.id)
|
||||
await ledger.mint(outputs=outputs, quote_id=invoice.id)
|
||||
|
||||
# now try to mint with the same outputs again
|
||||
invoice2 = await wallet1.request_mint(128)
|
||||
pay_if_regtest(invoice2.bolt11)
|
||||
|
||||
await assert_err(
|
||||
ledger.mint(outputs, id=invoice2.id),
|
||||
ledger.mint(outputs=outputs, quote_id=invoice2.id),
|
||||
"outputs have already been signed before.",
|
||||
)
|
||||
|
||||
@@ -191,16 +276,79 @@ async def test_melt_with_same_outputs_twice(wallet1: Wallet, ledger: Ledger):
|
||||
# we use the outputs once for minting
|
||||
invoice2 = await wallet1.request_mint(128)
|
||||
pay_if_regtest(invoice2.bolt11)
|
||||
await ledger.mint(outputs, id=invoice2.id)
|
||||
await ledger.mint(outputs=outputs, quote_id=invoice2.id)
|
||||
|
||||
# use the same outputs for melting
|
||||
invoice3 = await wallet1.request_mint(128)
|
||||
mint_quote = await ledger.mint_quote(PostMintQuoteRequest(unit="sat", amount=128))
|
||||
melt_quote = await ledger.melt_quote(
|
||||
PostMeltQuoteRequest(unit="sat", request=mint_quote.request)
|
||||
)
|
||||
await assert_err(
|
||||
ledger.melt(wallet1.proofs, invoice3.bolt11, outputs=outputs),
|
||||
ledger.melt(proofs=wallet1.proofs, quote=melt_quote.quote, outputs=outputs),
|
||||
"outputs have already been signed before.",
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_melt_with_less_inputs_than_invoice(wallet1: Wallet, ledger: Ledger):
|
||||
invoice = await wallet1.request_mint(32)
|
||||
pay_if_regtest(invoice.bolt11)
|
||||
await wallet1.mint(32, id=invoice.id)
|
||||
|
||||
# outputs for fee return
|
||||
output_amounts = [1, 1, 1, 1]
|
||||
secrets, rs, derivation_paths = await wallet1.generate_n_secrets(
|
||||
len(output_amounts)
|
||||
)
|
||||
outputs, rs = wallet1._construct_outputs(output_amounts, secrets, rs)
|
||||
|
||||
# create a mint quote to pay
|
||||
mint_quote = await ledger.mint_quote(PostMintQuoteRequest(unit="sat", amount=128))
|
||||
# prepare melt quote
|
||||
melt_quote = await ledger.melt_quote(
|
||||
PostMeltQuoteRequest(unit="sat", request=mint_quote.request)
|
||||
)
|
||||
|
||||
assert melt_quote.amount + melt_quote.fee_reserve > sum_proofs(wallet1.proofs)
|
||||
|
||||
# try to pay with not enough inputs
|
||||
await assert_err(
|
||||
ledger.melt(proofs=wallet1.proofs, quote=melt_quote.quote, outputs=outputs),
|
||||
"not enough inputs provided for melt",
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_melt_with_more_inputs_than_invoice(wallet1: Wallet, ledger: Ledger):
|
||||
invoice = await wallet1.request_mint(130)
|
||||
pay_if_regtest(invoice.bolt11)
|
||||
await wallet1.mint(130, split=[64, 64, 2], id=invoice.id)
|
||||
|
||||
# outputs for fee return
|
||||
output_amounts = [1, 1, 1, 1]
|
||||
secrets, rs, derivation_paths = await wallet1.generate_n_secrets(
|
||||
len(output_amounts)
|
||||
)
|
||||
outputs, rs = wallet1._construct_outputs(output_amounts, secrets, rs)
|
||||
|
||||
# create a mint quote to pay
|
||||
mint_quote = await ledger.mint_quote(PostMintQuoteRequest(unit="sat", amount=128))
|
||||
# prepare melt quote
|
||||
melt_quote = await ledger.melt_quote(
|
||||
PostMeltQuoteRequest(unit="sat", request=mint_quote.request)
|
||||
)
|
||||
# fees are 0 because it's internal
|
||||
assert melt_quote.fee_reserve == 0
|
||||
|
||||
# make sure we have more inputs than the melt quote needs
|
||||
assert sum_proofs(wallet1.proofs) >= melt_quote.amount + melt_quote.fee_reserve
|
||||
payment_proof, return_outputs = await ledger.melt(
|
||||
proofs=wallet1.proofs, quote=melt_quote.quote, outputs=outputs
|
||||
)
|
||||
# we get 2 sats back because we overpaid
|
||||
assert sum([o.amount for o in return_outputs]) == 2
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_check_proof_state(wallet1: Wallet, ledger: Ledger):
|
||||
invoice = await wallet1.request_mint(64)
|
||||
@@ -209,6 +357,7 @@ async def test_check_proof_state(wallet1: Wallet, ledger: Ledger):
|
||||
|
||||
keep_proofs, send_proofs = await wallet1.split_to_send(wallet1.proofs, 10)
|
||||
|
||||
spendable, pending = await ledger.check_proof_state(proofs=send_proofs)
|
||||
assert sum(spendable) == len(send_proofs)
|
||||
assert sum(pending) == 0
|
||||
proof_states = await ledger.check_proofs_state(
|
||||
secrets=[p.secret for p in send_proofs]
|
||||
)
|
||||
assert all([p.state.value == "UNSPENT" for p in proof_states])
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
import copy
|
||||
import shutil
|
||||
from pathlib import Path
|
||||
from typing import List, Union
|
||||
|
||||
import pytest
|
||||
@@ -10,12 +8,12 @@ from cashu.core.base import Proof
|
||||
from cashu.core.errors import CashuError, KeysetNotFoundError
|
||||
from cashu.core.helpers import sum_proofs
|
||||
from cashu.core.settings import settings
|
||||
from cashu.wallet.crud import get_keyset, get_lightning_invoice, get_proofs
|
||||
from cashu.wallet.crud import get_keysets, get_lightning_invoice, get_proofs
|
||||
from cashu.wallet.wallet import Wallet
|
||||
from cashu.wallet.wallet import Wallet as Wallet1
|
||||
from cashu.wallet.wallet import Wallet as Wallet2
|
||||
from tests.conftest import SERVER_ENDPOINT
|
||||
from tests.helpers import get_real_invoice, is_regtest, pay_if_regtest
|
||||
from tests.helpers import get_real_invoice, is_fake, is_regtest, pay_if_regtest
|
||||
|
||||
|
||||
async def assert_err(f, msg: Union[str, CashuError]):
|
||||
@@ -56,47 +54,32 @@ async def wallet1(mint):
|
||||
name="wallet1",
|
||||
)
|
||||
await wallet1.load_mint()
|
||||
wallet1.status()
|
||||
yield wallet1
|
||||
|
||||
|
||||
@pytest_asyncio.fixture(scope="function")
|
||||
async def wallet2(mint):
|
||||
async def wallet2():
|
||||
wallet2 = await Wallet2.with_db(
|
||||
url=SERVER_ENDPOINT,
|
||||
db="test_data/wallet2",
|
||||
name="wallet2",
|
||||
)
|
||||
await wallet2.load_mint()
|
||||
wallet2.status()
|
||||
yield wallet2
|
||||
|
||||
|
||||
@pytest_asyncio.fixture(scope="function")
|
||||
async def wallet3(mint):
|
||||
dirpath = Path("test_data/wallet3")
|
||||
if dirpath.exists() and dirpath.is_dir():
|
||||
shutil.rmtree(dirpath)
|
||||
|
||||
wallet3 = await Wallet1.with_db(
|
||||
url=SERVER_ENDPOINT,
|
||||
db="test_data/wallet3",
|
||||
name="wallet3",
|
||||
)
|
||||
await wallet3.db.execute("DELETE FROM proofs")
|
||||
await wallet3.db.execute("DELETE FROM proofs_used")
|
||||
await wallet3.load_mint()
|
||||
wallet3.status()
|
||||
yield wallet3
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_keys(wallet1: Wallet):
|
||||
assert wallet1.keysets[wallet1.keyset_id].public_keys
|
||||
assert len(wallet1.keysets[wallet1.keyset_id].public_keys) == settings.max_order
|
||||
keyset = await wallet1._get_keys(wallet1.url)
|
||||
keysets = await wallet1._get_keys()
|
||||
keyset = keysets[0]
|
||||
assert keyset.id is not None
|
||||
assert keyset.id == "1cCNIAZ2X/w1"
|
||||
# assert keyset.id_deprecated == "eGnEWtdJ0PIM"
|
||||
if settings.debug_mint_only_deprecated:
|
||||
assert keyset.id == "eGnEWtdJ0PIM"
|
||||
else:
|
||||
assert keyset.id == "009a1f293253e41e"
|
||||
assert isinstance(keyset.id, str)
|
||||
assert len(keyset.id) > 0
|
||||
|
||||
@@ -106,13 +89,14 @@ async def test_get_keyset(wallet1: Wallet):
|
||||
assert wallet1.keysets[wallet1.keyset_id].public_keys
|
||||
assert len(wallet1.keysets[wallet1.keyset_id].public_keys) == settings.max_order
|
||||
# let's get the keys first so we can get a keyset ID that we use later
|
||||
keys1 = await wallet1._get_keys(wallet1.url)
|
||||
keysets = await wallet1._get_keys()
|
||||
keyset = keysets[0]
|
||||
# gets the keys of a specific keyset
|
||||
assert keys1.id is not None
|
||||
assert keys1.public_keys is not None
|
||||
keys2 = await wallet1._get_keys_of_keyset(wallet1.url, keys1.id)
|
||||
assert keyset.id is not None
|
||||
assert keyset.public_keys is not None
|
||||
keys2 = await wallet1._get_keys_of_keyset(keyset.id)
|
||||
assert keys2.public_keys is not None
|
||||
assert len(keys1.public_keys) == len(keys2.public_keys)
|
||||
assert len(keyset.public_keys) == len(keys2.public_keys)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@@ -130,32 +114,39 @@ async def test_get_keyset_from_db(wallet1: Wallet):
|
||||
assert keyset1.id == keyset2.id
|
||||
|
||||
# load it directly from the db
|
||||
keyset3 = await get_keyset(db=wallet1.db, id=keyset1.id)
|
||||
assert keyset3
|
||||
keysets_local = await get_keysets(db=wallet1.db, id=keyset1.id)
|
||||
assert keysets_local[0]
|
||||
keyset3 = keysets_local[0]
|
||||
assert keyset1.public_keys == keyset3.public_keys
|
||||
assert keyset1.id == keyset3.id
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_info(wallet1: Wallet):
|
||||
info = await wallet1._get_info(wallet1.url)
|
||||
info = await wallet1._get_info()
|
||||
assert info.name
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_nonexistent_keyset(wallet1: Wallet):
|
||||
await assert_err(
|
||||
wallet1._get_keys_of_keyset(wallet1.url, "nonexistent"),
|
||||
wallet1._get_keys_of_keyset("nonexistent"),
|
||||
KeysetNotFoundError(),
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_keyset_ids(wallet1: Wallet):
|
||||
keyset = await wallet1._get_keyset_ids(wallet1.url)
|
||||
assert isinstance(keyset, list)
|
||||
assert len(keyset) > 0
|
||||
assert keyset[-1] == wallet1.keyset_id
|
||||
keysets = await wallet1._get_keyset_ids()
|
||||
assert isinstance(keysets, list)
|
||||
assert len(keysets) > 0
|
||||
assert wallet1.keyset_id in keysets
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_request_mint(wallet1: Wallet):
|
||||
invoice = await wallet1.request_mint(64)
|
||||
assert invoice.payment_hash
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@@ -181,9 +172,9 @@ async def test_mint(wallet1: Wallet):
|
||||
@pytest.mark.asyncio
|
||||
async def test_mint_amounts(wallet1: Wallet):
|
||||
"""Mint predefined amounts"""
|
||||
invoice = await wallet1.request_mint(64)
|
||||
pay_if_regtest(invoice.bolt11)
|
||||
amts = [1, 1, 1, 2, 2, 4, 16]
|
||||
invoice = await wallet1.request_mint(sum(amts))
|
||||
pay_if_regtest(invoice.bolt11)
|
||||
await wallet1.mint(amount=sum(amts), split=amts, id=invoice.id)
|
||||
assert wallet1.balance == 27
|
||||
assert wallet1.proof_amounts == amts
|
||||
@@ -192,9 +183,11 @@ async def test_mint_amounts(wallet1: Wallet):
|
||||
@pytest.mark.asyncio
|
||||
async def test_mint_amounts_wrong_sum(wallet1: Wallet):
|
||||
"""Mint predefined amounts"""
|
||||
|
||||
amts = [1, 1, 1, 2, 2, 4, 16]
|
||||
invoice = await wallet1.request_mint(sum(amts))
|
||||
await assert_err(
|
||||
wallet1.mint(amount=sum(amts) + 1, split=amts),
|
||||
wallet1.mint(amount=sum(amts) + 1, split=amts, id=invoice.id),
|
||||
"split must sum to amount",
|
||||
)
|
||||
|
||||
@@ -203,8 +196,9 @@ async def test_mint_amounts_wrong_sum(wallet1: Wallet):
|
||||
async def test_mint_amounts_wrong_order(wallet1: Wallet):
|
||||
"""Mint amount that is not part in 2^n"""
|
||||
amts = [1, 2, 3]
|
||||
invoice = await wallet1.request_mint(sum(amts))
|
||||
await assert_err(
|
||||
wallet1.mint(amount=sum(amts), split=[1, 2, 3]),
|
||||
wallet1.mint(amount=sum(amts), split=[1, 2, 3], id=invoice.id),
|
||||
f"Can only mint amounts with 2^n up to {2**settings.max_order}.",
|
||||
)
|
||||
|
||||
@@ -257,33 +251,45 @@ async def test_split_more_than_balance(wallet1: Wallet):
|
||||
@pytest.mark.asyncio
|
||||
async def test_melt(wallet1: Wallet):
|
||||
# mint twice so we have enough to pay the second invoice back
|
||||
invoice = await wallet1.request_mint(64)
|
||||
pay_if_regtest(invoice.bolt11)
|
||||
await wallet1.mint(64, id=invoice.id)
|
||||
invoice = await wallet1.request_mint(64)
|
||||
pay_if_regtest(invoice.bolt11)
|
||||
await wallet1.mint(64, id=invoice.id)
|
||||
topup_invoice = await wallet1.request_mint(128)
|
||||
pay_if_regtest(topup_invoice.bolt11)
|
||||
await wallet1.mint(128, id=topup_invoice.id)
|
||||
assert wallet1.balance == 128
|
||||
|
||||
total_amount, fee_reserve_sat = await wallet1.get_pay_amount_with_fees(
|
||||
invoice.bolt11
|
||||
)
|
||||
assert total_amount == 66
|
||||
|
||||
assert fee_reserve_sat == 2
|
||||
_, send_proofs = await wallet1.split_to_send(wallet1.proofs, total_amount)
|
||||
|
||||
invoice_to_pay = invoice.bolt11
|
||||
invoice_payment_hash = str(invoice.payment_hash)
|
||||
invoice_payment_request = ""
|
||||
invoice_payment_hash = ""
|
||||
if is_regtest:
|
||||
invoice_dict = get_real_invoice(64)
|
||||
invoice_to_pay = invoice_dict["payment_request"]
|
||||
invoice_payment_hash = str(invoice_dict["r_hash"])
|
||||
invoice_payment_request = invoice_dict["payment_request"]
|
||||
|
||||
if is_fake:
|
||||
invoice = await wallet1.request_mint(64)
|
||||
invoice_payment_hash = str(invoice.payment_hash)
|
||||
invoice_payment_request = invoice.bolt11
|
||||
|
||||
quote = await wallet1.get_pay_amount_with_fees(invoice_payment_request)
|
||||
total_amount = quote.amount + quote.fee_reserve
|
||||
|
||||
if is_regtest:
|
||||
# we expect a fee reserve of 2 sat for regtest
|
||||
assert total_amount == 66
|
||||
assert quote.fee_reserve == 2
|
||||
if is_fake:
|
||||
# we expect a fee reserve of 0 sat for fake
|
||||
assert total_amount == 64
|
||||
assert quote.fee_reserve == 0
|
||||
|
||||
_, send_proofs = await wallet1.split_to_send(wallet1.proofs, total_amount)
|
||||
|
||||
melt_response = await wallet1.pay_lightning(
|
||||
send_proofs, invoice=invoice_to_pay, fee_reserve_sat=fee_reserve_sat
|
||||
proofs=send_proofs,
|
||||
invoice=invoice_payment_request,
|
||||
fee_reserve_sat=quote.fee_reserve,
|
||||
quote_id=quote.quote,
|
||||
)
|
||||
|
||||
if is_regtest:
|
||||
assert melt_response.change, "No change returned"
|
||||
assert len(melt_response.change) == 1, "More than one change returned"
|
||||
# NOTE: we assume that we will get a token back from the same keyset as the ones we melted
|
||||
@@ -292,7 +298,7 @@ async def test_melt(wallet1: Wallet):
|
||||
assert melt_response.change[0].id == send_proofs[0].id, "Wrong keyset returned"
|
||||
|
||||
# verify that proofs in proofs_used db have the same melt_id as the invoice in the db
|
||||
assert invoice.payment_hash, "No payment hash in invoice"
|
||||
assert invoice_payment_hash, "No payment hash in invoice"
|
||||
invoice_db = await get_lightning_invoice(
|
||||
db=wallet1.db, payment_hash=invoice_payment_hash, out=True
|
||||
)
|
||||
@@ -305,7 +311,7 @@ async def test_melt(wallet1: Wallet):
|
||||
assert all([p.melt_id == invoice_db.id for p in proofs_used]), "Wrong melt_id"
|
||||
|
||||
# the payment was without fees so we need to remove it from the total amount
|
||||
assert wallet1.balance == 128 - (total_amount - fee_reserve_sat), "Wrong balance"
|
||||
assert wallet1.balance == 128 - (total_amount - quote.fee_reserve), "Wrong balance"
|
||||
assert wallet1.balance == 64, "Wrong balance"
|
||||
|
||||
|
||||
@@ -368,23 +374,23 @@ async def test_send_and_redeem(wallet1: Wallet, wallet2: Wallet):
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_invalidate_unspent_proofs(wallet1: Wallet):
|
||||
async def test_invalidate_all_proofs(wallet1: Wallet):
|
||||
"""Try to invalidate proofs that have not been spent yet. Should not work!"""
|
||||
invoice = await wallet1.request_mint(64)
|
||||
pay_if_regtest(invoice.bolt11)
|
||||
await wallet1.mint(64, id=invoice.id)
|
||||
await wallet1.invalidate(wallet1.proofs)
|
||||
assert wallet1.balance == 64
|
||||
assert wallet1.balance == 0
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_invalidate_unspent_proofs_without_checking(wallet1: Wallet):
|
||||
async def test_invalidate_unspent_proofs_with_checking(wallet1: Wallet):
|
||||
"""Try to invalidate proofs that have not been spent yet but force no check."""
|
||||
invoice = await wallet1.request_mint(64)
|
||||
pay_if_regtest(invoice.bolt11)
|
||||
await wallet1.mint(64, id=invoice.id)
|
||||
await wallet1.invalidate(wallet1.proofs, check_spendable=False)
|
||||
assert wallet1.balance == 0
|
||||
await wallet1.invalidate(wallet1.proofs, check_spendable=True)
|
||||
assert wallet1.balance == 64
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@@ -405,5 +411,21 @@ async def test_token_state(wallet1: Wallet):
|
||||
await wallet1.mint(64, id=invoice.id)
|
||||
assert wallet1.balance == 64
|
||||
resp = await wallet1.check_proof_state(wallet1.proofs)
|
||||
assert resp.dict()["spendable"]
|
||||
assert resp.dict()["pending"]
|
||||
assert resp.states[0].state.value == "UNSPENT"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_load_mint_keys_specific_keyset(wallet1: Wallet):
|
||||
await wallet1._load_mint_keys()
|
||||
if settings.debug_mint_only_deprecated:
|
||||
assert list(wallet1.keysets.keys()) == ["eGnEWtdJ0PIM"]
|
||||
else:
|
||||
assert list(wallet1.keysets.keys()) == ["009a1f293253e41e", "eGnEWtdJ0PIM"]
|
||||
await wallet1._load_mint_keys(keyset_id=wallet1.keyset_id)
|
||||
await wallet1._load_mint_keys(keyset_id="009a1f293253e41e")
|
||||
# expect deprecated keyset id to be present
|
||||
await wallet1._load_mint_keys(keyset_id="eGnEWtdJ0PIM")
|
||||
await assert_err(
|
||||
wallet1._load_mint_keys(keyset_id="nonexistent"),
|
||||
KeysetNotFoundError(),
|
||||
)
|
||||
|
||||
@@ -12,14 +12,13 @@ from tests.helpers import is_regtest
|
||||
|
||||
|
||||
@pytest_asyncio.fixture(scope="function")
|
||||
async def wallet(mint):
|
||||
async def wallet():
|
||||
wallet = await Wallet.with_db(
|
||||
url=SERVER_ENDPOINT,
|
||||
db="test_data/wallet",
|
||||
name="wallet",
|
||||
)
|
||||
await wallet.load_mint()
|
||||
wallet.status()
|
||||
yield wallet
|
||||
|
||||
|
||||
|
||||
@@ -35,25 +35,23 @@ def assert_amt(proofs: List[Proof], expected: int):
|
||||
|
||||
|
||||
@pytest_asyncio.fixture(scope="function")
|
||||
async def wallet1(mint):
|
||||
async def wallet1():
|
||||
wallet1 = await Wallet1.with_db(
|
||||
SERVER_ENDPOINT, "test_data/wallet_p2pk_1", "wallet1"
|
||||
)
|
||||
await migrate_databases(wallet1.db, migrations)
|
||||
await wallet1.load_mint()
|
||||
wallet1.status()
|
||||
yield wallet1
|
||||
|
||||
|
||||
@pytest_asyncio.fixture(scope="function")
|
||||
async def wallet2(mint):
|
||||
async def wallet2():
|
||||
wallet2 = await Wallet2.with_db(
|
||||
SERVER_ENDPOINT, "test_data/wallet_p2pk_2", "wallet2"
|
||||
)
|
||||
await migrate_databases(wallet2.db, migrations)
|
||||
wallet2.private_key = PrivateKey(secrets.token_bytes(32), raw=True)
|
||||
await wallet2.load_mint()
|
||||
wallet2.status()
|
||||
yield wallet2
|
||||
|
||||
|
||||
|
||||
130
tests/test_wallet_lightning.py
Normal file
130
tests/test_wallet_lightning.py
Normal file
@@ -0,0 +1,130 @@
|
||||
from typing import List, Union
|
||||
|
||||
import pytest
|
||||
import pytest_asyncio
|
||||
|
||||
from cashu.core.base import Proof
|
||||
from cashu.core.errors import CashuError
|
||||
from cashu.wallet.lightning import LightningWallet
|
||||
from tests.conftest import SERVER_ENDPOINT
|
||||
from tests.helpers import get_real_invoice, is_fake, is_regtest, pay_if_regtest
|
||||
|
||||
|
||||
async def assert_err(f, msg: Union[str, CashuError]):
|
||||
"""Compute f() and expect an error message 'msg'."""
|
||||
try:
|
||||
await f
|
||||
except Exception as exc:
|
||||
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")
|
||||
|
||||
|
||||
def assert_amt(proofs: List[Proof], expected: int):
|
||||
"""Assert amounts the proofs contain."""
|
||||
assert [p.amount for p in proofs] == expected
|
||||
|
||||
|
||||
async def reset_wallet_db(wallet: LightningWallet):
|
||||
await wallet.db.execute("DELETE FROM proofs")
|
||||
await wallet.db.execute("DELETE FROM proofs_used")
|
||||
await wallet.db.execute("DELETE FROM keysets")
|
||||
await wallet._load_mint()
|
||||
|
||||
|
||||
@pytest_asyncio.fixture(scope="function")
|
||||
async def wallet():
|
||||
wallet = await LightningWallet.with_db(
|
||||
url=SERVER_ENDPOINT,
|
||||
db="test_data/wallet1",
|
||||
name="wallet1",
|
||||
)
|
||||
await wallet.async_init()
|
||||
yield wallet
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_create_invoice(wallet: LightningWallet):
|
||||
invoice = await wallet.create_invoice(64)
|
||||
assert invoice.payment_request
|
||||
assert invoice.payment_request.startswith("ln")
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.skipif(is_regtest, reason="only works with FakeWallet")
|
||||
async def test_check_invoice_internal(wallet: LightningWallet):
|
||||
# fill wallet
|
||||
invoice = await wallet.create_invoice(64)
|
||||
assert invoice.payment_request
|
||||
assert invoice.checking_id
|
||||
status = await wallet.get_invoice_status(invoice.checking_id)
|
||||
assert status.paid
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.skipif(is_fake, reason="only works with Regtest")
|
||||
async def test_check_invoice_external(wallet: LightningWallet):
|
||||
# fill wallet
|
||||
invoice = await wallet.create_invoice(64)
|
||||
assert invoice.payment_request
|
||||
assert invoice.checking_id
|
||||
status = await wallet.get_invoice_status(invoice.checking_id)
|
||||
assert not status.paid
|
||||
pay_if_regtest(invoice.payment_request)
|
||||
status = await wallet.get_invoice_status(invoice.checking_id)
|
||||
assert status.paid
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.skipif(is_regtest, reason="only works with FakeWallet")
|
||||
async def test_pay_invoice_internal(wallet: LightningWallet):
|
||||
# fill wallet
|
||||
invoice = await wallet.create_invoice(64)
|
||||
assert invoice.payment_request
|
||||
assert invoice.checking_id
|
||||
await wallet.get_invoice_status(invoice.checking_id)
|
||||
assert wallet.available_balance >= 64
|
||||
|
||||
# pay invoice
|
||||
invoice2 = await wallet.create_invoice(16)
|
||||
assert invoice2.payment_request
|
||||
status = await wallet.pay_invoice(invoice2.payment_request)
|
||||
|
||||
assert status.ok
|
||||
|
||||
# check payment
|
||||
assert invoice2.checking_id
|
||||
status = await wallet.get_payment_status(invoice2.checking_id)
|
||||
assert status.paid
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.skipif(is_fake, reason="only works with Regtest")
|
||||
async def test_pay_invoice_external(wallet: LightningWallet):
|
||||
# fill wallet
|
||||
invoice = await wallet.create_invoice(64)
|
||||
assert invoice.payment_request
|
||||
assert invoice.checking_id
|
||||
pay_if_regtest(invoice.payment_request)
|
||||
status = await wallet.get_invoice_status(invoice.checking_id)
|
||||
assert status.paid
|
||||
assert wallet.available_balance >= 64
|
||||
|
||||
# pay invoice
|
||||
invoice_real = get_real_invoice(16)
|
||||
status = await wallet.pay_invoice(invoice_real["payment_request"])
|
||||
|
||||
assert status.ok
|
||||
|
||||
# check payment
|
||||
assert status.checking_id
|
||||
status = await wallet.get_payment_status(status.checking_id)
|
||||
assert status.paid
|
||||
@@ -1,12 +1,13 @@
|
||||
import asyncio
|
||||
import copy
|
||||
import json
|
||||
import secrets
|
||||
from typing import List
|
||||
|
||||
import pytest
|
||||
import pytest_asyncio
|
||||
|
||||
from cashu.core.base import Proof
|
||||
from cashu.core.base import Proof, SpentState
|
||||
from cashu.core.crypto.secp import PrivateKey, PublicKey
|
||||
from cashu.core.migrations import migrate_databases
|
||||
from cashu.core.p2pk import SigFlags
|
||||
@@ -16,7 +17,7 @@ from cashu.wallet.wallet import Wallet
|
||||
from cashu.wallet.wallet import Wallet as Wallet1
|
||||
from cashu.wallet.wallet import Wallet as Wallet2
|
||||
from tests.conftest import SERVER_ENDPOINT
|
||||
from tests.helpers import pay_if_regtest
|
||||
from tests.helpers import is_deprecated_api_only, pay_if_regtest
|
||||
|
||||
|
||||
async def assert_err(f, msg):
|
||||
@@ -36,25 +37,23 @@ def assert_amt(proofs: List[Proof], expected: int):
|
||||
|
||||
|
||||
@pytest_asyncio.fixture(scope="function")
|
||||
async def wallet1(mint):
|
||||
async def wallet1():
|
||||
wallet1 = await Wallet1.with_db(
|
||||
SERVER_ENDPOINT, "test_data/wallet_p2pk_1", "wallet1"
|
||||
)
|
||||
await migrate_databases(wallet1.db, migrations)
|
||||
await wallet1.load_mint()
|
||||
wallet1.status()
|
||||
yield wallet1
|
||||
|
||||
|
||||
@pytest_asyncio.fixture(scope="function")
|
||||
async def wallet2(mint):
|
||||
async def wallet2():
|
||||
wallet2 = await Wallet2.with_db(
|
||||
SERVER_ENDPOINT, "test_data/wallet_p2pk_2", "wallet2"
|
||||
)
|
||||
await migrate_databases(wallet2.db, migrations)
|
||||
wallet2.private_key = PrivateKey(secrets.token_bytes(32), raw=True)
|
||||
await wallet2.load_mint()
|
||||
wallet2.status()
|
||||
yield wallet2
|
||||
|
||||
|
||||
@@ -80,6 +79,16 @@ async def test_p2pk(wallet1: Wallet, wallet2: Wallet):
|
||||
)
|
||||
await wallet2.redeem(send_proofs)
|
||||
|
||||
proof_states = await wallet2.check_proof_state(send_proofs)
|
||||
assert all([p.state == SpentState.spent for p in proof_states.states])
|
||||
|
||||
if not is_deprecated_api_only:
|
||||
for state in proof_states.states:
|
||||
assert state.witness is not None
|
||||
witness_obj = json.loads(state.witness)
|
||||
assert len(witness_obj["signatures"]) == 1
|
||||
assert len(witness_obj["signatures"][0]) == 128
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_p2pk_sig_all(wallet1: Wallet, wallet2: Wallet):
|
||||
@@ -222,9 +231,9 @@ async def test_p2pk_locktime_with_second_refund_pubkey(
|
||||
secret_lock = await wallet1.create_p2pk_lock(
|
||||
garbage_pubkey.serialize().hex(), # create lock to unspendable pubkey
|
||||
locktime_seconds=2, # locktime
|
||||
tags=Tags(
|
||||
[["refund", pubkey_wallet2, pubkey_wallet1]]
|
||||
), # multiple refund pubkeys
|
||||
tags=Tags([
|
||||
["refund", pubkey_wallet2, pubkey_wallet1]
|
||||
]), # multiple refund pubkeys
|
||||
) # sender side
|
||||
_, send_proofs = await wallet1.split_to_send(
|
||||
wallet1.proofs, 8, secret_lock=secret_lock
|
||||
@@ -379,9 +388,9 @@ async def test_p2pk_multisig_with_wrong_first_private_key(
|
||||
|
||||
|
||||
def test_tags():
|
||||
tags = Tags(
|
||||
[["key1", "value1"], ["key2", "value2", "value2_1"], ["key2", "value3"]]
|
||||
)
|
||||
tags = Tags([
|
||||
["key1", "value1"], ["key2", "value2", "value2_1"], ["key2", "value3"]
|
||||
])
|
||||
assert tags.get_tag("key1") == "value1"
|
||||
assert tags["key1"] == "value1"
|
||||
assert tags.get_tag("key2") == "value2"
|
||||
|
||||
@@ -8,6 +8,7 @@ import pytest_asyncio
|
||||
from cashu.core.base import Proof
|
||||
from cashu.core.crypto.secp import PrivateKey
|
||||
from cashu.core.errors import CashuError
|
||||
from cashu.core.settings import settings
|
||||
from cashu.wallet.wallet import Wallet
|
||||
from cashu.wallet.wallet import Wallet as Wallet1
|
||||
from cashu.wallet.wallet import Wallet as Wallet2
|
||||
@@ -46,31 +47,29 @@ async def reset_wallet_db(wallet: Wallet):
|
||||
|
||||
|
||||
@pytest_asyncio.fixture(scope="function")
|
||||
async def wallet1(mint):
|
||||
async def wallet1():
|
||||
wallet1 = await Wallet1.with_db(
|
||||
url=SERVER_ENDPOINT,
|
||||
db="test_data/wallet1",
|
||||
name="wallet1",
|
||||
)
|
||||
await wallet1.load_mint()
|
||||
wallet1.status()
|
||||
yield wallet1
|
||||
|
||||
|
||||
@pytest_asyncio.fixture(scope="function")
|
||||
async def wallet2(mint):
|
||||
async def wallet2():
|
||||
wallet2 = await Wallet2.with_db(
|
||||
url=SERVER_ENDPOINT,
|
||||
db="test_data/wallet2",
|
||||
name="wallet2",
|
||||
)
|
||||
await wallet2.load_mint()
|
||||
wallet2.status()
|
||||
yield wallet2
|
||||
|
||||
|
||||
@pytest_asyncio.fixture(scope="function")
|
||||
async def wallet3(mint):
|
||||
async def wallet3():
|
||||
dirpath = Path("test_data/wallet3")
|
||||
if dirpath.exists() and dirpath.is_dir():
|
||||
shutil.rmtree(dirpath)
|
||||
@@ -83,11 +82,14 @@ async def wallet3(mint):
|
||||
await wallet3.db.execute("DELETE FROM proofs")
|
||||
await wallet3.db.execute("DELETE FROM proofs_used")
|
||||
await wallet3.load_mint()
|
||||
wallet3.status()
|
||||
yield wallet3
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.skipif(
|
||||
settings.debug_mint_only_deprecated,
|
||||
reason="settings.debug_mint_only_deprecated is set",
|
||||
)
|
||||
async def test_bump_secret_derivation(wallet3: Wallet):
|
||||
await wallet3._init_private_key(
|
||||
"half depart obvious quality work element tank gorilla view sugar picture"
|
||||
@@ -95,23 +97,27 @@ async def test_bump_secret_derivation(wallet3: Wallet):
|
||||
)
|
||||
secrets1, rs1, derivation_paths1 = await wallet3.generate_n_secrets(5)
|
||||
secrets2, rs2, derivation_paths2 = await wallet3.generate_secrets_from_to(0, 4)
|
||||
assert wallet3.keyset_id == "1cCNIAZ2X/w1"
|
||||
assert wallet3.keyset_id == "009a1f293253e41e"
|
||||
assert secrets1 == secrets2
|
||||
assert [r.private_key for r in rs1] == [r.private_key for r in rs2]
|
||||
assert derivation_paths1 == derivation_paths2
|
||||
for s in secrets1:
|
||||
print('"' + s + '",')
|
||||
assert secrets1 == [
|
||||
"9d32fc57e6fa2942d05ee475d28ba6a56839b8cb8a3f174b05ed0ed9d3a420f6",
|
||||
"1c0f2c32e7438e7cc992612049e9dfcdbffd454ea460901f24cc429921437802",
|
||||
"327c606b761af03cbe26fa13c4b34a6183b868c52cda059fe57fdddcb4e1e1e7",
|
||||
"53476919560398b56c0fdc5dd92cf8628b1e06de6f2652b0f7d6e8ac319de3b7",
|
||||
"b2f5d632229378a716be6752fc79ac8c2b43323b820859a7956f2dfe5432b7b4",
|
||||
"485875df74771877439ac06339e284c3acfcd9be7abf3bc20b516faeadfe77ae",
|
||||
"8f2b39e8e594a4056eb1e6dbb4b0c38ef13b1b2c751f64f810ec04ee35b77270",
|
||||
"bc628c79accd2364fd31511216a0fab62afd4a18ff77a20deded7b858c9860c8",
|
||||
"59284fd1650ea9fa17db2b3acf59ecd0f2d52ec3261dd4152785813ff27a33bf",
|
||||
"576c23393a8b31cc8da6688d9c9a96394ec74b40fdaf1f693a6bb84284334ea0",
|
||||
]
|
||||
for d in derivation_paths1:
|
||||
print('"' + d + '",')
|
||||
assert derivation_paths1 == [
|
||||
"m/129372'/0'/2004500376'/0'",
|
||||
"m/129372'/0'/2004500376'/1'",
|
||||
"m/129372'/0'/2004500376'/2'",
|
||||
"m/129372'/0'/2004500376'/3'",
|
||||
"m/129372'/0'/2004500376'/4'",
|
||||
"m/129372'/0'/864559728'/0'",
|
||||
"m/129372'/0'/864559728'/1'",
|
||||
"m/129372'/0'/864559728'/2'",
|
||||
"m/129372'/0'/864559728'/3'",
|
||||
"m/129372'/0'/864559728'/4'",
|
||||
]
|
||||
|
||||
|
||||
@@ -191,7 +197,7 @@ async def test_restore_wallet_after_split_to_send(wallet3: Wallet):
|
||||
assert wallet3.balance == 0
|
||||
await wallet3.restore_promises_from_to(0, 100)
|
||||
assert wallet3.balance == 64 * 2
|
||||
await wallet3.invalidate(wallet3.proofs)
|
||||
await wallet3.invalidate(wallet3.proofs, check_spendable=True)
|
||||
assert wallet3.balance == 64
|
||||
|
||||
|
||||
@@ -216,7 +222,7 @@ async def test_restore_wallet_after_send_and_receive(wallet3: Wallet, wallet2: W
|
||||
assert wallet3.balance == 0
|
||||
await wallet3.restore_promises_from_to(0, 100)
|
||||
assert wallet3.balance == 64 + 2 * 32
|
||||
await wallet3.invalidate(wallet3.proofs)
|
||||
await wallet3.invalidate(wallet3.proofs, check_spendable=True)
|
||||
assert wallet3.balance == 32
|
||||
|
||||
|
||||
@@ -257,7 +263,7 @@ async def test_restore_wallet_after_send_and_self_receive(wallet3: Wallet):
|
||||
assert wallet3.balance == 0
|
||||
await wallet3.restore_promises_from_to(0, 100)
|
||||
assert wallet3.balance == 64 + 2 * 32 + 32
|
||||
await wallet3.invalidate(wallet3.proofs)
|
||||
await wallet3.invalidate(wallet3.proofs, check_spendable=True)
|
||||
assert wallet3.balance == 64
|
||||
|
||||
|
||||
@@ -290,7 +296,7 @@ async def test_restore_wallet_after_send_twice(
|
||||
await wallet3.restore_promises_from_to(0, 10)
|
||||
box.add(wallet3.proofs)
|
||||
assert wallet3.balance == 5
|
||||
await wallet3.invalidate(wallet3.proofs)
|
||||
await wallet3.invalidate(wallet3.proofs, check_spendable=True)
|
||||
assert wallet3.balance == 2
|
||||
|
||||
# again
|
||||
@@ -310,7 +316,7 @@ async def test_restore_wallet_after_send_twice(
|
||||
await wallet3.restore_promises_from_to(0, 15)
|
||||
box.add(wallet3.proofs)
|
||||
assert wallet3.balance == 7
|
||||
await wallet3.invalidate(wallet3.proofs)
|
||||
await wallet3.invalidate(wallet3.proofs, check_spendable=True)
|
||||
assert wallet3.balance == 2
|
||||
|
||||
|
||||
@@ -345,7 +351,7 @@ async def test_restore_wallet_after_send_and_self_receive_nonquadratic_value(
|
||||
await wallet3.restore_promises_from_to(0, 20)
|
||||
box.add(wallet3.proofs)
|
||||
assert wallet3.balance == 138
|
||||
await wallet3.invalidate(wallet3.proofs)
|
||||
await wallet3.invalidate(wallet3.proofs, check_spendable=True)
|
||||
assert wallet3.balance == 64
|
||||
|
||||
# again
|
||||
@@ -362,5 +368,5 @@ async def test_restore_wallet_after_send_and_self_receive_nonquadratic_value(
|
||||
assert wallet3.balance == 0
|
||||
await wallet3.restore_promises_from_to(0, 50)
|
||||
assert wallet3.balance == 182
|
||||
await wallet3.invalidate(wallet3.proofs)
|
||||
await wallet3.invalidate(wallet3.proofs, check_spendable=True)
|
||||
assert wallet3.balance == 64
|
||||
|
||||
Reference in New Issue
Block a user