Skip to content

Alchemy Plugin#14

Merged
samholmes merged 5 commits intomainfrom
sam/alchemy-plugin
Feb 25, 2026
Merged

Alchemy Plugin#14
samholmes merged 5 commits intomainfrom
sam/alchemy-plugin

Conversation

@samholmes
Copy link
Copy Markdown
Contributor

@samholmes samholmes commented Jan 21, 2026

CHANGELOG

Does this branch warrant an entry to the CHANGELOG?

  • Yes
  • No

Dependencies

Description

none

Note

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 new WebhookRegistry to per-plugin handlers.

Introduces a new alchemy plugin that creates/updates/deletes Alchemy Address Activity webhooks, validates X-Alchemy-Signature via 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/webhookPort and alchemyAuthToken, adds express + types, bumps cleaner-config), and wires plugin construction through makeAllPlugins(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.

We're not yet using these, so why have them enabled right now.
@samholmes samholmes marked this pull request as ready for review February 11, 2026 01:35
Comment thread src/server.ts Outdated
Comment thread src/plugins/alchemy.ts Outdated
Comment thread package.json Outdated
Copy link
Copy Markdown
Member

@paullinator paullinator left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)

  1. Swallowed exceptions in webhookRegistry return 200 OK to Alchemy, preventing retries
  2. Race condition in concurrent processBatchedChanges() — can create duplicate webhooks
  3. No retry backoff — tight 1-second loop hammering Alchemy API during outages
  4. destroy() doesn't cancel in-flight batch processing — can re-create timers on destroyed plugin

Critical Test Gap (1)

  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)

  1. Initialization failure permanently prevents webhook discovery
  2. Concurrent signing key recovery race condition (no promise deduplication)
  3. 14 concurrent getTeamWebhooks() API calls on startup
  4. Unsafe (undefined as unknown) as T type casts in API client

Suggestions (2)

  1. Cross-cased subscribe/unsubscribe can leave phantom entries in pending arrays
  2. parseInt on 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.

Comment thread src/util/webhookRegistry.ts
Comment thread src/plugins/alchemy.ts
Comment thread src/plugins/alchemy.ts
Comment thread src/plugins/alchemy.ts
Comment thread src/plugins/alchemy.ts
Comment thread src/plugins/alchemy.ts
Comment thread src/util/alchemyNotifyApi.ts Outdated
Comment thread src/plugins/alchemy.ts Outdated
Comment thread src/plugins/alchemy.ts Outdated
Comment thread test/plugins/alchemy.test.ts
Comment thread src/plugins/allPlugins.ts Outdated
Comment thread src/plugins/alchemy.ts
Comment thread src/plugins/alchemy.ts
Comment thread src/plugins/allPlugins.ts Outdated
Comment thread src/plugins/allPlugins.ts Outdated
Comment on lines +133 to +134
pluginId: 'solana',
network: 'SOLANA_MAINNET',
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this tested compatible with Solana? We do const normalizedAddress = address.toLowerCase(), which doesn't work with base58.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread src/plugins/alchemy.ts
const logger: Logger = makeLogger('alchemy', pluginId)

// Track subscribed addresses (normalized lowercase address -> original address)
const subscribedAddresses = new Map<string, string>()
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As mentioned, this normalization doesn't really work for base58 addresses. Can we come up with a design that doesn't require normalization?

Comment thread src/services.ts Outdated
// Create singleton instances
export const notifyApi = makeAlchemyNotifyApi()
export const signingKeyStore = makeSigningKeyStore({ notifyApi })
export const webhookRegistry = makeWebhookRegistry()
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread src/server.ts Outdated
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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"

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

worker.ts would be better. The file contains a websocket server and http server for the webhook.

Copy link
Copy Markdown

@cursor cursor bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread src/plugins/alchemy.ts
initializingPromise = doInitialize()
await initializingPromise
initializingPromise = null
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)

Fix in Cursor Fix in Web

Comment thread test/plugins/alchemy.test.ts
Copy link
Copy Markdown
Contributor

@swansontec swansontec left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One change, but I'm approving because it's trivial.

Comment thread src/index.ts
Comment on lines -71 to -74
main().catch(error => {
logger.error({ err: error }, 'main error')
process.exit(1)
})
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would leave this catch statement in place, since "the buck starts here". This is our last chance to log if anything goes wrong.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The function changed from async to sync

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.
@samholmes samholmes merged commit 7af5274 into main Feb 25, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants