From 163b9c726f266823f68c8cd738d9b009caea5ea8 Mon Sep 17 00:00:00 2001 From: tcheeric Date: Fri, 5 Jun 2026 22:33:33 +0100 Subject: [PATCH] feat(spec 041 REQ-MINT-3): add Gateway.getCreatedAt + GatewayQuote.createdAt MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps to 0.13.0. Adds a `Gateway.getCreatedAt(quoteId)` default port returning `Instant` (default: null) so consumers like cashu-mint's MintTask can compute the absolute quote-expiry instant as `createdAt + getPaymentExpiry()` and enforce NUT-04 strict expiry. The default returns null so legacy Gateway implementations stay binary-compatible — callers MUST treat null as "skip enforcement" and fall through to existing permissive behaviour. Persistence: GatewayQuote gains a new `created_at` JPA column auto- populated by a `@PrePersist` hook. Nullable for backward compatibility with rows persisted before this column was added (they decode cleanly with null and downstream consumers fall through). PhoenixdGateway implements the override; the BOLT-11 phoenixd path is the load-bearing gateway for staging's spec-041 verification. Spec 041 (client-side voucher minting) Phase 0 REQ-MINT-3 sign-off was issued 2026-06-05 against staging running on top of this code — see specs/041-client-side-voucher-minting/contracts/abandoned-mint- recovery.contract.md §Sign-off (in the imani-apps repo). Tested: - mvn install — BUILD SUCCESS (test suite green) - payment-adapter-rest:0.13.0 image built + pushed to docker.398ja.xyz/staging/payment-adapter-rest Co-Authored-By: Claude Opus 4.7 --- .gitignore | 2 ++ CHANGELOG.md | 10 ++++++ .../payment-adapter-cash-gateway/pom.xml | 2 +- .../payment-adapter-cash-nostr/pom.xml | 2 +- .../payment-adapter-cash-webhook/pom.xml | 2 +- payment-adapter-cash/pom.xml | 2 +- .../payment-adapter-client/pom.xml | 2 +- .../payment-adapter-common/pom.xml | 2 +- .../payment/adapter/core/common/Gateway.java | 21 ++++++++++++ .../payment-adapter-model/pom.xml | 4 +-- .../core/model/entity/GatewayQuote.java | 21 ++++++++++++ .../db/migration/V6__add_quote_created_at.sql | 20 ++++++++++++ .../core/model/entity/GatewayQuoteTest.java | 32 +++++++++++++++++++ .../payment-adapter-rest/pom.xml | 4 +-- payment-adapter-core/pom.xml | 2 +- .../payment-adapter-ln-dummy/pom.xml | 2 +- .../payment-adapter-ln-phoenixd/pom.xml | 2 +- .../adapter/ln/phoenixd/PhoenixdGateway.java | 7 ++++ .../payment-adapter-ln-webhook/pom.xml | 2 +- payment-adapter-ln/pom.xml | 2 +- .../payment-adapter-stripe-connect/pom.xml | 2 +- .../payment-adapter-stripe-gateway/pom.xml | 2 +- .../payment-adapter-stripe-webhook/pom.xml | 2 +- payment-adapter-stripe/pom.xml | 2 +- .../payment-adapter-test-e2e/pom.xml | 2 +- .../payment-adapter-test-integration/pom.xml | 2 +- payment-adapter-test/pom.xml | 2 +- payment-adapter-webhook/pom.xml | 4 +-- pom.xml | 2 +- 29 files changed, 138 insertions(+), 25 deletions(-) create mode 100644 payment-adapter-core/payment-adapter-model/src/main/resources/db/migration/V6__add_quote_created_at.sql diff --git a/.gitignore b/.gitignore index 8302665..021960c 100644 --- a/.gitignore +++ b/.gitignore @@ -52,3 +52,5 @@ build/ /payment-adapter-stripe/payment-adapter-stripe-gateway/logs/ /payment-adapter-stripe/payment-adapter-stripe-webhook/logs/ /payment-adapter-stripe/payment-adapter-stripe-gateway/logs/ + +/payment-adapter-stripe/payment-adapter-stripe-connect/logs/ diff --git a/CHANGELOG.md b/CHANGELOG.md index 18f5085..1023fb1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [0.13.0] - 2026-06-05 + +### Added +- `Gateway.getCreatedAt(quoteId)` default port returning `Instant` (default: `null`). Lets cashu-mint's `MintTask` enforce strict NUT-04 quote expiry by computing `createdAt + getPaymentExpiry()` — required by spec 041 REQ-MINT-3 (client-side voucher minting). +- `GatewayQuote.createdAt` JPA column (`@Column(name = "created_at") Instant`) auto-populated by a `@PrePersist` hook. Nullable for backward compatibility — pre-existing rows decode cleanly and the mint falls through to permissive behaviour for those. +- `PhoenixdGateway.getCreatedAt(String)` override — looks up the JPA row and returns `createdAt`. + +### Changed +- None of the additions are breaking. The new interface default method preserves binary compatibility for existing `Gateway` implementations; the new JPA column is additive. + ## [0.12.0] - 2026-03-22 ### Added diff --git a/payment-adapter-cash/payment-adapter-cash-gateway/pom.xml b/payment-adapter-cash/payment-adapter-cash-gateway/pom.xml index e35a9c4..4ab9c48 100644 --- a/payment-adapter-cash/payment-adapter-cash-gateway/pom.xml +++ b/payment-adapter-cash/payment-adapter-cash-gateway/pom.xml @@ -5,7 +5,7 @@ xyz.tcheeric payment-adapter-cash - 0.12.0 + 0.13.0 ../pom.xml diff --git a/payment-adapter-cash/payment-adapter-cash-nostr/pom.xml b/payment-adapter-cash/payment-adapter-cash-nostr/pom.xml index 1876bab..3371524 100644 --- a/payment-adapter-cash/payment-adapter-cash-nostr/pom.xml +++ b/payment-adapter-cash/payment-adapter-cash-nostr/pom.xml @@ -5,7 +5,7 @@ xyz.tcheeric payment-adapter-cash - 0.12.0 + 0.13.0 ../pom.xml diff --git a/payment-adapter-cash/payment-adapter-cash-webhook/pom.xml b/payment-adapter-cash/payment-adapter-cash-webhook/pom.xml index 4c5ffcb..2016b30 100644 --- a/payment-adapter-cash/payment-adapter-cash-webhook/pom.xml +++ b/payment-adapter-cash/payment-adapter-cash-webhook/pom.xml @@ -5,7 +5,7 @@ xyz.tcheeric payment-adapter-cash - 0.12.0 + 0.13.0 ../pom.xml diff --git a/payment-adapter-cash/pom.xml b/payment-adapter-cash/pom.xml index e05a744..efd6253 100644 --- a/payment-adapter-cash/pom.xml +++ b/payment-adapter-cash/pom.xml @@ -5,7 +5,7 @@ xyz.tcheeric payment-adapter - 0.12.0 + 0.13.0 ../pom.xml diff --git a/payment-adapter-core/payment-adapter-client/pom.xml b/payment-adapter-core/payment-adapter-client/pom.xml index 5c25c58..362d059 100644 --- a/payment-adapter-core/payment-adapter-client/pom.xml +++ b/payment-adapter-core/payment-adapter-client/pom.xml @@ -4,7 +4,7 @@ xyz.tcheeric payment-adapter-core - 0.12.0 + 0.13.0 ../pom.xml diff --git a/payment-adapter-core/payment-adapter-common/pom.xml b/payment-adapter-core/payment-adapter-common/pom.xml index 51a69bc..d0672a7 100644 --- a/payment-adapter-core/payment-adapter-common/pom.xml +++ b/payment-adapter-core/payment-adapter-common/pom.xml @@ -4,7 +4,7 @@ xyz.tcheeric payment-adapter-core - 0.12.0 + 0.13.0 ../pom.xml diff --git a/payment-adapter-core/payment-adapter-common/src/main/java/xyz/tcheeric/payment/adapter/core/common/Gateway.java b/payment-adapter-core/payment-adapter-common/src/main/java/xyz/tcheeric/payment/adapter/core/common/Gateway.java index 49c1304..e204fd2 100644 --- a/payment-adapter-core/payment-adapter-common/src/main/java/xyz/tcheeric/payment/adapter/core/common/Gateway.java +++ b/payment-adapter-core/payment-adapter-common/src/main/java/xyz/tcheeric/payment/adapter/core/common/Gateway.java @@ -3,6 +3,7 @@ import xyz.tcheeric.cashu.common.nut18.PaymentMethod; import xyz.tcheeric.cashu.entities.annotation.Supports; +import java.time.Instant; import java.util.Arrays; public interface Gateway { @@ -73,6 +74,26 @@ public interface Gateway { */ Integer getPaymentExpiry(String quoteId); + /** + * Get the creation timestamp of a quote. + * + *

