Conversation
…ain tests Add tests/integration/_shared/build-calldata.ts exporting buildCalldata and its Calldata return type. The implementation matches what is already duplicated across the 4 protocol on-chain integration tests (wrapped, aave-v4, rocket-pool, uniswap), with two API tweaks: - chainId is required (passed via an options object) instead of captured from module scope, so the helper cannot silently pick the wrong chain when a protocol is deployed on multiple. - toOverride is opt-in via the same options object, used by contracts marked userSpecifiedAddress (e.g. Uniswap V3 pools whose address is computed per pair). The next commit converts the 4 call sites to use this module and drops their local copies. The two-commit split keeps the API addition reviewable in isolation from the mechanical replacement.
…alldata
Drop the local buildCalldata function from each of the 4 protocol
on-chain integration tests; import from
tests/integration/_shared/build-calldata.ts instead. Each call site
now passes { chainId: CHAIN_ID } as the options object. Uniswap's
toOverride hook is preserved through the shared API for future
userSpecifiedAddress protocols.
Per-protocol assertion models are unchanged. The
expectCallAcceptedByBytecode helper (used by aave-v4 and rocket-pool)
stays local to each file; extracting that is out of scope here.
Verified against real RPC endpoints:
- wrapped: 3/3 pass against Sepolia
- aave-v4: 6/6 pass against mainnet
- rocket-pool: 6/6 pass against mainnet
- uniswap: 11/11 pass via the public-RPC fallback path
Total: 26/26 tests pass; pnpm type-check clean; biome clean on the
buildCalldata-related changes (the 6 remaining biome warnings in
aave-v4 and rocket-pool are pre-existing, all in their local
expectCallAcceptedByBytecode helper which this PR does not touch).
…data
Drop the local buildCalldata function from protocol-chainlink-onchain.test.ts
and import from tests/integration/_shared/build-calldata.ts. Each of the 6
call sites passes { chainId: CHAIN_ID }; the 3 sites that target
userSpecifiedAddress contracts (CCIP-BnM token) additionally pass
toOverride: CCIP_BNM_SEPOLIA.
This is the conversion the original buildCalldata extraction missed.
toOverride was carried through the shared helper's options object but
had no caller in the converted set; chainlink is the only file that
actually exercises it (CCIP token contracts whose runtime address is
supplied per call). With this commit, every parameter of the shared
helper's API has a real caller.
Verified: 6/6 chainlink tests pass against public Sepolia RPC; 32/32
tests pass across all 5 converted protocol on-chain test files
(wrapped, aave-v4, rocket-pool, uniswap, chainlink).
…in test Superfluid's local buildCalldata diverged from the original 4 in one load-bearing way: after reshapeArgsForAbi, it ran coerceArgsForAbi to coerce stringly-typed leaves before encoding. The file's comment was explicit: 'Reproduce the production pipeline ... same order as plugins/web3/steps/write-contract-core.ts'. This matters because ethers.encodeFunctionData treats any non-empty string as truthy, so the literal 'false' encodes as true without coerce. Add coerceArgs?: boolean to BuildCalldataOptions, default false. The default preserves the behavior of the 4 protocols converted previously, which were verified passing without the coerce step. Superfluid opts in by passing coerceArgs: true through its callAndDecode and estimateGasError wrappers. Why not always coerce: changing the encoding path under the 4 already- verified protocols would be a behavior change worth its own verification pass, out of scope here. The latent question -- whether test/prod encoding divergence in those 4 hides bugs -- is left as a follow-up. Verified: - superfluid: 24/24 pass against public Sepolia RPC (includes create-pool, which passes 'false' for two bool inputs and would silently encode as true if coerce did not run). - wrapped/aave-v4/rocket-pool/uniswap: 26/26 pass unchanged (coerceArgs defaults off; behavior preserved).
Read-only wrappers over the Hyperliquid Info REST API (api.hyperliquid.xyz/info) for vault operator reporting and account-state queries. 8 actions: - clearinghouse-state perp account positions, margin, value - vault-details vault metadata, leader, portfolio, APR - validator-summaries all validators: stake, jailed, uptime - funding-history historical funding rates for a coin - spot-deploy-state HIP-1 token deployment auction state - referral referral status and rewards - sub-accounts master user's subaccount list - active-asset-data per-coin leverage, sizes, mark price No credentials -- Info API is public POST JSON. SSRF-guarded via safeFetch. Shared HTTP helper at steps/info-request-core.ts keeps the 8 step files DRY without exporting from "use step" files. Deviation from spec: ticket named the 8th endpoint "activeAssetCtx" but that's a WebSocket subscription type only, not an Info POST endpoint. The Info POST equivalent (same user+coin context: leverage, max trade sizes, available balance, markPx) is "activeAssetData" -- shipped as that. Smoke-tested: all 8 endpoints return shaped JSON against api.hyperliquid.xyz.
Review-team fixes for PR #1294 (KEEP-572): - info-request-core: explicit JSON content-type + parse-error branch. Previously a 2xx HTML/empty response would surface as a misleading "Network error" via the outer catch; now logs as VALIDATION and returns a clear "Invalid JSON response" or "non-JSON content-type" message. - Add shared isEvmAddress(value) helper; replace `if (!input.user)` in all six user/vault-address steps with a regex-checked guard. Catches "undefined"/"null" template substitutions and malformed hex before round-tripping to Hyperliquid. - funding-history: require startTime/endTime to be positive integers (rejects 0, negatives, decimals) and enforce endTime >= startTime. - active-asset-data: in-code comment explaining the activeAssetCtx (WebSocket only) -> activeAssetData (Info POST equivalent) swap. - test.ts: dedupe HYPERLIQUID_INFO_URL constant by importing from info-request-core.
8 tests cover the postInfo HTTP wrapper and isEvmAddress helper:
- 200 + JSON returns {success:true, data}
- non-2xx returns 'HTTP <status>: <body>' + logs EXTERNAL_SERVICE
- 200 + non-JSON content-type rejected + logs VALIDATION
- 200 + malformed JSON body rejected + logs VALIDATION
- thrown error returns getErrorMessage envelope + logs EXTERNAL_SERVICE
- isEvmAddress accepts checksummed and uppercase hex, rejects
too-short / too-long / missing-prefix / non-hex / non-string inputs
Auto-generated by `pnpm discover-plugins` — was missed in the initial commit because the explicit staging list omitted this barrel file. Caught by the `Plugin Discovery Integrity` CI check.
The lazy import in index.ts (`getTestFunction: async () => await import("./test")`)
is meant to keep test.ts off the client bundle, but Turbopack still
statically analyzes the file. When test.ts imports safeFetch
(transitively pulls in node:async_hooks) and info-request-core.ts
(declared "server-only"), the production build errors with:
the chunking context (unknown) does not support external modules
(request: node:async_hooks)
Mirror the webhook/discord pattern: use plain `fetch()` for the
connection smoke test, inline the URL constant. The test only ever
runs server-side (called via the dynamic-imported getTestFunction)
and the URL is hardcoded to a known host, so SSRF surface remains
zero. Reverts the I2 review nit that introduced the cross-module
import.
Adds a placeholder `protocols/hyperliquid.ts` so the Hub > Protocols
tab surfaces Hyperliquid alongside Safe, Aave, etc. The plugin in
`plugins/hyperliquid/` still owns the runtime actions (Info REST API);
this entry is metadata-only.
Implementation: a naive `defineProtocol` with the same slug as the
plugin would have the protocol-to-integration shim overwrite the
plugin's 8 actions in the integrationRegistry. To preserve them while
still appearing in Hub:
- Add `hubOnly?: boolean` to ProtocolDefinition. When true:
- `contracts`, `actions`, `events` must all be empty
- `discover-plugins` codegen skips
`registerIntegration(protocolToPlugin(...))`
- The protocol registers in protocolRegistry (Hub) only
When real HyperEVM on-chain actions land, drop the `hubOnly` flag and
add real contracts/actions here.
Verified:
- /api/protocols returns hyperliquid (Hub card renders)
- /api/mcp/schemas still returns all 8 hyperliquid actions
- Total action count steady at 420; integration count 31
Stylized H + waveform mark in Hyperliquid's mint-teal (#97FCE4) on
dark green, rounded-corner 256x256 PNG. Matches the existing protocol
icon convention at public/protocols/{slug}.png.
Replace with the official Hyperliquid wordmark when brand-asset
clearance is available.
Previous commit shipped a hand-rendered H+waveform placeholder. This is the real brand mark sourced from https://app.hyperliquid.xyz/apple-touch-icon.png (180x180 RGBA), resized to 256x256 to match the other protocol icons.
Hyperliquid is a hubOnly protocol (no on-chain actions yet) whose 8 runtime actions live in the `hyperliquid` plugin. The Hub card showed "Coming soon" and the detail modal "Actions (0)" because both only read protocol-registry actions. - _protocols-tab.tsx: `withPluginActions` enriches hubOnly protocols server-side by reading `getIntegration(slug)` and mapping the plugin's actions into the protocol's `actions` array (slug, label, description, type "read", required inputs derived from configFields). The synthesized slugs match the plugin action ids, so "Use in Workflow" resolves to e.g. hyperliquid/clearinghouse-state. - protocol-card-v2.tsx: drop the border-t divider above the action row for a cleaner card. Result: Hyperliquid card reads "View Actions"; detail modal shows "Actions (8)" with each action's READ badge, description, and inputs (e.g. "user (address)"), and "No inputs required" only for validator-summaries.
Superfluid's original local buildCalldata appended 'and no override given' to the missing-address error so a contributor debugging a userSpecifiedAddress contract on a chain that lacks a default address got an immediate hint to pass toOverride. The shared helper dropped that suffix during extraction. Restore it.
The helper has branching (coerceArgs gating, toOverride fallback) and error paths that were only exercised transitively through 6 integration tests requiring a live RPC. With most of those integration suites gating on RPC env vars not exposed to CI, the helper's logic had no direct coverage in the test-unit job. Add 12 unit tests covering: - toOverride takes precedence over addresses[chainId] - toOverride supplies the address for userSpecifiedAddress contracts - calldata encoding matches a direct ethers.Interface encode - returned action and contract match what callers assert on - coerceArgs: false documents the 'false' -> true trap that motivates the option - coerceArgs: true preserves boolean 'false' and 'true' values - error: unknown action slug - error: contract with no ABI - error: no address and no toOverride (includes the restored hint suffix) Tests pass an explicit coerceArgs value rather than relying on the default, so they survive a future default flip without rework.
…n spurious max-retries (KEEP-576)
Under fan-in the Workflow DevKit loses a step's completion event and
re-fires it (despite httpRequestStep.maxRetries=0), throwing
`Step "...httpRequestStep" exceeded max retries (1 retry)`. The catch
handler's spurious-retry reconciliation reads getCompletedStepOutput once,
but the real step body's success row is committed ~0.3-0.5s LATER than the
throw. The one-shot read misses, recovery is skipped, the node is nullified,
and convergence is unblocked early -- so a downstream combine resolves
{{@bungee:Bungee}} to null and aborts with "Node X produced no data".
Confirmed on prod: ~2/6 runs of the forked Across+Bungee bridge optimizer
reproduced it after the earlier in-memory-preserve change shipped; bungee
logged status=success ~0.5s AFTER combine errored.
Add a bounded poll: on a spurious-retries miss, await the recorded success
for up to 3s (250ms interval) inside a step boundary (DB-backed, replay-safe
via memoization) before falling back to nullify. Extracts the pure poll loop
(pollForCompletedOutput) into its own dependency-free module so it imports
cleanly into the workflow bundle and is deterministically unit-testable with
an injected clock.
Tests: poll-for-completed-output.test.ts covers first-hit (no sleep),
recovery after several misses (the late-landing case), timeout->null, and
one-shot (timeoutMs=0). Full unit suite green.
…sing function Previously JSON.parse(contract.abi) returned any and the .find predicate was the only thing giving the result a shape. That hid a real bug: .find() returns undefined when the action.function doesn't exist in the ABI, and the undefined fell through to reshapeArgsForAbi / coerceArgsForAbi with no clear error. Export FunctionAbiEntry from lib/abi/struct-args.ts and use it as the basis for an AbiEntry shape that JSON.parse is cast to. TypeScript now surfaces the undefined case, so add an explicit throw with a clear contract+function reference for the caller. Add a unit test exercising the new throw path.
Previously, sampleInputs[inp.name] silently substituted '' for any
typo'd key because the helper iterated only over declared action.inputs.
A test like { tokenID: '1' } (capital D) instead of { tokenId: '1' }
would encode tokenId as empty string and either revert at the contract
or encode bogus zero-padded data, with no hint that the test fixture
was the bug.
Validate the sampleInputs object up front. If any key is not in
action.inputs, throw with a clear message listing the declared input
names. Place the check after the action lookup so the error
references a real action; before the ABI check so an unrelated ABI
problem does not mask an input typo.
Verified: 56/56 integration tests still pass (no existing call site
passes undeclared keys); 2 new unit tests cover the throw paths.
The original opt-in default was 'safer' only in the sense of preserving
the verified-passing state of the 4 first-converted suites. But 'passing'
and 'encoding the same calldata production would' are different things:
none of those 4 currently has a bool fixture input, so coerce vs no-coerce
is observationally indistinguishable for them today. The day someone
adds an action with a bool input and writes a fixture like
{ ..., autoCompound: 'false' }, the test will silently encode true (the
foot-gun coerceArgs exists to prevent) and pass against a chain that
does not care.
Default-off codified that latent foot-gun. Flip to default-on so tests
match the production write pipeline by default; pass coerceArgs: false
only to deliberately exercise the un-coerced shape (e.g. a regression
test pinning the trap).
Mechanically: change the gate from 'options.coerceArgs ? coerce : reshape'
to 'options.coerceArgs === false ? reshape : coerce' (undefined now
means coerce).
Drop the now-redundant coerceArgs: true from superfluid's wrappers;
the helper's doc comment carries the intent.
Verified: 56/56 integration tests still pass across all 6 converted
protocols (wrapped, aave-v4, rocket-pool, uniswap, chainlink, superfluid)
under the new default. None of the 4 first-converted protocols has bool
inputs in its current fixtures, so the flip is observationally a no-op
for them today; the point is to keep it that way as new fixtures land.
…plugin feat: add Hyperliquid Info plugin (KEEP-572)
The previous signature was buildCalldata(protocol, slug, sampleInputs,
options) - three positional args plus an options object. The first
three positions were easy to misorder, since the (slug, sampleInputs)
pair is both vaguely 'the action description' and TypeScript's only
guardrail was the slug being a string vs sampleInputs being a record.
Move to a single params object: buildCalldata({ protocol, actionSlug,
sampleInputs, chainId, toOverride?, coerceArgs? }). Trade-off: call
sites are wider; benefit: every argument is named and labelled at the
call site, and adding future options (e.g. function-name override,
custom encoder) does not change positional shape.
Renamed BuildCalldataOptions -> BuildCalldataParams to reflect that the
type now includes required fields.
Verified: 16/16 unit tests pass; 56/56 integration tests pass across
all 6 converted protocols.
…y-race fix(executor): wait for late-landing step success before nullifying on spurious max-retries
Three of the four protocol on-chain tests this PR touched still gated on INTEGRATION_TEST_RPC_URL / INTEGRATION_TEST_MAINNET_RPC_URL via `describe.skipIf(!RPC_URL)`. Neither secret is exposed to CI's test-integration job (only CHAIN_RPC_CONFIG is), so all three skipped silently -- meaning CI never actually exercised the shared buildCalldata helper for wrapped, aave-v4, or rocket-pool. Switch to the same 3-tier resolution the uniswap test already uses: CHAIN_RPC_CONFIG -> individual CHAIN_*_RPC env vars -> public RPC default. Drop the skipIf and the `if (!RPC_URL) return;` guard in beforeAll. Tests now always run. For mainnet (aave-v4, rocket-pool) the public default is PUBLIC_RPCS.ETH_MAINNET / ETH_MAINNET_FALLBACK so the tests don't require any env vars at all to function -- CI's CHAIN_RPC_CONFIG just swaps in the paid staging endpoints transparently. Verified locally with no env vars set: 15/15 pass (3 wrapped + 6 aave-v4 + 6 rocket-pool) in ~34s against public RPCs only.
Previous patterns only matched two specific filenames and one wildcard. Tests with new output filenames (like test-onchain-output.txt) slipped past. Replace with two broader globs covering anything in .claude/ that ends in -output.txt or starts with test-*-output.txt.
Same fix as 0ab1d84 applied to two more files that gated on INTEGRATION_TEST_RPC_URL: switch to the parseRpcConfig / createRpcUrlResolver pattern, drop describe.skipIf and the `if (!RPC_URL) return;` guard, fall back to PUBLIC_RPCS.SEPOLIA when no env vars are set. Tests now always run. For protocol-superfluid-onchain.test.ts, only the outer "Superfluid on-chain integration" describe was gated. The second describe in the same file ("DISPATCH_FAILURE_RE shape") was already ungated; that behavior is unchanged. Not included (and won't be by this same pattern): - tests/integration/protocol-coverage/aave-v3/sepolia/coverage.test.ts - tests/integration/protocol-coverage/superfluid/sepolia/coverage.test.ts These don't use the RPC URL directly. They gate on SEPOLIA_RPC_URL as a proxy for "the full E2E env is up" -- the tests themselves hit PROTOCOL_E2E_BASE_URL (default http://localhost:3000) and wait on real workflow executions (180s timeouts). Ungating them via CHAIN_RPC_CONFIG would not make them runnable on CI; it would just shift the failure mode from "skipped" to "could not reach localhost". Standing CI up for those requires starting the keeperhub server in the test-integration job, which is a separate change. - tests/integration/eip7702-spike.test.ts Three describes gate on PARA_API_KEY / PIMLICO_API_KEY -- third-party service credentials, not RPC URLs. Out of scope for the CHAIN_RPC_CONFIG migration. Verified locally: 32/32 pass (8 web3-write + 24 superfluid) in 15.47s against public RPCs only.
…alldata refactor: KEEP-579 extract buildCalldata from protocol on-chain integration tests
…-tests test: KEEP-579 unblock onchain integration tests on CI
…1307) protocol-frax-ether-v2-onchain.test.ts was added by KEEP-578 after the KEEP-579 PRs were already in review. It used the same two anti-patterns KEEP-579 was eliminating: a local copy of buildCalldata, and describe.skipIf(!RPC_URL) gating on INTEGRATION_TEST_MAINNET_RPC_URL. Net effect: the file was skipping silently on every CI integration run because the test-integration job does not expose INTEGRATION_TEST_*_RPC_URL secrets - only CHAIN_RPC_CONFIG. The CI green checkmark hid 4 unrun encoding assertions. Apply the same mechanical conversion KEEP-579 did for the other 6 protocol on-chain tests: - drop the local buildCalldata, import from tests/integration/_shared - pass { protocol, actionSlug, sampleInputs, chainId } to the shared helper (which now also runs coerceArgs by default for production parity) - replace getRpcUrlByChainId with the parseRpcConfig + createRpcUrlResolver pipeline, falling back to PUBLIC_RPCS.ETH_MAINNET when no env vars are set - drop describe.skipIf(!RPC_URL) and the matching beforeAll guard Verified locally with no env vars set: 4/4 pass in 5.39s against the public Ethereum mainnet RPC fallback. type-check clean.
…chain test: unblock frax-ether-v2 onchain test on CI
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Promote the following merged PRs from staging to prod:
Post-deploy verification
deploy-keeperhubworkflow finishes greencurl -fsS https://app.keeperhub.com/api/healthreturns 200curl -fsS https://app.keeperhub.com/api/mcp/schemas | jq '.actions|keys|map(select(startswith("hyperliquid/")))|length'returns 8 (Hyperliquid plugin live)