Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .mlc_config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"ignorePatterns": [
{
"pattern": "^https://celoscan\\.io"
}
]
}
54 changes: 25 additions & 29 deletions ADDING_EVENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,17 +55,20 @@ function main(data) {
}
```

**Enable hot-reload** to automatically update the filter function in Terraform:
**Deploy the updated filter to QuickNode** using the deploy script:

```bash
# For governance events
npm run dev:webhook:governor
# Deploy a specific webhook
./bin/deploy-quicknode-filter.sh --webhook healthcheck # SortedOracles
./bin/deploy-quicknode-filter.sh --webhook governor # MentoGovernor

# For oracle/sorted-oracles events
npm run dev:webhook:healthcheck
# Deploy both
./bin/deploy-quicknode-filter.sh
```

This watches for changes to your filter function file and automatically base64-encodes it into [`infra/quicknode.tf`](infra/quicknode.tf).
The deploy script reads the ABI and contract addresses from the `/* template: evmAbiFilter ... */` comment header at the top of each filter file and applies the update live via `PATCH /webhooks/{id}/template/evmAbiFilterGo` (no downtime required).

> **Note:** The old `npm run dev:webhook:*` scripts and `bin/update-quicknode-filter.js` are legacy — they updated `infra/quicknode.tf` which is never applied to live webhooks (`ignore_all_server_changes = true`). Use `deploy-quicknode-filter.sh` instead.

#### 3. Test the Filter Function with Real Blockchain Data

Expand Down Expand Up @@ -104,36 +107,29 @@ Once your filter function works correctly:

**Pro tip:** Using real data ensures your fixture accurately represents what QuickNode will send in production.

#### 5. Clean Up and Migrate to Terraform
#### 5. Clean Up and Make Permanent

1. **Delete the temporary webhook** from the [QuickNode Webhooks Dashboard](https://dashboard.quicknode.com/webhooks)
1. **Write Terraform code** in [`infra/quicknode.tf`](infra/quicknode.tf) to automate webhook creation:
1. **Delete the temporary test webhook** from the [QuickNode Webhooks Dashboard](https://dashboard.quicknode.com/webhooks) (the one you created manually for testing).

```hcl
resource "quicknode_webhook" "your_event_webhook" {
name = "Your Event Webhook"
url = google_cloudfunctions2_function.watchdog_notifications.url
network = "celo-mainnet"
dataset = "block"
enabled = true
2. **Update the filter file comment header** with your final ABI and contract address:

filter_function = base64encode(file("${path.module}/quicknode-filter-functions/your-filter.js"))
```js
/*
template: evmAbiFilter
abi: [{...your trimmed ABI events...}]
contracts: 0xYourContractAddress
*/
```

headers = {
"X-AUTH-TOKEN" = var.x_auth_token
}
}
```
Keep only the events your handler actually uses — this reduces Cloud Function invocation volume.

1. **Deploy the webhook via Terraform**:
3. **Deploy to the permanent webhook** via:

```bash
cd infra
terraform plan # Review changes
terraform apply # Create the webhook
```
```bash
./bin/deploy-quicknode-filter.sh --webhook <healthcheck|governor>
```

**Note:** If updating an existing webhook's filter function, you may need to pause it first or destroy and recreate it (see [README](README.md#workflow) for details).
> **Note:** Terraform (`infra/quicknode.tf`) manages webhook _creation_ but cannot update filter configuration on existing webhooks (`ignore_all_server_changes = true`). All filter updates go through `bin/deploy-quicknode-filter.sh`.

### Part 2: Implement TypeScript Event Handler

Expand Down
58 changes: 23 additions & 35 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -209,55 +209,41 @@ The centralized event system makes adding new events straightforward—just upda

## Developing QuickNode Webhook Filter Functions

QuickNode webhooks use [JavaScript filter functions](https://www.quicknode.com/docs/streams/filters?#example-filter-functions) that run on QuickNode's servers to determine which blockchain events should trigger notifications to our Cloud Function. These filters are base64-encoded and stored in [`infra/quicknode.tf`](./infra/quicknode.tf) under the `filter_function` properties.
QuickNode webhooks use the `evmAbiFilter` template to match blockchain events by ABI event signature. Filter configuration (ABI and contract addresses) lives in the comment header of each filter file under [`infra/quicknode-filter-functions/`](./infra/quicknode-filter-functions/).

> **Note:** The `filter_function` base64 blobs in `infra/quicknode.tf` and the old `bin/update-quicknode-filter.js` script are **legacy artefacts**. They are not deployed — `ignore_all_server_changes = true` in Terraform prevents Terraform from pushing any changes to existing webhooks. All deploys go through `bin/deploy-quicknode-filter.sh`.

### Workflow

1. [OPTIONAL] If you want to first double-check which code is actually deployed right now:
- Navigate to the [QuickNode Webhooks Dashboard](https://dashboard.quicknode.com/webhooks)
- Click the Webhook you're developing
- Copy the webhook ID from the URL into your clipboard
- Obtain the current filter function via:
1. **Edit the filter file** at [`infra/quicknode-filter-functions/sorted-oracles.js`](./infra/quicknode-filter-functions/sorted-oracles.js) or [`governor.js`](./infra/quicknode-filter-functions/governor.js).

```bash
curl -X GET \
"https://api.quicknode.com/webhooks/rest/v1/webhooks/$webhook_id" \
-H "accept: application/json" \
-H "x-api-key: $quicknode_api_key" \
| jq -r .filter_function \
| base64 -d
```
The comment header at the top of each file is the deployment source of truth:

2. **Open the filter function** in plain JavaScript at [`infra/quicknode-filter-functions/sorted-oracles.js`](./infra/quicknode-filter-functions/sorted-oracles.js)
```js
/*
template: evmAbiFilter
abi: [{...}]
contracts: 0xYourContractAddress
*/
```

3. **Enable hot-reload** to automatically update the Terraform file:
2. **Deploy to QuickNode** using the deploy script:

```sh
# Run the cmd for the webhook you're interested in
npm run dev:webhook:healthcheck
./bin/deploy-quicknode-filter.sh --webhook healthcheck # SortedOracles
./bin/deploy-quicknode-filter.sh --webhook governor # MentoGovernor
./bin/deploy-quicknode-filter.sh # both
```

This will watch for changes to `sorted-oracles.js` (which we're using as the healthcheck) and automatically:
- Base64 encode the updated function
- Update the `filter_function` field in the `quicknode_webhook_healthcheck` resource in `quicknode.tf`
- Create a timestamped backup of `quicknode.tf` before making changes

4. **Make your changes** to `sorted-oracles.js` and save the file. The script will automatically update `quicknode.tf`.

5. **Review the changes** with `git diff infra/quicknode.tf` to ensure the filter function was updated correctly.
This reads the ABI and contract addresses from the comment header, builds the `templateArgs` payload, and calls `PATCH /webhooks/{id}/template/evmAbiFilterGo` to apply the update live (no downtime).

6. **Deploy to QuickNode**:
3. **Verify** by checking the [QuickNode Webhooks Dashboard](https://dashboard.quicknode.com/webhooks) or inspecting the raw webhook via:

```sh
cd infra
terraform plan # Review changes
terraform apply # Deploy
```bash
curl -X GET "https://api.quicknode.com/webhooks/rest/v1/webhooks/$webhook_id" \
-H "x-api-key: $quicknode_api_key"
```

