diff --git a/README.md b/README.md index 6273737..9b96bed 100644 --- a/README.md +++ b/README.md @@ -63,11 +63,11 @@ Polls Soroban RPC for SmartDrop contract events and stores decoded event state i Registers subscriber endpoints for SmartDrop lifecycle events and delivers signed JSON payloads with retry tracking. **Events:** -- `airdrop.created` -- `airdrop.executing` -- `airdrop.completed` -- `airdrop.failed` — fired automatically when an airdrop expires (see below), in addition to any other failure path -- `recipient.claimed` +- `airdrop.failed` — **(Active)** fired automatically when an airdrop expires (see below), in addition to any other failure path +- `airdrop.created` — *(Planned, not yet implemented)* +- `airdrop.executing` — *(Planned, not yet implemented)* +- `airdrop.completed` — *(Planned, not yet implemented)* +- `recipient.claimed` — *(Planned, not yet implemented)* **Features:** - Webhook endpoint CRUD with secrets kept out of list responses @@ -730,26 +730,20 @@ curl http://localhost:4000/health ## Webhooks -Register endpoints that receive HTTP POST callbacks when SmartDrop indexes farming/pool events. +Register endpoints that receive HTTP POST callbacks when SmartDrop indexes contract lifecycle events or price alerts. ### Supported event types -| Event | Description | Wired up? | -|-------|-------------|-----------| -| `pool.created` | A new farming pool was created on-chain | No — registered event type, no dispatch path yet | -| `pool.assets_locked` | Assets were locked into a pool | No — registered event type, no dispatch path yet | -| `pool.assets_unlocked` | Assets were unlocked from a pool | No — registered event type, no dispatch path yet | -| `pool.rewards_distributed` | Pool distributed rewards to participants | No — registered event type, no dispatch path yet | -| `pool.closed` | Pool was closed | No — registered event type, no dispatch path yet | -| `airdrop.failed` | An airdrop has failed or expired | **Yes** — dispatched by `airdropExpiry.js` on expiry | -| `price.alert` | Existing price-alert event | **Yes** — dispatched by `alertsService` | -| `*` | Wildcard — subscribe to every known event | Only matches events with an active dispatch path | - -> **Note:** `airdrop.created`, `airdrop.executing`, `airdrop.completed`, and -> `recipient.claimed` are defined as valid event types in `webhookEvents.js` but -> have no active dispatch path yet — only `airdrop.failed` is dispatched today. -> Support for the remaining airdrop lifecycle events will be added as a separate -> feature on top of the live webhook dispatcher. +| Event | Status | Description | +|-------|--------|-------------| +| `airdrop.failed` | **Active** | Fired automatically when an airdrop expires | +| `price.alert` | **Active** | Existing price-alert event | +| `pool.created` | *Planned* | A new farming pool was created on-chain (not yet implemented) | +| `pool.assets_locked` | *Planned* | Assets were locked into a pool (not yet implemented) | +| `pool.assets_unlocked` | *Planned* | Assets were unlocked from a pool (not yet implemented) | +| `pool.rewards_distributed` | *Planned* | Pool distributed rewards to participants (not yet implemented) | +| `pool.closed` | *Planned* | Pool was closed (not yet implemented) | +| `*` | - | Wildcard — subscribe to every known event | ### API @@ -760,8 +754,7 @@ Content-Type: application/json { "url": "https://example.com/webhooks/smartdrop", - "events": ["pool.assets_locked", "pool.rewards_distributed"], - "filters": { "pool_id": "pool_123" }, // optional + "events": ["airdrop.failed", "price.alert"], "secret": "whsec_at_least_16_chars", // optional, generated if omitted "description": "Production webhook" // optional } @@ -784,20 +777,8 @@ POST /api/v1/webhooks/:id/test ``` Sends a synthetic `pool.assets_locked` payload to the registered URL and returns the resulting delivery summary. Limited to 5 calls/min/IP by default. -> **SSRF protection.** Webhook targets are validated against private/internal -> network ranges (RFC-1918, loopback, link-local, IPv6 ULA/link-local, CGNAT, -> etc.) both when registered **and** again at delivery time — and the outbound -> connection is pinned to the validated public IP, with redirects disabled — so -> a `test` call (or any real dispatch) cannot be used as an internal-network -> reconnaissance oracle. A blocked target is refused up front with a `422 -> WEBHOOK_TARGET_BLOCKED` error and is never delivered. -> -> **Reduced error detail.** The `last_error` field returned by the test endpoint -> is a coarse category (`unreachable` | `error_response` | `delivery_failed`), -> not the raw low-level network error string (e.g. `ECONNREFUSED`). The raw -> detail is still written to server-side logs for operators; only the public -> response is sanitized, to avoid turning the test endpoint into an information -> leak about internal reachability. See issue #96. +> [!IMPORTANT] +> A successful test delivery only confirms that the webhook URL can successfully receive deliveries from the service. It does **not** imply that the subscribed event type will ever fire for real. For example, `pool.assets_locked` is a planned event and is not yet implemented (it will never fire in production). Only `airdrop.failed` and `price.alert` are actively dispatched today. #### Inspect deliveries (admin dashboard feed) ``` diff --git a/openapi.yaml b/openapi.yaml index ace340f..9742c40 100644 --- a/openapi.yaml +++ b/openapi.yaml @@ -282,10 +282,8 @@ paths: example: url: https://example.com/webhooks/smartdrop events: - - airdrop.completed - - recipient.claimed - filters: - pool_id: pool_123 + - airdrop.failed + - price.alert secret: whsec_myverys3cretkey x-rate-limit: window: 60s @@ -301,10 +299,8 @@ paths: id: wh_a1b2c3d4e5f6g7h8 url: https://example.com/webhooks/smartdrop events: - - airdrop.completed - - recipient.claimed - filters: - pool_id: pool_123 + - airdrop.failed + - price.alert active: true secret_preview: whse...key created_at: "2026-06-27T12:00:00.000Z" @@ -346,7 +342,7 @@ paths: - id: wh_a1b2c3d4e5f6g7h8 url: https://example.com/webhooks/smartdrop events: - - airdrop.completed + - airdrop.failed active: true secret_preview: whse...key created_at: "2026-06-27T12:00:00.000Z" @@ -396,7 +392,7 @@ paths: id: wh_a1b2c3d4e5f6g7h8 url: https://example.com/webhooks/smartdrop events: - - airdrop.completed + - airdrop.failed active: false secret_preview: whse...key created_at: "2026-06-27T12:00:00.000Z" @@ -409,10 +405,15 @@ paths: /api/v1/webhooks/{id}/test: post: operationId: testWebhookEndpoint - summary: Send a test ping to a webhook endpoint + summary: Send a test event to a webhook endpoint description: | - Queues a test `ping` event delivery to the specified webhook endpoint. + Queues a test `pool.assets_locked` event delivery to the specified webhook endpoint. The delivery is processed asynchronously. + + ⚠️ **Warning:** A successful test delivery only confirms that the webhook URL can successfully + receive deliveries. It does not imply that the subscribed event type will ever fire for real, + as pool events are not yet implemented. Only `airdrop.failed` and `price.alert` are actively + dispatched today. tags: - Webhooks parameters: @@ -426,8 +427,8 @@ paths: window: 60s max: 10 responses: - "202": - description: Test ping queued for delivery + '202': + description: Test payload queued for delivery content: application/json: schema: @@ -439,10 +440,10 @@ paths: delivery: id: dlv_x1y2z3 endpoint_id: wh_a1b2c3d4e5f6g7h8 - event: ping + event: pool.assets_locked payload: - event: ping - timestamp: "2026-06-27T12:00:00.000Z" + event: pool.assets_locked + timestamp: '2026-06-27T12:00:00.000Z' status: pending attempt_count: 0 attempts: [] @@ -1347,8 +1348,30 @@ components: - airdrop.completed - airdrop.failed - recipient.claimed - - ping - description: SmartDrop lifecycle events + - pool.created + - pool.assets_locked + - pool.assets_unlocked + - pool.rewards_distributed + - pool.closed + - price.alert + - '*' + description: | + SmartDrop webhook events. + + **Active / implemented events:** + - `airdrop.failed` (fired when an airdrop expires) + - `price.alert` (fired when a price alert triggers) + + **Planned / not yet implemented events:** + - `airdrop.created` + - `airdrop.executing` + - `airdrop.completed` + - `recipient.claimed` + - `pool.created` + - `pool.assets_locked` + - `pool.assets_unlocked` + - `pool.rewards_distributed` + - `pool.closed` WebhookDelivery: type: object diff --git a/src/services/webhookEvents.js b/src/services/webhookEvents.js index 60fc545..325aaf8 100644 --- a/src/services/webhookEvents.js +++ b/src/services/webhookEvents.js @@ -1,5 +1,10 @@ 'use strict'; +// None of the pool events are actually dispatched by the codebase today (the +// event indexer only parses airdrop lifecycle events). They are registered +// here as forward-looking placeholders, but subscribing to them will not +// trigger any deliveries until the corresponding indexer and dispatch logic +// are implemented. const POOL_EVENTS = Object.freeze([ 'pool.created', 'pool.assets_locked',