Skip to content

README-documented pool.* webhook events are never dispatched anywhere in the codebase — subscriptions are permanently, silently dead #127

Description

@prodbycorne

Overview

README.md documents five pool.* webhook event types as real, subscribable events — with a worked example showing a client registering a webhook for exactly two of them — but no code path anywhere in this repository ever dispatches any pool.* event. Every webhook subscribed to a pool.* event type is permanently, silently dead: it will never receive a delivery, and nothing tells the subscriber that.

README.md documents:

| `pool.created` | A new farming pool was created on-chain |
| `pool.assets_locked` | Assets were locked into a pool |
| `pool.assets_unlocked` | Assets were unlocked from a pool |
| `pool.rewards_distributed` | Pool distributed rewards to participants |
| `pool.closed` | Pool was closed |

and gives this exact worked example for POST /webhooks:

{ "url": "...", "events": ["pool.assets_locked", "pool.rewards_distributed"], ... }

and documents the test endpoint as sending "a synthetic pool.assets_locked payload."

But webhookDispatcher.dispatch() — the only function anywhere in this codebase that actually delivers a webhook for a real event — is called from exactly one call site in the entire src/ tree:

$ grep -rn "dispatcher.dispatch\|webhookDispatcher.dispatch" src/
src/jobs/airdropExpiry.js:70:        await webhookDispatcher.dispatch({

...and that single call site dispatches only event_type: 'airdrop.failed'. No route, no job, and no part of the indexer ever calls dispatch() with any pool.* event type. Confirming further: src/indexer/eventParser.js's EVENT_FIELDS — the exhaustive list of on-chain contract events this indexer knows how to decode at all — only defines airdrop_created, recipient_added, token_claimed, airdrop_expired. There is no pool_created/pool_locked/etc. parsing logic anywhere in the current indexer; per its single commit message ("add soroban event indexer"), the indexer as it exists today was built exclusively around airdrop lifecycle events, even though historical (closed) issue #1 was originally titled "Create Event Indexer for Pool Lock/Unlock/Boost Events" — the indexer appears to have fully pivoted to airdrop-only events at some point, without webhookEvents.js's POOL_EVENTS registration or the README's documentation being updated to match.

This means: a real integrator, following the README's own worked example exactly (events: ["pool.assets_locked", "pool.rewards_distributed"]), registers a webhook that webhookCreateBodySchema's validation (webhookEvents.isValidSubscription) happily accepts as valid — because pool.assets_locked and pool.rewards_distributed are registered, known event names in webhookEvents.js's POOL_EVENTS — and then never receives a single delivery for it, ever, with the webhook sitting there indefinitely reporting no deliveries and no errors (there's nothing to error on, since nothing ever tries to deliver to it), giving no signal distinguishing "correctly configured, just no matching events have happened yet" from "this event type doesn't actually exist in this deployment."

Requirements

  • Update README.md's webhook events documentation to accurately reflect which event types have a real, working dispatch path today (currently: only airdrop.failed) — either remove the pool.* events from the documented/example payloads entirely, or clearly mark them as "planned, not yet implemented."
  • Either wire up real pool.* event dispatch (requires extending the indexer to parse pool-related on-chain events, which per the above does not currently exist at all, and adding the corresponding dispatch() calls) as a genuine feature, or remove POOL_EVENTS from webhookEvents.js's registered/subscribable event list until that work exists, so webhookCreateBodySchema validation doesn't accept subscriptions to event types that can never fire.
  • If keeping POOL_EVENTS registered as a forward-looking placeholder is intentional, at minimum follow the pattern already established for AIRDROP_EVENTS in the same file — that constant's comment explicitly says "Only 'airdrop.failed' is registered here... nothing in the codebase dispatches [the others] yet" — extend an equivalent, equally explicit comment to POOL_EVENTS, since today it has no such caveat at all and reads as fully live.
  • Update POST /webhooks/:id/test's documentation/behavior consideration: it sends a synthetic pool.assets_locked test payload specifically, which — given pool.assets_locked is otherwise entirely undeliverable — means the only way a subscriber to pool.assets_locked will ever see any payload from this event type is the synthetic test one, which could itself be misleading (a successful test delivery reasonably implies "this webhook is correctly wired up and will receive real events," which is not true here).

Acceptance Criteria

  • README's webhook events table and worked examples accurately reflect real, currently-dispatchable event types, or clearly flag which are not yet implemented.
  • webhookEvents.js's POOL_EVENTS either has dispatch support wired up (indexer parsing + dispatch() calls), or is removed from isValidSubscription's accepted set (with a migration/communication plan for any already-registered subscriptions, if this is a live production API), or carries the same explicit "not yet wired up" caveat comment AIRDROP_EVENTS already has for its own not-yet-implemented members.
  • The webhook test-endpoint documentation is updated to clarify that a successful test delivery does not imply the subscribed event type will ever fire for real, for any event type without a live dispatch path.

Additional Notes

More precise references

  • README.md:453-457,470,490,506,513: confirmed the pool.* events table, the worked POST /webhooks example subscribing to pool.assets_locked/pool.rewards_distributed, the test-endpoint description referencing a synthetic pool.assets_locked payload, and a sample delivery payload using pool.assets_locked as the example event type.
  • src/services/webhookDispatcher.js:155-178 (dispatch): confirmed this is the sole function that delivers a webhook for a real (non-test) event, and confirmed via grep -rn "dispatcher.dispatch\|webhookDispatcher.dispatch" src/ that it is called from exactly one location.
  • src/jobs/airdropExpiry.js:70-79: confirmed the one and only real dispatch() call site, with event_type: 'airdrop.failed' hardcoded.
  • src/indexer/eventParser.js:4-9 (EVENT_FIELDS): confirmed the complete list of decodable event names is airdrop_created, recipient_added, token_claimed, airdrop_expired — no pool-related event name appears anywhere in this file.
  • src/services/webhookEvents.js:3-9 (POOL_EVENTS) vs :13-20 (AIRDROP_EVENTS, with its explicit "only airdrop.failed... registering unused event names here would let a client subscribe to something that can never fire" comment): confirmed POOL_EVENTS carries no equivalent caveat, despite being in the identical situation (registered, validated, acceptable at subscription time, but with zero working dispatch path).
  • src/services/webhookDispatcher.js:180-191 (sendTest): confirmed the hardcoded const eventType = 'pool.assets_locked'; used for all test deliveries regardless of what the webhook is actually subscribed to.
  • Historical issue Create Event Indexer for Pool Lock/Unlock/Boost Events #1 (closed) — "Create Event Indexer for Pool Lock/Unlock/Boost Events" — confirmed via its title that pool events were the indexer's original intended scope, providing context for why POOL_EVENTS exists in webhookEvents.js at all despite the indexer having since pivoted entirely to airdrop events.

Additional edge cases

  • If any webhooks have already been registered in a real deployment with pool.* subscriptions (impossible to verify from source alone, but plausible given the README explicitly walks a new integrator through doing exactly that), removing POOL_EVENTS from the valid-subscription set would need either a migration step (deactivating or flagging those existing webhooks) or, at minimum, a clear operational note — simply changing validation going forward wouldn't retroactively inform anyone who already configured a dead subscription based on the README's current instructions.
  • Worth checking whether openapi.yaml documents pool.* events the same way README.md does (I did not find pool. references in openapi.yaml during this review, suggesting the OpenAPI spec and the README may already disagree with each other independent of this issue — worth a quick audit while fixing this, since whichever document is treated as authoritative should be updated consistently).

Test/reproduction plan

  • Register a webhook via POST /webhooks with events: ["pool.assets_locked"] exactly per the README's own example; confirm webhookCreateBodySchema accepts it (it does today).
  • Search the codebase (as done above) for any call to dispatch() with a pool.* event type; confirm there are none.
  • After a fix, either confirm a real dispatch path exists and is tested end-to-end (indexer → dispatch() → delivery), or confirm the schema rejects pool.* subscriptions with a clear error, or confirm the README/code comments now accurately flag the gap.

Cross-references

Metadata

Metadata

Assignees

Labels

GrantFox OSSIssue tracked in GrantFox OSSMaybe RewardedIssue may be eligible for a GrantFox rewardOfficial CampaignCampaign: Official CampaignOfficial Campaign | FWC26Campaign: Official Campaign | FWC26Third CampaignCampaign: Third CampaignbugSomething isn't workingdocumentationImprovements or additions to documentationvery hardExtremely hard — deep expertise, careful design, and significant time requiredwebhooksWebhook delivery and notification

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions