config: read a VALUE from a deployed contract as a config-call argument (#140) - #146
Merged
Merged
Conversation
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.
This was referenced Jul 17, 2026
Closed
robercano
approved these changes
Jul 17, 2026
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.
What & why (issue #140)
Config-call args could be
literalor{ 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. readtoken.decimals()and feed it into asetXon another contract).Closes part of #140 (the
module:confighalf). See "Studio follow-up" below.Design decision (the issue asked to design first)
Chosen option (b): a first-class
readarg kind in the config spec — NOT sugar over core'sresolver.Rationale:
@redeploy/configdeliberately excludes core'sresolverkind (config's validated union is onlyRefArg | LiteralArg). A declarativereadarg is validatable, studio-serializable, and slots cleanly into config's existingresolveArg/buildConfigCallpipeline, whereas reusingresolverwould drag core-only machinery across the dependency boundary and doesn't fit config's declarative-step model.New config-local arg type:
contract(matchingRefArg.contract) = deploy-id of the source contract to read FROM.read?(call: ReadCall): Promise<ResolvedArg>onConfigExecutor— backward-compatible: existing executors (deploy-server, tests) are untouched; a spec that uses areadarg against an executor withoutread()fails fast withREAD_UNSUPPORTEDbefore any on-chain write.Ordering / correctness invariants
deployedAddresses.orderedStepsafter the configuring step (documented intypes.ts). No new dependency graph needed.alreadyCompletedcheck beforebuildConfigCall). Proven by unit + e2e resume tests.Validation
contract(and unknown nestedrefcontracts) →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.function/contractand nestedread-inside-read → rejected at the schema level.eth_call, so a wrong name reverts cleanly.Why this PR also touches
packages/verify(cross-module type ripple)Extending the shared, published
ConfigArgunion breaks its one downstream consumer:@redeploy/verify'sconfig-drift.tsresolveArgdidreturn arg.valuefor non-ref args, which no longer typechecks now thatReadArg(novalue) 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 leavemainred. The verify change is minimal: an exhaustiveswitch (arg.kind)that throwsConfigVerifyError("UNSUPPORTED_ARG", …)forreadargs (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 producesConfigArgfrom its ownStudioConfigArgtype, so the wider union is additive there.)Gates (run in-worktree; CI re-runs via gate.sh matrix)
pnpm -r buildandpnpm -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%.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:studiohalf of #140 (arg editor: pick a source contract + view function) is deferred to a newbacklogissue. It cannot be decoupled within this run: studio's serializer must compile against config's newConfigArgtype, 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