**⚠️ Important:** QuickNode rejects updates to active webhooks. You must either:
- Pause the webhook first (set `status = "paused"` in the resource, run `terraform apply`, then update the filter function, then set `status = "active"` and `terraform apply` again)
- OR comment out the webhook resource, `terraform apply` to delete it, uncomment with your changes, and `terraform apply` to recreate it

### Filter Function Structure

The filter functions follow QuickNode's `evmAbiFilter` template:
Expand All @@ -267,6 +253,8 @@ The filter functions follow QuickNode's `evmAbiFilter` template:
- They can include custom filtering logic (e.g., filtering by specific token addresses)
- They return matching events or `null` if no matches found

> **Important quirk:** The `evmAbiFilter` template filters by topic hash only — the `contracts` address filter in `templateArgs` is currently **silently ignored** by QuickNode's API. All addresses that emit a matching event signature will trigger the webhook. Address filtering must be done in the Cloud Function handler.

See the [QuickNode Webhooks documentation](https://www.quicknode.com/docs/webhooks/getting-started) for more details on filter function syntax.

## Deploying from Scratch
Expand Down
36 changes: 27 additions & 9 deletions bin/deploy-quicknode-filter.sh
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,16 @@ set -euo pipefail
# =============================================================================
# DEPLOY QUICKNODE FILTER FUNCTIONS
#
# QuickNode's API rejects PATCH requests to active webhooks, so Terraform
# can't manage filter_function updates via normal `terraform apply`.
# This script handles the pause → update → activate lifecycle manually.
# Webhooks use the evmAbiFilter template. Updates are applied live via:
# PATCH /webhooks/{id}/template/evmAbiFilterGo
# No pause or downtime needed — template arg updates take effect immediately.
#
# Usage:
# ./bin/deploy-quicknode-filter.sh [--webhook healthcheck|governor|all]
#
# Prerequisites:
# - gcloud CLI authenticated with access to the governance-watchdog project
# - curl, base64, python3 available
# - curl, python3 available
# - QuickNode API key stored in GCP Secret Manager as "quicknode-api-key"
# =============================================================================

Expand All @@ -28,11 +28,25 @@ REPO_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)"
FILTER_DIR="${REPO_ROOT}/infra/quicknode-filter-functions"

