Simulate-then-send: preview a write's outcome before asking for a
signature. The structural guarantee, in the family of
ApprovalFlow: a transaction cannot be sent
without a completed simulation -- confirm is the only door into
Sending, and it only opens from Previewing.
elm-web3 already has the capability: Contract.Call.withFrom runs the
write's calldata as an eth_call from the user's address, returning what
the transaction would do (or its revert) for free.
-- user clicks the primary button:
( { model | sim = SimulateFirst.start model.sim }
, web3Cmd (Call.encode simulatedCall) -- readCall withFrom
)
-- simulation answer (callResult or failed for our id):
{ model | sim = SimulateFirst.onSimResult (Ok decodedPreview) model.sim }
-- user confirms the previewed outcome:
( { model | sim = SimulateFirst.confirm model.sim }
, web3Cmd (Send.encode theRealWrite)
)
-- tx lifecycle:
{ model | sim = SimulateFirst.onTx txMsg model.sim }Wallet rejection is not failure: rejecting the signature returns to
Previewing -- the preview is still true; the user just declined.
CSS classes: web3-simulate, web3-simulate--idle/--simulating/--preview/
--sending/--done/--refused, web3-simulate__preview,
web3-simulate__button, web3-simulate__error.
Apply the simulation's answer. Only meaningful in Simulating.
Previewing -> Sending AwaitingSignature -- the only door into
Sending; unreachable without a completed simulation.
Drive the inner transaction. Only meaningful in Sending:
confirmed -> Done, failed -> Refused, wallet-rejected -> back to
Previewing (the preview is still valid).
The one valid control per step, plus the preview/receipt/error surface.
Previewingcarries the simulated outcome (already decoded/rendered to a string by the app);Refusedcarries the reason (simulation revert, tx failure).