diff --git a/cashu-gateway-client/pom.xml b/cashu-gateway-client/pom.xml index 9ba0075..4bca1b8 100644 --- a/cashu-gateway-client/pom.xml +++ b/cashu-gateway-client/pom.xml @@ -4,7 +4,7 @@ xyz.tcheeric cashu-gateway - 0.3.0 + 0.3.1 cashu-gateway-client diff --git a/cashu-gateway-common/pom.xml b/cashu-gateway-common/pom.xml index 2c83ffd..1b0f596 100644 --- a/cashu-gateway-common/pom.xml +++ b/cashu-gateway-common/pom.xml @@ -4,7 +4,7 @@ xyz.tcheeric cashu-gateway - 0.3.0 + 0.3.1 cashu-gateway-common diff --git a/cashu-gateway-dummy/pom.xml b/cashu-gateway-dummy/pom.xml index 59a764b..809196f 100644 --- a/cashu-gateway-dummy/pom.xml +++ b/cashu-gateway-dummy/pom.xml @@ -4,7 +4,7 @@ xyz.tcheeric cashu-gateway - 0.3.0 + 0.3.1 cashu-gateway-dummy diff --git a/cashu-gateway-dummy/src/test/java/xyz/tcheeric/gateway/dummy/DummyGatewayTest.java b/cashu-gateway-dummy/src/test/java/xyz/tcheeric/gateway/dummy/DummyGatewayTest.java index c018cea..ea81d67 100644 --- a/cashu-gateway-dummy/src/test/java/xyz/tcheeric/gateway/dummy/DummyGatewayTest.java +++ b/cashu-gateway-dummy/src/test/java/xyz/tcheeric/gateway/dummy/DummyGatewayTest.java @@ -29,4 +29,45 @@ void testGatewayMethodsReturnNonNull() { assertNotNull(payment); assertNotNull(preimage); } + + /** + * Ensures paying with the exact quoteId returned by createMeltQuote produces a preimage + * and that the stored preimage for that quoteId matches what pay() returned (consistency). + */ + @Test + void testQuoteIdConsistencyOnPay() { + DummyGateway gateway = new DummyGateway(); + + String invoice = "lnbc1-test-invoice"; + String quoteId = gateway.createMeltQuote(5, invoice, "consistency"); + + String preimage = gateway.pay(quoteId); + String storedPreimage = gateway.getPaymentPreimage(quoteId); + + assertNotNull(preimage); + assertEquals(preimage, storedPreimage); + } + + /** + * Verifies that attempting to pay with an unknown/stale quoteId is rejected. + */ + @Test + void testPayWithUnknownQuoteIdThrows() { + DummyGateway gateway = new DummyGateway(); + org.junit.jupiter.api.Assertions.assertThrows(IllegalArgumentException.class, + () -> gateway.pay("unknown-quote-id")); + } + + /** + * Confirms that getRequest returns the same invoice that was provided when creating the quote. + */ + @Test + void testGetRequestMatchesCreatedInvoice() { + DummyGateway gateway = new DummyGateway(); + String invoice = "lnbc1-sample-invoice"; + String quoteId = gateway.createMeltQuote(7, invoice, "match-invoice"); + + String fetched = gateway.getRequest(quoteId); + assertEquals(invoice, fetched); + } } diff --git a/cashu-gateway-model/pom.xml b/cashu-gateway-model/pom.xml index ca6cc99..fd371a7 100644 --- a/cashu-gateway-model/pom.xml +++ b/cashu-gateway-model/pom.xml @@ -5,10 +5,10 @@ xyz.tcheeric cashu-gateway - 0.3.0 + 0.3.1 cashu-gateway-model - 0.3.0 + 0.3.1 cashu-gateway-model Demo project for Spring Boot diff --git a/cashu-gateway-phoenixd/pom.xml b/cashu-gateway-phoenixd/pom.xml index 59baf14..20394e9 100644 --- a/cashu-gateway-phoenixd/pom.xml +++ b/cashu-gateway-phoenixd/pom.xml @@ -4,7 +4,7 @@ xyz.tcheeric cashu-gateway - 0.3.0 + 0.3.1 cashu-gateway-phoenixd diff --git a/cashu-gateway-phoenixd/src/main/java/xyz/tcheeric/gateway/phoenixd/PhoenixdGateway.java b/cashu-gateway-phoenixd/src/main/java/xyz/tcheeric/gateway/phoenixd/PhoenixdGateway.java index 5b03240..a0ea713 100644 --- a/cashu-gateway-phoenixd/src/main/java/xyz/tcheeric/gateway/phoenixd/PhoenixdGateway.java +++ b/cashu-gateway-phoenixd/src/main/java/xyz/tcheeric/gateway/phoenixd/PhoenixdGateway.java @@ -198,6 +198,12 @@ public String pay(String quoteId) { QuoteClient quoteClient = new QuoteClient(); GatewayQuote quote = quoteClient.getByEntityId(quoteId); + if (quote == null) { + throw new IllegalStateException("Unknown quoteId: " + quoteId); + } + if (!quoteId.equals(quote.getQuoteId())) { + throw new IllegalStateException("Mismatched quoteId: requested=" + quoteId + ", stored=" + quote.getQuoteId()); + } String request = quote.getRequest(); if (request == null) { diff --git a/cashu-gateway-phoenixd/src/test/java/xyz/tcheeric/gateway/phoenixd/PhoenixdGatewayTest.java b/cashu-gateway-phoenixd/src/test/java/xyz/tcheeric/gateway/phoenixd/PhoenixdGatewayTest.java index 2713178..758aa4e 100644 --- a/cashu-gateway-phoenixd/src/test/java/xyz/tcheeric/gateway/phoenixd/PhoenixdGatewayTest.java +++ b/cashu-gateway-phoenixd/src/test/java/xyz/tcheeric/gateway/phoenixd/PhoenixdGatewayTest.java @@ -134,6 +134,67 @@ public void testPayBoltInvoice() throws Exception { } } + // ensures the created payment carries the exact quoteId used to initiate payment (no stale/mismatched quoteId) + @Test + public void testQuoteIdConsistencyOnPay() throws Exception { + PayBolt11InvoiceInvoiceResponse payResp = new PayBolt11InvoiceInvoiceResponse(); + payResp.setPaymentId("pid2"); + payResp.setPaymentPreimage("pre2"); + payResp.setPaymentHash("hash2"); + payResp.setRecipientAmountSat(5); + payResp.setRoutingFeeSat(1); + + GatewayQuote[] savedQuote = new GatewayQuote[1]; + GatewayPayment[] savedPayment = new GatewayPayment[1]; + when(service.payBolt11Invoice(any())).thenReturn(payResp); + try ( + MockedConstruction quotes = mockConstruction(QuoteClient.class, + (mock, context) -> { + when(mock.create(any(GatewayQuote.class))).thenAnswer(inv -> { + GatewayQuote q = inv.getArgument(0); + savedQuote[0] = q; + return q; + }); + when(mock.getByEntityId(anyString())).thenAnswer(inv -> savedQuote[0]); + }); + MockedConstruction payments = mockConstruction(PaymentClient.class, + (mock, context) -> { + when(mock.create(any(GatewayPayment.class))).thenAnswer(inv -> { + GatewayPayment p = inv.getArgument(0); + savedPayment[0] = p; + return p; + }); + }) + ) { + String quoteId = gateway.createMeltQuote(5, "lnbc1consistency", "consistency"); + gateway.pay(quoteId); + + Assertions.assertNotNull(savedPayment[0]); + Assertions.assertEquals(quoteId, savedPayment[0].getQuoteId()); + } + } + + // verifies paying with an unknown/stale quoteId is rejected with a clear error + @Test + public void testPayWithUnknownQuoteIdThrows() { + PayBolt11InvoiceInvoiceResponse payResp = new PayBolt11InvoiceInvoiceResponse(); + payResp.setPaymentId("pid"); + payResp.setRecipientAmountSat(1); + payResp.setRoutingFeeSat(0); + // when(service.payBolt11Invoice(any())).thenReturn(payResp); + + try ( + MockedConstruction quotes = mockConstruction(QuoteClient.class, + (mock, context) -> { + // Simulate repository returning null for unknown quoteId + when(mock.getByEntityId(anyString())).thenReturn(null); + }); + MockedConstruction ignored = mockConstruction(PaymentClient.class) + ) { + Assertions.assertThrows(IllegalStateException.class, () -> gateway.pay("stale-or-unknown-quote")); + } + } + // verifies paying a lightning address invoice results in a paid payment record @Test public void testPayLnInvoice() throws Exception { diff --git a/cashu-gateway-rest/pom.xml b/cashu-gateway-rest/pom.xml index ab46268..25114b3 100644 --- a/cashu-gateway-rest/pom.xml +++ b/cashu-gateway-rest/pom.xml @@ -5,11 +5,11 @@ xyz.tcheeric cashu-gateway - 0.3.0 + 0.3.1 xyz.tcheeric cashu-gateway-rest - 0.3.0 + 0.3.1 cashu-gateway-rest A simple JPA application to manage quotes and invoices diff --git a/cashu-gateway-webhook/pom.xml b/cashu-gateway-webhook/pom.xml index b64ea66..75cca40 100644 --- a/cashu-gateway-webhook/pom.xml +++ b/cashu-gateway-webhook/pom.xml @@ -5,12 +5,12 @@ xyz.tcheeric cashu-gateway - 0.3.0 + 0.3.1 xyz.tcheeric cashu-gateway-webhook - 0.3.0 + 0.3.1 cashu-gateway-webhook war diff --git a/docs/reference/changelog.md b/docs/reference/changelog.md index 8658f9a..63d8be5 100644 --- a/docs/reference/changelog.md +++ b/docs/reference/changelog.md @@ -2,6 +2,11 @@ This document summarizes notable changes to the cashu-gateway project. Versions follow semantic versioning when possible. +## 0.3.1 + +- Phoenixd: reject unknown or mismatched quoteId during pay(), ensuring the wallet’s POST /mint/bolt11 quoteId matches the mint’s generated quote from POST /mint/quote/bolt11. +- Tests: add unit tests for quoteId consistency and unknown/stale IDs in both Phoenixd and Dummy gateways. + ## 0.3.0 - REST: enable POST create for Quote and Payment via Spring Data REST and expose IDs. diff --git a/pom.xml b/pom.xml index e5197c7..5f24a8a 100644 --- a/pom.xml +++ b/pom.xml @@ -4,7 +4,7 @@ xyz.tcheeric cashu-gateway - 0.3.0 + 0.3.1 pom cashu-gateway