Skip to content

feat: migrate erc20/erc4626 handlers to defineTask, parity-gated (G1) - #26

Merged
halaprix merged 2 commits into
mainfrom
feat/g1-handler-migration
Jul 23, 2026
Merged

halaprix merged 2 commits into
mainfrom
feat/g1-handler-migration

Conversation

@halaprix

Copy link
Copy Markdown
Owner

Summary

Implements spec G1 (1.3.0):

  • Handlers reimplemented on defineTask, public signatures byte-identical: every contract call optional: true (silent-undefined parity), call-creation order matching the legacy step layout, and — replicating the legacy accessor coercions — every consumed value passes through step-transparent coercion derives (asBigInt-style), so a successful-but-malformed executor value shapes to undefined exactly as 1.x always did, and a malformed balance gates convertToAssets off instead of reaching the encoder.
  • Core hardening (review round): a call arg/target resolving to literal undefined (e.g. a derive returning undefined) now skip-chains instead of leaking into the executor's encoder where it could reject a whole shared batch.
  • Parity gate 1: src/__tests__/parity-g1.test.ts — 28 scenarios toStrictEqual against the legacy oracle (moved to src/__tests__/fixtures/legacy-handlers/, excluded from the runtime bundle; deleted after one minor per spec), covering per-field failures, step-2 gating, bulk slicing, absent-vs-undefined position keys, and per-field malformed-success values. Documented accepted deltas: runSettled diagnostics now retain executor errors (kind/data toBe-identical, cause preserved); handler calls are dedup-eligible (bulk + dedupe: true merges).
  • Parity gate 2: live mainnet fork test (describe.runIf(RPC_URL)) comparing both impls on sDAI/USDC — self-skips in CI (no RPC secret); offline fixtures are the merge gate per plan.

Test evidence

338/338 (+31: 28 parity + 3 core undefined-arg tests), compat-vs-dist 38/38, full gate build-first. Bundle ~neutral (17.2KB gzip).

Spec: spec.md § 1.3.0 G1.

halaprix added 2 commits July 23, 2026 21:55
Reimplement buildErc20Task/buildErc4626Task internally on defineTask
(t.call/t.derive) — public signatures and return shapes unchanged. Every
call is optional: true, replicating 1.0's silent-undefined-per-field
semantics; convertToAssets takes the raw balanceOf call ref (not the
coerced value) as its arg, reproducing the old step-2 balance-gating
exactly. position's conditional-key shape (T11) is reproduced with
t.derive, returning undefined or an object whose maxWithdraw/maxRedeem
keys are present only when those calls resolved.

The pre-migration implementations move to
src/__tests__/fixtures/legacy-handlers/{erc20,erc4626}.ts as the
parity-test oracle (buildErc20TaskLegacy/buildErc4626TaskLegacy),
test-only, excluded from the runtime bundle. src/__tests__/parity-g1.test.ts
runs a scenario table (with/without owner, each field failing
individually, bulk batching/slicing, dedupe) through both implementations
via runMultistepTasks, asserting toStrictEqual. src/__tests__/live/
holds an opt-in live mainnet fork test gated on RPC_URL.

Two accepted, documented deltas: under runSettled, the new impl retains
diagnostics.optionalFailures (legacy always reported []); new calls are
dedup-eligible, so bulk + { dedupe: true } can now merge identical calls
across entries. Net bundle effect: -348 bytes gzip (17.53KB -> 17.19KB).
…eview P1)

resolveAll's call-mode resolution treated an upstream 'u' (optional-failed)
or 'f' (hard failure) position as unusable, but let a 'v' state whose VALUE
happens to be undefined - a derive that legitimately computed undefined,
most commonly a handler's own coercion derive demoting a malformed executor
value - pass straight through as a real argument. A dependent call could
then be dispatched with that bogus value; Eip1193Executor encodes args
synchronously inside executeMulticall, so a bad argument there could reject
the whole physical batch, not just the one call depending on it.

resolveAll now treats a 'v'-with-undefined-value position the same as
'u'/'f' in call-mode only (derives are unaffected - they legitimately
consume undefined), synthesizing a "argument resolved to undefined" cause
when there's no real upstream error to forward.

