Support NUT-XX (signatures on quotes) for mint and wallet side (#670)

* nut-19 sign mint quote

* ephemeral key for quote

* `mint` adjustments + crypto/nut19.py

* wip: mint side working

* fix import

* post-merge fixups

* more fixes

* make format

* move nut19 to nuts directory

* `key` -> `privkey` and `pubkey`

* make format

* mint_info method for nut-19 support

* fix tests imports

* fix signature missing positional argument + fix db migration format not correctly escaped + pass in NUT-19 keypair to `request_mint` `request_mint_with_callback`

* make format

* fix `get_invoice_status`

* rename to xx

* nutxx -> nut20

* mypy

* remove `mint_quote_signature_required` as per spec

* wip edits

* clean up

* fix tests

* fix deprecated api tests

* fix redis tests

* fix cache tests

* fix regtest mint external

* fix mint regtest

* add test without signature

* test pubkeys in quotes

* wip

* add compat

---------

Co-authored-by: callebtc <93376500+callebtc@users.noreply.github.com>
This commit is contained in:
lollerfirst
2024-12-15 00:39:53 +01:00
committed by GitHub
parent 399c201552
commit d98d166df1
30 changed files with 505 additions and 243 deletions

View File

@@ -14,7 +14,8 @@ from cashu.core.models import (
PostRestoreRequest,
PostRestoreResponse,
)
from cashu.core.nuts import MINT_NUT
from cashu.core.nuts import nut20
from cashu.core.nuts.nuts import MINT_NUT
from cashu.core.settings import settings
from cashu.mint.ledger import Ledger
from cashu.wallet.crud import bump_secret_derivation
@@ -189,12 +190,13 @@ async def test_split(ledger: Ledger, wallet: Wallet):
async def test_mint_quote(ledger: Ledger):
response = httpx.post(
f"{BASE_URL}/v1/mint/quote/bolt11",
json={"unit": "sat", "amount": 100},
json={"unit": "sat", "amount": 100, "pubkey": "02" + "00" * 32},
)
assert response.status_code == 200, f"{response.url} {response.status_code}"
result = response.json()
assert result["quote"]
assert result["request"]
assert result["pubkey"] == "02" + "00" * 32
# deserialize the response
resp_quote = PostMintQuoteResponse(**result)
@@ -232,6 +234,7 @@ async def test_mint_quote(ledger: Ledger):
# check if DEPRECATED paid flag is also returned
assert result2["paid"] is True
assert resp_quote.paid is True
assert resp_quote.pubkey == "02" + "00" * 32
@pytest.mark.asyncio
@@ -244,10 +247,16 @@ async def test_mint(ledger: Ledger, wallet: Wallet):
await pay_if_regtest(mint_quote.request)
secrets, rs, derivation_paths = await wallet.generate_secrets_from_to(10000, 10001)
outputs, rs = wallet._construct_outputs([32, 32], secrets, rs)
assert mint_quote.privkey
signature = nut20.sign_mint_quote(mint_quote.quote, outputs, mint_quote.privkey)
outputs_payload = [o.dict() for o in outputs]
response = httpx.post(
f"{BASE_URL}/v1/mint/bolt11",
json={"quote": mint_quote.quote, "outputs": outputs_payload},
json={
"quote": mint_quote.quote,
"outputs": outputs_payload,
"signature": signature,
},
timeout=None,
)
assert response.status_code == 200, f"{response.url} {response.status_code}"
@@ -261,6 +270,44 @@ async def test_mint(ledger: Ledger, wallet: Wallet):
assert "s" in result["signatures"][0]["dleq"]
@pytest.mark.asyncio
@pytest.mark.skipif(
settings.debug_mint_only_deprecated,
reason="settings.debug_mint_only_deprecated is set",
)
async def test_mint_bolt11_no_signature(ledger: Ledger, wallet: Wallet):
"""
For backwards compatibility, we do not require a NUT-20 signature
for minting with bolt11.
"""
response = httpx.post(
f"{BASE_URL}/v1/mint/quote/bolt11",
json={
"unit": "sat",
"amount": 64,
# no pubkey
},
)
assert response.status_code == 200, f"{response.url} {response.status_code}"
result = response.json()
assert result["pubkey"] is None
await pay_if_regtest(result["request"])
secrets, rs, derivation_paths = await wallet.generate_secrets_from_to(10000, 10001)
outputs, rs = wallet._construct_outputs([32, 32], secrets, rs)
outputs_payload = [o.dict() for o in outputs]
response = httpx.post(
f"{BASE_URL}/v1/mint/bolt11",
json={
"quote": result["quote"],
"outputs": outputs_payload,
# no signature
},
timeout=None,
)
assert response.status_code == 200, f"{response.url} {response.status_code}"
@pytest.mark.asyncio
@pytest.mark.skipif(
settings.debug_mint_only_deprecated,