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:
callebtc
2023-07-08 22:50:17 +02:00
committed by GitHub
parent 56040594b7
commit 73b015b642
11 changed files with 90 additions and 40 deletions

View File

@@ -114,7 +114,7 @@ cashu info
Returns: Returns:
```bash ```bash
Version: 0.12.2 Version: 0.12.3
Debug: False Debug: False
Cashu dir: /home/user/.cashu Cashu dir: /home/user/.cashu
Wallet: wallet Wallet: wallet

View File

@@ -253,6 +253,10 @@ class CheckSpendableRequest(BaseModel):
class CheckSpendableResponse(BaseModel): class CheckSpendableResponse(BaseModel):
spendable: List[bool] 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): class CheckFeesRequest(BaseModel):

View File

@@ -8,7 +8,7 @@ from pydantic import BaseSettings, Extra, Field
env = Env() env = Env()
VERSION = "0.12.2" VERSION = "0.12.3"
def find_env_file(): def find_env_file():

View File

@@ -2,7 +2,7 @@ import asyncio
import json import json
import math import math
import time 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 from loguru import logger
@@ -186,6 +186,15 @@ class Ledger:
"""Checks whether the proof was already spent.""" """Checks whether the proof was already spent."""
return not proof.secret in self.proofs_used 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]: def _verify_secret_criteria(self, proof: Proof) -> Literal[True]:
"""Verifies that a secret is present and is not too long (DOS prevention).""" """Verifies that a secret is present and is not too long (DOS prevention)."""
if proof.secret is None or proof.secret == "": if proof.secret is None or proof.secret == "":
@@ -815,12 +824,14 @@ class Ledger:
return status, preimage, return_promises return status, preimage, return_promises
async def check_spendable(self, proofs: List[Proof]): async def check_proof_state(
"""Checks if provided proofs are valid and have not been spent yet. self, proofs: List[Proof]
Used by wallets to check if their proofs have been redeemed by a receiver. ) -> 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 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 still spendable to the proofs they have provided in order to figure out which proof is spendable or pending
and which isn't. and which isn't.
Args: Args:
@@ -828,8 +839,11 @@ class Ledger:
Returns: Returns:
List[bool]: List of which proof is still spendable (True if still spendable, else False) 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): async def check_fees(self, pr: str):
"""Returns the fee reserve (in sat) that a wallet must add to its proofs """Returns the fee reserve (in sat) that a wallet must add to its proofs

View File

@@ -173,17 +173,18 @@ async def melt(payload: PostMeltRequest) -> Union[CashuError, GetMeltResponse]:
@router.post( @router.post(
"/check", "/check",
name="Check spendable", name="Check proof state",
summary="Check whether a proof has already been spent", summary="Check whether a proof is spent already or is pending in a transaction",
) )
async def check_spendable( async def check_spendable(
payload: CheckSpendableRequest, payload: CheckSpendableRequest,
) -> CheckSpendableResponse: ) -> CheckSpendableResponse:
"""Check whether a secret has been spent already or not.""" """Check whether a secret has been spent already or not."""
logger.trace(f"> POST /check: {payload}") logger.trace(f"> POST /check: {payload}")
spendableList = await ledger.check_spendable(payload.proofs) spendableList, pendingList = await ledger.check_proof_state(payload.proofs)
logger.trace(f"< POST /check: {spendableList}") logger.trace(f"< POST /check <spendable>: {spendableList}")
return CheckSpendableResponse(spendable=spendableList) logger.trace(f"< POST /check <pending>: {pendingList}")
return CheckSpendableResponse(spendable=spendableList, pending=pendingList)
@router.post( @router.post(

View File

@@ -415,6 +415,7 @@ async def receive_cli(
@coro @coro
async def burn(ctx: Context, token: str, all: bool, force: bool, delete: str): async def burn(ctx: Context, token: str, all: bool, force: bool, delete: str):
wallet: Wallet = ctx.obj["WALLET"] wallet: Wallet = ctx.obj["WALLET"]
await wallet.load_proofs()
if not delete: if not delete:
await wallet.load_mint() await wallet.load_mint()
if not (all or token or force or delete) or (token and all): if not (all or token or force or delete) or (token and all):

View File

@@ -498,13 +498,13 @@ class LedgerAPI:
return frst_proofs, scnd_proofs return frst_proofs, scnd_proofs
@async_set_requests @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. Cheks whether the secrets in proofs are already spent or not and returns a list of booleans.
""" """
payload = CheckSpendableRequest(proofs=proofs) 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""" """strips away fields from the model that aren't necessary for the /split"""
return { return {
"proofs": {i: {"secret"} for i in range(len(proofs))}, "proofs": {i: {"secret"} for i in range(len(proofs))},
@@ -512,13 +512,13 @@ class LedgerAPI:
resp = self.s.post( resp = self.s.post(
self.url + "/check", 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() resp.raise_for_status()
return_dict = resp.json() return_dict = resp.json()
self.raise_on_error(return_dict) self.raise_on_error(return_dict)
spendable = CheckSpendableResponse.parse_obj(return_dict) states = CheckSpendableResponse.parse_obj(return_dict)
return spendable return states
@async_set_requests @async_set_requests
async def check_fees(self, payment_request: str): async def check_fees(self, payment_request: str):
@@ -769,8 +769,8 @@ class Wallet(LedgerAPI):
raise Exception("could not pay invoice.") raise Exception("could not pay invoice.")
return status.paid return status.paid
async def check_spendable(self, proofs): async def check_proof_state(self, proofs):
return await super().check_spendable(proofs) return await super().check_proof_state(proofs)
# ---------- TOKEN MECHANIS ---------- # ---------- TOKEN MECHANIS ----------
@@ -971,8 +971,8 @@ class Wallet(LedgerAPI):
""" """
invalidated_proofs: List[Proof] = [] invalidated_proofs: List[Proof] = []
if check_spendable: if check_spendable:
spendables = await self.check_spendable(proofs) proof_states = await self.check_proof_state(proofs)
for i, spendable in enumerate(spendables.spendable): for i, spendable in enumerate(proof_states.spendable):
if not spendable: if not spendable:
invalidated_proofs.append(proofs[i]) invalidated_proofs.append(proofs[i])
else: else:

View File

@@ -13,7 +13,7 @@ entry_points = {"console_scripts": ["cashu = cashu.wallet.cli.cli:cli"]}
setuptools.setup( setuptools.setup(
name="cashu", name="cashu",
version="0.12.2", version="0.12.3",
description="Ecash wallet and mint for Bitcoin Lightning", description="Ecash wallet and mint for Bitcoin Lightning",
long_description=long_description, long_description=long_description,
long_description_content_type="text/markdown", long_description_content_type="text/markdown",

View File

@@ -93,20 +93,20 @@ async def ledger():
yield ledger yield ledger
@pytest.fixture(autouse=True, scope="session") # @pytest.fixture(autouse=True, scope="session")
def mint_3338(): # def mint_3338():
settings.mint_listen_port = 3338 # settings.mint_listen_port = 3338
settings.port = 3338 # settings.port = 3338
settings.mint_url = "http://localhost:3338" # settings.mint_url = "http://localhost:3338"
settings.port = settings.mint_listen_port # settings.port = settings.mint_listen_port
config = uvicorn.Config( # config = uvicorn.Config(
"cashu.mint.app:app", # "cashu.mint.app:app",
port=settings.mint_listen_port, # port=settings.mint_listen_port,
host="127.0.0.1", # host="127.0.0.1",
) # )
server = UvicornServer(config=config, private_key="SECOND_PRIVATE_KEY") # server = UvicornServer(config=config, private_key="SECOND_PRIVATE_KEY")
server.start() # server.start()
time.sleep(1) # time.sleep(1)
yield server # yield server
server.stop() # server.stop()

View File

@@ -1,9 +1,11 @@
import asyncio import asyncio
import json
import pytest import pytest
import pytest_asyncio import pytest_asyncio
import requests import requests
from cashu.core.base import CheckSpendableRequest, CheckSpendableResponse, Proof
from cashu.core.settings import settings from cashu.core.settings import settings
from tests.conftest import ledger from tests.conftest import ledger
@@ -54,3 +56,22 @@ async def test_api_mint_validation(ledger):
assert "error" in response.json() assert "error" in response.json()
response = requests.get(f"{BASE_URL}/mint?amount=1") response = requests.get(f"{BASE_URL}/mint?amount=1")
assert "error" not in response.json() assert "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

View File

@@ -367,3 +367,12 @@ async def test_p2sh_receive_with_wrong_wallet(wallet1: Wallet, wallet2: Wallet):
wallet1.proofs, 8, secret_lock wallet1.proofs, 8, secret_lock
) # sender side ) # sender side
await assert_err(wallet2.redeem(send_proofs), "lock not found.") # wrong receiver 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"]