Skip to content

config: read a VALUE from a deployed contract as a config-call argument (#140) - #146

Merged
robercano-ghbot merged 4 commits into
mainfrom
feat/issue-140-config-read-arg
Jul 17, 2026
Merged

config: read a VALUE from a deployed contract as a config-call argument (#140)#146
robercano-ghbot merged 4 commits into
mainfrom
feat/issue-140-config-read-arg

Conversation

@robercano-ghbot

Copy link
Copy Markdown
Collaborator

What & why (issue #140)

Config-call args could be literal or { kind: "ref", contract } (address of a deployed contract). This adds the ability to pass a value read from a deployed contract's view/pure function as an argument to another config call (e.g. read token.decimals() and feed it into a setX on another contract).

Closes part of #140 (the module:config half). See "Studio follow-up" below.

Design decision (the issue asked to design first)

Chosen option (b): a first-class read arg kind in the config spec — NOT sugar over core's resolver.

Rationale: @redeploy/config deliberately excludes core's resolver kind (config's validated union is only RefArg | LiteralArg). A declarative read arg is validatable, studio-serializable, and slots cleanly into config's existing resolveArg/buildConfigCall pipeline, whereas reusing resolver would drag core-only machinery across the dependency boundary and doesn't fit config's declarative-step model.

New config-local arg type:

interface ReadArg { kind: "read"; contract: string; function: string; args?: ReadCallArg[] }
type ReadCallArg = RefArg | LiteralArg;   // read-call args are literal|ref only — no nested reads
type ConfigArg = RefArg | LiteralArg | ReadArg;
  • contract (matching RefArg.contract) = deploy-id of the source contract to read FROM.
  • On-chain reads route through the injected executor via a new optional read?(call: ReadCall): Promise<ResolvedArg> on ConfigExecutor — backward-compatible: existing executors (deploy-server, tests) are untouched; a spec that uses a read arg against an executor without read() fails fast with READ_UNSUPPORTED before any on-chain write.

Ordering / correctness invariants

  • All deploys precede config, so a read's source contract address is always available in deployedAddresses.
  • If a read value depends on a prior config step's effect, place the reading step in orderedSteps after the configuring step (documented in types.ts). No new dependency graph needed.
  • Reads happen only when a step actually executes — a journaled/skipped step performs zero reads on resume (gated behind the alreadyCompleted check before buildConfigCall). Proven by unit + e2e resume tests.

Validation

  • Unknown read contract (and unknown nested ref contracts) → MISSING_REF (same clean failure as address refs) — this is the acceptance "fails validation cleanly when the read target isn't deployable before the step" case.
  • Empty function/contract and nested read-inside-read → rejected at the schema level.
  • Config has no ABI awareness, so non-view / type-mismatch cannot be checked here (documented) — Studio will enforce view-only at authoring time via its manifest; at execution the read is a read-only eth_call, so a wrong name reverts cleanly.

Why this PR also touches packages/verify (cross-module type ripple)

Extending the shared, published ConfigArg union breaks its one downstream consumer: @redeploy/verify's config-drift.ts resolveArg did return arg.value for non-ref args, which no longer typechecks now that ReadArg (no value) is in the union. The CI gate builds the whole monorepo (pnpm -r build/typecheck), so config + this verify fix must land together — splitting would leave main red. The verify change is minimal: an exhaustive switch (arg.kind) that throws ConfigVerifyError("UNSUPPORTED_ARG", …) for read args (drift verification doesn't support read-derived expected/args values yet), plus tests. No on-chain read path was added to verify. (Studio does NOT break — it only produces ConfigArg from its own StudioConfigArg type, so the wider union is additive there.)

Gates (run in-worktree; CI re-runs via gate.sh matrix)

  • pnpm -r build and pnpm -r typecheck: pass across all 7 packages (incl. studio, deploy-server).
  • @redeploy/config: lint/typecheck/test pass (147 tests), coverage 95.56% (threshold 80).
  • @redeploy/verify: lint/test pass (138 tests), coverage 96.35%.
  • e2e Anvil (config): ran (Foundry available) — real deploy, read a live view value, feed it into another call, assert on-chain; plus a partial/resume scenario proving the read is never re-issued for journaled steps.

Reviews (consensus "all" per gates.json)

Both the config change and the verify ripple-fix were reviewed through all four lenses — correctness, tests, security, performance — all APPROVE.

Studio follow-up (deferred — filed separately)

The module:studio half of #140 (arg editor: pick a source contract + view function) is deferred to a new backlog issue. It cannot be decoupled within this run: studio's serializer must compile against config's new ConfigArg type, which only exists once this PR merges — and doing both in one worker would violate the hard module-boundary rule. Filed as a follow-up so #140's studio acceptance is completed on a later loop tick.

🤖 Generated with Claude Code

Add a first-class "read" config-call argument kind so a step argument can be
the return value of a view/pure function call on an already-deployed
contract (e.g. read token.decimals() and pass it to another step), instead
of only a ref or a literal.

- steps/types.ts: new ReadArg / ReadCallArg types (config-local, not
  re-exported from core); ConfigArg becomes RefArg | LiteralArg | ReadArg;
  ConfigArgExtended gains ReadArg alongside AddressRef. Nested reads are
  disallowed by construction (ReadCallArg = RefArg | LiteralArg).
- steps/schema.ts: readArgSchema / readCallArgSchema (discriminated union
  without a "read" branch, so nested reads are rejected at the shape level)
  wired into configArgSchema and configArgExtendedSchema.
- steps/validate.ts: MISSING_REF now also covers a read arg's own `contract`
  and any nested ref inside its `args`; documents that ABI/type-awareness is
  out of scope for this package (studio's job at authoring time).

Issue #140.
Add ConfigExecutor.read?(call: ReadCall): Promise<ResolvedArg> — optional so
existing executors (deploy-server, other callers) stay source-compatible.

- execute/types.ts: new ReadCall type; ConfigExecutor.read is optional.
- execute/errors.ts: new READ_UNSUPPORTED error code, thrown before any
  on-chain call is attempted when a step has a read arg but the injected
  executor has no read() method.
- execute/execute.ts: resolveArg/buildConfigCall become async to await
  executor.read() for "read" args (resolving the read's own ref/literal args
  synchronously first). Reads happen only at step EXECUTION time, inside
  buildConfigCall, which executeStep calls only for non-skipped steps — so a
  step already recorded in the journal is skipped before any read is
  attempted, and a resumed run performs no reads for the steps it skips.

Issue #140.
Unit tests:
- schema/validate: valid read args parse; unknown source contract ->
  MISSING_REF; empty function/contract rejected; nested read inside a
  read-call's args rejected.
- execute: a ReadFakeExecutor's read() result flows into the consuming
  call's args (both no-arg and ref/literal read-call args); an executor
  without read() throws READ_UNSUPPORTED cleanly before execute() is ever
  reached; a read-arg step already journaled is skipped on resume and
  read() is never invoked for it.

E2E (Anvil, gated by isFoundryAvailable()):
- ChainConfigExecutor gains a real read() (viem readContract) plus a
  Registry.register ABI fragment; RecordingExecutor forwards/records reads.
- New scenario: register a minter address in Registry (a prior ordered
  step), then read registry.lookup(key) and use the result as a
  grantRole.account on Token — proving the on-chain read result (not a
  hardcoded literal) drives the grant.
- New partial/resume scenario: interrupt before the reading step's on-chain
  tx, resume, and prove idempotency (the registering step never re-runs,
  and a fully-journaled third run performs zero reads).

Issue #140.
@redeploy/config's ConfigArg union grew a config-local `read` kind
(issue #140), which broke resolveArg()'s fallthrough `return arg.value`
(TS2339, since ReadArg has no `.value`). Config-drift verification does
not execute on-chain reads to derive expected/actual values, so
resolveArg() now switches exhaustively on `arg.kind` and throws a new
ConfigVerifyError("UNSUPPORTED_ARG") with a precise message when a
`read` arg is encountered, instead of silently mishandling it.

Adds UNSUPPORTED_ARG to ConfigVerifyErrorCode and covers the new
throw path for grantRole.account, setX read-descriptor expected/args,
and wire read-descriptor args.
@robercano-ghbot
robercano-ghbot merged commit c250e4d into main Jul 17, 2026
6 checks passed
@robercano-ghbot
robercano-ghbot deleted the feat/issue-140-config-read-arg branch July 17, 2026 10:16
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