Skip to content

Webhook signing, usage-event compaction, timelocked emergency withdrawal, discount codes - #719

Open
frienzy514-png wants to merge 3 commits into
Dev-AdeTutu:mainfrom
frienzy514-png:feat/issues-685-686-687-688
Open

Webhook signing, usage-event compaction, timelocked emergency withdrawal, discount codes#719
frienzy514-png wants to merge 3 commits into
Dev-AdeTutu:mainfrom
frienzy514-png:feat/issues-685-686-687-688

Conversation

@frienzy514-png

Copy link
Copy Markdown

Summary

Implements the 4 issues assigned to me on this repo:

#688 — Webhook signing (backend/)

  • Every webhook registered via POST /api/webhooks/low-balance now gets a per-provider HMAC signing secret (auto-generated if not supplied, returned once in the response).
  • Outbound deliveries (registry fireWebhook/retry queue/shutdown drain, and the legacy PROVIDER_WEBHOOK_URL path) now carry X-Signature-256: sha256=<hmac>, with a global WEBHOOK_SECRET env var as fallback.
  • Fixed a bug where registry-fired webhooks silently never fired unless the legacy PROVIDER_WEBHOOK_URL env var also happened to be set.
  • New backend/src/lib/webhookSignature.ts; verification steps documented in backend/API.md.

#685 — Usage-event retention/compaction (backend/)

  • compactUsageEvents(): rolls submitted events older than 90 days into a new usage_summary table (per (date, meter_id)), archives events older than 365 days to a gzipped JSONL file before deleting them, leaves pending/failed events untouched, and runs VACUUM.
  • Scheduled daily at 02:00 UTC (startUsageCompactionWorker); also exposed via POST /api/usage-events/compact (run on demand) and GET /api/usage-events/summary (query the rollup).

