Skip to content

Commit e33d8b6

Browse files
authored
Merge pull request #57 from 398ja/release/0.1.4
Release/0.1.4
2 parents 10bda5b + 3acd782 commit e33d8b6

8 files changed

Lines changed: 246 additions & 3 deletions

File tree

.gitignore

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
/target/
1+
target/

CLAUDE.md

Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
# CLAUDE.md
2+
3+
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
4+
5+
## Project Overview
6+
7+
phoenixd-java is a Java 21 client library for ACINQ's phoenixd REST API. It wraps HTTP endpoints for Lightning Network operations (invoices, payments, address handling) into typed Java requests and responses.
8+
9+
## Maven Commands
10+
11+
### Build & Test
12+
```bash
13+
# Clean build with unit tests
14+
mvn clean install
15+
16+
# Run all tests (unit + integration)
17+
mvn -q verify
18+
19+
# Build specific module
20+
mvn clean install -pl phoenixd-rest -am
21+
```
22+
23+
### Test Individual Modules
24+
```bash
25+
# Test specific module
26+
mvn test -pl phoenixd-rest
27+
28+
# Run single test class
29+
mvn test -pl phoenixd-rest -Dtest=ClassName
30+
31+
# Run single test method
32+
mvn test -pl phoenixd-rest -Dtest=ClassName#methodName
33+
```
34+
35+
### Code Coverage
36+
```bash
37+
# Generate coverage report (requires mvn verify)
38+
mvn clean verify
39+
40+
# View aggregated report at:
41+
# target/site/jacoco-aggregate/index.html
42+
43+
# Coverage threshold: 80% (enforced by jacoco-maven-plugin)
44+
```
45+
46+
### Docker Build (Jib)
47+
```bash
48+
# Build and publish phoenixd-rest container
49+
./mvnw deploy -pl phoenixd-rest -am
50+
51+
# Build and publish phoenixd-mock container
52+
./mvnw deploy -pl phoenixd-mock -am
53+
54+
# Images are pushed to docker.398ja.xyz with version and 'latest' tags
55+
```
56+
57+
## Module Architecture
58+
59+
The project is a multi-module Maven build with a layered architecture:
60+
61+
```
62+
phoenixd-java (parent)
63+
├── phoenixd-base # Core abstractions and configuration
64+
├── phoenixd-model # Request params and response DTOs
65+
├── phoenixd-rest # HTTP client implementation
66+
├── phoenixd-mock # Mock server for testing
67+
└── phoenixd-test # Integration tests
68+
```
69+
70+
### Module Dependencies
71+
- **phoenixd-base**: Foundation layer with `Request`, `Operation`, `Response` interfaces and `Configuration` utility
72+
- **phoenixd-model**: Depends on phoenixd-base; defines all param/response POJOs (e.g., `CreateInvoiceParam`, `PayInvoiceResponse`)
73+
- **phoenixd-rest**: Depends on phoenixd-model and phoenixd-base; implements abstract operations (GET, POST, etc.) and concrete request classes
74+
- **phoenixd-test**: Depends on phoenixd-rest; runs integration tests against a live phoenixd instance
75+
- **phoenixd-mock**: Standalone mock server; no dependencies on other modules
76+
77+
### Request-Operation Pattern
78+
79+
The library uses a two-layer pattern:
80+
1. **Request** layer (`AbstractRequest` + concrete implementations in `phoenixd-rest/request/impl/rest/`):
81+
- Takes a `Param` object and an `Operation`
82+
- Calls `operation.execute()` and deserializes the response body into a typed response object
83+
- Example: `CreateBolt11InvoiceRequest`, `PayLightningAddressRequest`
84+
85+
2. **Operation** layer (`AbstractOperation` + HTTP method implementations in `phoenixd-rest/operation/impl/`):
86+
- Handles HTTP mechanics: building URIs, auth headers (Basic Auth), sending requests via `HttpClient`
87+
- Replaces path variables (e.g., `{invoice}`) from `Param` fields
88+
- Returns raw response body as string
89+
- Example: `GetOperation`, `PostOperation`
90+
91+
### PayRequestFactory
92+
Located in `phoenixd-rest/request/impl/rest/PayRequestFactory.java`, this factory detects whether a payment string is:
93+
- A Lightning address (contains `@`)
94+
- A BOLT11 invoice (matches `^(lnbc|lntb|lnsb|lnbcrt)[0-9]*[a-z0-9]+$` case-insensitive)
95+
96+
Returns the appropriate `BasePayRequest` subclass.
97+
98+
## Configuration
99+
100+
Tests require a running phoenixd instance and the following environment variables:
101+
```bash
102+
PHOENIXD_USERNAME=...
103+
PHOENIXD_PASSWORD=...
104+
PHOENIXD_BASE_URL=http://localhost:9740 # or your phoenixd URL
105+
```
106+
107+
Additional test-specific config lives in `phoenixd-test/src/test/resources/app.properties` (e.g., `test.pay_lnaddress`).
108+
109+
Configuration resolution follows: **ENV > system properties > app.properties**. The `Configuration` class in phoenixd-base handles this.
110+
111+
## Logging
112+
113+
Uses SLF4J with Lombok's `@Slf4j`. phoenixd-rest ships `simplelogger.properties` with:
114+
- Default log level: INFO
115+
- `xyz.tcheeric` package: DEBUG
116+
- Date/time and thread name enabled
117+
118+
Override at runtime:
119+
```bash
120+
JAVA_TOOL_OPTIONS="-Dorg.slf4j.simpleLogger.defaultLogLevel=info -Dorg.slf4j.simpleLogger.log.xyz.tcheeric=debug"
121+
```
122+
123+
Or for Spring Boot apps, set in `application.properties`:
124+
```
125+
logging.level.xyz.tcheeric=DEBUG
126+
```
127+
128+
**Logged events**:
129+
- HTTP request/response (method, URI, status, timeout)
130+
- Configuration resolution (where values came from)
131+
- Payment request detection logic (PayRequestFactory)
132+
- Sensitive headers (Authorization) are redacted
133+
134+
## Code Style & Conventions
135+
136+
From `.github/copilot-instructions.md`:
137+
- Commit messages follow `type: description` format (e.g., `fix: handle null node`, `feat: add invoice decoding`)
138+
- Use present tense verbs for the description
139+
- Breaking changes must be flagged with **BREAKING** in the PR/commit message
140+
- All new code should have test coverage
141+
- Use Lombok annotations (`@Data`, `@Slf4j`, `@NonNull`, `@SneakyThrows`) consistently
142+
143+
## Testing Notes
144+
145+
- Unit tests use JUnit 5 and AssertJ
146+
- phoenixd-rest uses MockWebServer (OkHttp) for HTTP mocking
147+
- Integration tests in phoenixd-test require a live phoenixd instance
148+
- When tests fail, check that env vars are set and phoenixd is reachable
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
/home/runner/work/phoenixd-java/phoenixd-java/phoenixd-base/src/main/java/xyz/tcheeric/phoenixd/common/rest/Operation.java
2+
/home/runner/work/phoenixd-java/phoenixd-java/phoenixd-base/src/main/java/xyz/tcheeric/phoenixd/common/rest/VoidRequestParam.java
3+
/home/runner/work/phoenixd-java/phoenixd-java/phoenixd-base/src/main/java/xyz/tcheeric/phoenixd/common/rest/Request.java
4+
/home/runner/work/phoenixd-java/phoenixd-java/phoenixd-base/src/main/java/xyz/tcheeric/phoenixd/common/rest/Response.java
5+
/home/runner/work/phoenixd-java/phoenixd-java/phoenixd-base/src/main/java/xyz/tcheeric/phoenixd/common/rest/VoidResponse.java
6+
/home/runner/work/phoenixd-java/phoenixd-java/phoenixd-base/src/main/java/xyz/tcheeric/phoenixd/common/rest/util/Configuration.java
7+
/home/runner/work/phoenixd-java/phoenixd-java/phoenixd-base/src/main/java/xyz/tcheeric/phoenixd/common/rest/util/Constants.java

