Skip to content

Commit

Permalink
Merge branch 'fix-get-quote-endpoint' into ngutech21-fix-get-quote-api
Browse files Browse the repository at this point in the history
  • Loading branch information
callebtc committed Jan 24, 2024
2 parents d7ebf87 + bfdd0da commit 29e309b
Show file tree
Hide file tree
Showing 7 changed files with 82 additions and 12 deletions.
16 changes: 14 additions & 2 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,9 @@ NOSTR_RELAYS=["wss://nostr-pub.wellorder.net"]
# Wallet API port
API_PORT=4448

# Wallet default unit
WALLET_UNIT="sat"

# --------- MINT ---------

# Network
Expand All @@ -38,8 +41,17 @@ MINT_INFO_CONTACT=[["email","[email protected]"], ["twitter","@me"], ["nostr", "np
MINT_INFO_MOTD="Message to users"

MINT_PRIVATE_KEY=supersecretprivatekey
# increment derivation path to rotate to a new keyset
MINT_DERIVATION_PATH="0/0/0/0"

# Increment derivation path to rotate to a new keyset
# Example: m/0'/0'/0' -> m/0'/0'/1'
MINT_DERIVATION_PATH="m/0'/0'/0'"

# Multiple derivation paths and units. Unit is parsed from the derivation path.
# m/0'/0'/0' is "sat" (default)
# m/0'/1'/0' is "msat"
# m/0'/2'/0' is "usd"
# In this example, we have 2 keysets for sat, 1 for msat and 1 for usd
# MINT_DERIVATION_PATH_LIST=["m/0'/0'/0'", "m/0'/0'/1'", "m/0'/1'/0'", "m/0'/2'/0'"]

MINT_DATABASE=data/mint

Expand Down
4 changes: 2 additions & 2 deletions cashu/core/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -365,7 +365,7 @@ class PostMeltRequest(BaseModel):
quote: str = Field(..., max_length=settings.mint_max_request_length) # quote id
inputs: List[Proof] = Field(..., max_items=settings.mint_max_request_length)
outputs: Union[List[BlindedMessage], None] = Field(
..., max_items=settings.mint_max_request_length
None, max_items=settings.mint_max_request_length
)


Expand All @@ -379,7 +379,7 @@ class PostMeltRequest_deprecated(BaseModel):
proofs: List[Proof] = Field(..., max_items=settings.mint_max_request_length)
pr: str = Field(..., max_length=settings.mint_max_request_length)
outputs: Union[List[BlindedMessage], None] = Field(
..., max_items=settings.mint_max_request_length
None, max_items=settings.mint_max_request_length
)


Expand Down
2 changes: 1 addition & 1 deletion cashu/mint/ledger.py
Original file line number Diff line number Diff line change
Expand Up @@ -625,7 +625,7 @@ async def melt(

# make sure that the outputs (for fee return) are in the same unit as the quote
if outputs:
await self._verify_outputs(outputs)
await self._verify_outputs(outputs, skip_amount_check=True)
assert outputs[0].id, "output id not set"
outputs_unit = self.keysets[outputs[0].id].unit
assert melt_quote.unit == outputs_unit.name, (
Expand Down
8 changes: 4 additions & 4 deletions cashu/mint/router.py
Original file line number Diff line number Diff line change
Expand Up @@ -207,7 +207,7 @@ async def get_mint_quote(quote: str) -> PostMintQuoteResponse:
paid=mint_quote.paid,
expiry=mint_quote.expiry,
)
logger.trace(f"< POST /v1/mint/quote/{quote}")
logger.trace(f"< GET /v1/mint/quote/bolt11/{quote}")
return resp


Expand Down Expand Up @@ -253,7 +253,7 @@ async def get_melt_quote(payload: PostMeltQuoteRequest) -> PostMeltQuoteResponse


@router.get(
"/v1/melt/quote/{quote}",
"/v1/melt/quote/bolt11/{quote}",
summary="Get melt quote",
response_model=PostMeltQuoteResponse,
response_description="Get an existing melt quote to check its status.",
Expand All @@ -262,15 +262,15 @@ async def melt_quote(quote: str) -> PostMeltQuoteResponse:
"""
Get melt quote state.
"""
logger.trace(f"> POST /v1/melt/quote/{quote}")
logger.trace(f"> GET /v1/melt/quote/bolt11/{quote}")
melt_quote = await ledger.get_melt_quote(quote)
resp = PostMeltQuoteResponse(
quote=melt_quote.quote,
amount=melt_quote.amount,
fee_reserve=melt_quote.fee_reserve,
paid=melt_quote.paid,
)
logger.trace(f"< POST /v1/melt/quote/{quote}")
logger.trace(f"< GET /v1/melt/quote/bolt11/{quote}")
return resp


Expand Down
10 changes: 7 additions & 3 deletions cashu/mint/verification.py
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,9 @@ async def verify_inputs_and_outputs(
if outputs and not self._verify_output_spending_conditions(proofs, outputs):
raise TransactionError("validation of output spending conditions failed.")

async def _verify_outputs(self, outputs: List[BlindedMessage]):
async def _verify_outputs(
self, outputs: List[BlindedMessage], skip_amount_check=False
):
"""Verify that the outputs are valid."""
logger.trace(f"Verifying {len(outputs)} outputs.")
# Verify all outputs have the same keyset id
Expand All @@ -108,8 +110,10 @@ async def _verify_outputs(self, outputs: List[BlindedMessage]):
if not self.keysets[outputs[0].id].active:
raise TransactionError("keyset id inactive.")
# Verify amounts of outputs
if not all([self._verify_amount(o.amount) for o in outputs]):
raise TransactionError("invalid amount.")
# we skip the amount check for NUT-8 change outputs (which can have amount 0)
if not skip_amount_check:
if not all([self._verify_amount(o.amount) for o in outputs]):
raise TransactionError("invalid amount.")
# verify that only unique outputs were used
if not self._verify_no_duplicate_outputs(outputs):
raise TransactionError("duplicate outputs.")
Expand Down
16 changes: 16 additions & 0 deletions tests/test_mint_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,14 @@ async def test_mint_quote(ledger: Ledger):
invoice = bolt11.decode(result["request"])
assert invoice.amount_msat == 100 * 1000

# get mint quote again from api
response = httpx.get(
f"{BASE_URL}/v1/mint/quote/bolt11/{result['quote']}",
)
assert response.status_code == 200, f"{response.url} {response.status_code}"
result2 = response.json()
assert result2["quote"] == result["quote"]


@pytest.mark.asyncio
@pytest.mark.skipif(
Expand Down Expand Up @@ -236,6 +244,14 @@ async def test_melt_quote_internal(ledger: Ledger, wallet: Wallet):
# TODO: internal invoice, fee should be 0
assert result["fee_reserve"] == 0

# get melt quote again from api
response = httpx.get(
f"{BASE_URL}/v1/melt/quote/bolt11/{result['quote']}",
)
assert response.status_code == 200, f"{response.url} {response.status_code}"
result2 = response.json()
assert result2["quote"] == result["quote"]


@pytest.mark.asyncio
@pytest.mark.skipif(
Expand Down
38 changes: 38 additions & 0 deletions tests/test_mint_api_deprecated.py
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,44 @@ async def test_melt_internal(ledger: Ledger, wallet: Wallet):
assert result["paid"] is True


@pytest.mark.asyncio
async def test_melt_internal_no_change_outputs(ledger: Ledger, wallet: Wallet):
# Clients without NUT-08 will not send change outputs
# internal invoice
invoice = await wallet.request_mint(64)
pay_if_regtest(invoice.bolt11)
await wallet.mint(64, id=invoice.id)
assert wallet.balance == 64

# create invoice to melt to
invoice = await wallet.request_mint(64)

invoice_payment_request = invoice.bolt11

quote = await wallet.melt_quote(invoice_payment_request)
assert quote.amount == 64
assert quote.fee_reserve == 0

inputs_payload = [p.to_dict() for p in wallet.proofs]

# outputs for change
secrets, rs, derivation_paths = await wallet.generate_n_secrets(1)
outputs, rs = wallet._construct_outputs([2], secrets, rs)

response = httpx.post(
f"{BASE_URL}/melt",
json={
"pr": invoice_payment_request,
"proofs": inputs_payload,
},
timeout=None,
)
assert response.status_code == 200, f"{response.url} {response.status_code}"
result = response.json()
assert result.get("preimage") is not None
assert result["paid"] is True


@pytest.mark.asyncio
@pytest.mark.skipif(
is_fake,
Expand Down

0 comments on commit 29e309b

Please sign in to comment.