Skip to content
Open
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
31 changes: 31 additions & 0 deletions examples/nodejs-transaction-review-webhook/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# Port to listen on. ngrok this with `ngrok http $PORT`.
PORT=4040

# Shared secret used by Dynamic to HMAC-sign incoming requests. Must match
# the "Webhook Secret" value saved in the Dynamic dashboard. Leave empty to
# accept unsigned requests (NOT recommended).
WEBHOOK_SECRET=

# Path to your Ed25519 private key in PEM format. The matching public key
# (printed by `npm run keygen`) is what you paste into the dashboard's
# "Response Verification Key" field. Leave empty to send unsigned responses.
WEBHOOK_PRIVATE_KEY_PATH=./private.pem

# Alternative to WEBHOOK_PRIVATE_KEY_PATH for hosted/containerized deploys:
# the base64-encoded PEM private key, so no key file has to be mounted on disk.
# Takes precedence over WEBHOOK_PRIVATE_KEY_PATH when set. Encode with:
# base64 -w0 private.pem
WEBHOOK_PRIVATE_KEY_PEM=

# Default decision mode. Override per-request via ?mode=... query string.
# allow -> { proceed: true }
# deny -> { proceed: false, reason: <DENY_REASON> }
# slow -> sleep SLOW_MS then allow (exercises your DENY/ALLOW failure policy)
# crash -> hang up the connection without responding
MODE=allow

# Reason returned with denials (any string).
DENY_REASON=Denied by example webhook

# Milliseconds to sleep before responding when MODE=slow.
SLOW_MS=10000
3 changes: 3 additions & 0 deletions examples/nodejs-transaction-review-webhook/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
node_modules/
.env
*.pem
143 changes: 143 additions & 0 deletions examples/nodejs-transaction-review-webhook/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
# Transaction Review webhook — example server