# Webhook IDs (from QuickNode API)
# Webhook IDs are server-assigned by QuickNode and will change if webhooks are deleted and recreated.
# To find current IDs: curl -s -H "x-api-key: <key>" https://api.quicknode.com/webhooks/rest/v1/webhooks | jq '.data[] | {id, name}'
# Or check the URL when viewing a webhook in the QuickNode dashboard.
HEALTHCHECK_WEBHOOK_ID="dc35c3c4-b839-49f6-836b-6ffb7c087419"
GOVERNOR_WEBHOOK_ID="73a99141-e8cb-411a-9732-c42a031cebe6"

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

These UUIDs are now part of the deployment contract, but they are not stable across terraform recreate/bootstrap flows because QuickNode assigns them on creation. After the next destroy/recreate, this script will PATCH the wrong webhook unless it resolves IDs dynamically (for example from Terraform state or by querying the API by name) instead of hardcoding them here.

QN_API_BASE="https://api.quicknode.com/webhooks/rest/v1/webhooks"

# Global array to track temp files created by deploy_webhook invocations.
# A single EXIT trap at script level cleans them all up, avoiding the problem
# of per-call traps overwriting the previous trap registration.
TMP_FILES=()
cleanup_temp_files() {
if ((${#TMP_FILES[@]} > 0)); then
rm -f "${TMP_FILES[@]}"
fi
}
trap cleanup_temp_files EXIT

# ------------------------------------------------------------------------------
log() { printf '\n\033[1m%s\033[0m\n' "$*"; }
success() { printf '✅ %s\n' "$*"; }
Expand Down Expand Up @@ -96,9 +110,7 @@ deploy_webhook() {
# The internal template ID for PATCH is "evmAbiFilterGo" (evmAbiFilter is the display name).
local payload_file
payload_file=$(mktemp /tmp/qn_payload.XXXXXX.json)
# Ensure temp file is always cleaned up, even on SIGINT/SIGTERM
# shellcheck disable=SC2064
trap "rm -f '${payload_file}'" EXIT
TMP_FILES+=("${payload_file}")

# Build templateArgs payload: abiJson must be a raw JSON string (not a parsed object).
# Use env vars to avoid shell quoting issues with large ABI strings.
Expand Down Expand Up @@ -128,8 +140,14 @@ with open(os.environ["QN_PAYLOAD_FILE"], "w") as f:

info "Contracts: $(python3 -c "import json; d=json.load(open('${payload_file}')); print(', '.join(d['templateArgs']['contracts']))")"

# Update via template endpoint — internal name is evmAbiFilterGo (evmAbiFilter is display name)
# No pause/unpause needed for template updates.
# Update via template endpoint.
# NOTE on field names vs the OpenAPI spec:
# The public OpenAPI spec (evmAbiFilter schema) lists the field as "abi".
# The actual live endpoint (evmAbiFilterGo) requires "abiJson" — empirically
# confirmed: sending "abi" returns a 500, sending "abiJson" succeeds.
# The display name in the UI is "evmAbiFilter"; the internal PATCH path uses
# "evmAbiFilterGo". Both discrepancies are QuickNode API inconsistencies.
# No pause/unpause needed — template updates are applied hot.
log "Updating template args via /template/evmAbiFilterGo endpoint..."
local update_response
update_response=$(curl_api -X PATCH "${QN_API_BASE}/${webhook_id}/template/evmAbiFilterGo" \
Expand Down
148 changes: 0 additions & 148 deletions bin/update-quicknode-filter.js

This file was deleted.

Loading
Loading