Skip to content

Commit b0f1a7f

Browse files
committed
fix: singleton SecretManagerServiceClient + governor address guard + docs cleanup
## Root cause fix Every call to getSecret() instantiated a new SecretManagerServiceClient, opening a gRPC channel that was never closed. ~169 requests over 28 minutes → 488 MiB OOM. Fix: singleton client at module level. Secret values are NOT cached (would break rotation on warm instances). ## Governor address guard QuickNode evmAbiFilter ignores the 'contracts' field in templateArgs (silently, empirically confirmed). Added MENTO_GOVERNOR_ADDRESS guard in process-event.ts so events from other OZ Governor contracts on Celo don't produce false notifications. ## Deploy script fixes - Global TMP_FILES array + single EXIT trap (fixes trap overwrite on multiple deploy_webhook calls) - Code comment explaining why templateArgs uses 'abiJson' not 'abi' (spec says 'abi', live evmAbiFilterGo endpoint requires 'abiJson') - Comment explaining hardcoded webhook UUIDs and how to update them - Updated Terraform filter_function blobs to current trimmed ABIs ## Docs & cleanup - Delete bin/update-quicknode-filter.js (zombie script, never worked) - Remove dead dev:webhook:* npm scripts - Consolidate quicknode.tf comment: UPDATE=script, RECREATE=Terraform - Rewrite README + ADDING_EVENTS.md to reflect actual deploy workflow ## Tests - 5 unit tests for get-secret.ts (singleton behavior, no caching, errors) - 7 unit tests for process-event.ts governor address guard (canonical address, wrong address, all 4 event types, mixed-case, MedianUpdated) - npm test now runs vitest before integration tests
1 parent 7e63af8 commit b0f1a7f

16 files changed

+1889
-270
lines changed

ADDING_EVENTS.md

Lines changed: 25 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -55,17 +55,20 @@ function main(data) {
5555
}
5656
```
5757

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

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

64-
# For oracle/sorted-oracles events
65-
npm run dev:webhook:healthcheck
65+
# Deploy both
66+
./bin/deploy-quicknode-filter.sh
6667
```
6768

68-
This watches for changes to your filter function file and automatically base64-encodes it into [`infra/quicknode.tf`](infra/quicknode.tf).
69+
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).
70+
71+
> **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.
6972
7073
#### 3. Test the Filter Function with Real Blockchain Data
7174

@@ -104,36 +107,29 @@ Once your filter function works correctly:
104107

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

107-
#### 5. Clean Up and Migrate to Terraform
110+
#### 5. Clean Up and Make Permanent
108111

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

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

120-
filter_function = base64encode(file("${path.module}/quicknode-filter-functions/your-filter.js"))
116+
```js
117+
/*
118+
template: evmAbiFilter
119+
abi: [{...your trimmed ABI events...}]
120+
contracts: 0xYourContractAddress
121+
*/
122+
```
121123

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

128-
1. **Deploy the webhook via Terraform**:
126+
3. **Deploy to the permanent webhook** via:
129127

130-
```bash
131-
cd infra
132-
terraform plan # Review changes
133-
terraform apply # Create the webhook
134-
```
128+
```bash
129+
./bin/deploy-quicknode-filter.sh --webhook <healthcheck|governor>
130+
```
135131

136-
**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).
132+
> **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`.
137133
138134
### Part 2: Implement TypeScript Event Handler
139135

README.md

Lines changed: 23 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -209,55 +209,41 @@ The centralized event system makes adding new events straightforward—just upda
209209

210210
## Developing QuickNode Webhook Filter Functions
211211

212-
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.
212+
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/).
213+
214+
> **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`.
213215
214216
### Workflow
215217

216-
1. [OPTIONAL] If you want to first double-check which code is actually deployed right now:
217-
- Navigate to the [QuickNode Webhooks Dashboard](https://dashboard.quicknode.com/webhooks)
218-
- Click the Webhook you're developing
219-
- Copy the webhook ID from the URL into your clipboard
220-
- Obtain the current filter function via:
218+
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).
221219

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

231-
2. **Open the filter function** in plain JavaScript at [`infra/quicknode-filter-functions/sorted-oracles.js`](./infra/quicknode-filter-functions/sorted-oracles.js)
222+
```js
223+
/*
224+
template: evmAbiFilter
225+
abi: [{...}]
226+
contracts: 0xYourContractAddress
227+
*/
228+
```
232229

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