#686 — Timelocked emergency withdrawal (contracts/)

  • emergency_withdraw(amount, recipient) is now a two-step, timelocked flow: first call announces and starts a 48h timelock; calling again with the same args after the timelock elapses executes the transfer (capped at the contract's balance).
  • Caps the withdrawable amount at cumulative revenue ever collected (TOTAL_REVENUE), so a compromised admin key can't drain more than customers actually paid in.
  • Adds cancel_emergency_withdrawal and get_pending_emergency_withdrawal.

#687 — Promotional discount codes (contracts/)

  • admin_create_discount / admin_revoke_discount / get_discount / is_discount_valid.
  • make_payment_with_discount(meter_id, payer, amount, plan, code) validates the code and charges amount * (100 - discount_pct) / 100 through the same payment path as make_payment.

Testing

  • Backend: new integration tests (backend/tests/usage-event-compaction.integration.test.ts, updated backend/tests/mqtt-webhook.integration.test.ts) — all pass under npm test. Also repaired pre-existing wrong-arity registerWebhook calls in the webhook integration suite so it actually exercises the signing path (was silently broken before this PR).
  • Contracts: cargo check (library) and cargo build --target wasm32v1-none --release are clean. Note: contracts/solar_grid's #[cfg(test)] mod tests block does not currently compile on main — 191 pre-existing errors (mostly Symbol passed where meter_id: String is now expected, plus a ContractEvents::iter API drift), unrelated to this PR. New tests for both contract features were added and confirmed to introduce zero new compiler errors (191 before and after, cross-referenced by line range against cargo check --tests output), but can't be run end-to-end until that pre-existing breakage is fixed separately.

Every webhook registered via POST /api/webhooks/low-balance now gets a
per-provider signing secret (generated automatically if not supplied,
returned exactly once in the registration response). Outbound deliveries
from the webhook registry (fireWebhook / retry queue / shutdown drain) and
the legacy PROVIDER_WEBHOOK_URL path now carry an
`X-Signature-256: sha256=<hmac>` header, computed as
HMAC-SHA256(secret, rawBody), with a global WEBHOOK_SECRET env var as
fallback.

Also fixes a bug in checkAndNotifyLowBalance() where registry-fired
webhooks silently never fired unless the legacy PROVIDER_WEBHOOK_URL env
var happened to also be set.

Adds backend/src/lib/webhookSignature.ts with the shared sign/verify
helpers, documents verification steps in backend/API.md, and repairs the
mqtt-webhook.integration.test.ts webhook-registry tests (wrong call arity,
non-functional cross-test cleanup) so they actually exercise the signing
path.

Closes Dev-AdeTutu#688
…n job

usage_events grows ~1KB/event and reaches multiple GB within months at
fleet scale. Adds compactUsageEvents() (Closes Dev-AdeTutu#685):

- Keeps detailed 'submitted' rows for USAGE_DETAIL_RETENTION_DAYS (90)
- Rolls older rows into a new usage_summary table, aggregated per
  (date, meter_id) — total_units, total_cost, event_count — via a single
  SQL upsert so repeated runs accumulate correctly instead of overwriting
- Archives rows older than USAGE_ARCHIVE_RETENTION_DAYS (365) to a gzipped
  JSONL file under USAGE_ARCHIVE_DIR before deleting them, so raw records
  survive even though they leave the live DB (point the dir at an
  s3fs/gcsfuse mount to treat it as real cold storage)
- Leaves 'pending'/'failed' events alone regardless of age so retry/replay
  keeps working
- Runs VACUUM after deleting rows to reclaim disk space
- Scheduled daily at 02:00 UTC via startUsageCompactionWorker(), wired into
  server startup alongside the existing retry worker

Also adds POST /api/usage-events/compact (run on demand) and
GET /api/usage-events/summary (query the rollup), both admin-authed, plus
two Prometheus counters and a real-SQLite integration test suite.

Closes Dev-AdeTutu#685
Closes Dev-AdeTutu#686 — emergency admin fund withdrawal function:

- Reworks emergency_withdraw into a two-step, timelocked flow:
  `emergency_withdraw(amount, recipient)` first call announces the intent
  and starts a 48h timelock (emits withdrawal_announced); calling again
  with the same amount/recipient after the timelock elapses executes the
  transfer, capped at the contract's current token balance (emits
  emergency_withdrawal). Still requires the contract to be frozen and the
  caller to be admin, like the previous immediate-drain version.
- Caps the withdrawable amount at TOTAL_REVENUE, a new cumulative
  (never-decremented) counter of gross revenue ever collected via
  make_payment / make_payment_with_discount — so a compromised admin key
  can't drain more than customers have actually paid in, regardless of the
  contract's raw token balance.
- Adds cancel_emergency_withdrawal (clears a pending announcement) and
  get_pending_emergency_withdrawal (view).
- Replaces the previous immediate full-drain emergency_withdraw(to) — that
  version had no cap and no timelock, which is exactly the "immediate
  withdrawal: security risk if admin key compromised" scenario Dev-AdeTutu#686 calls
  out avoiding.

Closes Dev-AdeTutu#687 — promotional discount codes:

- New DiscountCode type + DataKey::DiscountCode(code) storage, and
  admin_create_discount / admin_revoke_discount / get_discount /
  is_discount_valid.
- make_payment_with_discount(meter_id, payer, amount, plan, code) validates
  the code (exists, active, not expired, under its usage limit), computes
  final_cost = amount * (100 - discount_pct) / 100, increments the usage
  counter, and charges final_cost through the same payment path as
  make_payment (extracted into a shared execute_payment helper). Emits
  discount_applied and returns the final charged amount.

New ContractError variants: TimelockNotElapsed, AmountExceedsRevenue,
NoWithdrawalAnnounced, DiscountCodeNotFound/AlreadyExists/Inactive/Expired/
Exhausted, InvalidDiscountPercent. README.md documents the new event
topics.

Verification note: the crate's `#[cfg(test)] mod tests` block does not
currently compile on main (191 pre-existing errors — mostly Symbol-vs-
String meter_id mismatches, plus a ContractEvents::iter API drift),
unrelated to this change and out of scope to fix here. Verified instead
via `cargo check` (library-only, clean) and
`cargo build --target wasm32v1-none --release` (clean); new tests for both
features were confirmed to add zero new compiler errors by diffing
`cargo check --tests` output before/after (191 errors either way) and
cross-referencing error line numbers against the new code's line ranges.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant