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:
|
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
|
||||||
|
|||||||
@@ -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):
|
||||||
|
|||||||
@@ -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():
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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):
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
2
setup.py
2
setup.py
@@ -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",
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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"]
|
||||||
|
|||||||
Reference in New Issue
Block a user