phoenixd-mock/src/main/java/xyz/tcheeric/phoenixd/mock/MockLnServer.java

Lines changed: 86 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,16 @@
88
import java.io.OutputStream;
99
import java.net.InetSocketAddress;
1010
import java.nio.charset.StandardCharsets;
11+
import java.security.SecureRandom;
1112

1213
@RequiredArgsConstructor
1314
public class MockLnServer {
15+
/**
16+
* Bech32 generator values used in checksum calculation.
17+
* These are fixed values defined by the Bech32 specification.
18+
*/
19+
private static final int[] BECH32_GENERATOR = {0x3b6a57b2, 0x26508e6d, 0x1ea119fa, 0x3d4233dd, 0x2a1462b3};
20+
1421
private HttpServer server;
1522

1623
private final int port;
@@ -44,12 +51,89 @@ private void handlePayLightningAddress(HttpExchange exchange) throws IOException
4451
}
4552

4653
private void handleCreateInvoice(HttpExchange exchange) throws IOException {
47-
// Generate a unique mock bolt11 invoice
54+
// Generate a unique mock bolt11 invoice with valid Bech32 encoding
4855
String invoiceId = Long.toHexString(System.nanoTime());
49-
String bolt11 = "lnbc1mock" + invoiceId;
56+
String bolt11 = generateValidBolt11Invoice();
5057
writeJson(exchange, "{\"amountSat\":10,\"paymentHash\":\"hash" + invoiceId + "\",\"serialized\":\"" + bolt11 + "\"}");
5158
}
5259

