mirror of
https://github.com/aljazceru/nutshell.git
synced 2025-12-20 02:24:20 +01:00
Nut07/proof pending (#277)
* add pending state * proofs spendable check and tests * bump version to 0.12.3 * remove sleep for testing * comment clarify * use list comprehension in pending list
This commit is contained in:
@@ -114,7 +114,7 @@ cashu info
|
||||
|
||||
Returns:
|
||||
```bash
|
||||
Version: 0.12.2
|
||||
Version: 0.12.3
|
||||
Debug: False
|
||||
Cashu dir: /home/user/.cashu
|
||||
Wallet: wallet
|
||||
|
||||
@@ -253,6 +253,10 @@ class CheckSpendableRequest(BaseModel):
|
||||
|
||||
class CheckSpendableResponse(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)
|
||||
|
||||
|
||||
class CheckFeesRequest(BaseModel):
|
||||
|
||||
@@ -8,7 +8,7 @@ from pydantic import BaseSettings, Extra, Field
|
||||
|
||||
env = Env()
|
||||
|
||||
VERSION = "0.12.2"
|
||||
VERSION = "0.12.3"
|
||||
|
||||
|
||||
def find_env_file():
|
||||
|
||||
@@ -2,7 +2,7 @@ import asyncio
|
||||
import json
|
||||
import math
|
||||
import time
|
||||
from typing import Dict, List, Literal, Optional, Set, Union
|
||||
from typing import Dict, List, Literal, Optional, Set, Tuple, Union
|
||||
|
||||
from loguru import logger
|
||||
|
||||
@@ -186,6 +186,15 @@ class Ledger:
|
||||
"""Checks whether the proof was already spent."""
|
||||
return not proof.secret in self.proofs_used
|
||||
|
||||
async def _check_pending(self, proofs: List[Proof]):
|
||||
"""Checks whether the proof is still pending."""
|
||||
proofs_pending = await self.crud.get_proofs_pending(db=self.db)
|
||||
pending_secrets = [pp.secret for pp in proofs_pending]
|
||||
pending_states = [
|
||||
True if p.secret in pending_secrets else False for p in proofs
|
||||
]
|
||||
return pending_states
|
||||
|
||||
def _verify_secret_criteria(self, proof: Proof) -> Literal[True]:
|
||||
"""Verifies that a secret is present and is not too long (DOS prevention)."""
|
||||
if proof.secret is None or proof.secret == "":
|
||||
@@ -815,12 +824,14 @@ class Ledger:
|
||||
|
||||
return status, preimage, return_promises
|
||||
|
||||
async def check_spendable(self, proofs: List[Proof]):
|
||||
"""Checks if provided proofs are valid and have not been spent yet.
|
||||
Used by wallets to check if their proofs have been redeemed by a receiver.
|
||||
async def check_proof_state(
|
||||
self, proofs: List[Proof]
|
||||
) -> Tuple[List[bool], List[bool]]:
|
||||
"""Checks if provided proofs are spend or are pending.
|
||||
Used by wallets to check if their proofs have been redeemed by a receiver or they are still in-flight in a transaction.
|
||||
|
||||
Returns a list in the same order as the provided proofs. Wallet must match the list
|
||||
to the proofs they have provided in order to figure out which proof is still spendable
|
||||
Returns two lists that are in the same order as the provided proofs. Wallet must match the list
|
||||
to the proofs they have provided in order to figure out which proof is spendable or pending
|
||||
and which isn't.
|
||||
|
||||
Args:
|
||||
@@ -828,8 +839,11 @@ class Ledger:
|
||||
|
||||
Returns:
|
||||
List[bool]: List of which proof is still spendable (True if still spendable, else False)
|
||||
List[bool]: List of which proof are pending (True if pending, else False)
|
||||
"""
|
||||
return [self._check_spendable(p) for p in proofs]
|
||||
spendable = [self._check_spendable(p) for p in proofs]
|
||||
pending = await self._check_pending(proofs)
|
||||
return spendable, pending
|
||||
|
||||
async def check_fees(self, pr: str):
|
||||
"""Returns the fee reserve (in sat) that a wallet must add to its proofs
|
||||
|
||||
@@ -173,17 +173,18 @@ async def melt(payload: PostMeltRequest) -> Union[CashuError, GetMeltResponse]:
|
||||
|
||||
@router.post(
|
||||
"/check",
|
||||
name="Check spendable",
|
||||
summary="Check whether a proof has already been spent",
|
||||
name="Check proof state",
|
||||
summary="Check whether a proof is spent already or is pending in a transaction",
|
||||
)
|
||||
async def check_spendable(
|
||||
payload: CheckSpendableRequest,
|
||||
) -> CheckSpendableResponse:
|
||||
"""Check whether a secret has been spent already or not."""
|
||||
logger.trace(f"> POST /check: {payload}")
|
||||
spendableList = await ledger.check_spendable(payload.proofs)
|
||||
logger.trace(f"< POST /check: {spendableList}")
|
||||
return CheckSpendableResponse(spendable=spendableList)
|
||||
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(
|
||||
|
||||
@@ -415,6 +415,7 @@ async def receive_cli(
|
||||
@coro
|
||||
async def burn(ctx: Context, token: str, all: bool, force: bool, delete: str):
|
||||
wallet: Wallet = ctx.obj["WALLET"]
|
||||
await wallet.load_proofs()
|
||||
if not delete:
|
||||
await wallet.load_mint()
|
||||
if not (all or token or force or delete) or (token and all):
|
||||
|
||||
@@ -498,13 +498,13 @@ class LedgerAPI:
|
||||
return frst_proofs, scnd_proofs
|
||||
|
||||
@async_set_requests
|
||||
async def check_spendable(self, proofs: List[Proof]):
|
||||
async def check_proof_state(self, proofs: List[Proof]):
|
||||
"""
|
||||
Cheks whether the secrets in proofs are already spent or not and returns a list of booleans.
|
||||
"""
|
||||
payload = CheckSpendableRequest(proofs=proofs)
|
||||
|
||||
def _check_spendable_include_fields(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))},
|
||||
@@ -512,13 +512,13 @@ class LedgerAPI:
|
||||
|
||||
resp = self.s.post(
|
||||
self.url + "/check",
|
||||
json=payload.dict(include=_check_spendable_include_fields(proofs)), # type: ignore
|
||||
json=payload.dict(include=_check_proof_state_include_fields(proofs)), # type: ignore
|
||||
)
|
||||
resp.raise_for_status()
|
||||
return_dict = resp.json()
|
||||
self.raise_on_error(return_dict)
|
||||
spendable = CheckSpendableResponse.parse_obj(return_dict)
|
||||
return spendable
|
||||
states = CheckSpendableResponse.parse_obj(return_dict)
|
||||
return states
|
||||
|
||||
@async_set_requests
|
||||
async def check_fees(self, payment_request: str):
|
||||
@@ -769,8 +769,8 @@ class Wallet(LedgerAPI):
|
||||
raise Exception("could not pay invoice.")
|
||||
return status.paid
|
||||
|
||||
async def check_spendable(self, proofs):
|
||||
return await super().check_spendable(proofs)
|
||||
async def check_proof_state(self, proofs):
|
||||
return await super().check_proof_state(proofs)
|
||||
|
||||
# ---------- TOKEN MECHANIS ----------
|
||||
|
||||
@@ -971,8 +971,8 @@ class Wallet(LedgerAPI):
|
||||
"""
|
||||
invalidated_proofs: List[Proof] = []
|
||||
if check_spendable:
|
||||
spendables = await self.check_spendable(proofs)
|
||||
for i, spendable in enumerate(spendables.spendable):
|
||||
proof_states = await self.check_proof_state(proofs)
|
||||
for i, spendable in enumerate(proof_states.spendable):
|
||||
if not spendable:
|
||||
invalidated_proofs.append(proofs[i])
|
||||
else:
|
||||
|
||||
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.12.2",
|
||||
version="0.12.3",
|
||||
description="Ecash wallet and mint for Bitcoin Lightning",
|
||||
long_description=long_description,
|
||||
long_description_content_type="text/markdown",
|
||||
|
||||
@@ -93,20 +93,20 @@ async def ledger():
|
||||
yield ledger
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True, scope="session")
|
||||
def mint_3338():
|
||||
settings.mint_listen_port = 3338
|
||||
settings.port = 3338
|
||||
settings.mint_url = "http://localhost:3338"
|
||||
settings.port = settings.mint_listen_port
|
||||
config = uvicorn.Config(
|
||||
"cashu.mint.app:app",
|
||||
port=settings.mint_listen_port,
|
||||
host="127.0.0.1",
|
||||
)
|
||||
# @pytest.fixture(autouse=True, scope="session")
|
||||
# def mint_3338():
|
||||
# settings.mint_listen_port = 3338
|
||||
# settings.port = 3338
|
||||
# settings.mint_url = "http://localhost:3338"
|
||||
# settings.port = settings.mint_listen_port
|
||||
# config = uvicorn.Config(
|
||||
# "cashu.mint.app:app",
|
||||
# port=settings.mint_listen_port,
|
||||
# host="127.0.0.1",
|
||||
# )
|
||||
|
||||
server = UvicornServer(config=config, private_key="SECOND_PRIVATE_KEY")
|
||||
server.start()
|
||||
time.sleep(1)
|
||||
yield server
|
||||
server.stop()
|
||||
# server = UvicornServer(config=config, private_key="SECOND_PRIVATE_KEY")
|
||||
# server.start()
|
||||
# time.sleep(1)
|
||||
# yield server
|
||||
# server.stop()
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import asyncio
|
||||
import json
|
||||
|
||||
import pytest
|
||||
import pytest_asyncio
|
||||
import requests
|
||||
|
||||
from cashu.core.base import CheckSpendableRequest, CheckSpendableResponse, Proof
|
||||
from cashu.core.settings import settings
|
||||
from tests.conftest import ledger
|
||||
|
||||
@@ -54,3 +56,22 @@ async def test_api_mint_validation(ledger):
|
||||
assert "error" in response.json()
|
||||
response = requests.get(f"{BASE_URL}/mint?amount=1")
|
||||
assert "error" 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)
|
||||
response = requests.post(
|
||||
f"{BASE_URL}/check",
|
||||
data=payload.json(),
|
||||
)
|
||||
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
|
||||
|
||||
@@ -367,3 +367,12 @@ async def test_p2sh_receive_with_wrong_wallet(wallet1: Wallet, wallet2: Wallet):
|
||||
wallet1.proofs, 8, secret_lock
|
||||
) # sender side
|
||||
await assert_err(wallet2.redeem(send_proofs), "lock not found.") # wrong receiver
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_token_state(wallet1: Wallet):
|
||||
await wallet1.mint(64)
|
||||
assert wallet1.balance == 64
|
||||
resp = await wallet1.check_proof_state(wallet1.proofs)
|
||||
assert resp.dict()["spendable"]
|
||||
assert resp.dict()["pending"]
|
||||
|
||||
Reference in New Issue
Block a user