235232
```sh
236-
# Run the cmd for the webhook you're interested in
237-
npm run dev:webhook:healthcheck
233+
./bin/deploy-quicknode-filter.sh --webhook healthcheck # SortedOracles
234+
./bin/deploy-quicknode-filter.sh --webhook governor # MentoGovernor
235+
./bin/deploy-quicknode-filter.sh # both
238236
```
239237

240-
This will watch for changes to `sorted-oracles.js` (which we're using as the healthcheck) and automatically:
241-
- Base64 encode the updated function
242-
- Update the `filter_function` field in the `quicknode_webhook_healthcheck` resource in `quicknode.tf`
243-
- Create a timestamped backup of `quicknode.tf` before making changes
244-
245-
4. **Make your changes** to `sorted-oracles.js` and save the file. The script will automatically update `quicknode.tf`.
246-
247-
5. **Review the changes** with `git diff infra/quicknode.tf` to ensure the filter function was updated correctly.
238+
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).
248239

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

251-
```sh
252-
cd infra
253-
terraform plan # Review changes
254-
terraform apply # Deploy
242+
```bash
243+
curl -X GET "https://api.quicknode.com/webhooks/rest/v1/webhooks/$webhook_id" \
244+
-H "x-api-key: $quicknode_api_key"
255245
```
256246

257-
**⚠️ Important:** QuickNode rejects updates to active webhooks. You must either:
258-
- 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)
259-
- OR comment out the webhook resource, `terraform apply` to delete it, uncomment with your changes, and `terraform apply` to recreate it
260-
261247
### Filter Function Structure
262248

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

256+
> **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.
257+
270258
See the [QuickNode Webhooks documentation](https://www.quicknode.com/docs/webhooks/getting-started) for more details on filter function syntax.
271259

272260
## Deploying from Scratch

bin/deploy-quicknode-filter.sh

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

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

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

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

39+
# Global array to track temp files created by deploy_webhook invocations.
40+
# A single EXIT trap at script level cleans them all up, avoiding the problem
41+
# of per-call traps overwriting the previous trap registration.
42+
TMP_FILES=()
43+
cleanup_temp_files() {
44+
if ((${#TMP_FILES[@]} > 0)); then
45+
rm -f "${TMP_FILES[@]}"
46+
fi
47+
}
48+
trap cleanup_temp_files EXIT
49+
3650
# ------------------------------------------------------------------------------
3751
log() { printf '\n\033[1m%s\033[0m\n' "$*"; }
3852
success() { printf '✅ %s\n' "$*"; }
@@ -96,9 +110,7 @@ deploy_webhook() {
96110
# The internal template ID for PATCH is "evmAbiFilterGo" (evmAbiFilter is the display name).
97111
local payload_file
98112
payload_file=$(mktemp /tmp/qn_payload.XXXXXX.json)
99-
# Ensure temp file is always cleaned up, even on SIGINT/SIGTERM
100-
# shellcheck disable=SC2064
101-
trap "rm -f '${payload_file}'" EXIT
113+
TMP_FILES+=("${payload_file}")
102114

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

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

131-
# Update via template endpoint — internal name is evmAbiFilterGo (evmAbiFilter is display name)
132-
# No pause/unpause needed for template updates.
143+
# Update via template endpoint.
144+
# NOTE on field names vs the OpenAPI spec:
145+
# The public OpenAPI spec (evmAbiFilter schema) lists the field as "abi".
146+
# The actual live endpoint (evmAbiFilterGo) requires "abiJson" — empirically
147+
# confirmed: sending "abi" returns a 500, sending "abiJson" succeeds.
148+
# The display name in the UI is "evmAbiFilter"; the internal PATCH path uses
149+
# "evmAbiFilterGo". Both discrepancies are QuickNode API inconsistencies.
150+
# No pause/unpause needed — template updates are applied hot.
133151
log "Updating template args via /template/evmAbiFilterGo endpoint..."
134152
local update_response
135153
update_response=$(curl_api -X PATCH "${QN_API_BASE}/${webhook_id}/template/evmAbiFilterGo" \

bin/update-quicknode-filter.js

Lines changed: 0 additions & 148 deletions
This file was deleted.

0 commit comments

Comments
 (0)