60+
/**
61+
* Generates a minimally valid BOLT11 invoice for testing purposes.
62+
* Format: ln + currency + amount + separator + data + checksum
63+
*
64+
* This generates a mock invoice that passes basic Bech32 validation but is not
65+
* cryptographically valid for actual Lightning Network payments.
66+
*/
67+
private String generateValidBolt11Invoice() {
68+
// Human-readable part: ln + bc (bitcoin mainnet) + 10n (10 nanosats = ~0 sats for testing)
69+
String hrp = "lnbc10n";
70+
71+
// Generate random payment hash (32 bytes = 52 chars in bech32, roughly)
72+
// For a minimal valid invoice, we need at least timestamp + payment hash
73+
// Bech32 charset: qpzry9x8gf2tvdw0s3jn54khce6mua7l
74+
StringBuilder data = new StringBuilder();
75+
76+
// Generate 52 random bech32 characters (represents ~32 bytes of data)
77+
for (int i = 0; i < 52; i++) {
78+
data.append(charset.charAt(SECURE_RANDOM.nextInt(charset.length())));
79+
}
80+
81+
// Calculate and append Bech32 checksum (6 characters)
82+
String checksum = calculateBech32Checksum(hrp, data.toString());
83+
84+
return hrp + "1" + data.toString() + checksum;
85+
}
86+
87+
/**
88+
* Calculates Bech32 checksum for the given HRP and data.
89+
* Simplified implementation for mock purposes.
90+
*/
91+
private String calculateBech32Checksum(String hrp, String data) {
92+
// Expand HRP
93+
int[] values = new int[hrp.length() * 2 + 1 + data.length() + 6];
94+
int idx = 0;
95+
for (int i = 0; i < hrp.length(); i++) {
96+
values[idx++] = hrp.charAt(i) >> 5;
97+
}
98+
values[idx++] = 0;
99+
for (int i = 0; i < hrp.length(); i++) {
100+
values[idx++] = hrp.charAt(i) & 31;
101+
}
102+
for (int i = 0; i < data.length(); i++) {
103+
values[idx++] = BECH32_CHARSET.indexOf(data.charAt(i));
104+
}
105+
for (int i = 0; i < 6; i++) {
106+
values[idx++] = 0;
107+
}
108+
109+
// Calculate checksum using Bech32 polymod
110+
int polymod = polymod(values) ^ 1;
111+
112+
StringBuilder checksum = new StringBuilder();
113+
for (int i = 0; i < 6; i++) {
114+
checksum.append(BECH32_CHARSET.charAt((polymod >> (5 * (5 - i))) & 31));
115+
}
116+
117+
return checksum.toString();
118+
}
119+
120+
/**
121+
* Bech32 polymod function for checksum calculation.
122+
*/
123+
private int polymod(int[] values) {
124+
int chk = 1;
125+
for (int value : values) {
126+
int top = chk >> 25;
127+
chk = (chk & 0x1ffffff) << 5 ^ value;
128+
for (int i = 0; i < 5; i++) {
129+
if (((top >> i) & 1) != 0) {
130+
chk ^= BECH32_GENERATOR[i];
131+
}
132+
}
133+
}
134+
return chk;
135+
}
136+
53137
private void handleDecodeInvoice(HttpExchange exchange) throws IOException {
54138
writeJson(exchange, "{\"amount\":1000,\"description\":\"1 Blockaccino\"}");
55139
}
Binary file not shown.
Binary file not shown.
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
xyz/tcheeric/phoenixd/mock/MockLnServer.class
2+
xyz/tcheeric/phoenixd/mock/Main.class
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
/home/runner/work/phoenixd-java/phoenixd-java/phoenixd-mock/src/main/java/xyz/tcheeric/phoenixd/mock/MockLnServer.java
2+
/home/runner/work/phoenixd-java/phoenixd-java/phoenixd-mock/src/main/java/xyz/tcheeric/phoenixd/mock/Main.java

0 commit comments

Comments
 (0)