Skip to content

refactor(hindsight): decoders built once, solver knowledge as handles - #428

Closed
tamaralipows wants to merge 12 commits into
feat/hindsight-calldata-decodingfrom
tnl/hindsight-refactor
Closed

refactor(hindsight): decoders built once, solver knowledge as handles#428
tamaralipows wants to merge 12 commits into
feat/hindsight-calldata-decodingfrom
tnl/hindsight-refactor

Conversation

@tamaralipows

@tamaralipows tamaralipows commented Aug 13, 2026

Copy link
Copy Markdown
Contributor

tamaralipows and others added 12 commits August 12, 2026 17:21
Attribution now carries the solver's SolverKnowledge trait object,
resolved when the solver is named. Callers consult the trait on the
handle; the per-method free functions that re-derived the
implementation from the solver name on every call (swap_intent,
output_recipient) are deleted. A solver with no implementation
resolves to a shared no-op handle, so call sites never branch on
whether a solver has code.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Venue decoders are built when the registry loads, each holding its
venue's addresses as state, and live on the venue's registry entry.
The decode path resolves the entry point (an Address) to that entry
and calls trait objects — the per-transaction string match and the
per-transaction boxing are gone. The one name-to-code binding left is
venues::DECODERS, a registration table consulted once at load; an
address-book venue with no row fails the load, replacing has_decoder.

TradeDecoder loses its provider type parameter: a constructor table
cannot be generic, so DecodeContext carries alloy's DynProvider and
the Decoder erases its provider once at construction. The sender and
intent decoder lists are built once per Decoder (EntityDecoders).
DecodeContext.venue is deleted — decoders already hold their
addresses. Tests build contexts through a shared CtxFixture instead
of hand-assembling every field.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Every decoder used to set GasScope on its flow; the same three-line
rule now lives in one place (decode::gas_scope), derived from facts
the role and the flow already establish: gas is charged only when the
flow tracks the sender and the sender net-sent the input token —
whole transaction for a direct solver entry, the solver frame for a
venue entry, nothing for intent fills. TraderFlow loses the field.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
netting_decoders.rs and intents/netting.rs merge into netting.rs: the
engine (sender_flow, venue_flow, find_intent_trade) plus the two
generic decoders (SenderNetting, IntentNetting) in one place. Venue
netting decoders stay in venues/ and call the engine. Decoder name
labels in the records are unchanged.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
solvers::settled_intent does the venue-agnostic half of a
calldata-primary decode in one call: find the solver frame, resolve
the solver's knowledge handle, recover its swap intent and declared
output recipient. RelayCalldata shrinks to that call plus Relay's own
guards and fee basis; the next venue's calldata decoder is the same
thin shape.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
README and CLAUDE.md now describe decoders as built once — venue
decoders on the venue's registry entry via venues::DECODERS, solver
knowledge behind one resolved handle, gas derived by decode::gas_scope,
one netting module — and say why matching stays venue-keyed while
decoding centers on the solver call.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The orchestrator skipped the intent-vs-flow disagreement warning by
comparing the winning decoder's name to a string literal — renaming
the decoder would silently disable the skip. The property is now a
defaulted TradeDecoder method the calldata decoder overrides, and the
decode step hands back the decoder itself instead of its name.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A venue's decoders were each cloning the whole address-book section,
three copies per venue counting the registry entry. Each decoder now
holds exactly what it reads — fee collectors for most, plus entry
points for Relay's rebalance decode; MetaMask keeps the full section
since it also reads the solver-alias table. Also extracts the intent
recovery out of decode_transaction to stay under the line limit.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Decoders received a live RPC provider plus a raw code cache through
the decode context, though the only question they ever ask beyond the
transaction is whether an address holds contract code. That question
is now a trait the decode layer owns (ContractCode); the RPC-backed
answer with its cross-block cache is an adapter owned by the Decoder.
Netting tests exercise the EOA/contract distinction with an in-memory
fake instead of a mocked RPC client, and the test fixture no longer
builds a transport at all.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The headline verdict and every dashboard number compare gross amounts
since July; the net-of-gas figures were secondary record columns with
no consumer. Removed end to end: the trader-paid gas derivation, the
solver-frame gas isolation from the trace, the settled_gas trade
field, the settled_gas_cost / settled_amount_out_net_gas / net_bps
record columns, and the gas-to-token price conversion. The report's
per-trade and per-group bps switch to the gross delta (raw_bps),
which every recorded dataset already carries. Fynd's own quotes stay
gas-aware; this removes only the settled-side bookkeeping.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
"Intent" already names the trader role for orders a solver settles
on the trader's behalf (CoW, 1inch orders). The calldata type is a
different thing — the swap's terms as the solver's calldata declares
them — so it is now DeclaredSwap, read via SolverKnowledge::
declared_swap, with the shared one-step read returning a
SolverDeclaration. Record columns are unchanged.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The handle resolver still searched IMPLEMENTATIONS by name at trade
time, and callers reached it by converting a solver address to a name
first. The join now happens once, at address-book load: each registry
solver entry carries its SolverKnowledge handle, and every
address-based path (solver frame, entry point, veto and integrator
log scans) resolves address -> entry -> trait call, with no string in
between. The name-keyed resolver remains only where the name itself
is the on-chain datum: a venue-declared solver id and the fallback
attribution labels.

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

Copy link
Copy Markdown
Contributor Author

Superseded by #431 (solver-first decoding): the venue-decoder architecture this PR refactored is deleted there — venues are address-book data, decoding is per solver. The commits that survive the inversion (gas removal, and the load-time registry binding rebuilt as SolverDecoder) are part of #431.

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