diff --git a/README.MD b/README.MD index b18c740..c21528c 100644 --- a/README.MD +++ b/README.MD @@ -1,144 +1,105 @@ -# Portal Java SDK - Documentation +# Portal Java SDK -## Introduction - -A Java client for the Portal WebSocket Server, providing Nostr-based authentication and Lightning Network payment processing capabilities. - ---- +Java 17 SDK for the [Portal REST API](https://github.com/PortalTechnologiesInc/lib). ## Installation -1. Add the Jitpack repository to your `build.gradle`: - ```groovy - repositories { - maven { url 'https://jitpack.io' } - } - ``` - - Or if you are using Maven, add the following to your `pom.xml`: - ```xml - - jitpack.io - https://jitpack.io - - ``` - -2. Add the dependency to your `build.gradle`: - ```groovy - dependencies { - implementation 'com.github.PortalTechnologiesInc:java-sdk:0.3.0' - } - ``` - - Or if you are using Maven, add the following to your `pom.xml`: - ```xml - - com.github.PortalTechnologiesInc - java-sdk - 0.3.0 - - ``` - -3. Once you're done, you may now proceed integrating the SDK into your project. - ---- - -## Versioning & Compatibility - -The Java SDK version is kept in sync with the [Portal SDK Daemon](https://hub.docker.com/r/getportal/sdk-daemon) (`getportal/sdk-daemon`). - -**Compatibility rule:** the `major.minor` version of the SDK must match the `major.minor` version of the SDK Daemon. The patch version (`x` in `0.3.x`) is independent and can differ — it only contains bug fixes. - -| SDK version | SDK Daemon version | -|-------------|-------------------| -| `0.3.x` | `0.3.x` | - -**Example:** SDK `0.3.0` works with `getportal/sdk-daemon:0.3.1`, but not with `getportal/sdk-daemon:0.4.0`. - -When upgrading to a new `major.minor`, update both the SDK dependency and the Docker image tag together. +```kotlin +// settings.gradle.kts +dependencyResolutionManagement { + repositories { maven { url = uri("https://jitpack.io") } } +} - ---- - -## Basic Usage - -### Initialization - -Create an instance of `PortalSDK` by passing the websocket endpoint of your portal server: - -```java -var portalSDK = new PortalSDK(wsEndpoint); +// build.gradle.kts +dependencies { + implementation("com.github.PortalTechnologiesInc:java-sdk:0.4.0") +} ``` -### Connecting to the server +## Setup -Establish the WebSocket connection, then authenticate with your token: +Choose how you want to receive async results: ```java -portalSDK.connect(); -portalSDK.authenticate(authToken); +// Manual polling — you call pollUntilComplete() yourself, no background threads +PortalClient client = new PortalClient( + PortalClientConfig.create("http://localhost:3000", "token") +); + +// Auto-polling — background scheduler, just use done() +PortalClient client = new PortalClient( + PortalClientConfig.create("http://localhost:3000", "token") + .autoPolling(500) // poll every 500ms +); + +// Webhooks — portal-rest POSTs to your server, just use done() +PortalClient client = new PortalClient( + PortalClientConfig.create("http://localhost:3000", "token") + .webhookSecret("my-secret") +); ``` +## Async operations -### Sending a command +All async methods return `AsyncOperation` with `streamId` (available immediately) +and `done` (`CompletableFuture` that resolves when the operation completes). -You can send a command to the server by calling the `sendCommand` method. +### Manual polling ```java -portalSDK.sendCommand(request, (response, err) -> { - if(err != null) { - logger.error("error sending command: {}", err); - return; - } - logger.info("command sent successfully: {}", response); -}); +AsyncOperation op = client.requestSinglePayment( + mainKey, List.of(), + new SinglePaymentRequestContent("Coffee", 1000, Currency.MILLISATS, null, null, null) +); + +// blocks until paid/rejected/timeout +InvoiceStatus result = client.pollUntilComplete(op, PollOptions.defaults().timeoutMs(60_000)); +System.out.println(result.status); // "paid", "timeout", "user_rejected", ... ``` -### Basic example +### Auto-polling ```java -portalSDK.sendCommand(new CalculateNextOccurrenceRequest("weekly", System.currentTimeMillis() / 1000), (res, err) -> { - if(err != null) { - logger.error("error calculating next occurrence: {}", err); - return; - } - logger.info("next occurrence: {}", res.next_occurrence()); -}); +// client configured with .autoPolling(500) +AsyncOperation op = client.requestSinglePayment(...); +op.done().thenAccept(result -> System.out.println(result.status)); ``` ---- - -## Available Commands -Commands are implemented as specific request classes in [`src/main/java/cc/getportal/command/request/`](./src/main/java/cc/getportal/command/request/), and used via the `sendCommand()` method of the [`PortalSDK`](./src/main/java/cc/getportal/PortalSDK.java) class. +### Webhooks -Some key available commands include: - -- [`AuthRequest`](./src/main/java/cc/getportal/command/request/AuthRequest.java): Authenticate using a token. -- [`KeyHandshakeUrlRequest`](./src/main/java/cc/getportal/command/request/KeyHandshakeUrlRequest.java): Get handshake URL for key and relays. -- [`RequestSinglePaymentRequest`](./src/main/java/cc/getportal/command/request/RequestSinglePaymentRequest.java): Request a single payment. -- [`MintCashuRequest`](./src/main/java/cc/getportal/command/request/MintCashuRequest.java): Mint Cashu tokens. - -> See [`src/main/java/cc/getportal/command/request/`](./src/main/java/cc/getportal/command/request/) for all available commands and additional details. - -To use a command, instantiate its request class and pass it to `PortalSDK.sendCommand(...)`. The full list of commands may evolve; check the request folder for the latest options. - ---- +```java +// client configured with .webhookSecret("my-secret") +AsyncOperation op = client.requestSinglePayment(...); +op.done().thenAccept(result -> System.out.println(result.status)); -## Example Integrations +// in your HTTP server's POST /webhook handler: +client.deliverWebhookPayload(rawBody, request.getHeader("X-Portal-Signature")); +``` -- See [portal-demo](https://github.com/PortalTechnologiesInc/portal-demo) for a Kotlin example. +## Async methods ---- +| Method | Resolves to | +|--------|-------------| +| `requestSinglePayment(mainKey, subkeys, content)` | `AsyncOperation` | +| `requestPaymentRaw(mainKey, subkeys, content)` | `AsyncOperation` | +| `requestRecurringPayment(mainKey, subkeys, content)` | `AsyncOperation` | +| `requestInvoice(recipientKey, subkeys, params)` | `AsyncOperation` | +| `requestCashu(recipientKey, subkeys, mintUrl, unit, amount)` | `AsyncOperation` | +| `authenticateKey(mainKey, subkeys)` | `AsyncOperation` | +| `newKeyHandshakeUrl(staticToken, noRequest)` | `AsyncOperation` | -## Main API +## Sync methods -- `PortalSDK` - Main client class -- `PortalRequest` - Represents a request to the server -- `PortalResponse` - Represents a response from the server -- `PortalNotification` - Represents a notification from the server +`health()`, `version()`, `info()`, `fetchProfile()`, `payInvoice()`, +`closeRecurringPayment()`, `issueJwt()`, `verifyJwt()`, `addRelay()`, `removeRelay()`, +`mintCashu()`, `burnCashu()`, `sendCashuDirect()`, `calculateNextOccurrence()`, +`fetchNip05Profile()`, `getWalletInfo()` ---- +## Versioning -## Support +Java SDK `major.minor` must match the portal-rest (sdk-daemon) version. -For questions or issues, see the official documentation or open an issue on the project's GitHub repository. \ No newline at end of file +| Java SDK | sdk-daemon | +|----------|------------| +| 0.4.x | 0.4.x | +| 0.3.x | 0.3.x | diff --git a/build.gradle.kts b/build.gradle.kts index 4ac816e..b98dd95 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -4,7 +4,7 @@ plugins { } group = "cc.getportal" -version = "0.3.0" +version = "0.4.0" java { toolchain { @@ -23,7 +23,7 @@ publishing { groupId = "cc.getportal" artifactId = "portal-java-sdk" - version = "0.3.0" + version = "0.4.0" } } } @@ -40,9 +40,6 @@ dependencies { implementation("org.slf4j:slf4j-api:2.0.17") runtimeOnly("org.slf4j:slf4j-simple:2.0.17") - // WebSocket Client - implementation("org.java-websocket:Java-WebSocket:1.6.0") - // Json serialization implementation("com.google.code.gson:gson:2.13.2") diff --git a/src/main/java/cc/getportal/AsyncOperation.java b/src/main/java/cc/getportal/AsyncOperation.java new file mode 100644 index 0000000..78c7d4c --- /dev/null +++ b/src/main/java/cc/getportal/AsyncOperation.java @@ -0,0 +1,9 @@ +package cc.getportal; + +import java.util.concurrent.CompletableFuture; + +/** + * Wraps an async operation: the stream ID is available immediately, + * while the {@code done} future resolves when a terminal event arrives. + */ +public record AsyncOperation(String streamId, CompletableFuture done) {} diff --git a/src/main/java/cc/getportal/PollOptions.java b/src/main/java/cc/getportal/PollOptions.java new file mode 100644 index 0000000..ef89c1e --- /dev/null +++ b/src/main/java/cc/getportal/PollOptions.java @@ -0,0 +1,33 @@ +package cc.getportal; + +import org.jetbrains.annotations.Nullable; + +import java.util.function.Consumer; + +/** + * Builder-style options for polling a stream until completion. + */ +public class PollOptions { + public long intervalMs = 1000; + public long timeoutMs = 0; // 0 = no timeout + @Nullable public Consumer onEvent; + + public static PollOptions defaults() { + return new PollOptions(); + } + + public PollOptions intervalMs(long ms) { + this.intervalMs = ms; + return this; + } + + public PollOptions timeoutMs(long ms) { + this.timeoutMs = ms; + return this; + } + + public PollOptions onEvent(Consumer cb) { + this.onEvent = cb; + return this; + } +} diff --git a/src/main/java/cc/getportal/PortalClient.java b/src/main/java/cc/getportal/PortalClient.java new file mode 100644 index 0000000..414ae6c --- /dev/null +++ b/src/main/java/cc/getportal/PortalClient.java @@ -0,0 +1,626 @@ +package cc.getportal; + +import cc.getportal.model.*; +import com.google.gson.*; +import com.google.gson.reflect.TypeToken; +import org.jetbrains.annotations.Nullable; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.crypto.Mac; +import javax.crypto.spec.SecretKeySpec; +import java.io.IOException; +import java.lang.reflect.Type; +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.nio.charset.StandardCharsets; +import java.security.InvalidKeyException; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.time.Duration; +import java.util.HexFormat; +import java.util.List; +import java.util.Map; +import java.util.concurrent.*; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.Consumer; +import java.util.function.Function; + +/** + * Portal REST API client (Java 17+). + * + *

