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
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1 +1 @@
/target/
target/
148 changes: 148 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
# CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

## Project Overview

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.

## Maven Commands

### Build & Test
```bash
# Clean build with unit tests
mvn clean install

# Run all tests (unit + integration)
mvn -q verify

# Build specific module
mvn clean install -pl phoenixd-rest -am
```

### Test Individual Modules
```bash
# Test specific module
mvn test -pl phoenixd-rest

# Run single test class
mvn test -pl phoenixd-rest -Dtest=ClassName

# Run single test method
mvn test -pl phoenixd-rest -Dtest=ClassName#methodName
```

### Code Coverage
```bash
# Generate coverage report (requires mvn verify)
mvn clean verify

# View aggregated report at:
# target/site/jacoco-aggregate/index.html

# Coverage threshold: 80% (enforced by jacoco-maven-plugin)
```

### Docker Build (Jib)
```bash
# Build and publish phoenixd-rest container
./mvnw deploy -pl phoenixd-rest -am

# Build and publish phoenixd-mock container
./mvnw deploy -pl phoenixd-mock -am

# Images are pushed to docker.398ja.xyz with version and 'latest' tags
```

## Module Architecture

The project is a multi-module Maven build with a layered architecture:

```
phoenixd-java (parent)
├── phoenixd-base # Core abstractions and configuration
├── phoenixd-model # Request params and response DTOs
├── phoenixd-rest # HTTP client implementation
├── phoenixd-mock # Mock server for testing
└── phoenixd-test # Integration tests
```

### Module Dependencies
- **phoenixd-base**: Foundation layer with `Request`, `Operation`, `Response` interfaces and `Configuration` utility
- **phoenixd-model**: Depends on phoenixd-base; defines all param/response POJOs (e.g., `CreateInvoiceParam`, `PayInvoiceResponse`)
- **phoenixd-rest**: Depends on phoenixd-model and phoenixd-base; implements abstract operations (GET, POST, etc.) and concrete request classes
- **phoenixd-test**: Depends on phoenixd-rest; runs integration tests against a live phoenixd instance
- **phoenixd-mock**: Standalone mock server; no dependencies on other modules

### Request-Operation Pattern

The library uses a two-layer pattern:
1. **Request** layer (`AbstractRequest` + concrete implementations in `phoenixd-rest/request/impl/rest/`):
- Takes a `Param` object and an `Operation`
- Calls `operation.execute()` and deserializes the response body into a typed response object
- Example: `CreateBolt11InvoiceRequest`, `PayLightningAddressRequest`

2. **Operation** layer (`AbstractOperation` + HTTP method implementations in `phoenixd-rest/operation/impl/`):
- Handles HTTP mechanics: building URIs, auth headers (Basic Auth), sending requests via `HttpClient`
- Replaces path variables (e.g., `{invoice}`) from `Param` fields
- Returns raw response body as string
- Example: `GetOperation`, `PostOperation`

### PayRequestFactory
Located in `phoenixd-rest/request/impl/rest/PayRequestFactory.java`, this factory detects whether a payment string is:
- A Lightning address (contains `@`)
- A BOLT11 invoice (matches `^(lnbc|lntb|lnsb|lnbcrt)[0-9]*[a-z0-9]+$` case-insensitive)

Returns the appropriate `BasePayRequest` subclass.

## Configuration

Tests require a running phoenixd instance and the following environment variables:
```bash
PHOENIXD_USERNAME=...
PHOENIXD_PASSWORD=...
PHOENIXD_BASE_URL=http://localhost:9740 # or your phoenixd URL
```

Additional test-specific config lives in `phoenixd-test/src/test/resources/app.properties` (e.g., `test.pay_lnaddress`).

Configuration resolution follows: **ENV > system properties > app.properties**. The `Configuration` class in phoenixd-base handles this.

## Logging

Uses SLF4J with Lombok's `@Slf4j`. phoenixd-rest ships `simplelogger.properties` with:
- Default log level: INFO
- `xyz.tcheeric` package: DEBUG
- Date/time and thread name enabled

Override at runtime:
```bash
JAVA_TOOL_OPTIONS="-Dorg.slf4j.simpleLogger.defaultLogLevel=info -Dorg.slf4j.simpleLogger.log.xyz.tcheeric=debug"
```

Or for Spring Boot apps, set in `application.properties`:
```
logging.level.xyz.tcheeric=DEBUG
```

**Logged events**:
- HTTP request/response (method, URI, status, timeout)
- Configuration resolution (where values came from)
- Payment request detection logic (PayRequestFactory)
- Sensitive headers (Authorization) are redacted

## Code Style & Conventions

From `.github/copilot-instructions.md`:
- Commit messages follow `type: description` format (e.g., `fix: handle null node`, `feat: add invoice decoding`)
- Use present tense verbs for the description
- Breaking changes must be flagged with **BREAKING** in the PR/commit message
- All new code should have test coverage
- Use Lombok annotations (`@Data`, `@Slf4j`, `@NonNull`, `@SneakyThrows`) consistently

## Testing Notes