A self-contained Express app that implements Dynamic's [Transaction Review
webhook contract](https://docs.dynamic.xyz/overview/wallets/embedded-wallets/mpc/transaction-review)
end-to-end. Use it to validate the feature locally against your Dynamic sandbox
environment before wiring up your real backend.

What it does:

- Listens on `POST /webhook`.
- Verifies the `x-dynamic-signature` HMAC-SHA256 request header against your
shared secret. Requests with a missing or wrong signature get a `401` and
`{ proceed: false, reason: "Invalid signature" }`.
- Optionally signs every response with Ed25519 and sets the
`x-dynamic-response-signature` header so Dynamic's response verification path
can be exercised.
- Exposes four hot-switchable decision modes via the `?mode=` query string so
you can flip behavior without restarting the server:

| mode | behavior |
| ------- | ------------------------------------------------------------------- |
| `allow` | `{ proceed: true }` |
| `deny` | `{ proceed: false, reason: "<DENY_REASON>" }` |
| `slow` | Sleep for `SLOW_MS` ms before responding (exercises failure policy) |
| `crash` | Tear down the TCP socket without writing a response |

The default mode comes from `MODE` in `.env`. Anything else falls back to
the default.

## Setup

> Requires Node 18+ and pnpm. Tested on Node 22.

```bash
cd examples/nodejs-transaction-review-webhook
pnpm install
cp .env.example .env
pnpm keygen # writes private.pem + public.pem, prints the public key
pnpm dev # starts the server with hot-reload on $PORT (default 4040)
```

`pnpm keygen` will refuse to overwrite existing keys — delete `private.pem`
and `public.pem` first if you really want to rotate.

## Expose it to Dynamic

Dynamic needs to reach your local server, so tunnel it:

```bash
ngrok http 4040
```

Copy the `https://<random>.ngrok-free.app` URL ngrok prints — that's your
**Webhook URL**.

## Configure in the dashboard

In the Dynamic dashboard for your sandbox environment, go to
**Wallets → Transaction Review** and fill in:

| Field | Value |
| ----------------------------- | -------------------------------------------------- |
| **Webhook URL** | `https://<your-tunnel>.ngrok-free.app/webhook` |
| **Webhook Secret** | Same string you put in `WEBHOOK_SECRET` in `.env` |
| **Response Verification Key** | Paste the contents of `public.pem` (printed above) |
| **Failure Policy** | `DENY` (default) — recommended for testing |

Save. You're now ready to drive transactions through the SDK.

## Fastest validation — the dashboard "Send test" button

You don't need to drive a real SDK transaction. Once the URL/secret/public
key are in the form, the side panel exposes a **Test webhook** card with a
scenario dropdown (`Sign message`, `EVM transaction`, `EVM token transfer`,
`EVM user operation`, `EVM typed data`, `Solana transaction`) and a **Send
test** button. Clicking it drives a synthetic payload through the same HMAC
signing, Ed25519 verification, timeout, and failure-policy machinery as the
live signing path — but with no events, no DB writes, and no signing
operation involved.

The result panel shows the decision badge (`Approved` / `Denied` / `Failure
policy applied`), latency, HTTP status, signature verification state, and
collapsible request/response bodies. Flip `?mode=allow|deny|slow|crash` in
the **Webhook URL** field and press _Send test_ again to confirm each
scenario without ever leaving the dashboard.

## What to verify with a real signing operation

Trigger any signing operation from the SDK (`signMessage`, EVM tx, Solana tx,
ERC-4337 UserOp — doesn't matter; the webhook fires for all of them). For
each scenario flip the mode and re-trigger:

1. **Default approve** (`MODE=allow` or `?mode=allow`)

- Webhook logs the incoming request with the `requestId`, `walletId`, `chain`.
- Signing completes; SDK gets a signature.
- Event `waas.transaction.review.approved` is published.

2. **Deny with reason** (`?mode=deny`)

- Webhook responds `{ proceed: false, reason: "Denied by example webhook" }`.
- SDK surfaces a `TransactionReviewDenied` error whose message contains the
reason.
- Event `waas.transaction.review.denied` is published.

3. **Response signing**

- With `WEBHOOK_PRIVATE_KEY_PATH` set and the matching public key saved in
the dashboard, Dynamic accepts the response. Try corrupting the public key
in the dashboard (e.g. change one base64 char) — Dynamic should fall back
to your failure policy and reject the response signature.

4. **Failure policy** (`?mode=slow` with `SLOW_MS` > the configured timeout)

- Configured `DENY`: signing is blocked with a transaction-review-unreachable
error.
- Re-save the dashboard config with `ALLOW`: signing proceeds despite the
timeout. Same `?mode=slow` exercises both.

5. **Hard crash** (`?mode=crash`)
- Webhook tears down the TCP connection. Dynamic treats this like any other
transport failure and applies the failure policy.

## Sanity check

`GET http://localhost:$PORT/health` is a minimal liveness probe. Your effective
config (mode, whether HMAC verification / response signing are enabled) is
printed to the console on startup — check there to confirm your `.env` was
picked up, rather than exposing it over HTTP.

```bash
curl -s http://localhost:4040/health | jq
# {
# "status": "ok"
# }
```

## Not production code

This is a reference server. It can log request bodies to stdout (opt-in via
`LOG_BODIES=true`), has no persistence, and doesn't validate payload shapes
beyond what's needed to demonstrate the contract. Use it to verify the wiring;
build your real webhook against the [Transaction Review documentation](https://docs.dynamic.xyz/overview/wallets/embedded-wallets/mpc/transaction-review).
22 changes: 22 additions & 0 deletions examples/nodejs-transaction-review-webhook/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
{
"name": "nodejs-transaction-review-webhook",
"version": "1.0.0",
"private": true,
"description": "Reference Express server for Dynamic's Transaction Review webhooks. Verifies HMAC request signatures, optionally Ed25519-signs responses, and exposes hot-switchable approve/deny/slow/crash modes for end-to-end testing.",
"scripts": {
"dev": "tsx watch src/server.ts",
"start": "tsx src/server.ts",
"keygen": "tsx src/keygen.ts"
},
"dependencies": {
"dotenv": "16.6.1",
"express": "4.21.2"
},
"devDependencies": {
"@types/express": "4.17.23",
"@types/node": "22.10.5",
"tsx": "4.22.4",
"typescript": "5.4.4"
},
"packageManager": "pnpm@11.13.0+sha512.88d94724d8f2e6c186744a5584c6e59ecac869ec7ba15e9cb4cd628e8dc7066820b2481d8ee3b51ea8da323a7378068aa58c556a3720d32b7c20a051d088363a"
}
Loading