Configure via {@link PortalClientConfig}: + *

    + *
  • Manual polling — call {@link #pollUntilComplete} yourself
  • + *
  • Auto-polling — background scheduler resolves {@code done()} automatically
  • + *
  • Webhooks — call {@link #deliverWebhookPayload} from your HTTP server
  • + *
+ * + *

All three modes use the same {@code AsyncOperation} return type — + * only how terminal events are delivered differs. + */ +public class PortalClient implements AutoCloseable { + + private static final Logger log = LoggerFactory.getLogger(PortalClient.class); + + private final PortalClientConfig config; + private final HttpClient http; + private final Gson gson; + + /** Pending streams: future resolves with terminal raw JSON, mapped to T by the registered mapper. */ + private final ConcurrentHashMap pending = new ConcurrentHashMap<>(); + + @Nullable private ScheduledExecutorService pollingScheduler; + + // ------------------------------------------------------------------------- + // Construction + // ------------------------------------------------------------------------- + + public PortalClient(PortalClientConfig config) { + this.config = config; + this.http = HttpClient.newBuilder() + .connectTimeout(Duration.ofSeconds(10)) + .build(); + this.gson = new GsonBuilder() + .registerTypeAdapter(Currency.class, new CurrencySerializer()) + .registerTypeAdapter(Currency.class, new CurrencyDeserializer()) + .registerTypeAdapterFactory(new RawJsonTypeAdapterFactory()) + .create(); + + if (config.isAutoPollingEnabled()) { + startPollingScheduler(config.autoPollingIntervalMs); + } + } + + /** Shut down the auto-polling scheduler (if started). */ + @Override + public void close() { + if (pollingScheduler != null) { + pollingScheduler.shutdown(); + } + } + + // ------------------------------------------------------------------------- + // Auto-polling scheduler + // ------------------------------------------------------------------------- + + private void startPollingScheduler(long intervalMs) { + pollingScheduler = Executors.newSingleThreadScheduledExecutor(r -> { + Thread t = new Thread(r, "portal-poller"); + t.setDaemon(true); + return t; + }); + pollingScheduler.scheduleAtFixedRate(() -> { + for (Map.Entry entry : pending.entrySet()) { + String streamId = entry.getKey(); + PendingStream ps = entry.getValue(); + try { + int last = ps.lastIndex().get(); + EventsResponse resp = getEvents(streamId, last < 0 ? null : last); + if (resp == null || resp.events == null) continue; + for (StreamEvent event : resp.events) { + ps.lastIndex().set(event.index); + notifyListeners(ps, event); + if (event.isTerminal()) { + pending.remove(streamId); + ps.future().complete(event._raw != null ? event._raw : new JsonObject()); + break; + } + } + } catch (Exception e) { + log.warn("Auto-poll error for stream {}: {}", streamId, e.getMessage()); + } + } + }, 0, intervalMs, TimeUnit.MILLISECONDS); + log.debug("Auto-polling started (interval={}ms)", intervalMs); + } + + // ------------------------------------------------------------------------- + // Internal HTTP helpers + // ------------------------------------------------------------------------- + + private static class ApiResponse { + boolean success; + JsonElement data; + String error; + } + + private T request(String method, String path, @Nullable Object body, Type responseType) + throws IOException, InterruptedException, PortalSDKException { + String url = config.baseUrl.replaceAll("/+$", "") + path; + log.debug("{} {}", method, url); + + HttpRequest.Builder builder = HttpRequest.newBuilder(URI.create(url)) + .timeout(Duration.ofSeconds(30)); + + if (config.authToken != null) { + builder.header("Authorization", "Bearer " + config.authToken); + } + + String jsonBody = (body != null) ? gson.toJson(body) : "{}"; + switch (method) { + case "GET" -> builder.GET(); + case "DELETE" -> builder.method("DELETE", HttpRequest.BodyPublishers.ofString(jsonBody)); + default -> { + builder.header("Content-Type", "application/json"); + builder.method(method, HttpRequest.BodyPublishers.ofString(jsonBody)); + } + } + + HttpResponse resp = http.send(builder.build(), HttpResponse.BodyHandlers.ofString()); + String ct = resp.headers().firstValue("content-type").orElse(""); + + if (!ct.contains("application/json")) { + if (resp.statusCode() >= 400) throw new PortalSDKException("HTTP " + resp.statusCode() + ": " + resp.body()); + if (responseType == String.class) //noinspection unchecked + return (T) resp.body(); + return null; + } + + ApiResponse wrapper = gson.fromJson(resp.body(), ApiResponse.class); + if (!wrapper.success) { + throw new PortalSDKException(wrapper.error != null ? wrapper.error : "API error (HTTP " + resp.statusCode() + ")"); + } + if (wrapper.data == null || responseType == Void.class) return null; + //noinspection unchecked + return (T) gson.fromJson(wrapper.data, responseType); + } + + private T get(String path, Type type) throws IOException, InterruptedException, PortalSDKException { + return request("GET", path, null, type); + } + + private T post(String path, @Nullable Object body, Type type) throws IOException, InterruptedException, PortalSDKException { + return request("POST", path, body, type); + } + + private T delete(String path, Object body, Type type) throws IOException, InterruptedException, PortalSDKException { + return request("DELETE", path, body, type); + } + + // ------------------------------------------------------------------------- + // Pending stream management + // ------------------------------------------------------------------------- + + private record PendingStream( + CompletableFuture future, + CopyOnWriteArrayList> listeners, + AtomicInteger lastIndex + ) {} + + private AsyncOperation registerStream(String streamId, Function mapper) { + CompletableFuture raw = new CompletableFuture<>(); + pending.put(streamId, new PendingStream(raw, new CopyOnWriteArrayList<>(), new AtomicInteger(-1))); + return new AsyncOperation<>(streamId, raw.thenApply(mapper)); + } + + private void notifyListeners(PendingStream ps, StreamEvent event) { + for (Consumer l : ps.listeners()) { + try { l.accept(event); } catch (Exception e) { log.warn("Listener error", e); } + } + } + + private void deliverEvent(String streamId, StreamEvent event) { + PendingStream ps = pending.get(streamId); + if (ps == null) { + log.debug("No pending stream for {}, ignoring event type={}", streamId, event.type); + return; + } + notifyListeners(ps, event); + if (event.isTerminal()) { + pending.remove(streamId); + ps.future().complete(event._raw != null ? event._raw : new JsonObject()); + } + } + + /** + * Subscribe to intermediate events for a stream (non-terminal included). + * + * @return unsubscribe handle + */ + public Runnable onEvent(String streamId, Consumer callback) { + PendingStream ps = pending.get(streamId); + if (ps == null) return () -> {}; + ps.listeners().add(callback); + return () -> ps.listeners().remove(callback); + } + + /** Cancel a pending stream. */ + public void cancel(String streamId) { + PendingStream ps = pending.remove(streamId); + if (ps != null) ps.future().completeExceptionally(new PortalSDKException("Stream cancelled: " + streamId)); + } + + // ------------------------------------------------------------------------- + // Health / Version / Info + // ------------------------------------------------------------------------- + + public String health() throws IOException, InterruptedException, PortalSDKException { + return get("/health", String.class); + } + + public VersionResponse version() throws IOException, InterruptedException, PortalSDKException { + return get("/version", VersionResponse.class); + } + + public InfoResponse info() throws IOException, InterruptedException, PortalSDKException { + return get("/info", InfoResponse.class); + } + + /** + * Get the content for {@code /.well-known/nostr.json} (NIP-05 verification). + * Maps names to public keys and optionally to relay lists. + * No authentication required. + */ + public Nip05WellKnownResponse wellKnownNostrJson() + throws IOException, InterruptedException, PortalSDKException { + return get("/well-known/nostr.json", Nip05WellKnownResponse.class); + } + + // ------------------------------------------------------------------------- + // Key Handshake + // ------------------------------------------------------------------------- + + public AsyncOperation newKeyHandshakeUrl( + @Nullable String staticToken, boolean noRequest + ) throws IOException, InterruptedException, PortalSDKException { + Map body = new java.util.HashMap<>(); + body.put("static_token", staticToken); + body.put("no_request", noRequest); + JsonObject resp = post("/key-handshake", body, JsonObject.class); + String streamId = resp.get("stream_id").getAsString(); + return registerStream(streamId, json -> gson.fromJson(json, KeyHandshakeResult.class)); + } + + // ------------------------------------------------------------------------- + // Authenticate Key + // ------------------------------------------------------------------------- + + public AsyncOperation authenticateKey( + String mainKey, List subkeys + ) throws IOException, InterruptedException, PortalSDKException { + Map body = Map.of("main_key", mainKey, "subkeys", subkeys); + JsonObject resp = post("/authenticate-key", body, JsonObject.class); + String streamId = resp.get("stream_id").getAsString(); + return registerStream(streamId, json -> gson.fromJson(json, AuthResponseData.class)); + } + + // ------------------------------------------------------------------------- + // Payments + // ------------------------------------------------------------------------- + + public AsyncOperation requestSinglePayment( + String mainKey, List subkeys, SinglePaymentRequestContent content + ) throws IOException, InterruptedException, PortalSDKException { + Map body = new java.util.HashMap<>(); + body.put("main_key", mainKey); + body.put("subkeys", subkeys); + body.put("payment_request", gson.toJsonTree(content)); + JsonObject resp = post("/payments/single", body, JsonObject.class); + String streamId = resp.get("stream_id").getAsString(); + return registerStream(streamId, + json -> gson.fromJson(json.getAsJsonObject("status"), InvoiceStatus.class)); + } + + public AsyncOperation requestPaymentRaw( + String mainKey, List subkeys, InvoiceRequestContent content + ) throws IOException, InterruptedException, PortalSDKException { + Map body = new java.util.HashMap<>(); + body.put("main_key", mainKey); + body.put("subkeys", subkeys); + body.put("payment_request", gson.toJsonTree(content)); + JsonObject resp = post("/payments/raw", body, JsonObject.class); + String streamId = resp.get("stream_id").getAsString(); + return registerStream(streamId, + json -> gson.fromJson(json.getAsJsonObject("status"), InvoiceStatus.class)); + } + + public AsyncOperation requestRecurringPayment( + String mainKey, List subkeys, RecurringPaymentRequestContent content + ) throws IOException, InterruptedException, PortalSDKException { + Map body = new java.util.HashMap<>(); + body.put("main_key", mainKey); + body.put("subkeys", subkeys); + body.put("payment_request", gson.toJsonTree(content)); + JsonObject resp = post("/payments/recurring", body, JsonObject.class); + String streamId = resp.get("stream_id").getAsString(); + return registerStream(streamId, + json -> gson.fromJson(json.getAsJsonObject("status"), RecurringPaymentResponseContent.class)); + } + + public String closeRecurringPayment( + String mainKey, List subkeys, String subscriptionId + ) throws IOException, InterruptedException, PortalSDKException { + Map body = Map.of( + "main_key", mainKey, "subkeys", subkeys, "subscription_id", subscriptionId); + JsonObject resp = post("/payments/recurring/close", body, JsonObject.class); + return resp.get("message").getAsString(); + } + + // ------------------------------------------------------------------------- + // Profile + // ------------------------------------------------------------------------- + + public @Nullable Profile fetchProfile(String mainKey) + throws IOException, InterruptedException, PortalSDKException { + String path = "/profile/" + java.net.URLEncoder.encode(mainKey, StandardCharsets.UTF_8); + JsonObject resp = get(path, JsonObject.class); + JsonElement profileEl = resp.get("profile"); + if (profileEl == null || profileEl.isJsonNull()) return null; + return gson.fromJson(profileEl, Profile.class); + } + + // ------------------------------------------------------------------------- + // Invoices + // ------------------------------------------------------------------------- + + public AsyncOperation requestInvoice( + String recipientKey, List subkeys, RequestInvoiceParams params + ) throws IOException, InterruptedException, PortalSDKException { + Map body = new java.util.HashMap<>(); + body.put("recipient_key", recipientKey); + body.put("subkeys", subkeys); + body.put("content", gson.toJsonTree(params)); + JsonObject resp = post("/invoices/request", body, JsonObject.class); + String streamId = resp.get("stream_id").getAsString(); + return registerStream(streamId, json -> gson.fromJson(json, InvoicePaymentResponse.class)); + } + + public PayInvoiceResponse payInvoice(String invoice) + throws IOException, InterruptedException, PortalSDKException { + return post("/invoices/pay", Map.of("invoice", invoice), PayInvoiceResponse.class); + } + + // ------------------------------------------------------------------------- + // JWT + // ------------------------------------------------------------------------- + + public String issueJwt(String targetKey, int durationHours) + throws IOException, InterruptedException, PortalSDKException { + JsonObject resp = post("/jwt/issue", + Map.of("target_key", targetKey, "duration_hours", durationHours), JsonObject.class); + return resp.get("token").getAsString(); + } + + public String verifyJwt(String pubkey, String token) + throws IOException, InterruptedException, PortalSDKException { + JsonObject resp = post("/jwt/verify", Map.of("pubkey", pubkey, "token", token), JsonObject.class); + return resp.get("target_key").getAsString(); + } + + // ------------------------------------------------------------------------- + // Cashu + // ------------------------------------------------------------------------- + + public AsyncOperation requestCashu( + String recipientKey, List subkeys, + String mintUrl, String unit, long amount + ) throws IOException, InterruptedException, PortalSDKException { + Map body = Map.of( + "recipient_key", recipientKey, "subkeys", subkeys, + "mint_url", mintUrl, "unit", unit, "amount", amount); + JsonObject resp = post("/cashu/request", body, JsonObject.class); + String streamId = resp.get("stream_id").getAsString(); + return registerStream(streamId, + json -> gson.fromJson(json.getAsJsonObject("status"), CashuResponseStatus.class)); + } + + public String sendCashuDirect(String mainKey, List subkeys, String token) + throws IOException, InterruptedException, PortalSDKException { + JsonObject resp = post("/cashu/send-direct", + Map.of("main_key", mainKey, "subkeys", subkeys, "token", token), JsonObject.class); + return resp.get("message").getAsString(); + } + + public String mintCashu( + String mintUrl, String unit, long amount, + @Nullable String staticAuthToken, @Nullable String description + ) throws IOException, InterruptedException, PortalSDKException { + Map body = new java.util.HashMap<>(); + body.put("mint_url", mintUrl); + body.put("unit", unit); + body.put("amount", amount); + if (staticAuthToken != null) body.put("static_auth_token", staticAuthToken); + if (description != null) body.put("description", description); + JsonObject resp = post("/cashu/mint", body, JsonObject.class); + return resp.get("token").getAsString(); + } + + public long burnCashu( + String mintUrl, String unit, String token, @Nullable String staticAuthToken + ) throws IOException, InterruptedException, PortalSDKException { + Map body = new java.util.HashMap<>(); + body.put("mint_url", mintUrl); + body.put("unit", unit); + body.put("token", token); + if (staticAuthToken != null) body.put("static_auth_token", staticAuthToken); + JsonObject resp = post("/cashu/burn", body, JsonObject.class); + return resp.get("amount").getAsLong(); + } + + // ------------------------------------------------------------------------- + // Relays + // ------------------------------------------------------------------------- + + public String addRelay(String relay) throws IOException, InterruptedException, PortalSDKException { + JsonObject resp = post("/relays", Map.of("relay", relay), JsonObject.class); + return resp.get("relay").getAsString(); + } + + public String removeRelay(String relay) throws IOException, InterruptedException, PortalSDKException { + JsonObject resp = delete("/relays", Map.of("relay", relay), JsonObject.class); + return resp.get("relay").getAsString(); + } + + // ------------------------------------------------------------------------- + // Calendar + // ------------------------------------------------------------------------- + + public @Nullable Long calculateNextOccurrence(String calendar, long from) + throws IOException, InterruptedException, PortalSDKException { + JsonObject resp = post("/calendar/next-occurrence", + Map.of("calendar", calendar, "from", from), JsonObject.class); + JsonElement next = resp.get("next_occurrence"); + if (next == null || next.isJsonNull()) return null; + return next.getAsLong(); + } + + // ------------------------------------------------------------------------- + // NIP-05 + // ------------------------------------------------------------------------- + + public Nip05Profile fetchNip05Profile(String nip05) + throws IOException, InterruptedException, PortalSDKException { + String path = "/nip05/" + java.net.URLEncoder.encode(nip05, StandardCharsets.UTF_8); + JsonObject resp = get(path, JsonObject.class); + return gson.fromJson(resp.get("profile"), Nip05Profile.class); + } + + // ------------------------------------------------------------------------- + // Wallet + // ------------------------------------------------------------------------- + + public WalletInfoResponse getWalletInfo() throws IOException, InterruptedException, PortalSDKException { + return get("/wallet/info", WalletInfoResponse.class); + } + + // ------------------------------------------------------------------------- + // Events (low-level) + // ------------------------------------------------------------------------- + + public EventsResponse getEvents(String streamId, @Nullable Integer after) + throws IOException, InterruptedException, PortalSDKException { + String path = "/events/" + java.net.URLEncoder.encode(streamId, StandardCharsets.UTF_8) + + (after != null ? "?after=" + after : ""); + return get(path, EventsResponse.class); + } + + // ------------------------------------------------------------------------- + // Manual polling + // ------------------------------------------------------------------------- + + /** + * Manually poll until the operation resolves. Blocks the calling thread. + *

+ * Use this when auto-polling is not enabled and you want synchronous-style code. + * If auto-polling is active, you don't need to call this — use {@code op.done()} instead. + * + * @param operation the async operation to wait for + * @param options polling interval, timeout, and per-event callback + */ + public T pollUntilComplete(AsyncOperation operation, PollOptions options) + throws PortalSDKException, IOException, InterruptedException { + String streamId = operation.streamId(); + long startMs = System.currentTimeMillis(); + Integer lastIndex = null; + + while (true) { + if (options.timeoutMs > 0 && System.currentTimeMillis() - startMs > options.timeoutMs) { + throw new PortalSDKException("Polling timed out for stream: " + streamId); + } + + EventsResponse response = getEvents(streamId, lastIndex); + if (response != null && response.events != null) { + for (StreamEvent event : response.events) { + lastIndex = event.index; + if (options.onEvent != null) options.onEvent.accept(event); + deliverEvent(streamId, event); + if (event.isTerminal()) { + try { + return operation.done().get(); + } catch (ExecutionException | InterruptedException e) { + throw new PortalSDKException("Stream failed: " + e.getCause().getMessage()); + } + } + } + } + Thread.sleep(options.intervalMs); + } + } + + // ------------------------------------------------------------------------- + // Webhook delivery + // ------------------------------------------------------------------------- + + /** + * Verify the {@code X-Portal-Signature} header and deliver the webhook payload. + * Requires {@link PortalClientConfig#webhookSecret(String)} to be set. + * + * @param rawBody raw request body bytes — do NOT parse before calling this + * @param signature value of the {@code X-Portal-Signature} header + */ + public void deliverWebhookPayload(byte[] rawBody, String signature) throws PortalSDKException { + if (config.webhookSecret == null) throw new PortalSDKException("No webhookSecret configured"); + try { + Mac mac = Mac.getInstance("HmacSHA256"); + mac.init(new SecretKeySpec(config.webhookSecret.getBytes(StandardCharsets.UTF_8), "HmacSHA256")); + byte[] computed = mac.doFinal(rawBody); + String hex = HexFormat.of().formatHex(computed); + if (!MessageDigest.isEqual( + hex.getBytes(StandardCharsets.UTF_8), + signature.getBytes(StandardCharsets.UTF_8))) { + throw new PortalSDKException("Invalid webhook signature"); + } + } catch (NoSuchAlgorithmException | InvalidKeyException e) { + throw new PortalSDKException("HMAC error: " + e.getMessage()); + } + WebhookPayload payload = gson.fromJson(new String(rawBody, StandardCharsets.UTF_8), WebhookPayload.class); + deliverWebhookPayload(payload); + } + + /** Deliver an already-parsed webhook payload (skips signature verification). */ + public void deliverWebhookPayload(WebhookPayload payload) { + log.debug("webhook received streamId={} type={}", payload.streamId, payload.type); + StreamEvent thin = new StreamEvent(); + thin.type = payload.type; + thin.index = payload.index; + thin.timestamp = payload.timestamp; + thin._raw = payload._raw; + deliverEvent(payload.streamId, thin); + } + + // ------------------------------------------------------------------------- + // Helpers + // ------------------------------------------------------------------------- + + @SuppressWarnings("unchecked") + private Map toMap(Object obj) { + JsonObject json = gson.toJsonTree(obj).getAsJsonObject(); + return (Map) gson.fromJson(json, Map.class); + } + + // ------------------------------------------------------------------------- + // TypeAdapterFactory — populates _raw on StreamEvent and WebhookPayload + // ------------------------------------------------------------------------- + + private static class RawJsonTypeAdapterFactory implements TypeAdapterFactory { + @Override + public TypeAdapter create(Gson gson, TypeToken type) { + Class raw = type.getRawType(); + boolean isStreamEvent = StreamEvent.class.isAssignableFrom(raw); + boolean isWebhookPayload = WebhookPayload.class == raw; + if (!isStreamEvent && !isWebhookPayload) return null; + + TypeAdapter delegate = gson.getDelegateAdapter(this, type); + TypeAdapter jsonObjAdapter = gson.getAdapter(JsonObject.class); + + return new TypeAdapter<>() { + @Override + public void write(com.google.gson.stream.JsonWriter out, T value) throws java.io.IOException { + delegate.write(out, value); + } + + @Override + public T read(com.google.gson.stream.JsonReader in) throws java.io.IOException { + JsonObject jo = jsonObjAdapter.read(in); + T result = delegate.fromJsonTree(jo); + if (result instanceof StreamEvent se) se._raw = jo; + else if (result instanceof WebhookPayload wp) wp._raw = jo; + return result; + } + }; + } + } +} diff --git a/src/main/java/cc/getportal/PortalClientConfig.java b/src/main/java/cc/getportal/PortalClientConfig.java new file mode 100644 index 0000000..2c1fb1c --- /dev/null +++ b/src/main/java/cc/getportal/PortalClientConfig.java @@ -0,0 +1,59 @@ +package cc.getportal; + +import org.jetbrains.annotations.Nullable; + +/** + * Configuration for {@link PortalClient}. + * + *

{@code
+ * // Manual polling (default — no background threads)
+ * PortalClientConfig.create("http://localhost:3000", "token")
+ *
+ * // Auto-polling (background scheduler)
+ * PortalClientConfig.create("http://localhost:3000", "token").autoPolling(500)
+ *
+ * // Webhooks
+ * PortalClientConfig.create("http://localhost:3000", "token").webhookSecret("secret")
+ * }
+ */ +public class PortalClientConfig { + + public final String baseUrl; + public final String authToken; + + long autoPollingIntervalMs = 0; // 0 = disabled + @Nullable String webhookSecret = null; + + private PortalClientConfig(String baseUrl, String authToken) { + this.baseUrl = baseUrl; + this.authToken = authToken; + } + + /** Create a base config. No background threads are started by default. */ + public static PortalClientConfig create(String baseUrl, String authToken) { + return new PortalClientConfig(baseUrl, authToken); + } + + /** + * Enable automatic background polling. The client will start a scheduler + * that polls all active streams every {@code intervalMs} milliseconds, + * resolving their {@code done} futures automatically. + */ + public PortalClientConfig autoPolling(long intervalMs) { + this.autoPollingIntervalMs = intervalMs; + return this; + } + + /** + * Enable webhook signature verification. When set, {@code deliverWebhookPayload(rawBody, signature)} + * will verify the {@code X-Portal-Signature} HMAC-SHA256 header before delivering. + */ + public PortalClientConfig webhookSecret(String secret) { + this.webhookSecret = secret; + return this; + } + + public boolean isAutoPollingEnabled() { + return autoPollingIntervalMs > 0; + } +} diff --git a/src/main/java/cc/getportal/PortalSDK.java b/src/main/java/cc/getportal/PortalSDK.java deleted file mode 100644 index 5c0fdbc..0000000 --- a/src/main/java/cc/getportal/PortalSDK.java +++ /dev/null @@ -1,177 +0,0 @@ -package cc.getportal; - -import cc.getportal.command.PortalNotification; -import cc.getportal.command.PortalRequest; -import cc.getportal.command.PortalResponse; -import cc.getportal.command.request.AuthRequest; -import cc.getportal.model.Currency; -import com.google.gson.Gson; -import com.google.gson.GsonBuilder; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.net.URI; -import java.util.UUID; -import java.util.concurrent.ConcurrentHashMap; -import java.util.function.BiConsumer; -import java.util.function.Consumer; - -public class PortalSDK { - - private static final Logger logger = LoggerFactory.getLogger(PortalSDK.class); - - private final ConcurrentHashMap> commands = new ConcurrentHashMap<>(); - private final ConcurrentHashMap> activeStreams = new ConcurrentHashMap<>(); - - private final Gson gson; - private final String wsEndpoint; - Runnable onClose; - - String authToken = ""; - private boolean connected = false; - private PortalWsClient wsClient; - - public PortalSDK(@NotNull String wsEndpoint) { - this.gson = new GsonBuilder() - .registerTypeAdapter(Response.class, new ResponseDeserializer()) - .registerTypeAdapter(Currency.class, new CurrencySerializer()) - .registerTypeAdapter(Currency.class, new CurrencyDeserializer()) - .create(); - this.wsEndpoint = wsEndpoint; - } - - public PortalSDK onClose(@NotNull Runnable onClose) { - this.onClose = onClose; - return this; - } - - public void connect() throws InterruptedException { - wsClient = new PortalWsClient(URI.create(this.wsEndpoint), this); - wsClient.connectBlocking(); - } - - public void authenticate(@NotNull String authToken) { - this.authToken = authToken; - internalSendCommand(new AuthRequest(this.authToken), (res, err) -> { - if(err != null) { - logger.error("error auth request: {}", err); - return; - } - logger.info("Authenticated: {}", res.message()); - }); - } - - public void disconnect() throws InterruptedException { - connected = false; - - wsClient.close(); - - authToken = ""; - commands.clear(); - activeStreams.clear(); - } - - public , E extends PortalResponse, N extends PortalNotification> void sendCommand(@NotNull T req, @NotNull BiConsumer fun) { - if (!connected) { - throw new PortalSDKException("not connected. Use PortalSDK#connect() before."); - } - internalSendCommand(req,fun); - } - - , E extends PortalResponse, N extends PortalNotification> void internalSendCommand(@NotNull T req, @NotNull BiConsumer fun) { - var id = generateId(); - var p = req.isUnit() ? null : this.gson.toJson(req); - var command = String.format(""" - { - "id": "%s", - "cmd": "%s", - "params": %s - } - - """, id, req.name(), p); - logger.debug("Sending command {}: {}", req.name(), command); - - RegisteredNotification registeredNotification = null; - if (req.notificationType() != null) { - registeredNotification = new RegisteredNotification<>(req.notificationType(), req.notificationFun()); - } - this.commands.put(id, new RegisteredCommand<>(req.name(), req.responseType(), fun, registeredNotification)); - wsClient.send(command); - } - - private String generateId() { - return UUID.randomUUID().toString(); - } - - // Internal methods - - void setConnected(boolean connected) { - this.connected = connected; - } - - void callFun(@NotNull String msg) { - Response message = gson.fromJson(msg, Response.class); - if (message.isSuccess()) { - Response.Success success = message.success(); - RegisteredCommand registeredCommand = this.commands.get(success.id); - if (registeredCommand == null) { - logger.warn("Ignoring success for unknown command id: {}", success.id); - return; - } - - PortalResponse portalResponse = (PortalResponse) gson.fromJson(success.jsonElement, registeredCommand.responseType); - registeredCommand.fun.accept(portalResponse, null); - - // check if stream_id is present - - String streamId = success.streamId; - if(streamId != null) { - logger.debug("stream id present {}", streamId); - - var registeredNotification = registeredCommand.registeredNotification; - if(registeredNotification == null) { - logger.error("Losing stream id {} because no registered notification", streamId); - } else { - this.activeStreams.put(streamId, registeredNotification); - } - } - } - - if(message.isNotification()) { - Response.Notification notification = message.notification(); - - RegisteredNotification registeredNotification = this.activeStreams.get(notification.id); - if (registeredNotification == null) { - logger.warn("Ignoring notification for unknown stream id: {}", notification.id); - return; - } - - PortalNotification portalNotification = (PortalNotification) gson.fromJson(notification.jsonElement, registeredNotification.notificationType); - registeredNotification.fun.accept(portalNotification); - - - // remove from active streams map - if(portalNotification.deleteStream()) { - this.activeStreams.remove(notification.id); - } - - } - - if(message.isError()) { - Response.Error error = message.error(); - - RegisteredCommand registeredCommand = this.commands.get(error.id); - if (registeredCommand == null) { - logger.warn("Ignoring error for unknown command id: {}: {}", error.id, error.message); - return; - } - registeredCommand.fun.accept(null, error.message); - } - } - - record RegisteredCommand(String cmd, Class responseType, BiConsumer fun, @Nullable RegisteredNotification registeredNotification) {} - - record RegisteredNotification(Class notificationType, Consumer fun){} -} diff --git a/src/main/java/cc/getportal/PortalWsClient.java b/src/main/java/cc/getportal/PortalWsClient.java deleted file mode 100644 index a8660e1..0000000 --- a/src/main/java/cc/getportal/PortalWsClient.java +++ /dev/null @@ -1,70 +0,0 @@ -package cc.getportal; - -import java.net.URI; -import java.nio.ByteBuffer; - -import org.java_websocket.client.WebSocketClient; -import org.java_websocket.drafts.Draft; -import org.java_websocket.handshake.ServerHandshake; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -class PortalWsClient extends WebSocketClient { - - private static final Logger logger = LoggerFactory.getLogger(PortalWsClient.class); - - private final PortalSDK client; - - public PortalWsClient(URI serverUri, Draft draft, PortalSDK client) { - super(serverUri, draft); - this.client = client; - } - - public PortalWsClient(URI serverURI, PortalSDK client) { - super(serverURI); - this.client = client; - } - - @Override - public void onOpen(ServerHandshake handshakedata) { - logger.debug("new connection opened"); - client.setConnected(true); - } - - @Override - public void onClose(int code, String reason, boolean remote) { - logger.warn("closed with exit code '{}' additional info: '{}'", code, reason); - - client.setConnected(false); - - // Run function on close (catch so callback cannot kill WebSocket thread) - if (client.onClose != null) { - try { - client.onClose.run(); - } catch (Exception e) { - logger.error("onClose callback threw", e); - } - } - } - - @Override - public void onMessage(String message) { - logger.debug("received message: {}", message); - try { - client.callFun(message); - } catch (Exception e) { - logger.error("Error processing message (SDK or callback threw)", e); - } - } - - @Override - public void onMessage(ByteBuffer message) { - logger.debug("received ByteBuffer"); - } - - @Override - public void onError(Exception ex) { - logger.error("an error occurred", ex); - } - -} \ No newline at end of file diff --git a/src/main/java/cc/getportal/Response.java b/src/main/java/cc/getportal/Response.java deleted file mode 100644 index 21b0458..0000000 --- a/src/main/java/cc/getportal/Response.java +++ /dev/null @@ -1,86 +0,0 @@ -package cc.getportal; - -import com.google.gson.JsonElement; -import com.google.gson.JsonObject; -import org.jetbrains.annotations.Nullable; - -import java.util.LinkedHashMap; - -sealed class Response { - final String id; - final ResponseType type; - - public enum ResponseType { - ERROR, - SUCCESS, - NOTIFICATION - } - - public Response(String id, ResponseType type) { - this.id = id; - this.type = type; - } - - public boolean isError() { - return type == ResponseType.ERROR; - } - - public boolean isSuccess() { - return type == ResponseType.SUCCESS; - } - - public boolean isNotification() { - return type == ResponseType.NOTIFICATION; - } - - - public Error error() { - return (Error) this; - } - - public Success success() { - return (Success) this; - } - - public Notification notification() { - return (Notification) this; - } - - public static final class Error extends Response { - final String message; - - public Error(String id, String message) { - super(id, ResponseType.ERROR); - this.message = message; - } - } - - public static final class Success extends Response { - final String successType; - final LinkedHashMap data; - final JsonElement jsonElement; - - // Optional stream_id field - final String streamId; - public Success(String id, String successType, LinkedHashMap data, JsonElement jsonElement, @Nullable String streamId) { - super(id, ResponseType.SUCCESS); - this.successType = successType; - this.data = data; - this.jsonElement = jsonElement; - this.streamId = streamId; - } - } - - public static final class Notification extends Response { - final String notificationType; - final LinkedHashMap data; - final JsonElement jsonElement; - - public Notification(String id, String notificationType, LinkedHashMap data, JsonElement jsonElement) { - super(id, ResponseType.NOTIFICATION); - this.notificationType = notificationType; - this.data = data; - this.jsonElement = jsonElement; - } - } -} diff --git a/src/main/java/cc/getportal/ResponseDeserializer.java b/src/main/java/cc/getportal/ResponseDeserializer.java deleted file mode 100644 index 04635f9..0000000 --- a/src/main/java/cc/getportal/ResponseDeserializer.java +++ /dev/null @@ -1,42 +0,0 @@ -package cc.getportal; - -import com.google.gson.*; -import java.lang.reflect.Type; -import java.util.HashMap; -import java.util.LinkedHashMap; -import java.util.Map; - -class ResponseDeserializer implements JsonDeserializer { - - @Override - public Response deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) - throws JsonParseException { - - JsonObject obj = json.getAsJsonObject(); - - // read "base" fields - String id = obj.has("id") ? obj.get("id").getAsString() : null; - String type = obj.has("type") ? obj.get("type").getAsString() : null; - - return switch (type) { - - case "error" -> new Response.Error(id, obj.get("message").getAsString()); - case "success" -> { - JsonElement jsonElement = obj.get("data"); - LinkedHashMap data = context.deserialize(jsonElement, LinkedHashMap.class); - String successType = (String) data.get("type"); - String streamId = (String) data.get("stream_id"); - yield new Response.Success(id, successType, data, jsonElement, streamId); - } - - case "notification" -> { - JsonElement jsonElement = obj.get("data"); - LinkedHashMap data = context.deserialize(jsonElement, LinkedHashMap.class); - String notificationType = (String) data.get("type"); - yield new Response.Notification(id, notificationType, data, jsonElement); - } - - default -> throw new IllegalStateException("Unexpected value: " + type); - }; - } -} diff --git a/src/main/java/cc/getportal/StreamEvent.java b/src/main/java/cc/getportal/StreamEvent.java new file mode 100644 index 0000000..9e1d9fc --- /dev/null +++ b/src/main/java/cc/getportal/StreamEvent.java @@ -0,0 +1,61 @@ +package cc.getportal; + +import com.google.gson.JsonObject; + +import java.util.Set; + +/** + * A single event from a portal-rest async stream ({@code GET /events/:streamId}). + *

+ * The {@code type} field identifies the event kind. Use {@link #isTerminal()} to + * detect whether no more events are expected for the stream. + * The full raw JSON is available internally for deserialization but is not part + * of the public API. + */ +public class StreamEvent { + + public String type; + public int index; + public String timestamp; + + /** Full raw JSON — package-private, used by {@link PortalClient} for typed deserialization. */ + transient JsonObject _raw; + + // ---- Terminal type detection ---- + + static final Set TERMINAL_TYPES = Set.of( + "key_handshake", + "authenticate_key", + "recurring_payment_response", + "invoice_response", + "cashu_response", + "error" + ); + + static final Set TERMINAL_PAYMENT_STATUSES = Set.of( + "paid", "timeout", "error", "user_success", "user_failed", "user_rejected" + ); + + /** + * Returns {@code true} if this event is terminal — i.e. no further events + * are expected on the stream after this one. + */ + public boolean isTerminal() { + if (type == null) return false; + if (TERMINAL_TYPES.contains(type)) return true; + if ("payment_status_update".equals(type) && _raw != null) { + try { + JsonObject status = _raw.getAsJsonObject("status"); + if (status != null && status.has("status")) { + return TERMINAL_PAYMENT_STATUSES.contains(status.get("status").getAsString()); + } + } catch (Exception ignored) { /* not terminal */ } + } + return false; + } + + @Override + public String toString() { + return "StreamEvent{type='" + type + "', index=" + index + "}"; + } +} diff --git a/src/main/java/cc/getportal/WebhookPayload.java b/src/main/java/cc/getportal/WebhookPayload.java new file mode 100644 index 0000000..3e378ae --- /dev/null +++ b/src/main/java/cc/getportal/WebhookPayload.java @@ -0,0 +1,37 @@ +package cc.getportal; + +import com.google.gson.JsonObject; +import com.google.gson.annotations.SerializedName; + +/** + * Payload of a portal-rest webhook POST. + * Contains the {@code stream_id} and all event fields. + * The full raw JSON is available internally for typed deserialization. + */ +public class WebhookPayload { + + @SerializedName("stream_id") + public String streamId; + + public String type; + public int index; + public String timestamp; + + /** Full raw JSON — package-private, used by {@link PortalClient} for typed deserialization. */ + transient JsonObject _raw; + + /** Delegates terminal detection to {@link StreamEvent} logic. */ + public boolean isTerminal() { + if (type == null) return false; + if (StreamEvent.TERMINAL_TYPES.contains(type)) return true; + if ("payment_status_update".equals(type) && _raw != null) { + try { + JsonObject status = _raw.getAsJsonObject("status"); + if (status != null && status.has("status")) { + return StreamEvent.TERMINAL_PAYMENT_STATUSES.contains(status.get("status").getAsString()); + } + } catch (Exception ignored) { /* not terminal */ } + } + return false; + } +} diff --git a/src/main/java/cc/getportal/command/PortalNotification.java b/src/main/java/cc/getportal/command/PortalNotification.java deleted file mode 100644 index a46ca0c..0000000 --- a/src/main/java/cc/getportal/command/PortalNotification.java +++ /dev/null @@ -1,8 +0,0 @@ -package cc.getportal.command; - -public interface PortalNotification { - - default boolean deleteStream() { - return false; - } -} diff --git a/src/main/java/cc/getportal/command/PortalRequest.java b/src/main/java/cc/getportal/command/PortalRequest.java deleted file mode 100644 index 4361fa1..0000000 --- a/src/main/java/cc/getportal/command/PortalRequest.java +++ /dev/null @@ -1,43 +0,0 @@ -package cc.getportal.command; - -import org.jetbrains.annotations.ApiStatus; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; - -import java.util.function.Consumer; - -public abstract class PortalRequest { - - private transient final Consumer notificationFun; - - protected PortalRequest(@NotNull Consumer notificationFun) { - this.notificationFun = notificationFun; - } - - protected PortalRequest() { - this.notificationFun = null; - } - - @ApiStatus.Internal - public Consumer notificationFun() { - return notificationFun; - } - - @ApiStatus.Internal - @Nullable - public Class notificationType(){ - return null; - } - - @ApiStatus.Internal - public abstract String name(); - - @ApiStatus.Internal - public abstract Class responseType(); - - @ApiStatus.Internal - public boolean isUnit() { - return false; - } - -} diff --git a/src/main/java/cc/getportal/command/PortalResponse.java b/src/main/java/cc/getportal/command/PortalResponse.java deleted file mode 100644 index 1f35c75..0000000 --- a/src/main/java/cc/getportal/command/PortalResponse.java +++ /dev/null @@ -1,6 +0,0 @@ -package cc.getportal.command; - -public interface PortalResponse { - - -} diff --git a/src/main/java/cc/getportal/command/notification/CloseRecurringPaymentNotification.java b/src/main/java/cc/getportal/command/notification/CloseRecurringPaymentNotification.java deleted file mode 100644 index 42ef9f8..0000000 --- a/src/main/java/cc/getportal/command/notification/CloseRecurringPaymentNotification.java +++ /dev/null @@ -1,7 +0,0 @@ -package cc.getportal.command.notification; - -import cc.getportal.command.PortalNotification; -import org.jetbrains.annotations.Nullable; - -public record CloseRecurringPaymentNotification(@Nullable String reason, String subscription_id, String main_key, String recipient) implements PortalNotification { -} diff --git a/src/main/java/cc/getportal/command/notification/KeyHandshakeUrlNotification.java b/src/main/java/cc/getportal/command/notification/KeyHandshakeUrlNotification.java deleted file mode 100644 index 3826ab7..0000000 --- a/src/main/java/cc/getportal/command/notification/KeyHandshakeUrlNotification.java +++ /dev/null @@ -1,17 +0,0 @@ -package cc.getportal.command.notification; - -import cc.getportal.command.PortalNotification; - -import java.util.List; - -public record KeyHandshakeUrlNotification(String main_key, - List preferred_relays) implements PortalNotification { - - public String getMainKey() { - return main_key; - } - - public List getPreferredRelays() { - return preferred_relays; - } -} diff --git a/src/main/java/cc/getportal/command/notification/RequestSinglePaymentNotification.java b/src/main/java/cc/getportal/command/notification/RequestSinglePaymentNotification.java deleted file mode 100644 index 005f6f5..0000000 --- a/src/main/java/cc/getportal/command/notification/RequestSinglePaymentNotification.java +++ /dev/null @@ -1,34 +0,0 @@ -package cc.getportal.command.notification; - -import cc.getportal.command.PortalNotification; -import com.google.gson.annotations.SerializedName; -import org.jetbrains.annotations.Nullable; - -public record RequestSinglePaymentNotification(InvoiceStatus status) implements PortalNotification { - - @Override - public boolean deleteStream() { - InvoiceStatusType type = status.status(); - return type == InvoiceStatusType.USER_FAILED || type == InvoiceStatusType.USER_REJECTED; - } - - public record InvoiceStatus(InvoiceStatusType status, @Nullable String preimage, @Nullable String reason) { - } - - public enum InvoiceStatusType { - @SerializedName("paid") - PAID, - @SerializedName("timeout") - TIMEOUT, - @SerializedName("error") - ERROR, - @SerializedName("user_approved") - USER_APPROVED, - @SerializedName("user_success") - USER_SUCCESS, - @SerializedName("user_failed") - USER_FAILED, - @SerializedName("user_rejected") - USER_REJECTED - } -} diff --git a/src/main/java/cc/getportal/command/notification/UnitNotification.java b/src/main/java/cc/getportal/command/notification/UnitNotification.java deleted file mode 100644 index ee21232..0000000 --- a/src/main/java/cc/getportal/command/notification/UnitNotification.java +++ /dev/null @@ -1,6 +0,0 @@ -package cc.getportal.command.notification; - -import cc.getportal.command.PortalNotification; - -public class UnitNotification implements PortalNotification { -} diff --git a/src/main/java/cc/getportal/command/request/AddRelayRequest.java b/src/main/java/cc/getportal/command/request/AddRelayRequest.java deleted file mode 100644 index 7967749..0000000 --- a/src/main/java/cc/getportal/command/request/AddRelayRequest.java +++ /dev/null @@ -1,25 +0,0 @@ -package cc.getportal.command.request; - -import cc.getportal.command.PortalRequest; -import cc.getportal.command.notification.UnitNotification; -import cc.getportal.command.response.AddRelayResponse; - -public class AddRelayRequest extends PortalRequest { - - private final String relay; - - public AddRelayRequest(String relay) { - this.relay = relay; - } - - - @Override - public String name() { - return "AddRelay"; - } - - @Override - public Class responseType() { - return AddRelayResponse.class; - } -} diff --git a/src/main/java/cc/getportal/command/request/AuthRequest.java b/src/main/java/cc/getportal/command/request/AuthRequest.java deleted file mode 100644 index 89ee219..0000000 --- a/src/main/java/cc/getportal/command/request/AuthRequest.java +++ /dev/null @@ -1,25 +0,0 @@ -package cc.getportal.command.request; - -import cc.getportal.command.PortalRequest; -import cc.getportal.command.notification.UnitNotification; -import cc.getportal.command.response.AuthResponse; -import org.jetbrains.annotations.ApiStatus; - -@ApiStatus.Internal -public class AuthRequest extends PortalRequest { - final String token; - - public AuthRequest(String token) { - this.token = token; - } - - @Override - public String name() { - return "Auth"; - } - - @Override - public Class responseType() { - return AuthResponse.class; - } -} diff --git a/src/main/java/cc/getportal/command/request/AuthenticateKeyRequest.java b/src/main/java/cc/getportal/command/request/AuthenticateKeyRequest.java deleted file mode 100644 index 257e8ad..0000000 --- a/src/main/java/cc/getportal/command/request/AuthenticateKeyRequest.java +++ /dev/null @@ -1,28 +0,0 @@ -package cc.getportal.command.request; - -import cc.getportal.command.PortalRequest; -import cc.getportal.command.notification.UnitNotification; -import cc.getportal.command.response.AuthenticateKeyResponse; - -import java.util.List; - -public class AuthenticateKeyRequest extends PortalRequest { - - private final String main_key; - private final List subkeys; - - public AuthenticateKeyRequest(String mainKey, List subkeys) { - main_key = mainKey; - this.subkeys = subkeys; - } - - @Override - public String name() { - return "AuthenticateKey"; - } - - @Override - public Class responseType() { - return AuthenticateKeyResponse.class; - } -} diff --git a/src/main/java/cc/getportal/command/request/BurnCashuRequest.java b/src/main/java/cc/getportal/command/request/BurnCashuRequest.java deleted file mode 100644 index e8cd3d0..0000000 --- a/src/main/java/cc/getportal/command/request/BurnCashuRequest.java +++ /dev/null @@ -1,31 +0,0 @@ -package cc.getportal.command.request; - -import cc.getportal.command.PortalRequest; -import cc.getportal.command.notification.UnitNotification; -import cc.getportal.command.response.BurnCashuResponse; -import org.jetbrains.annotations.Nullable; - -public class BurnCashuRequest extends PortalRequest { - private final String mint_url; - @Nullable - private final String static_auth_token; - private final String unit; - private final String token; - - public BurnCashuRequest(String mintUrl, @Nullable String staticAuthToken, String unit, String token) { - mint_url = mintUrl; - static_auth_token = staticAuthToken; - this.unit = unit; - this.token = token; - } - - @Override - public String name() { - return "BurnCashu"; - } - - @Override - public Class responseType() { - return BurnCashuResponse.class; - } -} diff --git a/src/main/java/cc/getportal/command/request/CalculateNextOccurrenceRequest.java b/src/main/java/cc/getportal/command/request/CalculateNextOccurrenceRequest.java deleted file mode 100644 index 2b53f87..0000000 --- a/src/main/java/cc/getportal/command/request/CalculateNextOccurrenceRequest.java +++ /dev/null @@ -1,26 +0,0 @@ -package cc.getportal.command.request; - -import cc.getportal.command.PortalRequest; -import cc.getportal.command.notification.UnitNotification; -import cc.getportal.command.response.CalculateNextOccurrenceResponse; - -public class CalculateNextOccurrenceRequest extends PortalRequest { - - private final String calendar; - private final String from; - - public CalculateNextOccurrenceRequest(String calendar, long from) { - this.calendar = calendar; - this.from = String.valueOf(from); - } - - @Override - public String name() { - return "CalculateNextOccurrence"; - } - - @Override - public Class responseType() { - return CalculateNextOccurrenceResponse.class; - } -} diff --git a/src/main/java/cc/getportal/command/request/CloseRecurringPaymentRequest.java b/src/main/java/cc/getportal/command/request/CloseRecurringPaymentRequest.java deleted file mode 100644 index 1ff519b..0000000 --- a/src/main/java/cc/getportal/command/request/CloseRecurringPaymentRequest.java +++ /dev/null @@ -1,30 +0,0 @@ -package cc.getportal.command.request; - -import cc.getportal.command.PortalRequest; -import cc.getportal.command.notification.UnitNotification; -import cc.getportal.command.response.CloseRecurringPaymentResponse; - -import java.util.List; - -public class CloseRecurringPaymentRequest extends PortalRequest { - - private final String main_key; - private final List subkeys; - private final String subscription_id; - - public CloseRecurringPaymentRequest(String mainKey, List subkeys, String subscriptionId) { - main_key = mainKey; - this.subkeys = subkeys; - subscription_id = subscriptionId; - } - - @Override - public String name() { - return "CloseRecurringPayment"; - } - - @Override - public Class responseType() { - return CloseRecurringPaymentResponse.class; - } -} diff --git a/src/main/java/cc/getportal/command/request/FetchNip05ProfileRequest.java b/src/main/java/cc/getportal/command/request/FetchNip05ProfileRequest.java deleted file mode 100644 index 0645214..0000000 --- a/src/main/java/cc/getportal/command/request/FetchNip05ProfileRequest.java +++ /dev/null @@ -1,24 +0,0 @@ -package cc.getportal.command.request; - -import cc.getportal.command.PortalRequest; -import cc.getportal.command.notification.UnitNotification; -import cc.getportal.command.response.FetchNip05ProfileResponse; - -public class FetchNip05ProfileRequest extends PortalRequest { - - private final String nip05; - - public FetchNip05ProfileRequest(String nip05) { - this.nip05 = nip05; - } - - @Override - public String name() { - return "FetchNip05Profile"; - } - - @Override - public Class responseType() { - return FetchNip05ProfileResponse.class; - } -} diff --git a/src/main/java/cc/getportal/command/request/FetchProfileRequest.java b/src/main/java/cc/getportal/command/request/FetchProfileRequest.java deleted file mode 100644 index 3402396..0000000 --- a/src/main/java/cc/getportal/command/request/FetchProfileRequest.java +++ /dev/null @@ -1,23 +0,0 @@ -package cc.getportal.command.request; - -import cc.getportal.command.PortalRequest; -import cc.getportal.command.notification.UnitNotification; -import cc.getportal.command.response.FetchProfileResponse; - -public class FetchProfileRequest extends PortalRequest { - private final String main_key; - - public FetchProfileRequest(String mainKey) { - main_key = mainKey; - } - - @Override - public String name() { - return "FetchProfile"; - } - - @Override - public Class responseType() { - return FetchProfileResponse.class; - } -} diff --git a/src/main/java/cc/getportal/command/request/GetWalletInfoRequest.java b/src/main/java/cc/getportal/command/request/GetWalletInfoRequest.java deleted file mode 100644 index 80480a6..0000000 --- a/src/main/java/cc/getportal/command/request/GetWalletInfoRequest.java +++ /dev/null @@ -1,26 +0,0 @@ -package cc.getportal.command.request; - -import cc.getportal.command.PortalRequest; -import cc.getportal.command.notification.UnitNotification; -import cc.getportal.command.response.GetWalletInfoResponse; - -public class GetWalletInfoRequest extends PortalRequest { - - public GetWalletInfoRequest() { - } - - @Override - public String name() { - return "GetWalletInfo"; - } - - @Override - public Class responseType() { - return GetWalletInfoResponse.class; - } - - @Override - public boolean isUnit() { - return true; - } -} diff --git a/src/main/java/cc/getportal/command/request/IssueJwtRequest.java b/src/main/java/cc/getportal/command/request/IssueJwtRequest.java deleted file mode 100644 index 0c6249e..0000000 --- a/src/main/java/cc/getportal/command/request/IssueJwtRequest.java +++ /dev/null @@ -1,27 +0,0 @@ -package cc.getportal.command.request; - -import cc.getportal.command.PortalRequest; -import cc.getportal.command.notification.UnitNotification; -import cc.getportal.command.response.IssueJwtResponse; - -public class IssueJwtRequest extends PortalRequest { - - private final String target_key; - private final long duration_hours; - - public IssueJwtRequest(String targetKey, long durationHours) { - target_key = targetKey; - duration_hours = durationHours; - } - - - @Override - public String name() { - return "IssueJwt"; - } - - @Override - public Class responseType() { - return IssueJwtResponse.class; - } -} diff --git a/src/main/java/cc/getportal/command/request/KeyHandshakeUrlRequest.java b/src/main/java/cc/getportal/command/request/KeyHandshakeUrlRequest.java deleted file mode 100644 index cb8ef8e..0000000 --- a/src/main/java/cc/getportal/command/request/KeyHandshakeUrlRequest.java +++ /dev/null @@ -1,39 +0,0 @@ -package cc.getportal.command.request; - -import cc.getportal.command.PortalRequest; -import cc.getportal.command.notification.KeyHandshakeUrlNotification; -import cc.getportal.command.response.KeyHandshakeUrlResponse; -import org.jetbrains.annotations.Nullable; - -import java.util.function.Consumer; - -public class KeyHandshakeUrlRequest extends PortalRequest { - - private final String static_token; - private final Boolean no_request; - - public KeyHandshakeUrlRequest(String staticToken, Boolean noRequest, Consumer notificationFun) { - super(notificationFun); - static_token = staticToken; - no_request = noRequest; - } - - public KeyHandshakeUrlRequest(Consumer notificationFun) { - this(null, null, notificationFun); - } - - @Override - public @Nullable Class notificationType() { - return KeyHandshakeUrlNotification.class; - } - - @Override - public String name() { - return "NewKeyHandshakeUrl"; - } - - @Override - public Class responseType() { - return KeyHandshakeUrlResponse.class; - } -} diff --git a/src/main/java/cc/getportal/command/request/ListenClosedRecurringPaymentRequest.java b/src/main/java/cc/getportal/command/request/ListenClosedRecurringPaymentRequest.java deleted file mode 100644 index 528169b..0000000 --- a/src/main/java/cc/getportal/command/request/ListenClosedRecurringPaymentRequest.java +++ /dev/null @@ -1,35 +0,0 @@ -package cc.getportal.command.request; - -import cc.getportal.command.PortalRequest; -import cc.getportal.command.notification.CloseRecurringPaymentNotification; -import cc.getportal.command.response.UnitResponse; -import org.jetbrains.annotations.Nullable; - -import java.util.function.Consumer; - -public class ListenClosedRecurringPaymentRequest extends PortalRequest { - - public ListenClosedRecurringPaymentRequest(Consumer notificationFun) { - super(notificationFun); - } - - @Override - public @Nullable Class notificationType() { - return CloseRecurringPaymentNotification.class; - } - - @Override - public String name() { - return "ListenClosedRecurringPayment"; - } - - @Override - public Class responseType() { - return UnitResponse.class; - } - - @Override - public boolean isUnit() { - return true; - } -} diff --git a/src/main/java/cc/getportal/command/request/MintCashuRequest.java b/src/main/java/cc/getportal/command/request/MintCashuRequest.java deleted file mode 100644 index b63bd7b..0000000 --- a/src/main/java/cc/getportal/command/request/MintCashuRequest.java +++ /dev/null @@ -1,34 +0,0 @@ -package cc.getportal.command.request; - -import cc.getportal.command.PortalRequest; -import cc.getportal.command.notification.UnitNotification; -import cc.getportal.command.response.MintCashuResponse; -import org.jetbrains.annotations.Nullable; - -public class MintCashuRequest extends PortalRequest { - private final String mint_url; - @Nullable - private final String static_auth_token; - private final String unit; - private final long amount; - @Nullable - private final String description; - - public MintCashuRequest(String mintUrl, @Nullable String staticAuthToken, String unit, long amount, @Nullable String description) { - mint_url = mintUrl; - static_auth_token = staticAuthToken; - this.unit = unit; - this.amount = amount; - this.description = description; - } - - @Override - public String name() { - return "MintCashu"; - } - - @Override - public Class responseType() { - return MintCashuResponse.class; - } -} diff --git a/src/main/java/cc/getportal/command/request/PayInvoiceRequest.java b/src/main/java/cc/getportal/command/request/PayInvoiceRequest.java deleted file mode 100644 index 56b32f6..0000000 --- a/src/main/java/cc/getportal/command/request/PayInvoiceRequest.java +++ /dev/null @@ -1,24 +0,0 @@ -package cc.getportal.command.request; - -import cc.getportal.command.PortalRequest; -import cc.getportal.command.notification.UnitNotification; -import cc.getportal.command.response.PayInvoiceResponse; - -public class PayInvoiceRequest extends PortalRequest { - - private final String invoice; - - public PayInvoiceRequest(String invoice) { - this.invoice = invoice; - } - - @Override - public String name() { - return "PayInvoice"; - } - - @Override - public Class responseType() { - return PayInvoiceResponse.class; - } -} diff --git a/src/main/java/cc/getportal/command/request/RemoveRelayRequest.java b/src/main/java/cc/getportal/command/request/RemoveRelayRequest.java deleted file mode 100644 index 365be2b..0000000 --- a/src/main/java/cc/getportal/command/request/RemoveRelayRequest.java +++ /dev/null @@ -1,25 +0,0 @@ -package cc.getportal.command.request; - -import cc.getportal.command.PortalRequest; -import cc.getportal.command.notification.UnitNotification; -import cc.getportal.command.response.RemoveRelayResponse; - -public class RemoveRelayRequest extends PortalRequest { - - private final String relay; - - public RemoveRelayRequest(String relay) { - this.relay = relay; - } - - - @Override - public String name() { - return "RemoveRelay"; - } - - @Override - public Class responseType() { - return RemoveRelayResponse.class; - } -} diff --git a/src/main/java/cc/getportal/command/request/RequestCashuRequest.java b/src/main/java/cc/getportal/command/request/RequestCashuRequest.java deleted file mode 100644 index 0599f54..0000000 --- a/src/main/java/cc/getportal/command/request/RequestCashuRequest.java +++ /dev/null @@ -1,34 +0,0 @@ -package cc.getportal.command.request; - -import cc.getportal.command.PortalRequest; -import cc.getportal.command.notification.UnitNotification; -import cc.getportal.command.response.RequestCashuResponse; - -import java.util.List; - -public class RequestCashuRequest extends PortalRequest { - private final String mint_url; - private final String unit; - private final long amount; - private final String recipient_key; - private final List subkeys; - - public RequestCashuRequest(String mintUrl, String unit, long amount, String recipientKey, List subkeys) { - mint_url = mintUrl; - this.unit = unit; - this.amount = amount; - recipient_key = recipientKey; - this.subkeys = subkeys; - } - - - @Override - public String name() { - return "RequestCashu"; - } - - @Override - public Class responseType() { - return RequestCashuResponse.class; - } -} diff --git a/src/main/java/cc/getportal/command/request/RequestInvoiceParams.java b/src/main/java/cc/getportal/command/request/RequestInvoiceParams.java deleted file mode 100644 index 1ddbfb5..0000000 --- a/src/main/java/cc/getportal/command/request/RequestInvoiceParams.java +++ /dev/null @@ -1,30 +0,0 @@ -package cc.getportal.command.request; - -import cc.getportal.model.Currency; -import com.google.gson.annotations.SerializedName; -import org.jetbrains.annotations.Nullable; - -/** - * Slim parameters for RequestInvoice command. - * Server (portal-rest) computes exchange rate from amount/currency. - * Request ID is derived from command.id. - */ -public record RequestInvoiceParams( - long amount, - Currency currency, - @SerializedName("expires_at") - String expiresAt, - @Nullable String description, - @Nullable String refund_invoice, - /** Optional request ID. If not provided, the command ID is used by the server. */ - @Nullable String request_id -) { - - public RequestInvoiceParams(long amount, Currency currency, long expiresAt, @Nullable String description, @Nullable String refund_invoice) { - this(amount, currency, String.valueOf(expiresAt), description, refund_invoice, null); - } - - public RequestInvoiceParams(long amount, Currency currency, long expiresAt, @Nullable String description, @Nullable String refund_invoice, @Nullable String requestId) { - this(amount, currency, String.valueOf(expiresAt), description, refund_invoice, requestId); - } -} diff --git a/src/main/java/cc/getportal/command/request/RequestInvoiceRequest.java b/src/main/java/cc/getportal/command/request/RequestInvoiceRequest.java deleted file mode 100644 index 2652041..0000000 --- a/src/main/java/cc/getportal/command/request/RequestInvoiceRequest.java +++ /dev/null @@ -1,30 +0,0 @@ -package cc.getportal.command.request; - -import cc.getportal.command.PortalRequest; -import cc.getportal.command.notification.UnitNotification; -import cc.getportal.command.response.RequestInvoiceResponse; - -import java.util.List; - -public class RequestInvoiceRequest extends PortalRequest { - - private final String recipient_key; - private final List subkeys; - private final RequestInvoiceParams content; - - public RequestInvoiceRequest(String recipientKey, List subkeys, RequestInvoiceParams content) { - recipient_key = recipientKey; - this.subkeys = subkeys; - this.content = content; - } - - @Override - public String name() { - return "RequestInvoice"; - } - - @Override - public Class responseType() { - return RequestInvoiceResponse.class; - } -} diff --git a/src/main/java/cc/getportal/command/request/RequestRecurringPaymentRequest.java b/src/main/java/cc/getportal/command/request/RequestRecurringPaymentRequest.java deleted file mode 100644 index 95b1a7e..0000000 --- a/src/main/java/cc/getportal/command/request/RequestRecurringPaymentRequest.java +++ /dev/null @@ -1,31 +0,0 @@ -package cc.getportal.command.request; - -import cc.getportal.command.PortalRequest; -import cc.getportal.model.RecurringPaymentRequestContent; -import cc.getportal.command.notification.UnitNotification; -import cc.getportal.command.response.RequestRecurringPaymentResponse; - -import java.util.List; - -public class RequestRecurringPaymentRequest extends PortalRequest { - - private final String main_key; - private final List subkeys; - private final RecurringPaymentRequestContent payment_request; - - public RequestRecurringPaymentRequest(String mainKey, List subkeys, RecurringPaymentRequestContent paymentRequest) { - main_key = mainKey; - this.subkeys = subkeys; - payment_request = paymentRequest; - } - - @Override - public String name() { - return "RequestRecurringPayment"; - } - - @Override - public Class responseType() { - return RequestRecurringPaymentResponse.class; - } -} diff --git a/src/main/java/cc/getportal/command/request/RequestSinglePaymentRequest.java b/src/main/java/cc/getportal/command/request/RequestSinglePaymentRequest.java deleted file mode 100644 index c779948..0000000 --- a/src/main/java/cc/getportal/command/request/RequestSinglePaymentRequest.java +++ /dev/null @@ -1,40 +0,0 @@ -package cc.getportal.command.request; - -import cc.getportal.command.PortalRequest; -import cc.getportal.model.SinglePaymentRequestContent; -import cc.getportal.command.notification.RequestSinglePaymentNotification; -import cc.getportal.command.response.RequestSinglePaymentResponse; -import org.jetbrains.annotations.Nullable; - -import java.util.List; -import java.util.function.Consumer; - -public class RequestSinglePaymentRequest extends PortalRequest { - - private final String main_key; - private final List subkeys; - private final SinglePaymentRequestContent payment_request; - - - public RequestSinglePaymentRequest(String mainKey, List subkeys, SinglePaymentRequestContent paymentRequest, Consumer notificationFun) { - super(notificationFun); - main_key = mainKey; - this.subkeys = subkeys; - payment_request = paymentRequest; - } - - @Override - public @Nullable Class notificationType() { - return RequestSinglePaymentNotification.class; - } - - @Override - public String name() { - return "RequestSinglePayment"; - } - - @Override - public Class responseType() { - return RequestSinglePaymentResponse.class; - } -} diff --git a/src/main/java/cc/getportal/command/request/SendCashuDirectRequest.java b/src/main/java/cc/getportal/command/request/SendCashuDirectRequest.java deleted file mode 100644 index f9ef1fa..0000000 --- a/src/main/java/cc/getportal/command/request/SendCashuDirectRequest.java +++ /dev/null @@ -1,31 +0,0 @@ -package cc.getportal.command.request; - -import cc.getportal.command.PortalRequest; -import cc.getportal.command.notification.UnitNotification; -import cc.getportal.command.response.SendCashuDirectResponse; - -import java.util.List; - -public class SendCashuDirectRequest extends PortalRequest { - - - private final String main_key; - private final List subkeys; - private final String token; - - public SendCashuDirectRequest(String mainKey, List subkeys, String token) { - main_key = mainKey; - this.subkeys = subkeys; - this.token = token; - } - - @Override - public String name() { - return "SendCashuDirect"; - } - - @Override - public Class responseType() { - return SendCashuDirectResponse.class; - } -} diff --git a/src/main/java/cc/getportal/command/request/SetProfileRequest.java b/src/main/java/cc/getportal/command/request/SetProfileRequest.java deleted file mode 100644 index a224c38..0000000 --- a/src/main/java/cc/getportal/command/request/SetProfileRequest.java +++ /dev/null @@ -1,25 +0,0 @@ -package cc.getportal.command.request; - -import cc.getportal.command.PortalRequest; -import cc.getportal.model.Profile; -import cc.getportal.command.notification.UnitNotification; -import cc.getportal.command.response.UnitResponse; - -public class SetProfileRequest extends PortalRequest { - - private final Profile profile; - - public SetProfileRequest(Profile profile) { - this.profile = profile; - } - - @Override - public String name() { - return "SetProfile"; - } - - @Override - public Class responseType() { - return UnitResponse.class; - } -} diff --git a/src/main/java/cc/getportal/command/request/VerifyJwtRequest.java b/src/main/java/cc/getportal/command/request/VerifyJwtRequest.java deleted file mode 100644 index 1d410b0..0000000 --- a/src/main/java/cc/getportal/command/request/VerifyJwtRequest.java +++ /dev/null @@ -1,27 +0,0 @@ -package cc.getportal.command.request; - -import cc.getportal.command.PortalRequest; -import cc.getportal.command.notification.UnitNotification; -import cc.getportal.command.response.VerifyJwtResponse; - -public class VerifyJwtRequest extends PortalRequest { - - private final String pubkey; - private final String token; - - public VerifyJwtRequest(String pubkey, String token) { - this.pubkey = pubkey; - this.token = token; - } - - - @Override - public String name() { - return "VerifyJwt"; - } - - @Override - public Class responseType() { - return VerifyJwtResponse.class; - } -} diff --git a/src/main/java/cc/getportal/command/response/AddRelayResponse.java b/src/main/java/cc/getportal/command/response/AddRelayResponse.java deleted file mode 100644 index ba863ca..0000000 --- a/src/main/java/cc/getportal/command/response/AddRelayResponse.java +++ /dev/null @@ -1,6 +0,0 @@ -package cc.getportal.command.response; - -import cc.getportal.command.PortalResponse; - -public record AddRelayResponse(String relay) implements PortalResponse { -} diff --git a/src/main/java/cc/getportal/command/response/AuthResponse.java b/src/main/java/cc/getportal/command/response/AuthResponse.java deleted file mode 100644 index 24af06d..0000000 --- a/src/main/java/cc/getportal/command/response/AuthResponse.java +++ /dev/null @@ -1,8 +0,0 @@ -package cc.getportal.command.response; - -import cc.getportal.command.PortalResponse; -import org.jetbrains.annotations.ApiStatus; - -@ApiStatus.Internal -public record AuthResponse(String message) implements PortalResponse { -} diff --git a/src/main/java/cc/getportal/command/response/AuthenticateKeyResponse.java b/src/main/java/cc/getportal/command/response/AuthenticateKeyResponse.java deleted file mode 100644 index e2467d4..0000000 --- a/src/main/java/cc/getportal/command/response/AuthenticateKeyResponse.java +++ /dev/null @@ -1,24 +0,0 @@ -package cc.getportal.command.response; - -import cc.getportal.command.PortalResponse; -import com.google.gson.annotations.SerializedName; -import org.jetbrains.annotations.Nullable; - -import java.util.List; - -public record AuthenticateKeyResponse(AuthResponseData event) implements PortalResponse { - - public record AuthResponseData(String user_key, String recipient, String challenge, AuthResponseStatus status) { - } - - public record AuthResponseStatus(AuthResponseStatusType status, @Nullable String reason, @Nullable List granted_permissions, - @Nullable String session_token) { - } - - public enum AuthResponseStatusType { - @SerializedName("approved") - APPROVED, - @SerializedName("declined") - DECLINED - } -} diff --git a/src/main/java/cc/getportal/command/response/BurnCashuResponse.java b/src/main/java/cc/getportal/command/response/BurnCashuResponse.java deleted file mode 100644 index 4c086cc..0000000 --- a/src/main/java/cc/getportal/command/response/BurnCashuResponse.java +++ /dev/null @@ -1,6 +0,0 @@ -package cc.getportal.command.response; - -import cc.getportal.command.PortalResponse; - -public record BurnCashuResponse(long amount) implements PortalResponse { -} diff --git a/src/main/java/cc/getportal/command/response/CalculateNextOccurrenceResponse.java b/src/main/java/cc/getportal/command/response/CalculateNextOccurrenceResponse.java deleted file mode 100644 index e272d2c..0000000 --- a/src/main/java/cc/getportal/command/response/CalculateNextOccurrenceResponse.java +++ /dev/null @@ -1,7 +0,0 @@ -package cc.getportal.command.response; - -import cc.getportal.command.PortalResponse; -import org.jetbrains.annotations.Nullable; - -public record CalculateNextOccurrenceResponse(@Nullable Long next_occurrence) implements PortalResponse { -} diff --git a/src/main/java/cc/getportal/command/response/CloseRecurringPaymentResponse.java b/src/main/java/cc/getportal/command/response/CloseRecurringPaymentResponse.java deleted file mode 100644 index 831452b..0000000 --- a/src/main/java/cc/getportal/command/response/CloseRecurringPaymentResponse.java +++ /dev/null @@ -1,8 +0,0 @@ -package cc.getportal.command.response; - -import cc.getportal.command.PortalResponse; - -public record CloseRecurringPaymentResponse(String message) implements PortalResponse { - - -} diff --git a/src/main/java/cc/getportal/command/response/FetchNip05ProfileResponse.java b/src/main/java/cc/getportal/command/response/FetchNip05ProfileResponse.java deleted file mode 100644 index 73abaf9..0000000 --- a/src/main/java/cc/getportal/command/response/FetchNip05ProfileResponse.java +++ /dev/null @@ -1,7 +0,0 @@ -package cc.getportal.command.response; - -import cc.getportal.command.PortalResponse; -import cc.getportal.model.Nip05Profile; - -public record FetchNip05ProfileResponse(Nip05Profile profile) implements PortalResponse { -} diff --git a/src/main/java/cc/getportal/command/response/FetchProfileResponse.java b/src/main/java/cc/getportal/command/response/FetchProfileResponse.java deleted file mode 100644 index 9c51193..0000000 --- a/src/main/java/cc/getportal/command/response/FetchProfileResponse.java +++ /dev/null @@ -1,8 +0,0 @@ -package cc.getportal.command.response; - -import cc.getportal.command.PortalResponse; -import cc.getportal.model.Profile; -import org.jetbrains.annotations.Nullable; - -public record FetchProfileResponse(@Nullable Profile profile) implements PortalResponse { -} diff --git a/src/main/java/cc/getportal/command/response/GetWalletInfoResponse.java b/src/main/java/cc/getportal/command/response/GetWalletInfoResponse.java deleted file mode 100644 index f4af656..0000000 --- a/src/main/java/cc/getportal/command/response/GetWalletInfoResponse.java +++ /dev/null @@ -1,6 +0,0 @@ -package cc.getportal.command.response; - -import cc.getportal.command.PortalResponse; - -public record GetWalletInfoResponse(String wallet_type, long balance_msat) implements PortalResponse { -} diff --git a/src/main/java/cc/getportal/command/response/IssueJwtResponse.java b/src/main/java/cc/getportal/command/response/IssueJwtResponse.java deleted file mode 100644 index be629ce..0000000 --- a/src/main/java/cc/getportal/command/response/IssueJwtResponse.java +++ /dev/null @@ -1,6 +0,0 @@ -package cc.getportal.command.response; - -import cc.getportal.command.PortalResponse; - -public record IssueJwtResponse(String token) implements PortalResponse { -} diff --git a/src/main/java/cc/getportal/command/response/KeyHandshakeUrlResponse.java b/src/main/java/cc/getportal/command/response/KeyHandshakeUrlResponse.java deleted file mode 100644 index fb6fd24..0000000 --- a/src/main/java/cc/getportal/command/response/KeyHandshakeUrlResponse.java +++ /dev/null @@ -1,7 +0,0 @@ -package cc.getportal.command.response; - -import cc.getportal.command.PortalResponse; - -public record KeyHandshakeUrlResponse(String url) implements PortalResponse { - -} diff --git a/src/main/java/cc/getportal/command/response/MintCashuResponse.java b/src/main/java/cc/getportal/command/response/MintCashuResponse.java deleted file mode 100644 index 34fd840..0000000 --- a/src/main/java/cc/getportal/command/response/MintCashuResponse.java +++ /dev/null @@ -1,6 +0,0 @@ -package cc.getportal.command.response; - -import cc.getportal.command.PortalResponse; - -public record MintCashuResponse(String token) implements PortalResponse { -} diff --git a/src/main/java/cc/getportal/command/response/PayInvoiceResponse.java b/src/main/java/cc/getportal/command/response/PayInvoiceResponse.java deleted file mode 100644 index 98858c8..0000000 --- a/src/main/java/cc/getportal/command/response/PayInvoiceResponse.java +++ /dev/null @@ -1,9 +0,0 @@ -package cc.getportal.command.response; - -import cc.getportal.command.PortalResponse; - -public record PayInvoiceResponse( - String preimage -) implements PortalResponse { - -} diff --git a/src/main/java/cc/getportal/command/response/RemoveRelayResponse.java b/src/main/java/cc/getportal/command/response/RemoveRelayResponse.java deleted file mode 100644 index e6b4839..0000000 --- a/src/main/java/cc/getportal/command/response/RemoveRelayResponse.java +++ /dev/null @@ -1,6 +0,0 @@ -package cc.getportal.command.response; - -import cc.getportal.command.PortalResponse; - -public record RemoveRelayResponse(String relay) implements PortalResponse { -} diff --git a/src/main/java/cc/getportal/command/response/RequestCashuResponse.java b/src/main/java/cc/getportal/command/response/RequestCashuResponse.java deleted file mode 100644 index 31157e3..0000000 --- a/src/main/java/cc/getportal/command/response/RequestCashuResponse.java +++ /dev/null @@ -1,7 +0,0 @@ -package cc.getportal.command.response; - -import cc.getportal.model.CashuResponseStatus; -import cc.getportal.command.PortalResponse; - -public record RequestCashuResponse(CashuResponseStatus status) implements PortalResponse { -} diff --git a/src/main/java/cc/getportal/command/response/RequestInvoiceResponse.java b/src/main/java/cc/getportal/command/response/RequestInvoiceResponse.java deleted file mode 100644 index 62bf59b..0000000 --- a/src/main/java/cc/getportal/command/response/RequestInvoiceResponse.java +++ /dev/null @@ -1,10 +0,0 @@ -package cc.getportal.command.response; - -import cc.getportal.command.PortalResponse; - -public record RequestInvoiceResponse( - String invoice, - String paymentHash -) implements PortalResponse { - -} diff --git a/src/main/java/cc/getportal/command/response/RequestRecurringPaymentResponse.java b/src/main/java/cc/getportal/command/response/RequestRecurringPaymentResponse.java deleted file mode 100644 index dc91991..0000000 --- a/src/main/java/cc/getportal/command/response/RequestRecurringPaymentResponse.java +++ /dev/null @@ -1,30 +0,0 @@ -package cc.getportal.command.response; - -import cc.getportal.model.Currency; -import cc.getportal.command.PortalResponse; -import cc.getportal.model.RecurrenceInfo; -import com.google.gson.annotations.SerializedName; -import org.jetbrains.annotations.Nullable; - -public record RequestRecurringPaymentResponse(RecurringPaymentResponseContent status) implements PortalResponse { - - - public record RecurringPaymentResponseContent(String request_id, RecurringPaymentStatus status) {} - - public record RecurringPaymentStatus(RequestRecurringPaymentStatusType status, - @Nullable String subscription_id, - @Nullable Long authorized_amount, - @Nullable Currency authorized_currency, - @Nullable RecurrenceInfo authorized_recurrence, - @Nullable String reason - ) { - - } - - public enum RequestRecurringPaymentStatusType { - @SerializedName("confirmed") - CONFIRMED, - @SerializedName("rejected") - REJECTED - } -} diff --git a/src/main/java/cc/getportal/command/response/RequestSinglePaymentResponse.java b/src/main/java/cc/getportal/command/response/RequestSinglePaymentResponse.java deleted file mode 100644 index 3b34608..0000000 --- a/src/main/java/cc/getportal/command/response/RequestSinglePaymentResponse.java +++ /dev/null @@ -1,7 +0,0 @@ -package cc.getportal.command.response; - -import cc.getportal.command.PortalResponse; - -public record RequestSinglePaymentResponse() implements PortalResponse { - -} diff --git a/src/main/java/cc/getportal/command/response/SendCashuDirectResponse.java b/src/main/java/cc/getportal/command/response/SendCashuDirectResponse.java deleted file mode 100644 index a3169c7..0000000 --- a/src/main/java/cc/getportal/command/response/SendCashuDirectResponse.java +++ /dev/null @@ -1,6 +0,0 @@ -package cc.getportal.command.response; - -import cc.getportal.command.PortalResponse; - -public record SendCashuDirectResponse(String message) implements PortalResponse { -} diff --git a/src/main/java/cc/getportal/command/response/UnitResponse.java b/src/main/java/cc/getportal/command/response/UnitResponse.java deleted file mode 100644 index 3612f5f..0000000 --- a/src/main/java/cc/getportal/command/response/UnitResponse.java +++ /dev/null @@ -1,6 +0,0 @@ -package cc.getportal.command.response; - -import cc.getportal.command.PortalResponse; - -public class UnitResponse implements PortalResponse { -} diff --git a/src/main/java/cc/getportal/command/response/VerifyJwtResponse.java b/src/main/java/cc/getportal/command/response/VerifyJwtResponse.java deleted file mode 100644 index 33d8864..0000000 --- a/src/main/java/cc/getportal/command/response/VerifyJwtResponse.java +++ /dev/null @@ -1,6 +0,0 @@ -package cc.getportal.command.response; - -import cc.getportal.command.PortalResponse; - -public record VerifyJwtResponse(String target_key) implements PortalResponse { -} diff --git a/src/main/java/cc/getportal/model/AuthResponseData.java b/src/main/java/cc/getportal/model/AuthResponseData.java new file mode 100644 index 0000000..de46be9 --- /dev/null +++ b/src/main/java/cc/getportal/model/AuthResponseData.java @@ -0,0 +1,11 @@ +package cc.getportal.model; + +import com.google.gson.annotations.SerializedName; + +public class AuthResponseData { + @SerializedName("user_key") + public String userKey; + public String recipient; + public String challenge; + public AuthResponseStatus status; +} diff --git a/src/main/java/cc/getportal/model/AuthResponseStatus.java b/src/main/java/cc/getportal/model/AuthResponseStatus.java new file mode 100644 index 0000000..499ad0e --- /dev/null +++ b/src/main/java/cc/getportal/model/AuthResponseStatus.java @@ -0,0 +1,14 @@ +package cc.getportal.model; + +import com.google.gson.annotations.SerializedName; +import java.util.List; + +public class AuthResponseStatus { + /** "approved" or "declined" */ + public String status; + public String reason; + @SerializedName("granted_permissions") + public List grantedPermissions; + @SerializedName("session_token") + public String sessionToken; +} diff --git a/src/main/java/cc/getportal/model/EventsResponse.java b/src/main/java/cc/getportal/model/EventsResponse.java new file mode 100644 index 0000000..2cdbba2 --- /dev/null +++ b/src/main/java/cc/getportal/model/EventsResponse.java @@ -0,0 +1,8 @@ +package cc.getportal.model; + +import cc.getportal.StreamEvent; +import java.util.List; + +public class EventsResponse { + public List events; +} diff --git a/src/main/java/cc/getportal/model/InfoResponse.java b/src/main/java/cc/getportal/model/InfoResponse.java new file mode 100644 index 0000000..105b294 --- /dev/null +++ b/src/main/java/cc/getportal/model/InfoResponse.java @@ -0,0 +1,5 @@ +package cc.getportal.model; + +public class InfoResponse { + public String pubkey; +} diff --git a/src/main/java/cc/getportal/model/InvoicePaymentResponse.java b/src/main/java/cc/getportal/model/InvoicePaymentResponse.java new file mode 100644 index 0000000..7255165 --- /dev/null +++ b/src/main/java/cc/getportal/model/InvoicePaymentResponse.java @@ -0,0 +1,9 @@ +package cc.getportal.model; + +import com.google.gson.annotations.SerializedName; + +public class InvoicePaymentResponse { + public String invoice; + @SerializedName("payment_hash") + public String paymentHash; +} diff --git a/src/main/java/cc/getportal/model/InvoiceStatus.java b/src/main/java/cc/getportal/model/InvoiceStatus.java new file mode 100644 index 0000000..da4a1ba --- /dev/null +++ b/src/main/java/cc/getportal/model/InvoiceStatus.java @@ -0,0 +1,15 @@ +package cc.getportal.model; + +import org.jetbrains.annotations.Nullable; + +/** + * Terminal status of a single or raw payment request. + * Delivered as the terminal event payload for {@code payment_status_update} streams. + */ +public class InvoiceStatus { + /** One of: {@code paid}, {@code timeout}, {@code error}, + * {@code user_approved}, {@code user_success}, {@code user_failed}, {@code user_rejected}. */ + public String status; + @Nullable public String preimage; + @Nullable public String reason; +} diff --git a/src/main/java/cc/getportal/model/KeyHandshakeResult.java b/src/main/java/cc/getportal/model/KeyHandshakeResult.java new file mode 100644 index 0000000..77bba7f --- /dev/null +++ b/src/main/java/cc/getportal/model/KeyHandshakeResult.java @@ -0,0 +1,11 @@ +package cc.getportal.model; + +import com.google.gson.annotations.SerializedName; +import java.util.List; + +public class KeyHandshakeResult { + @SerializedName("main_key") + public String mainKey; + @SerializedName("preferred_relays") + public List preferredRelays; +} diff --git a/src/main/java/cc/getportal/model/Nip05WellKnownResponse.java b/src/main/java/cc/getportal/model/Nip05WellKnownResponse.java new file mode 100644 index 0000000..a4dba48 --- /dev/null +++ b/src/main/java/cc/getportal/model/Nip05WellKnownResponse.java @@ -0,0 +1,15 @@ +package cc.getportal.model; + +import java.util.List; +import java.util.Map; + +/** + * Response from {@code GET /.well-known/nostr.json}. + * Used for NIP-05 verification — maps names to public keys and optionally to relay lists. + */ +public class Nip05WellKnownResponse { + /** Maps NIP-05 names (e.g. {@code "service"}) to hex public keys. */ + public Map names; + /** Optional: maps hex public keys to a list of preferred relay URLs. */ + public Map> relays; +} diff --git a/src/main/java/cc/getportal/model/PayInvoiceResponse.java b/src/main/java/cc/getportal/model/PayInvoiceResponse.java new file mode 100644 index 0000000..1f05e7d --- /dev/null +++ b/src/main/java/cc/getportal/model/PayInvoiceResponse.java @@ -0,0 +1,10 @@ +package cc.getportal.model; + +import com.google.gson.annotations.SerializedName; + +public class PayInvoiceResponse { + @SerializedName("payment_hash") + public String paymentHash; + @SerializedName("fees_paid") + public long feesPaid; +} diff --git a/src/main/java/cc/getportal/model/RecurringPaymentResponseContent.java b/src/main/java/cc/getportal/model/RecurringPaymentResponseContent.java new file mode 100644 index 0000000..29d5781 --- /dev/null +++ b/src/main/java/cc/getportal/model/RecurringPaymentResponseContent.java @@ -0,0 +1,9 @@ +package cc.getportal.model; + +import com.google.gson.annotations.SerializedName; + +public class RecurringPaymentResponseContent { + @SerializedName("request_id") + public String requestId; + public RecurringPaymentStatus status; +} diff --git a/src/main/java/cc/getportal/model/RecurringPaymentStatus.java b/src/main/java/cc/getportal/model/RecurringPaymentStatus.java new file mode 100644 index 0000000..93dd5b9 --- /dev/null +++ b/src/main/java/cc/getportal/model/RecurringPaymentStatus.java @@ -0,0 +1,22 @@ +package cc.getportal.model; + +import com.google.gson.annotations.SerializedName; + +/** Status of a recurring payment response: "confirmed" or "rejected". */ +public class RecurringPaymentStatus { + /** "confirmed" or "rejected" */ + public String status; + + // confirmed fields + @SerializedName("subscription_id") + public String subscriptionId; + @SerializedName("authorized_amount") + public long authorizedAmount; + @SerializedName("authorized_currency") + public Currency authorizedCurrency; + @SerializedName("authorized_recurrence") + public RecurrenceInfo authorizedRecurrence; + + // rejected fields + public String reason; +} diff --git a/src/main/java/cc/getportal/model/RequestInvoiceParams.java b/src/main/java/cc/getportal/model/RequestInvoiceParams.java new file mode 100644 index 0000000..c0b3e09 --- /dev/null +++ b/src/main/java/cc/getportal/model/RequestInvoiceParams.java @@ -0,0 +1,36 @@ +package cc.getportal.model; + +import com.google.gson.annotations.SerializedName; +import org.jetbrains.annotations.Nullable; + +public class RequestInvoiceParams { + public long amount; + public Currency currency; + public String description; + @SerializedName("subscription_id") + @Nullable public String subscriptionId; + @SerializedName("auth_token") + @Nullable public String authToken; + @SerializedName("request_id") + @Nullable public String requestId; + @SerializedName("expires_at") + @Nullable public Long expiresAt; + + public RequestInvoiceParams(long amount, Currency currency, String description) { + this.amount = amount; + this.currency = currency; + this.description = description; + } + + public RequestInvoiceParams(long amount, Currency currency, String description, + @Nullable String subscriptionId, @Nullable String authToken, + @Nullable String requestId, @Nullable Long expiresAt) { + this.amount = amount; + this.currency = currency; + this.description = description; + this.subscriptionId = subscriptionId; + this.authToken = authToken; + this.requestId = requestId; + this.expiresAt = expiresAt; + } +} diff --git a/src/main/java/cc/getportal/model/VersionResponse.java b/src/main/java/cc/getportal/model/VersionResponse.java new file mode 100644 index 0000000..41b94f1 --- /dev/null +++ b/src/main/java/cc/getportal/model/VersionResponse.java @@ -0,0 +1,9 @@ +package cc.getportal.model; + +import com.google.gson.annotations.SerializedName; + +public class VersionResponse { + public String version; + @SerializedName("git_commit") + public String gitCommit; +} diff --git a/src/main/java/cc/getportal/model/WalletInfoResponse.java b/src/main/java/cc/getportal/model/WalletInfoResponse.java new file mode 100644 index 0000000..213ab57 --- /dev/null +++ b/src/main/java/cc/getportal/model/WalletInfoResponse.java @@ -0,0 +1,12 @@ +package cc.getportal.model; + +import com.google.gson.annotations.SerializedName; + +public class WalletInfoResponse { + @SerializedName("balance_msat") + public long balanceMsat; + @SerializedName("node_pubkey") + public String nodePubkey; + @SerializedName("node_alias") + public String nodeAlias; +}