The approve-then-act state machine -- the most repeated flow in all of
web3. Every ERC-20 interaction (deposit, stake, swap, lock...) is really two
transactions gated by an allowance read, and every dapp hand-rolls the same
fragile ladder. This is that ladder, once, with the guards proven by fuzz
tests (ApprovalFlowTest) and modeled in proofs/tla/ApprovalSpec.tla.
+------------------+ insufficient +----------------+
| CheckingAllowance+---------------->| ApprovalNeeded |
+-------+----------+ +-------+--------+
| sufficient startApprove
v v
+--------------+ approve confirmed +------------+
| ReadyToAct |<---(via re-check)---| Approving |
+-------+------+ +------------+
startAction
v
+--------------+ tx confirmed +------------+
| Acting +-------------------->| Completed |
+--------------+ +------------+Design choices, deliberately:
ApprovalNeeded; rejecting the action returns to ReadyToAct -- the user
changed their mind, nothing is broken.ApproveTx messages are ignored
while Acting, ActionTx messages while Approving -- a late
confirmation from one leg can never corrupt the other (the same
no-cross-confusion rule elm-web3's SignSpec proves).Wire-up sketch:
-- fire the allowance read, then:
ApprovalFlow.update { required = amountWei } (AllowanceLoaded current) step
-- user clicks approve:
( { model | step = ApprovalFlow.startApprove model.step }
, web3Cmd approveCall
)
-- port messages for the approve tx:
ApprovalFlow.update req (ApproveTx txMsg) stepInputs to the machine. ApproveTx/ActionTx wrap the port's
Tx.Msg for whichever leg it belongs to -- route by your correlation id.
Advance the machine. required is the amount the action needs approved.
User clicked Approve: ApprovalNeeded -> Approving AwaitingSignature.
No-op from every other step -- this is the only door into Approving.
User clicked the action: ReadyToAct -> Acting AwaitingSignature.
No-op from every other step -- the only door into Acting, which is what
makes "never act on an unverified allowance" a structural guarantee rather
than a convention.
Leave a finished or blocked flow: Completed/Blocked ->
CheckingAllowance (a fresh flow re-reads the chain). No-op elsewhere.
True in Completed -- the flow finished; only reset leaves.
True when the user's next click should be Approve.
A two-step control: numbered step indicator plus the one button that is valid right now. The root carries a state modifier so every step is independently styleable.
CSS classes: web3-approval, web3-approval--checking, --approve,
--approving, --ready, --acting, --done, --blocked;
web3-approval__steps, web3-approval__step, web3-approval__step--active,
web3-approval__step--done, web3-approval__button, web3-approval__error.
Labels and handlers for the two-step control.
Where the flow is.
Approving/Actingcarry the inner transaction lifecycle;Blockedcarries the error that stopped the flow.