mirror of
https://github.com/aljazceru/nutshell.git
synced 2026-02-11 11:44:21 +01:00
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:
@@ -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,
|
||||
|
||||
@@ -2,6 +2,7 @@ import httpx
|
||||
import pytest
|
||||
import pytest_asyncio
|
||||
|
||||
from cashu.core.nuts import nut20
|
||||
from cashu.core.settings import settings
|
||||
from cashu.mint.ledger import Ledger
|
||||
from cashu.wallet.wallet import Wallet
|
||||
@@ -29,21 +30,23 @@ async def wallet(ledger: Ledger):
|
||||
)
|
||||
async def test_api_mint_cached_responses(wallet: Wallet):
|
||||
# Testing mint
|
||||
invoice = await wallet.request_mint(64)
|
||||
await pay_if_regtest(invoice.request)
|
||||
mint_quote = await wallet.request_mint(64)
|
||||
await pay_if_regtest(mint_quote.request)
|
||||
|
||||
quote_id = invoice.quote
|
||||
quote_id = mint_quote.quote
|
||||
secrets, rs, derivation_paths = await wallet.generate_secrets_from_to(10010, 10011)
|
||||
outputs, rs = wallet._construct_outputs([32, 32], secrets, rs)
|
||||
assert mint_quote.privkey
|
||||
signature = nut20.sign_mint_quote(quote_id, outputs, mint_quote.privkey)
|
||||
outputs_payload = [o.dict() for o in outputs]
|
||||
response = httpx.post(
|
||||
f"{BASE_URL}/v1/mint/bolt11",
|
||||
json={"quote": quote_id, "outputs": outputs_payload},
|
||||
json={"quote": quote_id, "outputs": outputs_payload, "signature": signature},
|
||||
timeout=None,
|
||||
)
|
||||
response1 = httpx.post(
|
||||
f"{BASE_URL}/v1/mint/bolt11",
|
||||
json={"quote": quote_id, "outputs": outputs_payload},
|
||||
json={"quote": quote_id, "outputs": outputs_payload, "signature": signature},
|
||||
timeout=None,
|
||||
)
|
||||
assert response.status_code == 200, f"{response.status_code = }"
|
||||
@@ -57,17 +60,23 @@ async def test_api_mint_cached_responses(wallet: Wallet):
|
||||
reason="settings.mint_redis_cache_enabled is False",
|
||||
)
|
||||
async def test_api_swap_cached_responses(wallet: Wallet):
|
||||
quote = await wallet.request_mint(64)
|
||||
await pay_if_regtest(quote.request)
|
||||
mint_quote = await wallet.request_mint(64)
|
||||
await pay_if_regtest(mint_quote.request)
|
||||
|
||||
minted = await wallet.mint(64, quote.quote)
|
||||
minted = await wallet.mint(64, mint_quote.quote)
|
||||
secrets, rs, derivation_paths = await wallet.generate_secrets_from_to(10010, 10011)
|
||||
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)
|
||||
inputs_payload = [i.dict() for i in minted]
|
||||
outputs_payload = [o.dict() for o in outputs]
|
||||
response = httpx.post(
|
||||
f"{BASE_URL}/v1/swap",
|
||||
json={"inputs": inputs_payload, "outputs": outputs_payload},
|
||||
json={
|
||||
"inputs": inputs_payload,
|
||||
"outputs": outputs_payload,
|
||||
"signature": signature,
|
||||
},
|
||||
timeout=None,
|
||||
)
|
||||
response1 = httpx.post(
|
||||
@@ -86,15 +95,14 @@ async def test_api_swap_cached_responses(wallet: Wallet):
|
||||
reason="settings.mint_redis_cache_enabled is False",
|
||||
)
|
||||
async def test_api_melt_cached_responses(wallet: Wallet):
|
||||
quote = await wallet.request_mint(64)
|
||||
mint_quote = await wallet.request_mint(64)
|
||||
melt_quote = await wallet.melt_quote(invoice_32sat)
|
||||
|
||||
await pay_if_regtest(quote.request)
|
||||
minted = await wallet.mint(64, quote.quote)
|
||||
await pay_if_regtest(mint_quote.request)
|
||||
minted = await wallet.mint(64, mint_quote.quote)
|
||||
|
||||
secrets, rs, derivation_paths = await wallet.generate_secrets_from_to(10010, 10010)
|
||||
outputs, rs = wallet._construct_outputs([32], secrets, rs)
|
||||
|
||||
inputs_payload = [i.dict() for i in minted]
|
||||
outputs_payload = [o.dict() for o in outputs]
|
||||
response = httpx.post(
|
||||
|
||||
@@ -6,6 +6,7 @@ from cashu.core.base import Proof, Unit
|
||||
from cashu.core.models import (
|
||||
CheckSpendableRequest_deprecated,
|
||||
CheckSpendableResponse_deprecated,
|
||||
GetMintResponse_deprecated,
|
||||
PostRestoreRequest,
|
||||
PostRestoreResponse,
|
||||
)
|
||||
@@ -124,15 +125,20 @@ async def test_api_mint_validation(ledger):
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_mint(ledger: Ledger, wallet: Wallet):
|
||||
mint_quote = await wallet.request_mint(64)
|
||||
await pay_if_regtest(mint_quote.request)
|
||||
quote_response = httpx.get(
|
||||
f"{BASE_URL}/mint",
|
||||
params={"amount": 64},
|
||||
timeout=None,
|
||||
)
|
||||
mint_quote = GetMintResponse_deprecated.parse_obj(quote_response.json())
|
||||
await pay_if_regtest(mint_quote.pr)
|
||||
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}/mint",
|
||||
json={"outputs": outputs_payload},
|
||||
params={"hash": mint_quote.quote},
|
||||
params={"hash": mint_quote.hash},
|
||||
timeout=None,
|
||||
)
|
||||
assert response.status_code == 200, f"{response.url} {response.status_code}"
|
||||
|
||||
@@ -4,6 +4,7 @@ import pytest_asyncio
|
||||
from cashu.core.base import MeltQuoteState
|
||||
from cashu.core.helpers import sum_proofs
|
||||
from cashu.core.models import PostMeltQuoteRequest, PostMintQuoteRequest
|
||||
from cashu.core.nuts import nut20
|
||||
from cashu.core.settings import settings
|
||||
from cashu.mint.ledger import Ledger
|
||||
from cashu.wallet.wallet import Wallet
|
||||
@@ -119,9 +120,9 @@ async def test_melt_external(wallet1: Wallet, ledger: Ledger):
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.skipif(is_regtest, reason="only works with FakeWallet")
|
||||
async def test_mint_internal(wallet1: Wallet, ledger: Ledger):
|
||||
mint_quote = await wallet1.request_mint(128)
|
||||
await ledger.get_mint_quote(mint_quote.quote)
|
||||
mint_quote = await ledger.get_mint_quote(mint_quote.quote)
|
||||
wallet_mint_quote = await wallet1.request_mint(128)
|
||||
await ledger.get_mint_quote(wallet_mint_quote.quote)
|
||||
mint_quote = await ledger.get_mint_quote(wallet_mint_quote.quote)
|
||||
|
||||
assert mint_quote.paid, "mint quote should be paid"
|
||||
|
||||
@@ -136,7 +137,11 @@ async def test_mint_internal(wallet1: Wallet, ledger: Ledger):
|
||||
len(output_amounts)
|
||||
)
|
||||
outputs, rs = wallet1._construct_outputs(output_amounts, secrets, rs)
|
||||
await ledger.mint(outputs=outputs, quote_id=mint_quote.quote)
|
||||
assert wallet_mint_quote.privkey
|
||||
signature = nut20.sign_mint_quote(
|
||||
mint_quote.quote, outputs, wallet_mint_quote.privkey
|
||||
)
|
||||
await ledger.mint(outputs=outputs, quote_id=mint_quote.quote, signature=signature)
|
||||
|
||||
await assert_err(
|
||||
ledger.mint(outputs=outputs, quote_id=mint_quote.quote),
|
||||
@@ -151,9 +156,7 @@ async def test_mint_internal(wallet1: Wallet, ledger: Ledger):
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.skipif(is_fake, reason="only works with Regtest")
|
||||
async def test_mint_external(wallet1: Wallet, ledger: Ledger):
|
||||
quote = await ledger.mint_quote(PostMintQuoteRequest(amount=128, unit="sat"))
|
||||
assert not quote.paid, "mint quote should not be paid"
|
||||
assert quote.unpaid
|
||||
quote = await wallet1.request_mint(128)
|
||||
|
||||
mint_quote = await ledger.get_mint_quote(quote.quote)
|
||||
assert not mint_quote.paid, "mint quote already paid"
|
||||
@@ -179,7 +182,9 @@ async def test_mint_external(wallet1: Wallet, ledger: Ledger):
|
||||
len(output_amounts)
|
||||
)
|
||||
outputs, rs = wallet1._construct_outputs(output_amounts, secrets, rs)
|
||||
await ledger.mint(outputs=outputs, quote_id=quote.quote)
|
||||
assert quote.privkey
|
||||
signature = nut20.sign_mint_quote(quote.quote, outputs, quote.privkey)
|
||||
await ledger.mint(outputs=outputs, quote_id=quote.quote, signature=signature)
|
||||
|
||||
mint_quote_after_payment = await ledger.get_mint_quote(quote.quote)
|
||||
assert mint_quote_after_payment.issued, "mint quote should be issued"
|
||||
@@ -311,14 +316,18 @@ async def test_mint_with_same_outputs_twice(wallet1: Wallet, ledger: Ledger):
|
||||
len(output_amounts)
|
||||
)
|
||||
outputs, rs = wallet1._construct_outputs(output_amounts, secrets, rs)
|
||||
await ledger.mint(outputs=outputs, quote_id=mint_quote.quote)
|
||||
assert mint_quote.privkey
|
||||
signature = nut20.sign_mint_quote(mint_quote.quote, outputs, mint_quote.privkey)
|
||||
await ledger.mint(outputs=outputs, quote_id=mint_quote.quote, signature=signature)
|
||||
|
||||
# now try to mint with the same outputs again
|
||||
mint_quote_2 = await wallet1.request_mint(128)
|
||||
await pay_if_regtest(mint_quote_2.request)
|
||||
|
||||
assert mint_quote_2.privkey
|
||||
signature = nut20.sign_mint_quote(mint_quote_2.quote, outputs, mint_quote_2.privkey)
|
||||
await assert_err(
|
||||
ledger.mint(outputs=outputs, quote_id=mint_quote_2.quote),
|
||||
ledger.mint(outputs=outputs, quote_id=mint_quote_2.quote, signature=signature),
|
||||
"outputs have already been signed before.",
|
||||
)
|
||||
|
||||
@@ -338,7 +347,9 @@ async def test_melt_with_same_outputs_twice(wallet1: Wallet, ledger: Ledger):
|
||||
# we use the outputs once for minting
|
||||
mint_quote_2 = await wallet1.request_mint(128)
|
||||
await pay_if_regtest(mint_quote_2.request)
|
||||
await ledger.mint(outputs=outputs, quote_id=mint_quote_2.quote)
|
||||
assert mint_quote_2.privkey
|
||||
signature = nut20.sign_mint_quote(mint_quote_2.quote, outputs, mint_quote_2.privkey)
|
||||
await ledger.mint(outputs=outputs, quote_id=mint_quote_2.quote, signature=signature)
|
||||
|
||||
# use the same outputs for melting
|
||||
mint_quote = await ledger.mint_quote(PostMintQuoteRequest(unit="sat", amount=128))
|
||||
|
||||
@@ -5,7 +5,7 @@ import pytest_asyncio
|
||||
|
||||
from cashu.core.base import Method, MintQuoteState, ProofState
|
||||
from cashu.core.json_rpc.base import JSONRPCNotficationParams
|
||||
from cashu.core.nuts import WEBSOCKETS_NUT
|
||||
from cashu.core.nuts.nuts import WEBSOCKETS_NUT
|
||||
from cashu.core.settings import settings
|
||||
from cashu.wallet.wallet import Wallet
|
||||
from tests.conftest import SERVER_ENDPOINT
|
||||
|
||||
Reference in New Issue
Block a user