Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,15 @@
# Changelog

## 0.3.2 — 2026-09-26

### Fixed
- **A mock invoice's prefix now carries the amount it charges.** Every invoice began `lnbc10n`
(1 sat) whatever `amountSat` was requested, so a consumer reading the price off the invoice saw
1 sat. imani-gateway-core checks that a client mint's invoice charges what the sale costs
(imani-gateway-core#92), and refused every staging client mint because of it. The prefix is now
`lnbc{amountSat × 10}n`, the nano-BTC form of the requested amount, and an amount of zero or
less is written as an amountless invoice. The invoice is still not signed or payable.

## 0.3.1 — 2026-09-23

### Fixed
Expand Down
2 changes: 1 addition & 1 deletion phoenixd-base/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
<parent>
<groupId>xyz.tcheeric</groupId>
<artifactId>phoenixd-java</artifactId>
<version>0.3.1</version>
<version>0.3.2</version>
</parent>

<artifactId>phoenixd-base</artifactId>
Expand Down
2 changes: 1 addition & 1 deletion phoenixd-mock/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
<parent>
<groupId>xyz.tcheeric</groupId>
<artifactId>phoenixd-java</artifactId>
<version>0.3.1</version>
<version>0.3.2</version>
</parent>

<artifactId>phoenixd-mock</artifactId>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -157,7 +157,7 @@
String externalId = null;
long amountSat = 10;

if (body != null && !body.isEmpty()) {

Check warning on line 160 in phoenixd-mock/src/main/java/xyz/tcheeric/phoenixd/mock/MockLnServer.java

View workflow job for this annotation

GitHub Actions / Qodana Community for JVM

Constant values

Condition `body != null` is always `true`
for (String param : body.split("&")) {
String[] pair = param.split("=", 2);
if (pair.length == 2) {
Expand All @@ -176,14 +176,14 @@
// Generate a unique mock bolt11 invoice with valid Bech32 encoding
String invoiceId = Long.toHexString(System.nanoTime());
String paymentHash = "hash" + invoiceId;
String bolt11 = generateValidBolt11Invoice();
String bolt11 = generateValidBolt11Invoice(amountSat);

// Track the invoice for auto-settlement
InvoiceInfo invoiceInfo = new InvoiceInfo(paymentHash, bolt11, amountSat, externalId);
invoices.put(paymentHash, invoiceInfo);

// Also index by external ID (quote ID) for mockpay lookup
if (externalId != null && !externalId.isEmpty()) {

Check warning on line 186 in phoenixd-mock/src/main/java/xyz/tcheeric/phoenixd/mock/MockLnServer.java

View workflow job for this annotation

GitHub Actions / Qodana Community for JVM

Constant values

Condition `!externalId.isEmpty()` is always `true` when reached
invoicesByExternalId.put(externalId, invoiceInfo);
}

Expand Down Expand Up @@ -241,10 +241,14 @@
*
* This generates a mock invoice that passes basic Bech32 validation but is not
* cryptographically valid for actual Lightning Network payments.
*
* <p>The human-readable part carries the requested amount, as a real invoice does. It used
* to be a fixed {@code lnbc10n} (1 sat) whatever was asked for, so anything that reads the
* price off the invoice, such as imani-gateway-core's check that a client mint charged what
* the sale costs, refused every invoice this mock issued.
*/
private String generateValidBolt11Invoice() {
// Human-readable part: ln + bc (bitcoin mainnet) + 10n (10 nanosats = ~0 sats for testing)
String hrp = "lnbc10n";
String generateValidBolt11Invoice(long amountSat) {
String hrp = "lnbc" + bolt11Amount(amountSat);

// Generate random payment hash (32 bytes = 52 chars in bech32, roughly)
// For a minimal valid invoice, we need at least timestamp + payment hash
Expand All @@ -262,6 +266,14 @@
return hrp + "1" + data + checksum;
}

/**
* The BOLT11 amount for a whole number of sats. With the {@code n} (nano-BTC) multiplier one
* sat is 10 units. Zero or less is an amountless invoice, which BOLT11 writes as no amount.
*/
static String bolt11Amount(long amountSat) {
return amountSat > 0 ? (amountSat * 10) + "n" : "";
}

/**
* Calculates Bech32 checksum for the given HRP and data.
* Simplified implementation for mock purposes.
Expand Down Expand Up @@ -325,7 +337,7 @@
String body = new String(exchange.getRequestBody().readAllBytes(), StandardCharsets.UTF_8);
String invoice = null;

if (body != null && !body.isEmpty()) {

Check warning on line 340 in phoenixd-mock/src/main/java/xyz/tcheeric/phoenixd/mock/MockLnServer.java

View workflow job for this annotation

GitHub Actions / Qodana Community for JVM

Constant values

Condition `body != null` is always `true`
for (String param : body.split("&")) {
String[] pair = param.split("=", 2);
if (pair.length == 2 && "invoice".equals(pair[0])) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,31 @@ void shouldCreateInvoiceWithDefaultAmount() throws Exception {
assertThat(bolt11).startsWith("lnbc");
}

// The invoice charges what was asked for: 500 sat is 5000 nano-BTC in the BOLT11 prefix, so
// a consumer reading the price off the invoice sees 500, not the 1 sat the prefix used to say.
@Test
void createdInvoiceCarriesTheRequestedAmountInItsPrefix() throws Exception {
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(BASE_URL + "/createinvoice"))
.header("Content-Type", "application/x-www-form-urlencoded")
.POST(HttpRequest.BodyPublishers.ofString("amountSat=500"))
.build();

HttpResponse<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());

String bolt11 = objectMapper.readTree(response.body()).get("serialized").asText();
assertThat(bolt11).startsWith("lnbc5000n1");
}

// One sat is ten nano-BTC, and a non-positive amount is written as no amount at all.
@Test
void bolt11AmountEncodesWholeSatsWithTheNanoMultiplier() {
assertThat(MockLnServer.bolt11Amount(1)).isEqualTo("10n");
assertThat(MockLnServer.bolt11Amount(100)).isEqualTo("1000n");
assertThat(MockLnServer.bolt11Amount(0)).isEmpty();
assertThat(MockLnServer.bolt11Amount(-3)).isEmpty();
}

@Test
void shouldGetInvoiceStatus() throws Exception {
// Arrange: Create an invoice first
Expand Down
2 changes: 1 addition & 1 deletion phoenixd-model/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
<parent>
<groupId>xyz.tcheeric</groupId>
<artifactId>phoenixd-java</artifactId>
<version>0.3.1</version>
<version>0.3.2</version>
</parent>

<artifactId>phoenixd-model</artifactId>
Expand Down
4 changes: 2 additions & 2 deletions phoenixd-rest/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,12 @@
<parent>
<groupId>xyz.tcheeric</groupId>
<artifactId>phoenixd-java</artifactId>
<version>0.3.1</version>
<version>0.3.2</version>
</parent>

<artifactId>phoenixd-rest</artifactId>
<packaging>jar</packaging>
<version>0.3.1</version>
<version>0.3.2</version>
<name>phoenixd-rest</name>
<url>https://maven.apache.org</url>

Expand Down
2 changes: 1 addition & 1 deletion phoenixd-test/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
<parent>
<groupId>xyz.tcheeric</groupId>
<artifactId>phoenixd-java</artifactId>
<version>0.3.1</version>
<version>0.3.2</version>
</parent>

<artifactId>phoenixd-test</artifactId>
Expand Down
2 changes: 1 addition & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
<modelVersion>4.0.0</modelVersion>
<groupId>xyz.tcheeric</groupId>
<artifactId>phoenixd-java</artifactId>
<version>0.3.1</version>
<version>0.3.2</version>
<packaging>pom</packaging>

<modules>
Expand Down
Loading