- Unit tests use JUnit 5 and AssertJ
- phoenixd-rest uses MockWebServer (OkHttp) for HTTP mocking
- Integration tests in phoenixd-test require a live phoenixd instance
- When tests fail, check that env vars are set and phoenixd is reachable
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
/home/runner/work/phoenixd-java/phoenixd-java/phoenixd-base/src/main/java/xyz/tcheeric/phoenixd/common/rest/Operation.java
/home/runner/work/phoenixd-java/phoenixd-java/phoenixd-base/src/main/java/xyz/tcheeric/phoenixd/common/rest/VoidRequestParam.java
/home/runner/work/phoenixd-java/phoenixd-java/phoenixd-base/src/main/java/xyz/tcheeric/phoenixd/common/rest/Request.java
/home/runner/work/phoenixd-java/phoenixd-java/phoenixd-base/src/main/java/xyz/tcheeric/phoenixd/common/rest/Response.java
/home/runner/work/phoenixd-java/phoenixd-java/phoenixd-base/src/main/java/xyz/tcheeric/phoenixd/common/rest/VoidResponse.java
/home/runner/work/phoenixd-java/phoenixd-java/phoenixd-base/src/main/java/xyz/tcheeric/phoenixd/common/rest/util/Configuration.java
/home/runner/work/phoenixd-java/phoenixd-java/phoenixd-base/src/main/java/xyz/tcheeric/phoenixd/common/rest/util/Constants.java
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,16 @@
import java.io.OutputStream;
import java.net.InetSocketAddress;
import java.nio.charset.StandardCharsets;
import java.security.SecureRandom;

@RequiredArgsConstructor
public class MockLnServer {
/**
* Bech32 generator values used in checksum calculation.
* These are fixed values defined by the Bech32 specification.
*/
private static final int[] BECH32_GENERATOR = {0x3b6a57b2, 0x26508e6d, 0x1ea119fa, 0x3d4233dd, 0x2a1462b3};

private HttpServer server;

private final int port;
Expand Down Expand Up @@ -44,12 +51,89 @@ private void handlePayLightningAddress(HttpExchange exchange) throws IOException
}

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

/**
* Generates a minimally valid BOLT11 invoice for testing purposes.
* Format: ln + currency + amount + separator + data + checksum
*
* This generates a mock invoice that passes basic Bech32 validation but is not
* cryptographically valid for actual Lightning Network payments.
*/
private String generateValidBolt11Invoice() {
// Human-readable part: ln + bc (bitcoin mainnet) + 10n (10 nanosats = ~0 sats for testing)
String hrp = "lnbc10n";

// Generate random payment hash (32 bytes = 52 chars in bech32, roughly)
// For a minimal valid invoice, we need at least timestamp + payment hash
// Bech32 charset: qpzry9x8gf2tvdw0s3jn54khce6mua7l
StringBuilder data = new StringBuilder();

// Generate 52 random bech32 characters (represents ~32 bytes of data)
for (int i = 0; i < 52; i++) {
data.append(charset.charAt(SECURE_RANDOM.nextInt(charset.length())));
}

// Calculate and append Bech32 checksum (6 characters)
String checksum = calculateBech32Checksum(hrp, data.toString());

return hrp + "1" + data.toString() + checksum;
}

/**
* Calculates Bech32 checksum for the given HRP and data.
* Simplified implementation for mock purposes.
*/
private String calculateBech32Checksum(String hrp, String data) {
// Expand HRP
int[] values = new int[hrp.length() * 2 + 1 + data.length() + 6];
int idx = 0;
for (int i = 0; i < hrp.length(); i++) {
values[idx++] = hrp.charAt(i) >> 5;
}
values[idx++] = 0;
for (int i = 0; i < hrp.length(); i++) {
values[idx++] = hrp.charAt(i) & 31;
}
for (int i = 0; i < data.length(); i++) {
values[idx++] = BECH32_CHARSET.indexOf(data.charAt(i));
}
for (int i = 0; i < 6; i++) {
values[idx++] = 0;
}

// Calculate checksum using Bech32 polymod
int polymod = polymod(values) ^ 1;

StringBuilder checksum = new StringBuilder();
for (int i = 0; i < 6; i++) {
checksum.append(BECH32_CHARSET.charAt((polymod >> (5 * (5 - i))) & 31));
}

return checksum.toString();
}

/**
* Bech32 polymod function for checksum calculation.
*/
private int polymod(int[] values) {
int chk = 1;
for (int value : values) {
int top = chk >> 25;
chk = (chk & 0x1ffffff) << 5 ^ value;
for (int i = 0; i < 5; i++) {
if (((top >> i) & 1) != 0) {
chk ^= BECH32_GENERATOR[i];
}
}
}
return chk;
}

private void handleDecodeInvoice(HttpExchange exchange) throws IOException {
writeJson(exchange, "{\"amount\":1000,\"description\":\"1 Blockaccino\"}");
}
Expand Down
Binary file not shown.
Binary file not shown.
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
xyz/tcheeric/phoenixd/mock/MockLnServer.class
xyz/tcheeric/phoenixd/mock/Main.class
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
/home/runner/work/phoenixd-java/phoenixd-java/phoenixd-mock/src/main/java/xyz/tcheeric/phoenixd/mock/MockLnServer.java
/home/runner/work/phoenixd-java/phoenixd-java/phoenixd-mock/src/main/java/xyz/tcheeric/phoenixd/mock/Main.java
Loading