Conversation
c4c3bd2 to
f58d44d
Compare
We're not yet using these, so why have them enabled right now.
f58d44d to
3cb4b1a
Compare
paullinator
left a comment
There was a problem hiding this comment.
Review Summary
The overall architecture is well-designed — clean separation between plugin, API client, signing key store, and webhook registry. The timing-safe HMAC signature validation is solid, and the debounced batch address management is a good approach.
However, there are several concurrency and error handling issues that could cause silent data loss in production:
Critical Issues (4)
- Swallowed exceptions in webhookRegistry return 200 OK to Alchemy, preventing retries
- Race condition in concurrent
processBatchedChanges()— can create duplicate webhooks - No retry backoff — tight 1-second loop hammering Alchemy API during outages
destroy()doesn't cancel in-flight batch processing — can re-create timers on destroyed plugin
Critical Test Gap (1)
- Batch processing is never exercised in tests — fake timers are never advanced, so the most important code path (Alchemy API integration) has zero test coverage. Signature validation rejection paths are also untested.
Warnings (4)
- Initialization failure permanently prevents webhook discovery
- Concurrent signing key recovery race condition (no promise deduplication)
- 14 concurrent
getTeamWebhooks()API calls on startup - Unsafe
(undefined as unknown) as Ttype casts in API client
Suggestions (2)
- Cross-cased subscribe/unsubscribe can leave phantom entries in pending arrays
parseInton hex block number can produce NaN checkpoint
Note: Cursor Bugbot already flagged the cluster multi-worker affinity issue, network-check-before-auth ordering, and @types/express placement — those are omitted from this review.
See inline comments for specific details and recommended fixes.
3cb4b1a to
153c913
Compare
| pluginId: 'solana', | ||
| network: 'SOLANA_MAINNET', |
There was a problem hiding this comment.
Is this tested compatible with Solana? We do const normalizedAddress = address.toLowerCase(), which doesn't work with base58.
There was a problem hiding this comment.
Client-side we have no chagne-server integration with Solana. No testing done with Solana, but I'll make sure to make normalization an option for the plugins rather then default.
| const logger: Logger = makeLogger('alchemy', pluginId) | ||
|
|
||
| // Track subscribed addresses (normalized lowercase address -> original address) | ||
| const subscribedAddresses = new Map<string, string>() |
There was a problem hiding this comment.
As mentioned, this normalization doesn't really work for base58 addresses. Can we come up with a design that doesn't require normalization?
| // Create singleton instances | ||
| export const notifyApi = makeAlchemyNotifyApi() | ||
| export const signingKeyStore = makeSigningKeyStore({ notifyApi }) | ||
| export const webhookRegistry = makeWebhookRegistry() |
There was a problem hiding this comment.
I am not really a fan of this design, especially now that we have multiple process and IPC involved. I would rather have these things be dependency-injected into whoever needs them. This may involve some re-shuffling, but mostly in setup code.
For instance, export const allPlugins: AddressPlugin[] = [...] might need to turn into an export const getPlugins: ({ services... }) => ([...]). Then the various top-level processes can be in charge of setting up singletons and passing them in where they belong. This would also get rid of the need for await import('./server'), as the server would no longer have global state floating around.
There was a problem hiding this comment.
Each process gets it's own singleton. The coupling of these services to the rest of the app is simpler to reason about (we're only having one instance of each service as part of the worker process anyway). I can see how dep-injection could make sense here though.
If we want to remove await import('./server') then we'd also have to call a server() function that is exported form ./server.ts.
There was a problem hiding this comment.
I think "server.ts" is a weird name. Which server is this? The websocket server for the core, or the webhook server for Alchemy? I'd like to have it named "websocketServer.ts"
There was a problem hiding this comment.
worker.ts would be better. The file contains a websocket server and http server for the webhook.
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 2 potential issues.
Bugbot Autofix is OFF. To automatically fix reported issues with Cloud Agents, enable Autofix in the Cursor dashboard.
| initializingPromise = doInitialize() | ||
| await initializingPromise | ||
| initializingPromise = null | ||
| } |
There was a problem hiding this comment.
Initialization promise never reset on failure
High Severity
If doInitialize() rejects, the await on line 95 throws and line 96 (initializingPromise = null) is never reached. The rejected promise stays cached in initializingPromise. Every subsequent call to initialize() hits the initializingPromise != null check on line 92 and immediately re-throws the stale error. This permanently prevents the plugin from initializing, defeating the retry logic in processBatchedChanges. Compare with recoverSigningKeys in signingKeyStore.ts which correctly wraps the await in try/finally.
Additional Locations (1)
swansontec
left a comment
There was a problem hiding this comment.
One change, but I'm approving because it's trivial.
| main().catch(error => { | ||
| logger.error({ err: error }, 'main error') | ||
| process.exit(1) | ||
| }) |
There was a problem hiding this comment.
I would leave this catch statement in place, since "the buck starts here". This is our last chance to log if anything goes wrong.
There was a problem hiding this comment.
The function changed from async to sync
f0848cf to
566d67c
Compare
Implement a new plugin that uses Alchemy's Address Activity webhooks for real-time address monitoring on supported EVM chains. Features: - Batched address subscription updates (1s debounce) - HMAC-SHA256 webhook signature validation - Case-insensitive address matching with original case preservation - Support for all EVM-based that we support.
566d67c to
b3d41ce
Compare


CHANGELOG
Does this branch warrant an entry to the CHANGELOG?
Dependencies
Description
noneNote
High Risk
Adds a new externally reachable webhook HTTP surface and signature validation/auth logic, plus cluster-wide IPC relaying; failures here could drop or misroute activity updates or weaken webhook authentication.
Overview
Adds an Alchemy-based address activity pipeline: workers now host an Express HTTP endpoint for
/webhook/*(with GET/HEAD health checks) and dispatch incoming webhooks through a newWebhookRegistryto per-plugin handlers.Introduces a new
alchemyplugin that creates/updates/deletes Alchemy Address Activity webhooks, validatesX-Alchemy-Signaturevia HMAC using recovered signing keys, debounces subscription address updates, and broadcasts webhook activity across cluster workers via IPC so all workers emit updates consistently.Updates configuration/deps to support this (new
webhookHost/webhookPortandalchemyAuthToken, addsexpress+ types, bumpscleaner-config), and wires plugin construction throughmakeAllPlugins(opts)with Alchemy enabled for multiple EVM networks plus Solana; includes comprehensive Jest tests for the plugin behavior and signature/auth paths.Written by Cursor Bugbot for commit b3d41ce. This will update automatically on new commits. Configure here.