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;
+}