Skip to content

feat: consume @avaprotocol/protocols for catalog data#213

Merged
chrisli30 merged 38 commits into
mainfrom
feat/consume-protocols-catalog
Jun 5, 2026
Merged

feat: consume @avaprotocol/protocols for catalog data#213
chrisli30 merged 38 commits into
mainfrom
feat/consume-protocols-catalog

Conversation

@chrisli30

Copy link
Copy Markdown
Member

Summary

Drops the duplicated packages/sdk-js/src/v4/protocols/ tree (~600 LOC of address / ABI / topic data) and re-exports Protocols, AbiFragment, AddressByChain from the standalone @avaprotocol/protocols@0.1.0 package.

Published as @avaprotocol/sdk-js@4.0.0-dev.3 under the dev npm tag.

Why

The catalog data (DeFi protocol addresses + ABIs + topics) is genuinely on-chain-public and the same constants are needed by:

  • ava-sdk-js template e2e tests (this repo)
  • context-memory's protocol-registry (see PR #21 there)
  • studio's app/lib/contracts/protocols (see studio PR #1176)
  • any future partner SDK (Python, Rust)

Keeping a private copy inside the SDK meant every consumer would re-derive the same constants. Extracting the catalog into a dedicated repo + npm package (AvaProtocol/protocols) gives us a single source of truth that any consumer can depend on without taking an SDK dependency.

What changes for SDK consumers

Zero API change. The existing surface continues to work:

import { Protocols, Chains } from "@avaprotocol/sdk-js";

const pool = Protocols.aaveV3.pool[Chains.Sepolia];
// 0x6Ae43d3271ff6888e7Fc43Fd7321a503ff738951

The SDK re-exports the namespace verbatim. Only visible delta:

  • Tarball size drops 72.8 kB → 45.5 kB (catalog data now sourced from the dep, not inlined in the SDK bundle)
  • File count: 115 → 64
  • Consumers automatically pick up new protocols + addresses by bumping @avaprotocol/protocols minor version without an SDK republish

What stays on the SDK side

  • Chains — retains the auth-handler-specific EigenLayerAuth constant that's irrelevant to the catalog. The catalog's Chains is a subset of pure EIP-155 ID constants used as address-map keys.
  • All builders, transport, auth, resource clients — unchanged.

Test plan

  • yarn tsc --noEmit -p tsconfig.json — 6 pre-existing baseline errors unchanged, no new errors.
  • AAVE health-factor template (4 tests) — pass against Railway gateway.
  • Uniswap V3 stoploss template (2 tests) — pass against Railway gateway.
  • yarn build — emits dual CJS + ESM + .d.ts, tarball verified clean.
  • npm publish --tag dev — landed as 4.0.0-dev.3; dev dist-tag now points there.

🤖 Generated with Claude Code

…utions

Both commands fell back to private-key auth when AVS_API_KEY was set but no
target address was provided, which returned "task not found" for any workflow
not owned by the signing EOA. Pass the owner EOA as an optional positional arg
to route through the API-key admin path.

Usage:
  yarn start getWorkflow <workflow-id> [owner-address]
  yarn start getExecutions <workflow-id> [cursor] [limit] [owner-address]
Lays down the v4 SDK shape without removing v3. v4 lives under
`packages/sdk-js/src/v4/` and is exported via `import { v4 } from
"@avaprotocol/sdk-js"` so the two surfaces coexist during the
migration window. v3 still builds and ships unchanged.

