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:
```bash
Version: 0.12.2
Version: 0.12.3
Debug: False
Cashu dir: /home/user/.cashu
Wallet: wallet

View File

@@ -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):

View File

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

View 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

View File

@@ -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(

View File

@@ -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):

View File

@@ -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:

View File

@@ -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",

View File

@@ -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()

View File

@@ -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

View File

@@ -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"]