diff --git a/.gitignore b/.gitignore index b83d222..2f7896d 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1 @@ -/target/ +target/ diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..9025d64 --- /dev/null +++ b/CLAUDE.md @@ -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 diff --git a/phoenixd-base/target/maven-status/maven-compiler-plugin/compile/default-compile/inputFiles.lst b/phoenixd-base/target/maven-status/maven-compiler-plugin/compile/default-compile/inputFiles.lst new file mode 100644 index 0000000..a88bf28 --- /dev/null +++ b/phoenixd-base/target/maven-status/maven-compiler-plugin/compile/default-compile/inputFiles.lst @@ -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 diff --git a/phoenixd-mock/src/main/java/xyz/tcheeric/phoenixd/mock/MockLnServer.java b/phoenixd-mock/src/main/java/xyz/tcheeric/phoenixd/mock/MockLnServer.java index 9c0b2da..44bd190 100644 --- a/phoenixd-mock/src/main/java/xyz/tcheeric/phoenixd/mock/MockLnServer.java +++ b/phoenixd-mock/src/main/java/xyz/tcheeric/phoenixd/mock/MockLnServer.java @@ -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; @@ -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\"}"); } diff --git a/phoenixd-mock/target/classes/xyz/tcheeric/phoenixd/mock/Main.class b/phoenixd-mock/target/classes/xyz/tcheeric/phoenixd/mock/Main.class new file mode 100644 index 0000000..4ecc704 Binary files /dev/null and b/phoenixd-mock/target/classes/xyz/tcheeric/phoenixd/mock/Main.class differ diff --git a/phoenixd-mock/target/classes/xyz/tcheeric/phoenixd/mock/MockLnServer.class b/phoenixd-mock/target/classes/xyz/tcheeric/phoenixd/mock/MockLnServer.class new file mode 100644 index 0000000..c8436de Binary files /dev/null and b/phoenixd-mock/target/classes/xyz/tcheeric/phoenixd/mock/MockLnServer.class differ diff --git a/phoenixd-mock/target/maven-status/maven-compiler-plugin/compile/default-compile/createdFiles.lst b/phoenixd-mock/target/maven-status/maven-compiler-plugin/compile/default-compile/createdFiles.lst new file mode 100644 index 0000000..f7becd1 --- /dev/null +++ b/phoenixd-mock/target/maven-status/maven-compiler-plugin/compile/default-compile/createdFiles.lst @@ -0,0 +1,2 @@ +xyz/tcheeric/phoenixd/mock/MockLnServer.class +xyz/tcheeric/phoenixd/mock/Main.class diff --git a/phoenixd-mock/target/maven-status/maven-compiler-plugin/compile/default-compile/inputFiles.lst b/phoenixd-mock/target/maven-status/maven-compiler-plugin/compile/default-compile/inputFiles.lst new file mode 100644 index 0000000..1616b0c --- /dev/null +++ b/phoenixd-mock/target/maven-status/maven-compiler-plugin/compile/default-compile/inputFiles.lst @@ -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