Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 4 additions & 0 deletions cashu/core/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
2 changes: 1 addition & 1 deletion cashu/core/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

env = Env()

VERSION = "0.12.2"
VERSION = "0.12.3"


def find_env_file():
Expand Down
28 changes: 21 additions & 7 deletions cashu/mint/ledger.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
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

Expand Down Expand Up @@ -186,6 +186,15 @@ def _check_spendable(self, proof: Proof):
"""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 == "":
Expand Down Expand Up @@ -815,21 +824,26 @@ async def melt(

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:
proofs (List[Proof]): List of proofs to check.

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
Expand Down
11 changes: 6 additions & 5 deletions cashu/mint/router.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
1 change: 1 addition & 0 deletions cashu/wallet/cli/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
18 changes: 9 additions & 9 deletions cashu/wallet/wallet.py
Original file line number Diff line number Diff line change
Expand Up @@ -498,27 +498,27 @@ def _splitrequest_include_fields(proofs):
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))},
}

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):
Expand Down Expand Up @@ -769,8 +769,8 @@ async def pay_lightning(self, proofs: List[Proof], invoice: str, fee_reserve: in
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 ----------

Expand Down Expand Up @@ -971,8 +971,8 @@ async def invalidate(self, proofs: List[Proof], check_spendable=True):
"""
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:
Expand Down
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@

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",
Expand Down
34 changes: 17 additions & 17 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -93,20 +93,20 @@ async def start_mint_init(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",
)

server = UvicornServer(config=config, private_key="SECOND_PRIVATE_KEY")
server.start()
time.sleep(1)
yield server
server.stop()
# @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()
21 changes: 21 additions & 0 deletions tests/test_mint_api.py
Original file line number Diff line number Diff line change
@@ -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

Expand Down Expand Up @@ -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
9 changes: 9 additions & 0 deletions tests/test_wallet.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"]