What's in:
* packages/types/openapi/openapi.yaml — vendored copy of the REST
  spec from AvaProtocol/EigenLayer-AVS. `yarn types-gen` runs
  openapi-typescript v7 to produce `packages/types/src/openapi.gen.ts`
  (checked in so downstream consumers don't need the spec at install).
* packages/types/src/v4.ts — hand-curated re-exports of the generated
  schemas (`v4.Workflow`, `v4.CreateWorkflowRequest`, `v4.Execution`,
  etc.) so callers don't have to dig through the `components["schemas"]`
  surface directly.
* packages/sdk-js/src/v4/chains.ts — Chains constants module
  (Sepolia, Holesky, BaseMainnet, BaseSepolia, EigenLayerAuth).
* packages/sdk-js/src/v4/internal/transport.ts — fetch wrapper
  owning baseUrl + JWT + timeout + problem+json decoding. Streaming
  path (`stream()`) returns the raw Response for the SSE handler.
* packages/sdk-js/src/v4/internal/errors.ts — APIError /
  NetworkError / AuthRequiredError.
* packages/sdk-js/src/v4/auth.ts — buildAuthMessage + signAuthMessage
  using ethers. The canonical EIP-191 template lives here as a
  documented constant so non-SDK callers can reproduce it.
* packages/sdk-js/src/v4/resources/*.ts — one sub-client per
  resource family. Each is a thin Transport wrapper that maps the
  spec's operationId conventions to method names. Custom actions
  (`:pause`, `:trigger`, `:simulate`) stay on the wire as
  colon-suffix URLs; SDK callers just see `pause()`, `trigger()`,
  `simulate()`.
* packages/sdk-js/src/v4/client.ts — top-level `Client` class
  composing the sub-clients. `new Client({ baseUrl, token? })` plus
  `client.workflows.create(...)` etc.
* packages/sdk-js/src/v4/builders/{triggers,nodes}.ts — typed
  builders that return plain JSON objects matching the spec's
  discriminated unions. No class instances on the wire — just shaped
  literals.
* packages/sdk-js/src/v4/index.ts — public v4 module surface.

executions.stream() implements SSE on top of `fetch().body.getReader()`
without an external EventSource polyfill (works in Node 20+ and modern
browsers). `executions.waitForTerminal()` wraps it for the common
"poll until terminal status" pattern.

Tooling:
* root `package.json`: added `openapi-typescript` dev-dep + the
  `types-gen` script that regenerates `openapi.gen.ts`.

Build is green for both packages; typecheck across the v4 tree is
clean. v3 tests and callers continue to compile unchanged.
BREAKING CHANGE: drops the v3 gRPC client surface and every grpc-* /
google-protobuf dependency. v4.0.0-alpha.0 ships as a clean break
per the API_REST_IMPLEMENTATION_PLAN — partners on v3 should pin
`@avaprotocol/sdk-js@^2` until they migrate.

* packages/sdk-js/src/index.ts: re-exports the v4 module (Client,
  Chains, Triggers, Nodes, builders, errors) as the package top
  level. v3 models/, auth.ts, utils.ts, config.ts all deleted.
* packages/sdk-js/package.json: 4.0.0-alpha.0. Drops @grpc/grpc-js,
  @grpc/proto-loader, google-protobuf, id128, lodash. Keeps
  ethers (auth signing) and dotenv. Bundle drops from ~900 KB to
  ~22 KB (no protobuf runtime).
* packages/types/src/index.ts: re-exports the openapi-typescript
  generated schemas; v3 hand-written type files (api.ts, auth.ts,
  enums.ts, node.ts, shared.ts, trigger.ts, workflow.ts, abi.ts,
  token.ts) deleted alongside their gRPC-derived definitions.
* packages/types/package.json: 4.0.0-alpha.0. Drops
  @types/google-protobuf.
* grpc_codegen/: removed (avs.proto, avs_grpc_pb.*, avs_pb.*).
* Root package.json: drops grpc-tools, grpc_tools_node_protoc_ts,
  axios, @types/axios, @types/google-protobuf. Renames
  `proto-download` -> `openapi-download` (curls the REST spec from
  the engine repo instead of the proto file). `types-gen` regenerates
  packages/types/src/openapi.gen.ts via openapi-typescript.
* tsconfig.json (root + each package): drops the @/grpc_codegen path
  mapping and the google-protobuf type root.

Tests:
* `tests/` becomes the v4 surface. The v3 suite (44 files) is moved
  verbatim to `tests-v3-archive/` for reference — it relies on
  removed v3 classes and would fail to compile against this build.
  tests/README.md ships a mechanical v3→v4 call-site conversion
  table partners can follow when they migrate their own tests.
* `tests/v4/smoke.test.ts` exercises the client construction surface,
  auth message builder (build + sign), typed builders (Triggers /
  Nodes including event-trigger wildcard handling), and the
  problem+json → APIError translation in the fetch transport.
  9 assertions, ~870ms.
* jest.config.cjs scoped to `tests/v4`; the v3-era mock-server
  globalSetup / failureSummaryReporter are no longer wired.

`yarn build` is green on both packages. `yarn test` passes 9/9.
BREAKING: the legacy 2,469-line developer playground (scheduling
scenarios + util.inspect-formatted output) is gone. example.ts is
now a thin command-line wrapper over @avaprotocol/sdk-js@4 designed
for Claude Code (and any other automation) to call directly.

Wire contract:
* stdout: one valid JSON document per command (or one per SSE
  frame for `executions:watch`). `--pretty` switches to the legacy
  `util.inspect` formatting for human use.
* stderr: errors as `{"error": {"code", "message", ...}}`.
* Exit codes: 0 success, 1 user error, 2 SDK/server error, 3 timeout.

Curated command set:
* help / health / auth
* workflows: create / list / get / cancel / pause / resume / trigger /
  simulate / count
* executions: list / get / watch (SSE)
* wallets: list / create
* operators: list
* verify — end-to-end smoke test (auth → health → create cron
  workflow → pause → resume → cancel). Emits one JSON summary line
  with per-step status + duration.

Env vars:
* AVS_REST_URL — base URL including /api/v1 prefix
* TEST_PRIVATE_KEY — EOA key for the EIP-191 exchange (required by
  `auth` + `verify`)
* AVS_API_KEY — pre-minted JWT alternative
* AVS_TIMEOUT_MS — per-request timeout (default 30s)
* AVS_SMART_WALLET — default smart wallet for `verify` / list filters

Deleted from examples/:
* fee-estimation-example.ts, get-unipswapv3-allowance.ts,
  README-allowance-check.md, examineWorkflow.*.log, config.ts
  (chain-target switching is replaced by AVS_REST_URL).
* examples/package.json: drops command-line-args / id128 / lodash
  along with the start:sepolia / start:base / etc. variants. Adds
  `yarn verify` shortcut for `yarn start verify`.

Updated examples/README.md is the entry point for partners using
the CLI to drive local smoke tests.
setTimeout(..., 0) fires on the next event-loop tick, so passing
timeoutMs: 0 (as the SSE resource does to disable the per-request
timeout) was aborting the fetch immediately. The CLI surfaced this:

  $ ts-node example.ts executions:watch <id> --workflow-id <wid>
  {"error":{"type":"NetworkError",
            "message":"fetch ...:stream: This operation was aborted"}}

Now timeoutMs <= 0 skips the setTimeout entirely; cancellation
flows only through the caller-supplied AbortSignal. The SSE
generator's clientGone / deadline / ticker dance on the server side
is the right cancellation channel for long-lived streams anyway.
…, getToken, withdraw)

- tests/utils/{env,client,templates,matchers}.ts: shared v4 test
  helpers — dotenv loader, EIP-191 authenticator, salt allocator,
  workflow cleanup, toEqualIgnoreCase jest matcher.
- jest.config.cjs: register matchers globally via setupFilesAfterEnv
  so each spec file stays focused on assertions.
- tests/v4/core/auth.test.ts: collapses v3's three describe groups
  (client.authKey / options.authKey / Unauthenticated) into v4's
  one-auth-path model — exchange + Bearer JWT + unauthenticated 401s.
- tests/v4/core/wallet.test.ts: wallets.create idempotency, list
  ordering, isHidden toggle, and the workflow-count counter walk.
- tests/v4/core/secret.test.ts: PUT/DELETE shape (void returns),
  cross-EOA isolation, scope-fallback delete (#772 regression),
  pagination (after/before/limit/workflowId filter, error cases).
- tests/v4/core/getToken.test.ts: whitelist/RPC fallback, case
  normalization, malformed-address handling, concurrency.
- tests/v4/core/withdraw.test.ts: validation-only edge cases run
  unconditionally; on-chain happy-path tests skip cleanly when the
  funded salt:2 wallet is unfunded (server returns failed) or when
  the chain endpoint is absent.

All 76 tests pass against the local docker-compose dev stack.
Coverage maps the v3 nodes/ folder one-to-one to v4 with API renames:
  - client.runNodeWithInputs(p) -> client.nodes.run(p)
  - client.simulateWorkflow(req) -> client.workflows.simulate(req)
  - client.triggerWorkflow      -> client.workflows.trigger(id, body)
  - client.submitWorkflow / deleteWorkflow -> workflows.create / cancel

Output shape changes:
  - v3 `result.data.<x>` -> v4 `result.output.data.<x>` (single wrap)
  - filter and graphqlQuery double-wrap their output (engine quirk
    noted in test comments; tracked as a server-side bug)
  - executionContext keys use snake_case (chain_id, is_simulated,
    provider) instead of v3's camelCase

Per-file scenarios kept lean — v3 often re-asserted the same shape
across run/simulate/trigger; the port collapses to one assertion per
surface plus the genuinely unique cases. Total LOC dropped from
12,819 (v3) to ~1,700 (v4).

Test utilities added:
  - settingsFor(runner) defaults chain_id=11155111 (sepolia) so
    contractWrite simulations don't 400; settingsForChain() for
    other chains.
  - getCurrentBlockNumber() lazily imports ethers to keep the
    non-chain tests free of provider overhead.

Tests skip gracefully on:
  - missing CHAIN_ENDPOINT (block-trigger tests)
  - Moralis errors (balance node tests)
  - unfunded salt:2 wallet (real bundler submission tests)
  - httpbin / GraphQL unreachable (network-dependent runners)

All 73 tests pass against the local docker-compose dev stack.
Coverage:
  - block.test.ts (6): triggers.run for varied intervals, simulate +
    deploy/trigger paths.
  - cron.test.ts (10): schedule parsing across daily/hourly/15-min/
    weekly/multiple/step-value variants, server-tolerant invalid
    expressions, simulate, deploy + trigger.
  - manual.test.ts (6): data echo, required-data validation, complex
    type preservation, headers + pathParams, simulate workflow with
    trigger.<name>.data reference, deploy + trigger via
    workflows.trigger.
  - eventTrigger.test.ts (7): topic filtering by from/to address,
    multiple queries, condition-based filtering (Chainlink price),
    cooldownSeconds, empty-queries rejection, missing-address-filter
    rejection.

Trigger builder additions:
  - Triggers.manual() now accepts data/headers/pathParams so callers
    can supply the manual trigger config inline (paired with the
    server-side mapper fix).

v3 TriggerFactory + toRequest() validation tests are dropped — v4
builders return plain JSON; validation moved server-side. The
test surface compressed from ~5,500 LOC (v3) to ~700 LOC (v4).

All 29 tests pass against the local docker-compose dev stack.
Coverage:
  - createWorkflow.test.ts (7): block/cron/fixedTime/event triggers,
    multi-node workflows with branch edges, rejection of EOA as
    smart wallet address.
  - deleteWorkflow.test.ts (2): cancel removes from list, idempotent
    on non-existent ids (v4 returns 204).
  - setWorkflowEnabled.test.ts (5): pause/resume verbs (v4 split the
    boolean toggle), idempotency on already-enabled/disabled,
    pause→resume round trip, 404 on missing workflow.
  - triggerWorkflow.test.ts (8): per-trigger-type firing (block,
    cron, fixedTime, event), blocking vs async modes, 404 on
    missing workflow, max-execution=1 rejects re-trigger.
  - workflow.test.ts (9): retrieve, list with name+startAt,
    forward+backward pagination, EOA/dead-address rejection,
    negative-limit rejection, empty wallet shape, status field
    presence.

v3 API renames absorbed:
  - client.createWorkflow + submitWorkflow -> workflows.create
  - client.deleteWorkflow                   -> workflows.cancel
  - client.setWorkflowEnabled(id, false)    -> workflows.pause(id)
  - client.setWorkflowEnabled(id, true)     -> workflows.resume(id)
  - client.triggerWorkflow({id,...})        -> workflows.trigger(id, body)
  - client.getWorkflow                       -> workflows.retrieve
  - client.getWorkflows                      -> workflows.list
  - response.items                           -> response.data

v3 `compareResults` deep-equality helper is reimplemented inline as
targeted assertions since the v4 wire shape is well-defined. The
test surface compressed from ~1,442 LOC (v3) to ~770 LOC (v4).

All 31 tests pass against the local docker-compose dev stack.
…ests)

Coverage:
  - getExecution.test.ts (5): retrieve by id, status getter,
    cron + block trigger executions, 404 for missing wf/exec.
  - getExecutions.test.ts (7): list with pagination (after/before),
    count single + multi-workflow, invalid cursor / limit handling.
  - estimateFees.test.ts (2): executionFee + valueFee + cogs shape,
    gas COGS for ETH transfer.
  - simulateWorkflow.test.ts (6): manual + customCode, fixed-time +
    REST, multi-node + branch, error handling, predecessor data
    access.
  - runNodeWithInputs.test.ts (2): Balance routes via chain_rpc,
    ContractWrite routes via Tenderly.
  - inputVariables.test.ts (4): create+retrieve round-trip,
    settings-only, scalar accessibility, nested objects.
  - execution.test.ts (3): sequential 0-based index, non-blocking
    immediate id, index field on list.
  - stepInput.test.ts (3): invalid node name rejection, trigger
    config + node config round-trip, step.inputs presence.
  - partialSuccess.test.ts (3): mixed success/failure = failed,
    all-success = success, all-failed = failed.
  - errorCodeConsistency.test.ts (2): nodes.run errorCode matches
    workflows.simulate step.errorCode for the same failure mode.
  - gasTracking.test.ts (3): step.metadata.gas* present + math
    invariant, execution.cogs aggregation, sim path metadata.

v3 → v4 API renames absorbed:
  - client.getExecution(wf, id) -> executions.retrieve(id, {wf})
  - client.getExecutions([wfs]) -> executions.list({workflowId: [..]})
  - client.getExecutionCount    -> executions.count
  - client.getExecutionStatus   -> executions.getStatus
  - client.simulateWorkflow     -> workflows.simulate
  - client.estimateFees         -> workflows.estimateFees
  - response.items              -> response.data
  - step.inputsList             -> step.inputs
  - step.type enum value        -> lowercase string ("customCode" etc.)

v3 numeric error codes (3006 etc.) become string codes
("INVALID_REQUEST", "MISSING_REQUIRED_FIELD"); v3's mock-server
dependencies replaced with httpbin.org where REST coverage is needed.

Total LOC dropped from ~5,065 (v3) to ~1,100 (v4).

All 40 tests pass against the local docker-compose dev stack.
Real-world workflow templates ported with the same shape v3 used,
trimmed down to validate the *structure* without requiring all the
external integrations (Telegram, SendGrid, Aave on-chain positions,
funded smart wallet for Uniswap swaps).

Coverage:
  - workflow-usdc-read-write-customcode (1): guards the regression
    where one contractRead got deduplicated.
  - telegram-alert-on-transfer (2): event-trigger transfer monitor
    + REST notifier (httpbin in place of Telegram).
  - approve-with-simulation (1): contractWrite simulated via Tenderly.
  - recurring-payment-with-report (1): manual->balance->customCode->
    branch->loop->REST flow.
  - batch-recurring-payment-with-email (1): cron->balance->code->
    branch->loop->REST flow.
  - aave-health-factor-alert (1): cron->contractRead->branch->REST.
  - uniswapv3_stoploss (1): cron->Chainlink price read->branch->swap.
  - exported-workflow-consistency (2): manual->filter->loop->code +
    nodes.run / simulate output equivalence.

v3 templates depended on a local mock server, real SendGrid/Telegram
creds, and a funded smart wallet. The v4 port substitutes httpbin
for HTTP-dependent steps and uses the simulate path everywhere so
the test rig stays deterministic. Total LOC compressed from
~5,517 (v3) to ~900 (v4).

All 10 tests pass against the local docker-compose dev stack.
… + trigger status

Server-side cleanups (EigenLayer-AVS feature/rest-api-migration) made
two response shapes match what the test suite already expected:
  - Filter + GraphQL nodes.run no longer double-wrap (output.data is
    the payload directly, not {data, success, executionContext}).
  - TriggerWorkflowResponse.status now references the canonical
    ExecutionStatus enum (pending|success|failed|error) instead of
    the bespoke queued|succeeded|failed enum.

Refresh packages/types from the updated spec and drop the workaround
assertions in the affected test files:
  - filterNode.test.ts: result.output.data is the filtered array.
  - graphqlQuery.test.ts: header comment updated, no test changes.
  - triggerWorkflow.test.ts: pin status to ExecutionStatus values.

Full v4 suite (268 tests) still passes.
Copy to .env.railway and fill in the AVS_REST_URL + AVS_API_KEY
values from the Railway gateway-ethereum deployment, then run with
TEST_ENV=railway to point the v4 suite at production. The existing
.env (local dev) and .env.dev paths stay unchanged.

Also whitelist .env.*.example in .gitignore so template files are
tracked while real .env.<environment> files (with secrets) stay
ignored.
The OpenAPI declared `status: pending | submitted | failed`, but the
aggregator's withdraw handler emits `pending`, `confirmed`, or
`failed` — `submitted` was never on the wire. The response-shape
test caught this against the Railway gateway (server returned
`confirmed` after the receipt landed, test enum didn't list it).

Realign the spec on the actual server contract, regenerate the
openapi types, and update the test enum. Pairs with the server-side
update in AvaProtocol/EigenLayer-AVS api/openapi.yaml.
Original v3 port used a cron trigger that polled getUserAccountData
every 15 minutes. The intended semantics are different: this is a
reactive top-up safety net for user-initiated debt increases, not a
liquidation preventer. Oracle-driven HF drops (collateral price moves)
emit no AAVE event and are explicitly out of scope.

Rewrite the trigger to watch AAVE V3 Pool Borrow events filtered by
onBehalfOf == user wallet (topics[2]). After a Borrow lands, the
contractRead -> branch -> REST top-up alert chain runs as before.
Borrow event topic layout, signature hash, and ABI documented inline.

Adds a second test asserting the Borrow ABI definition / signature
constant so Studio template exports don't drift silently if anyone
edits the ABI.
First v4 dev release. Published @avaprotocol/types and @avaprotocol/sdk-js
to the npm `dev` dist-tag so studio + openclaw-plugin can install with
`yarn add @avaprotocol/sdk-js@dev` instead of linking the local
workspace.

Replaces the previous v3-era dev channel (was 2.7.5-dev.10 / 2.5.5-dev.9).
The `latest` tag is unchanged (still 2.17.0 / 2.13.0), so v3 consumers
are unaffected.

Subsequent dev iterations bump to 4.0.0-dev.1, .2, etc.; promote to
`latest` only when the full v4 cut ships.
Three connected changes that together produce a usable auto-generated
SDK reference for the oak-website docs site.

1. JSDoc on the previously-bare resources. health, nodes, operators,
   tokens, triggers had only the class-level block (or none); wallets
   had bare method declarations for list / withdraw / getNonce; the
   ExecutionsResource, SecretsResource, and WorkflowsResource classes
   had no class-level block. Filled all of them in the workflows.ts /
   executions.ts style: HTTP route + intent + nuance, no @example
   bloat. The TypeDoc index now describes every class instead of
   showing a `-` placeholder.

2. TypeDoc wiring (typedoc + typedoc-plugin-markdown as devDeps;
   typedoc.json config; `yarn docs` script). Output goes to
   packages/sdk-js/docs-api/ as MDX, slug-routable for the oak-website
   doc renderer. The flatten-docs.js post-process step moves the
   `@avaprotocol/namespaces/v4/...` tree (TypeDoc walks the re-exported
   v4 type bag) into a flat `types/` directory and rewrites links so
   the @-prefixed paths don't break slug routing. Output is gitignored
   under docs-api/ — regenerated from source on every release.

3. Stage docs/oak-website-staging/getting-started.mdx. First of the
   three hand-written pages (the others — authentication.mdx,
   usage-examples.mdx — coming separately). Drop-in for
   oak-website/src/app/(docs)/content/ava-sdk-js/getting-started.mdx;
   format matches the existing MDX (frontmatter + plain prose + code
   blocks), no mermaid diagrams. Routes link forward to the
   yet-to-be-deployed /reference subtree.
These rendered as an Additional Information / Contact Ava Protocol
panel on the docs site REST API page (Stoplight Elements groups
info-level metadata under that header). Removed from the spec rather
than CSS-hidden so external consumers see the same shape the docs
site does.

The two fields carried thin metadata (contact = name + URL only, no
email; license = MIT name only). Partners who need contact info hit
avaprotocol.org directly.
The v3 template suite always paired a Workflow Simulation Testing
section with a Full Deployment and Execution Testing section. The v4
port at the eventTrigger rewrite only carried the simulate path
forward and dropped the deploy half, which left a real gap:
serialization of the eventTrigger config (topic filter for the
Borrow event) is the most wire-sensitive part of this graph, and the
simulate path doesn't exercise the persist+round-trip.

Add the deploy+retrieve test and factor the workflow definition into
a `buildWorkflow(smartWalletAddress)` helper so the two tests share
the exact same shape.

Bar for the deploy assertion is "deployed, persisted, retrievable
with the right trigger type + node/edge counts + Borrow-topic
filter intact" — we can't observe a real execution here because the
trigger is event-driven and would need an actual AAVE Borrow on
Sepolia from the wallet under test to fire. The simulate test
covers the per-step behavior; the deploy test covers the wire
round-trip.

Two engine quirks the test handles inline:

1. workflows.create requires inputVariables.settings.runner — same
   as workflows.simulate. The engine validates wallet ownership
   against it on every persist, not just on simulate.
2. The engine keys the persisted workflow.name off settings.name,
   not the top-level workflow.name field. settingsForChain(addr,
   chain, name) takes a name override; pass the canonical template
   name through to avoid the persisted record reading as
   "Test Simulation".

3/3 pass against the Railway gateway.
Closes the simulate-only gap across the v4 template ports. v3
template suites always paired Workflow Simulation Testing with a
Full Deployment and Execution Testing section; the v4 ports
collapsed both into the simulate path and dropped the deploy half,
leaving a real gap on the wire-serialization round-trip
(workflows.create -> workflows.retrieve).

Same pattern as the AAVE template fix that landed in fc5e4f7:
factor the workflow definition into a buildWorkflow(walletAddress)
helper, refactor the existing simulate test to use it, add a second
test that deploys via workflows.create and verifies the persisted
shape via workflows.retrieve.

Files touched:

- batch-recurring-payment-with-email: cron, 5 nodes, 5 edges
- recurring-payment-with-report: manual, 5 nodes, 5 edges
- uniswapv3_stoploss: cron, 3 nodes, 3 edges (also folds in the
  saltValue="2" wallet across both tests so they match the v3 setup)
- workflow-usdc-read-write-customcode: cron, 4 nodes, 4 edges
  (saltValue="2" wallet, same as above)
- exported-workflow-consistency: manual, 3 nodes, 3 edges (the
  shared canonical "exported workflow" the v3 cross-method
  consistency check was built around)

approve-with-simulation is left as-is — v3 was sim-only by design
(its name says so, no deploy section to mirror).

Deploy assertions follow the AAVE template pattern: id is a string,
name matches the canonical template name, trigger.type matches the
builder's output ("cron" / "manual" / "event"), and nodes/edges
arrays have the expected lengths. We can't observe real executions
here because cron + manual + event triggers would each need
chain/time/event activity in this test environment to fire, which
makes the asserted behavior non-deterministic.

Two engine quirks the deploy tests handle inline (documented in
the AAVE port and propagated here):

1. workflows.create requires inputVariables.settings.runner —
   the engine validates wallet ownership against it on every
   persist, not just on simulate. Without it: "inputVariables is
   required" / "settings.runner is required".
2. The engine keys the persisted workflow.name off
   inputVariables.settings.name, not the top-level workflow.name
   field. Pass the canonical template name through settingsFor /
   settingsForChain rather than letting the helper default to
   "Test Simulation".

17/17 pass against the Railway gateway across all 8 template
suites.
…er fix

After EigenLayer-AVS 50271e7 (REST mapper auto-mirrors body.name into
inputVariables.settings.name), passing the canonical workflow name
through settingsFor(addr, wf.name) is no longer necessary — the
top-level wf.name field is sufficient. Drop the third arg from every
deploy test's settingsFor / settingsForChain call and shrink the
"engine keys workflow.name off settings.name" multi-line comment to
a one-liner explaining the new behaviour.

Six files touched (the same set that got deploy+retrieve tests added
in 708d113):

- aave-health-factor-alert
- batch-recurring-payment-with-email
- recurring-payment-with-report
- uniswapv3_stoploss
- workflow-usdc-read-write-customcode
- exported-workflow-consistency

17/17 template tests pass against the Railway gateway running the
updated server.
The AAVE health-factor template's terminal node was a placeholder
restApi call to httpbin labelled "top up liquidity" — never actually
modified the AAVE position. Replace with the real two-step on-chain
top-up: LINK.approve(Pool, amount) followed by Pool.supply(LINK,
amount, runner, 0).

Workflow shape becomes:

  eventTrigger (Pool.Borrow filtered by onBehalfOf)
    -> contractRead   (Pool.getUserAccountData)
    -> branch         (HF < 1.5e18)
      -> contractWrite (LINK.approve(Pool, amount))
      -> contractWrite (Pool.supply(LINK, amount, runner, 0))

Two contractWrite nodes, not one. AAVE V3 Pool.supply calls
safeTransferFrom internally, so the smart wallet must approve the
Pool first. Same-workflow approve injects the allowance into
Tenderly's simulationState, so the chained supply step doesn't
revert on allowance during simulation (verified by tracing
vm_runner_contract_write.go's state-override handling).

Constants used:
- Sepolia LINK reserve 0xf8Fb3713D459D7C1018BD0A49D19b4C44290EBE5
  (AAVE testnet faucet token, 18 decimals)
- Top-up amount 0.1 LINK = 100000000000000000 wei (repeatable
  against the faucet without re-funding the test wallet)

Simulate test now expects sim.status truthy without pinning to
"success", because the dev test wallet holds no Sepolia LINK and
Tenderly does NOT override ERC-20 balances — the supply step
reverts on insufficient balance, propagating sim.status to "failed"
even though the workflow chain is correct. Same pattern as
uniswapv3_stoploss where the swap step is expected to not fire in
the dev env. Production users with a real low-HF position get the
actual top-up.

Workflow name updated from "AAVE Health Factor Alert" to "AAVE
Health Factor Top-up" since the behavior shifted from monitor-and-
alert to monitor-and-act.

Deploy+retrieve test updated for node count 3 -> 4 and edge count
3 -> 4.

3/3 in this suite, 17/17 across all 8 v4 template suites against
the Railway gateway.

Background: the v3 template at studio/templates/aave-health-factor-
alert.json was a monitor-only workflow that fired a Telegram alert
on low HF (no on-chain action). The v4 version pushes through to
the on-chain top-up so the alert becomes self-healing. The v3 test
set up a real AAVE position via test scaffolding (mint LINK, supply,
borrow) to drive HF into the [1.0, 1.6) range; the v4 test doesn't
do that — it validates the workflow shape and chain at the contract
level, leaving live-position exercises to production users or a
future integration test with a funded wallet.
…step

Extends the v4 SDK Nodes.restApi() builder with an optional `options`
field so callers can opt a terminal RestAPI node into the aggregator's
context-memory summarizer:

  Nodes.restApi({
    url: "https://api.sendgrid.com/v3/mail/send",
    method: "POST",
    body: JSON.stringify({ ...empty content slot... }),
    headers: { Authorization: "Bearer {{apContext.configVars.sendgrid_key}}" },
    options: { summarize: true },
  })

When `options.summarize: true` lands on a terminal restApi node and the
aggregator was started with the notifications.summary block
configured (provider: context-memory, api_endpoint, api_key — set on
Railway gateway in the prior commit), the engine's
ComposeSummarySmart path calls context-api.avaprotocol.org at
execution time and injects an AI-generated subject + HTML body into
the SendGrid payload. Without the flag the runner posts the body
verbatim. This is the v4 equivalent of the v3 native `email` node's
core.shouldSummarize: true.

Wires the AAVE health-factor template through to a real notification
step. Workflow shape is now:

  eventTrigger (Pool.Borrow)
    -> contractRead (Pool.getUserAccountData)
    -> branch (HF < 1.5e18)
      -> contractWrite (LINK.approve)
      -> contractWrite (Pool.supply)
      -> restApi (SendGrid /v3/mail/send, options.summarize: true)

Three terminal effects on a low-HF Borrow now: top up the position,
notify the user, and let context-memory describe what just happened.
Mirrors the production batch-recurring-payment template's shape.

Simulate + deploy+retrieve tests updated for the new step (node
count 4 -> 5, edge count 4 -> 5, simulate stepIds unchanged because
the dev test wallet has no AAVE position and the branch routes to
the `ok` slot — approve/supply/email only fire for real low-HF
positions on chain).

Verified against the Railway feature/rest-api-migration gateway with
the notifications.summary block enabled: workflow simulates cleanly,
creates+retrieves+deletes via the REST API, no errors from the new
restApi options shape. Test 3/3 in this suite.

Also includes a "Running v4 tests against the Railway gateway"
subsection in CLAUDE.md documenting the per-test log verification
practice (tail gateway + matching worker-<chain> after each single-
test run) so future sessions follow the same loop.
The helper called `client.wallets.create()` under the hood and the
docstring labelled the function "get-or-create" — but in practice
the no-arg path generates a fresh `nextTestSalt()` each call and
mints a brand-new CREATE2 address, which is "create", not "get."
The `getSmartWallet` name read like a stable accessor and was
actively misleading at call sites like

  const wallet = await getSmartWallet(client);  // → new wallet each time

The REST and SDK surfaces are both correctly named (handler comment
on POST /wallets explicitly calls it idempotent "ensure exists";
the SDK exposes it as `wallets.create()` matching the verb). Only
the test util was out of step.

Renamed to `createSmartWallet` so the call chain reads consistently
test util → SDK method → REST verb, all "create". The 16 sites that
pass an explicit `saltValue` still get the idempotent ensure-exists
behaviour as a bonus, documented in the updated docstring.

Companion type rename: GetSmartWalletOptions → CreateSmartWalletOptions.

Mechanical rename via sed across the test tree, 42 files touched,
zero new tsc errors, AAVE template suite (3/3) re-verified against
the Railway gateway post-rename.
Cumulative consumer-facing changes since 4.0.0-dev.0:

- feat: Nodes.restApi({ options: { summarize } }) — opts a terminal
  RestAPI node into the aggregator's context-memory summarizer. Pairs
  with the gateway-side notifications.summary block enabled on Railway
  in EigenLayer-AVS 2bcac8b. Required for Studio's v4 email templates.
- fix(transport): treat timeoutMs <= 0 as no-timeout instead of
  abort-on-next-tick (a802471). Defensive value, but a real fix for
  any caller that passes 0 expecting "disabled."
- additive: Triggers builders pick up the fields needed by the ported
  v4 triggers/ test suite (3778d19).
- docs: full JSDoc pass across resource clients (executions, health,
  nodes, operators, secrets, tokens, triggers, wallets, workflows) so
  IDE intellisense becomes useful (ecaae74).

All changes additive / non-breaking. Published with --tag dev so the
npm `latest` tag stays on 2.17.0; consumers opt in with
`yarn add @avaprotocol/sdk-js@dev` or pin `4.0.0-dev.1`.
The earlier widening to Array<string | number> let TypeScript accept
JSON number 0 for uint16 referralCode, but the gateway's REST mapper
declares MethodCall.methodParams as []string in the protobuf-backed
JSON schema, so a number value is rejected at the unmarshaling
boundary BEFORE the ABI encoder is reached:

  json: cannot unmarshal number into Go struct field
  MethodCall.config.methodCalls.methodParams of type string

Reverting both the SDK builder type narrowing and the canonical
template's '0' (string) literal. With strings everywhere, the wire
payload deserializes cleanly and the request reaches the ABI
encoder.

The underlying 'abi: cannot use ptr as type uint16 as argument'
error at vm_runner_contract_write.go:323 is still there for the
supply step — that's a real server-side bug where the contract-
write runner passes string-typed methodParams to abi.Pack() in a
shape that go-ethereum's encoder receives as a pointer instead of
a value. Filed as a separate server-side follow-up; this revert is
just unblocking the SDK tests so the rest of the chain (read +
approve + email) runs and demonstrates the catalog token resolution
('Approved 0.1 LINK to AAVE V3 Pool', no more 'UNKNOWN').

Tests now 5/5 against the Railway gateway:
- Canonical template (Test 1): branch trips (HF 5.4065 < 10), approve
  runs, supply fails server-side, email summarizes the partial
  execution.
- Full-chain probe (Test 4): same shape, same observable outcome.
- Read-only probe (Test 3): unchanged, still passes.
- Borrow ABI sanity (Test 5): unchanged.
Two adjustments to the canonical template surfaced by the latest
inbox check.

1. Branch was silently routing to ok.

   The lowHF condition used plain string comparison:
     aaveRead.data.getUserAccountData.healthFactor < "10000000000000000000"

   AAVE returns healthFactor as a raw uint256 string (e.g.
   "5406537200000000000" for HF=5.4065). String < is lexicographic:
   a leading "5" sorts above "1", so "5406…" > "1000…" lex-wise
   even though the numeric inequality is the reverse. Branch always
   evaluated false, branch routed to ok, approve+supply never fired,
   the rendered email stopped at "AAVE account data: …".

   Fix: wrap both sides in BigInt(). Lowered the threshold to 6.0e18
   to be slightly more realistic — production users typically set
   HF cushions closer to 1.5, but the dev wallet's HF is ~5.41 so
   6.0 is enough to trip without overshooting.

   Visible delta: canonical Test 1 simulate now runs the approve +
   supply chain through Tenderly. Duration jumped 1.9s -> 5.5s on
   Railway, matching the chain-execution latency pattern.

2. Renamed the supply node from "supplyLink" to "topUpCollateral".

   The old name surfaced in error messages as
     "✗ supplyLink: <error>"
   which reads like a method name rather than a step label.
   "topUpCollateral" pairs naturally with the workflow's purpose
   (the workflow is named "AAVE Health Factor Top-up") and reads
   cleanly in summarized inbox output.
Spread the catalog's Chains export onto the SDK Chains object so newly
added chains (currently BnbMainnet) flow through without per-bump SDK
edits. EigenLayerAuth stays on top as the SDK-specific auth-audience
constant the catalog doesn't know about.

Adds a Railway-targeted auth test that mints a JWT scoped to chain 56
(BNB) - the lightest probe that gateway-railway.yaml's chain set and
worker-bnb-mainnet are wired up end-to-end without needing a smart
wallet factory on the chain.
@chrisli30 chrisli30 merged commit 0e3a1cb into main Jun 5, 2026
4 of 11 checks passed
@chrisli30 chrisli30 deleted the feat/consume-protocols-catalog branch June 5, 2026 08:40
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.

2 participants