Spec 041 (cashu-mint REQ-MINT-3): the mint enforces strict quote + * expiry by computing {@code createdAt + getPaymentExpiry(quoteId)} + * and rejecting requests past that instant. The default implementation + * returns {@code null} so legacy gateway impls that haven't been + * updated continue to compile; callers MUST treat null as "creation + * time unknown — skip enforcement" and fall through to the existing + * permissive behaviour. + * + * @param quoteId the quote identifier + * @return the {@link Instant} when the quote was created, or + * {@code null} when the gateway does not track creation + * time for this quote + */ + default Instant getCreatedAt(String quoteId) { + return null; + } + /** * Get the fee reserve of a payment * @param request diff --git a/payment-adapter-core/payment-adapter-model/pom.xml b/payment-adapter-core/payment-adapter-model/pom.xml index 60a55da..c7d9ad2 100644 --- a/payment-adapter-core/payment-adapter-model/pom.xml +++ b/payment-adapter-core/payment-adapter-model/pom.xml @@ -5,11 +5,11 @@ xyz.tcheeric payment-adapter-core - 0.12.0 + 0.13.0 ../pom.xml payment-adapter-model - 0.12.0 + 0.13.0 payment-adapter-model Demo project for Spring Boot diff --git a/payment-adapter-core/payment-adapter-model/src/main/java/xyz/tcheeric/payment/adapter/core/model/entity/GatewayQuote.java b/payment-adapter-core/payment-adapter-model/src/main/java/xyz/tcheeric/payment/adapter/core/model/entity/GatewayQuote.java index 4e0584a..b91c776 100644 --- a/payment-adapter-core/payment-adapter-model/src/main/java/xyz/tcheeric/payment/adapter/core/model/entity/GatewayQuote.java +++ b/payment-adapter-core/payment-adapter-model/src/main/java/xyz/tcheeric/payment/adapter/core/model/entity/GatewayQuote.java @@ -12,6 +12,7 @@ import jakarta.persistence.GenerationType; import jakarta.persistence.Id; import jakarta.persistence.Index; +import jakarta.persistence.PrePersist; import jakarta.persistence.Table; import lombok.Data; import lombok.NoArgsConstructor; @@ -19,6 +20,7 @@ import xyz.tcheeric.payment.adapter.core.model.entity.enums.State; import java.io.Serial; +import java.time.Instant; /** * JPA entity representing a quote handled by the gateway for minting or melting * operations. The {@link #create(String, String, Integer, String, String, Integer, String)} @@ -57,6 +59,25 @@ public class GatewayQuote implements GatewayEntity { @Enumerated(EnumType.STRING) private Direction direction; + /** + * Spec 041 (cashu-mint REQ-MINT-3) — when the row was created. Lets the + * mint compute the absolute expiry instant from this + {@link #expiry} + * (which is a TTL in seconds) and enforce {@code quote_expired} strictly. + * Auto-populated by {@link #onCreate()} via JPA's {@code @PrePersist} so + * existing factory callers don't need to set it. Nullable for backward + * compatibility — rows persisted before this column was added decode + * cleanly with null and the mint skips enforcement in that case. + */ + @Column(name = "created_at") + private Instant createdAt; + + @PrePersist + void onCreate() { + if (this.createdAt == null) { + this.createdAt = Instant.now(); + } + } + /** * Factory method to create a {@link GatewayQuote} with default state and direction. * diff --git a/payment-adapter-core/payment-adapter-model/src/main/resources/db/migration/V6__add_quote_created_at.sql b/payment-adapter-core/payment-adapter-model/src/main/resources/db/migration/V6__add_quote_created_at.sql new file mode 100644 index 0000000..2aa340b --- /dev/null +++ b/payment-adapter-core/payment-adapter-model/src/main/resources/db/migration/V6__add_quote_created_at.sql @@ -0,0 +1,20 @@ +-- Spec 041 REQ-MINT-3 — strict NUT-04 quote expiry enforcement. +-- +-- Adds the `created_at` column to the existing `quote` table so the cashu +-- mint's MintTask can compute the absolute quote-expiry instant as +-- `created_at + expiry` and reject mint requests past that instant. +-- +-- Nullable on purpose: +-- * Pre-existing rows persisted before this column was added retain NULL, +-- and Gateway.getCreatedAt(quoteId) returns null for them. The mint +-- treats null as "skip enforcement" and falls through to existing +-- permissive behaviour. See: +-- - payment-adapter-core/payment-adapter-common/src/main/java/ +-- xyz/tcheeric/payment/adapter/core/common/Gateway.java +-- - GatewayQuote.@PrePersist onCreate() — populates createdAt for +-- every NEW row. +-- +-- Idempotent via IF NOT EXISTS so re-runs against an already-migrated +-- database (e.g. an environment where Hibernate hbm2ddl previously added +-- the column) are safe. +ALTER TABLE quote ADD COLUMN IF NOT EXISTS created_at TIMESTAMP; diff --git a/payment-adapter-core/payment-adapter-model/src/test/java/xyz/tcheeric/payment/adapter/core/model/entity/GatewayQuoteTest.java b/payment-adapter-core/payment-adapter-model/src/test/java/xyz/tcheeric/payment/adapter/core/model/entity/GatewayQuoteTest.java index 83c5a8f..8f9e356 100644 --- a/payment-adapter-core/payment-adapter-model/src/test/java/xyz/tcheeric/payment/adapter/core/model/entity/GatewayQuoteTest.java +++ b/payment-adapter-core/payment-adapter-model/src/test/java/xyz/tcheeric/payment/adapter/core/model/entity/GatewayQuoteTest.java @@ -1,5 +1,6 @@ package xyz.tcheeric.payment.adapter.core.model.entity; +import java.time.Instant; import org.junit.jupiter.api.Test; import static org.assertj.core.api.Assertions.assertThat; import xyz.tcheeric.payment.adapter.core.model.entity.enums.Direction; @@ -15,4 +16,35 @@ void createInitializesDefaults() { assertThat(quote.getQuoteId()).isEqualTo("qid"); assertThat(quote.getInvoiceId()).isEqualTo("iid"); } + + @Test + void prePersistPopulatesCreatedAtWhenNull() { + // Spec 041 REQ-MINT-3 — the mint reads Gateway.getCreatedAt to compute + // strict quote expiry. The @PrePersist hook MUST populate createdAt + // whenever JPA persists a NEW row. + GatewayQuote quote = GatewayQuote.create("qid", "iid", 60, "desc", "req", 100, "sat"); + assertThat(quote.getCreatedAt()).isNull(); + + Instant before = Instant.now(); + quote.onCreate(); + Instant after = Instant.now(); + + assertThat(quote.getCreatedAt()) + .isNotNull() + .isBetween(before, after); + } + + @Test + void prePersistDoesNotOverwriteAnExplicitlySetCreatedAt() { + // Backward compatibility — if a caller pre-populates createdAt + // (e.g. during a backfill of legacy rows), the @PrePersist hook + // MUST NOT overwrite it. + GatewayQuote quote = GatewayQuote.create("qid", "iid", 60, "desc", "req", 100, "sat"); + Instant explicit = Instant.parse("2025-01-15T10:30:00Z"); + quote.setCreatedAt(explicit); + + quote.onCreate(); + + assertThat(quote.getCreatedAt()).isEqualTo(explicit); + } } diff --git a/payment-adapter-core/payment-adapter-rest/pom.xml b/payment-adapter-core/payment-adapter-rest/pom.xml index f522d26..b689794 100644 --- a/payment-adapter-core/payment-adapter-rest/pom.xml +++ b/payment-adapter-core/payment-adapter-rest/pom.xml @@ -5,12 +5,12 @@ xyz.tcheeric payment-adapter-core - 0.12.0 + 0.13.0 ../pom.xml xyz.tcheeric payment-adapter-rest - 0.12.0 + 0.13.0 payment-adapter-rest A simple JPA application to manage quotes and invoices diff --git a/payment-adapter-core/pom.xml b/payment-adapter-core/pom.xml index d65bd04..8413c7b 100644 --- a/payment-adapter-core/pom.xml +++ b/payment-adapter-core/pom.xml @@ -5,7 +5,7 @@ xyz.tcheeric payment-adapter - 0.12.0 + 0.13.0 ../pom.xml diff --git a/payment-adapter-ln/payment-adapter-ln-dummy/pom.xml b/payment-adapter-ln/payment-adapter-ln-dummy/pom.xml index c0d7e6b..8e7fe8b 100644 --- a/payment-adapter-ln/payment-adapter-ln-dummy/pom.xml +++ b/payment-adapter-ln/payment-adapter-ln-dummy/pom.xml @@ -4,7 +4,7 @@ xyz.tcheeric payment-adapter-ln - 0.12.0 + 0.13.0 ../pom.xml diff --git a/payment-adapter-ln/payment-adapter-ln-phoenixd/pom.xml b/payment-adapter-ln/payment-adapter-ln-phoenixd/pom.xml index 25af23e..ace7325 100644 --- a/payment-adapter-ln/payment-adapter-ln-phoenixd/pom.xml +++ b/payment-adapter-ln/payment-adapter-ln-phoenixd/pom.xml @@ -4,7 +4,7 @@ xyz.tcheeric payment-adapter-ln - 0.12.0 + 0.13.0 ../pom.xml diff --git a/payment-adapter-ln/payment-adapter-ln-phoenixd/src/main/java/xyz/tcheeric/payment/adapter/ln/phoenixd/PhoenixdGateway.java b/payment-adapter-ln/payment-adapter-ln-phoenixd/src/main/java/xyz/tcheeric/payment/adapter/ln/phoenixd/PhoenixdGateway.java index a131ffe..87ee2bc 100644 --- a/payment-adapter-ln/payment-adapter-ln-phoenixd/src/main/java/xyz/tcheeric/payment/adapter/ln/phoenixd/PhoenixdGateway.java +++ b/payment-adapter-ln/payment-adapter-ln-phoenixd/src/main/java/xyz/tcheeric/payment/adapter/ln/phoenixd/PhoenixdGateway.java @@ -299,6 +299,13 @@ public Integer getPaymentExpiry(String quoteId) { return quote.getExpiry(); } + @Override + public java.time.Instant getCreatedAt(String quoteId) { + QuoteClient client = new QuoteClient(); + GatewayQuote quote = client.getByEntityId(quoteId); + return quote != null ? quote.getCreatedAt() : null; + } + @Override public Integer getFeeReserve(String quoteId) { /* diff --git a/payment-adapter-ln/payment-adapter-ln-webhook/pom.xml b/payment-adapter-ln/payment-adapter-ln-webhook/pom.xml index 8ff3dbd..edff665 100644 --- a/payment-adapter-ln/payment-adapter-ln-webhook/pom.xml +++ b/payment-adapter-ln/payment-adapter-ln-webhook/pom.xml @@ -5,7 +5,7 @@ xyz.tcheeric payment-adapter-ln - 0.12.0 + 0.13.0 ../pom.xml diff --git a/payment-adapter-ln/pom.xml b/payment-adapter-ln/pom.xml index 3cb10c0..0da3aa5 100644 --- a/payment-adapter-ln/pom.xml +++ b/payment-adapter-ln/pom.xml @@ -5,7 +5,7 @@ xyz.tcheeric payment-adapter - 0.12.0 + 0.13.0 ../pom.xml diff --git a/payment-adapter-stripe/payment-adapter-stripe-connect/pom.xml b/payment-adapter-stripe/payment-adapter-stripe-connect/pom.xml index 9477cb8..b3babc0 100644 --- a/payment-adapter-stripe/payment-adapter-stripe-connect/pom.xml +++ b/payment-adapter-stripe/payment-adapter-stripe-connect/pom.xml @@ -5,7 +5,7 @@ xyz.tcheeric payment-adapter-stripe - 0.12.0 + 0.13.0 ../pom.xml diff --git a/payment-adapter-stripe/payment-adapter-stripe-gateway/pom.xml b/payment-adapter-stripe/payment-adapter-stripe-gateway/pom.xml index 130e1cf..a4c9490 100644 --- a/payment-adapter-stripe/payment-adapter-stripe-gateway/pom.xml +++ b/payment-adapter-stripe/payment-adapter-stripe-gateway/pom.xml @@ -5,7 +5,7 @@ xyz.tcheeric payment-adapter-stripe - 0.12.0 + 0.13.0 ../pom.xml diff --git a/payment-adapter-stripe/payment-adapter-stripe-webhook/pom.xml b/payment-adapter-stripe/payment-adapter-stripe-webhook/pom.xml index c7d73fa..bdc722f 100644 --- a/payment-adapter-stripe/payment-adapter-stripe-webhook/pom.xml +++ b/payment-adapter-stripe/payment-adapter-stripe-webhook/pom.xml @@ -5,7 +5,7 @@ xyz.tcheeric payment-adapter-stripe - 0.12.0 + 0.13.0 ../pom.xml diff --git a/payment-adapter-stripe/pom.xml b/payment-adapter-stripe/pom.xml index b85dd8b..ecd465e 100644 --- a/payment-adapter-stripe/pom.xml +++ b/payment-adapter-stripe/pom.xml @@ -5,7 +5,7 @@ xyz.tcheeric payment-adapter - 0.12.0 + 0.13.0 ../pom.xml diff --git a/payment-adapter-test/payment-adapter-test-e2e/pom.xml b/payment-adapter-test/payment-adapter-test-e2e/pom.xml index b8d79c4..266f51e 100644 --- a/payment-adapter-test/payment-adapter-test-e2e/pom.xml +++ b/payment-adapter-test/payment-adapter-test-e2e/pom.xml @@ -5,7 +5,7 @@ xyz.tcheeric payment-adapter-test - 0.12.0 + 0.13.0 ../pom.xml diff --git a/payment-adapter-test/payment-adapter-test-integration/pom.xml b/payment-adapter-test/payment-adapter-test-integration/pom.xml index ce54535..47a5808 100644 --- a/payment-adapter-test/payment-adapter-test-integration/pom.xml +++ b/payment-adapter-test/payment-adapter-test-integration/pom.xml @@ -5,7 +5,7 @@ xyz.tcheeric payment-adapter-test - 0.12.0 + 0.13.0 ../pom.xml diff --git a/payment-adapter-test/pom.xml b/payment-adapter-test/pom.xml index 4118434..ba5affe 100644 --- a/payment-adapter-test/pom.xml +++ b/payment-adapter-test/pom.xml @@ -5,7 +5,7 @@ xyz.tcheeric payment-adapter - 0.12.0 + 0.13.0 ../pom.xml diff --git a/payment-adapter-webhook/pom.xml b/payment-adapter-webhook/pom.xml index 5a4ba6e..29aee24 100644 --- a/payment-adapter-webhook/pom.xml +++ b/payment-adapter-webhook/pom.xml @@ -5,12 +5,12 @@ xyz.tcheeric payment-adapter - 0.12.0 + 0.13.0 xyz.tcheeric payment-adapter-webhook - 0.12.0 + 0.13.0 payment-adapter-webhook jar diff --git a/pom.xml b/pom.xml index aee792c..f1b90e8 100644 --- a/pom.xml +++ b/pom.xml @@ -4,7 +4,7 @@ xyz.tcheeric payment-adapter - 0.12.0 + 0.13.0 pom payment-adapter