erc4626's convertToAssets now takes the coerced `balance` ref (post-
asBigInt) instead of the raw balanceOf call ref, so a malformed-but-
"successful" balance demotes to undefined and skip-chains the dependent
call exactly like a real balanceOf failure - closing the gap for the one
place a raw call ref fed another call's argument position.

Added three core tests (derive-returning-undefined feeding a non-optional
call -> rejects with a synthesized cause; the same feeding an optional
call -> demotes + diagnostics; a second derive consuming v-undefined is
unaffected), plus parity scenarios: per-field malformed-success values for
both handlers, and - for the balanceOf case specifically - executor
call-count parity with legacy (step 2 not dispatched), position undefined,
fulfilled under both run() and runSettled, and diagnostics containing
exactly the synthesized-cause skip (documented as an accepted-delta
extension, not noise).
@halaprix
halaprix merged commit f1b0879 into main Jul 23, 2026
2 checks passed
@halaprix
halaprix deleted the feat/g1-handler-migration branch July 23, 2026 20:14
halaprix added a commit that referenced this pull request Jul 23, 2026
* chore: release v1.3.0

- Version bump 1.2.0 → 1.3.0 in package.json and npm-lock
- CHANGELOG.md: [1.3.0] entry with facts verified against git log v1.2.0..HEAD (PRs #25, #26, #27):
  * Added: MultichainResolver, examples/refinance.ts
  * Changed: handlers reimplemented on defineTask (parity-gated, observable deltas documented)
  * Bundle size 15→18KB (current 16.8KB measured)
- README.md: updated bundle badge, added MultichainResolver to API table, added Multichain section with compiling example and refinance.ts pointer
- MIGRATION.md: appended v1.2.x → v1.3.0 section (additive, no consumer action required)
- SUMMARY.md: updated TOC with new sections
- docs/benchmarks.md: updated size figures (13.9 → 16.8 KB)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* docs: fix API accuracy — multichain, pinBlock, ethers

- README multichain example: replace task instance reuse with factory pattern (flattened duplicate validation throws before any execution)
- api-reference MultichainResolver.chain(): returns MulticallResolver (cached), throws on unknown chainId (not undefined)
- Remove fake per-chain onPin Record — onPin is single inherited callback from BatchOptions; each chain reports once
- Remove fake chainId < 1 validation rule — real rule: lowest-chainId rejection on concurrent failures
- docs/index.html: Fix ethers claim — "requires an adapter" (domino doesn't depend on ethers)
- pinBlock performance: Clarify +1 RPC only for absent/tag blocks; explicit blockNumber/blockHash add none
- CHANGELOG: Correct MultichainResolver description (blocks map, not per-chain onPin; deterministic rejection)
- Verify all snippets compile and bundle badge matches measured size

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: halaprix <halaprix@users.noreply.github.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
halaprix added a commit that referenced this pull request Jul 24, 2026
…#26)

* feat: migrate erc20/erc4626 handlers to defineTask, parity-gated (G1)

Reimplement buildErc20Task/buildErc4626Task internally on defineTask
(t.call/t.derive) — public signatures and return shapes unchanged. Every
call is optional: true, replicating 1.0's silent-undefined-per-field
semantics; convertToAssets takes the raw balanceOf call ref (not the
coerced value) as its arg, reproducing the old step-2 balance-gating
exactly. position's conditional-key shape (T11) is reproduced with
t.derive, returning undefined or an object whose maxWithdraw/maxRedeem
keys are present only when those calls resolved.

The pre-migration implementations move to
src/__tests__/fixtures/legacy-handlers/{erc20,erc4626}.ts as the
parity-test oracle (buildErc20TaskLegacy/buildErc4626TaskLegacy),
test-only, excluded from the runtime bundle. src/__tests__/parity-g1.test.ts
runs a scenario table (with/without owner, each field failing
individually, bulk batching/slicing, dedupe) through both implementations
via runMultistepTasks, asserting toStrictEqual. src/__tests__/live/
holds an opt-in live mainnet fork test gated on RPC_URL.

Two accepted, documented deltas: under runSettled, the new impl retains
diagnostics.optionalFailures (legacy always reported []); new calls are
dedup-eligible, so bulk + { dedupe: true } can now merge identical calls
across entries. Net bundle effect: -348 bytes gzip (17.53KB -> 17.19KB).

* fix: skip-chain calls fed a v-undefined arg, not just u/f (external review P1)

resolveAll's call-mode resolution treated an upstream 'u' (optional-failed)
or 'f' (hard failure) position as unusable, but let a 'v' state whose VALUE
happens to be undefined - a derive that legitimately computed undefined,
most commonly a handler's own coercion derive demoting a malformed executor
value - pass straight through as a real argument. A dependent call could
then be dispatched with that bogus value; Eip1193Executor encodes args
synchronously inside executeMulticall, so a bad argument there could reject
the whole physical batch, not just the one call depending on it.

resolveAll now treats a 'v'-with-undefined-value position the same as
'u'/'f' in call-mode only (derives are unaffected - they legitimately
consume undefined), synthesizing a "argument resolved to undefined" cause
when there's no real upstream error to forward.

erc4626's convertToAssets now takes the coerced `balance` ref (post-
asBigInt) instead of the raw balanceOf call ref, so a malformed-but-
"successful" balance demotes to undefined and skip-chains the dependent
call exactly like a real balanceOf failure - closing the gap for the one
place a raw call ref fed another call's argument position.

Added three core tests (derive-returning-undefined feeding a non-optional
call -> rejects with a synthesized cause; the same feeding an optional
call -> demotes + diagnostics; a second derive consuming v-undefined is
unaffected), plus parity scenarios: per-field malformed-success values for
both handlers, and - for the balanceOf case specifically - executor
call-count parity with legacy (step 2 not dispatched), position undefined,
fulfilled under both run() and runSettled, and diagnostics containing
exactly the synthesized-cause skip (documented as an accepted-delta
extension, not noise).

---------

Co-authored-by: halaprix <halaprix@users.noreply.github.com>
halaprix added a commit that referenced this pull request Jul 24, 2026
* chore: release v1.3.0

- Version bump 1.2.0 → 1.3.0 in package.json and npm-lock
- CHANGELOG.md: [1.3.0] entry with facts verified against git log v1.2.0..HEAD (PRs #25, #26, #27):
  * Added: MultichainResolver, examples/refinance.ts
  * Changed: handlers reimplemented on defineTask (parity-gated, observable deltas documented)
  * Bundle size 15→18KB (current 16.8KB measured)
- README.md: updated bundle badge, added MultichainResolver to API table, added Multichain section with compiling example and refinance.ts pointer
- MIGRATION.md: appended v1.2.x → v1.3.0 section (additive, no consumer action required)
- SUMMARY.md: updated TOC with new sections
- docs/benchmarks.md: updated size figures (13.9 → 16.8 KB)


* docs: fix API accuracy — multichain, pinBlock, ethers

- README multichain example: replace task instance reuse with factory pattern (flattened duplicate validation throws before any execution)
- api-reference MultichainResolver.chain(): returns MulticallResolver (cached), throws on unknown chainId (not undefined)
- Remove fake per-chain onPin Record — onPin is single inherited callback from BatchOptions; each chain reports once
- Remove fake chainId < 1 validation rule — real rule: lowest-chainId rejection on concurrent failures
- docs/index.html: Fix ethers claim — "requires an adapter" (domino doesn't depend on ethers)
- pinBlock performance: Clarify +1 RPC only for absent/tag blocks; explicit blockNumber/blockHash add none
- CHANGELOG: Correct MultichainResolver description (blocks map, not per-chain onPin; deterministic rejection)
- Verify all snippets compile and bundle badge matches measured size


---------

Co-authored-by: halaprix <halaprix@users.noreply.github.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
halaprix added a commit that referenced this pull request Jul 24, 2026
…#26)

* feat: migrate erc20/erc4626 handlers to defineTask, parity-gated (G1)

Reimplement buildErc20Task/buildErc4626Task internally on defineTask
(t.call/t.derive) — public signatures and return shapes unchanged. Every
call is optional: true, replicating 1.0's silent-undefined-per-field
semantics; convertToAssets takes the raw balanceOf call ref (not the
coerced value) as its arg, reproducing the old step-2 balance-gating
exactly. position's conditional-key shape (T11) is reproduced with
t.derive, returning undefined or an object whose maxWithdraw/maxRedeem
keys are present only when those calls resolved.

The pre-migration implementations move to
src/__tests__/fixtures/legacy-handlers/{erc20,erc4626}.ts as the
parity-test oracle (buildErc20TaskLegacy/buildErc4626TaskLegacy),
test-only, excluded from the runtime bundle. src/__tests__/parity-g1.test.ts
runs a scenario table (with/without owner, each field failing
individually, bulk batching/slicing, dedupe) through both implementations
via runMultistepTasks, asserting toStrictEqual. src/__tests__/live/
holds an opt-in live mainnet fork test gated on RPC_URL.

Two accepted, documented deltas: under runSettled, the new impl retains
diagnostics.optionalFailures (legacy always reported []); new calls are
dedup-eligible, so bulk + { dedupe: true } can now merge identical calls
across entries. Net bundle effect: -348 bytes gzip (17.53KB -> 17.19KB).

* fix: skip-chain calls fed a v-undefined arg, not just u/f (external review P1)

resolveAll's call-mode resolution treated an upstream 'u' (optional-failed)
or 'f' (hard failure) position as unusable, but let a 'v' state whose VALUE
happens to be undefined - a derive that legitimately computed undefined,
most commonly a handler's own coercion derive demoting a malformed executor
value - pass straight through as a real argument. A dependent call could
then be dispatched with that bogus value; Eip1193Executor encodes args
synchronously inside executeMulticall, so a bad argument there could reject
the whole physical batch, not just the one call depending on it.

resolveAll now treats a 'v'-with-undefined-value position the same as
'u'/'f' in call-mode only (derives are unaffected - they legitimately
consume undefined), synthesizing a "argument resolved to undefined" cause
when there's no real upstream error to forward.

erc4626's convertToAssets now takes the coerced `balance` ref (post-
asBigInt) instead of the raw balanceOf call ref, so a malformed-but-
"successful" balance demotes to undefined and skip-chains the dependent
call exactly like a real balanceOf failure - closing the gap for the one
place a raw call ref fed another call's argument position.

Added three core tests (derive-returning-undefined feeding a non-optional
call -> rejects with a synthesized cause; the same feeding an optional
call -> demotes + diagnostics; a second derive consuming v-undefined is
unaffected), plus parity scenarios: per-field malformed-success values for
both handlers, and - for the balanceOf case specifically - executor
call-count parity with legacy (step 2 not dispatched), position undefined,
fulfilled under both run() and runSettled, and diagnostics containing
exactly the synthesized-cause skip (documented as an accepted-delta
extension, not noise).

---------

Co-authored-by: halaprix <halaprix@users.noreply.github.com>
halaprix added a commit that referenced this pull request Jul 24, 2026
* chore: release v1.3.0

- Version bump 1.2.0 → 1.3.0 in package.json and npm-lock
- CHANGELOG.md: [1.3.0] entry with facts verified against git log v1.2.0..HEAD (PRs #25, #26, #27):
  * Added: MultichainResolver, examples/refinance.ts
  * Changed: handlers reimplemented on defineTask (parity-gated, observable deltas documented)
  * Bundle size 15→18KB (current 16.8KB measured)
- README.md: updated bundle badge, added MultichainResolver to API table, added Multichain section with compiling example and refinance.ts pointer
- MIGRATION.md: appended v1.2.x → v1.3.0 section (additive, no consumer action required)
- SUMMARY.md: updated TOC with new sections
- docs/benchmarks.md: updated size figures (13.9 → 16.8 KB)


* docs: fix API accuracy — multichain, pinBlock, ethers

- README multichain example: replace task instance reuse with factory pattern (flattened duplicate validation throws before any execution)
- api-reference MultichainResolver.chain(): returns MulticallResolver (cached), throws on unknown chainId (not undefined)
- Remove fake per-chain onPin Record — onPin is single inherited callback from BatchOptions; each chain reports once
- Remove fake chainId < 1 validation rule — real rule: lowest-chainId rejection on concurrent failures
- docs/index.html: Fix ethers claim — "requires an adapter" (domino doesn't depend on ethers)
- pinBlock performance: Clarify +1 RPC only for absent/tag blocks; explicit blockNumber/blockHash add none
- CHANGELOG: Correct MultichainResolver description (blocks map, not per-chain onPin; deterministic rejection)
- Verify all snippets compile and bundle badge matches measured size


---------

Co-authored-by: halaprix <halaprix@users.noreply.github.com>
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.

1 participant