Skip to content

feat(web): predict flow with Foresight credits, slippage settings and a money-checked run - #1

Merged
tractorss merged 638 commits into
masterfrom
issue/2026-10-09-foresight-predict-flow
Oct 26, 2026
Merged

tractorss merged 638 commits into
masterfrom
issue/2026-10-09-foresight-predict-flow

Conversation

@tractorss

Copy link
Copy Markdown
Contributor

What this adds

The predict flow for a session: a visitor sets estimates on the option cards, presses Predict, and one popup takes them from wallet to placed prediction. It creates a trade wallet if needed, adds funds (sDAI, xDAI, Foresight credits, or a mix), then trades every option's pools towards the visitor's estimates in batches.

Predict popup

  • Estimates list, amount with share slider, and a summary that opens into Details. Details lists the planned trades per option (Mint, Split, Sell, Merge, Buy) with pool fees, the slippage in use, and options that stop short of their estimate.
  • The steps (trade wallet, add funds, place prediction) run as a timeline. The status line above the buttons is a callout with a severity glyph.
  • A settings card sets max slippage for trades and for the credits swap: Auto or custom, with low/high warnings and Reset.
  • Pools are re-read when Predict is pressed. If a plan moved beyond slippage, the run stops at "Prices moved" and the visitor confirms or goes back. A run is held to the plans it showed.
  • The success dialog is the run's finished timeline, with explorer links per step and batch, plus the notification-email panel.
  • Phones: a compact Predict bar rises from the bottom once something is picked and docks into the card (portrait only).

Foresight credits

  • Credits are spent through the Seer CreditsManager on Gnosis, the same route as futarchy-ui: manager.execute (Swapr swap) → WXDAI.withdraw → GnosisRouter.splitFromBase with native value, inside one batchValueExecute.
  • Ticked by default when usable. Capped at what the manager can cover, with a note. Withdrawable from the trade wallet.
  • The local chain gets a credits mock; yarn simulate:credits:gnosis-fork grants test credits.

Money safety

  • Every fork run checks a ledger of the visitor's wallet, trade wallet and the credits manager, to the wei:
    • nothing lost or stranded;
    • no overpay;
    • every pool lands within one tick of the estimate;
    • failures stay withdrawable;
    • what the popup showed is what was signed.
  • Recovery finds each step's own transaction from a pinned block, and an unanswered send by its nonce, so a step is never signed twice.
  • Batches are sized by the higher of a gas budget and a live estimate.

How it was tested

  • yarn workspace web test (vitest): 2,063 passed.
  • Component-harness Playwright specs (popup, card, settings, credits, trade wallet, slider, option card): 636 passed, across 320–1280 widths in light and dark.
  • yarn test:fork (money ledger on a gnosis fork): 29/29.
  • Popup end-to-end on a local gnosis-fork stack (predict-fork.spec.ts, predict-fork-credits.spec.ts): 7/7.
  • Red-team passes on the credits route, pricing and recovery. Planted-bug (mutation) checks for overpay, skipped legs, double spends and landing.

Worth knowing

  • Persisted runs. The stored run snapshot gained fields: leftOut, slippage, optionsPerBatch, fromBlock. Runs stored by older builds are not migrated.
  • Allow-list. The predict allow-list and its properties gained the credits calls; splitFromBase is the only call allowed to carry native value.
  • Vendored contracts. Seer's SeerCredits and CreditsManager are vendored under packages/contracts/contracts/vendor/trading-credits for the local mock. They pull in solmate (AGPL-3.0) as a dev dependency.
  • Fixtures. test:fork uses the newest indexed session with pools near 0.5. Re-seed with yarn simulate:session:gnosis-fork then yarn simulate:liquidity:gnosis-fork.

Follow-ups (filed, not in this PR)

  • The envio indexer and the hardhat fork node can stall on the local stack.
  • The tx-orchestrator drops a hash when a send errors after broadcast (predict guards against this itself).
  • sDAI rate margins.
  • Gas budget for very large sessions.
  • Fold the list on phones.
  • iOS safe-area viewport.
  • The Lens assertSwapExecutable guard is a no-op for Wrapped1155 outcome tokens.

🤖 Generated with Claude Code

tractorss and others added 30 commits October 19, 2026 13:19
…ver answers completes from the batch the chain mined, and Try the rest signs nothing again

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…and what a taken nonce means

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…d record and its progress

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…t, spend any credits the manager covers, and preview the credits swap's loss

The per-run credits cap and the refusal on the credits swap's loss are gone; the swap
keeps its slippage floor, and previewCreditsSwap names the loss for Details.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…n underlying, for Details

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…imeout

wagmi's mock connector sends eth_sendTransaction with viem's default 10 s, which a fork
executing a large batch outlasts; production wallets do not use this connector.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…onfig, so the dev wallet loads without the app's env

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…e size in the run's record

Measured on a Gnosis fork: ~0.57M per batch, ~1.25M per option, ~0.53M for the credits
leg; the model counts 0.7M, 1.5M and 0.7M against half the latest block's gas limit.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ed when a shown plan changed beyond the trade slippage

start(shown) compares each shown option's plan with the plan the pools give now; confirm()
goes ahead against the plans it named, and stops again if they move once more.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…quote and loss for Details

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…the swap's loss is shown rather than refused

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…, as it may be the credits transfer

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… custom percent, with a high-value threshold per swap kind

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ops short of its estimate, the credits swap and its loss, custom slippage, and a changed move's figures before

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…fore signing, and name the confirm

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…p's header, over its body, and confirm moved prices from its button

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…he moved ones, and take slippage and the credits swap's quote from the visitor's settings

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ort option and the credits swap, at 1280 and 375 in both themes

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ot a trade's

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…phone on its side has scrolled to its end

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ils showed

Each batch re-reads the pools before it is built and stops at moved, with the
new figures, when its plan differs from what the run is held to; confirm
merges them in and resumes, mid-run included. The credits swap is held to the
floor Details showed and its loss is re-quoted at every build; Details and the
check plan the split from what that floor mints, not from the credits at par.
An option shown no plan that now trades is moved, the moved tolerance is
capped at the default slippage, and a batch is sized under the 2^24 per-
transaction gas cap. Predict waits for the plans and the credits quote.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ne, and give the settings a Done

An out-of-range entry keeps its text and error after the field is left and
saves nothing. Custom slippage starts at 0.01%, warns under 0.1% on trades and
0.05% on the credits swap, and errors and warnings carry an icon. The credits
swap's card shows only to a visitor with credits; the panel's fields scroll
under a fade with a Done in the button's place, and Tab wraps at its edges.
The buy budget's unread share constant is replaced by a property over every
slippage setting.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… way back

At moved, Details opens on the changed figures, whose Before line takes the
warning tone, the status line names the one market that moved, and Go back
returns the form where nothing was signed. Details always names each kind's
max slippage, Auto included, as a row that opens the settings, sums the pool
fees across markets, and a credits swap losing 1% or more is said on the
status line. Under the Settings title the X closes the settings, and the
title enters as the panel does. A reverted batch names raising max slippage.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…entry, moved Details and Go back; redraw their snapshots

The settings, moved and Details snapshots redraw for Done, the warning icon,
the unit without a gap, the Auto row in Details and the summary opening on
moved figures. A slippage row in folded Details is text, not a button the
dialog's focus trap would stop on, and the reverted copy fits two lines at
320 px. Selectors name the footer's own button now that Done shares its class.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…dits quote where no route swaps them

A run that stops at moved after its deposit keeps the funds step it was
pressed with, as running and halted runs do. Credits ticked with no credits
route are never quoted, so the popup says they cannot be spent rather than
leave Predict checking prices.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…e same credits and slippage, and never under the live quote's floor

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…out of trading before it waits on the button

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ne on its first retry

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… stops at moved where the plan or the credits swap changed

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… chain or the gnosis fork with one command

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
tractorss and others added 28 commits October 25, 2026 13:07
…en minutes pass, then asks first (R15-G4)

A coded wallet error is read once; whatever the nonce says short of mined or
taken, the send is held and its step halts at `held`. A retry within ten
minutes reads the nonce again and signs nothing while it is unused. Past ten
minutes the step halts at `resign` and the popup asks, with the button "Sign
again", before the step is signed. A nonce taken by another transaction lets
the hold go and the step is signed again on the next press. A node that names
insufficient funds or a revert refused the send, so that is not held. The
4 s grace is gone.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…is dead, with Check again and Discard (R15-G9)

The send behind a submitted step stays in the snapshot as `sent`, nonce
included, until the step settles. When reconcile reports the hash unseen,
the nonce is read: under another transaction it lets the hash go and the
step is signed again on the next press; mined as the same transaction under
another hash, the run waits on that one; pending or mined as this hash, it
reads as pending. Still unused, the hash is kept at `unseen` (Check again,
Discard) for ten minutes from the send, then the run asks before signing
again, as a held send does. A stored hash with no nonce kept goes straight
to the ask. driver.discard() lets go of the hash and throws the run away,
only in those two states.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
… receipt reads mined or reverted (R15-G14)

Recovery reads the receipt of each submitted step of such a run and stores
the step as confirmed or pending once it has one, so Close and the card's
Discard forget the run. A hash with no receipt keeps the run stored.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…e and the app's, so a lagging node never reuses one (R16-CL-3, CX-2)

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…nothing and stops at wrong network or wallet, retryable (R16-CL-1)

viem's ChainMismatchError and AccountNotFoundError, wagmi's connector errors and EIP-1193 4100/4900/4901
are raised before eth_sendTransaction, so no nonce can be in use.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…only if it still reads unused (R16-CX-1)

Covers the held-send path through the step's canSkip and the unseen-hash path through a fresh reconcile.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…eld sends by their nonce, so Close and Discard can forget it (R16-CL-2, CX-3)

Applies to a run naming other contracts and to one whose options differ from the session's. A send mined
is stored confirmed; one whose nonce was taken, or stayed unused past the hold, is dropped; a hash stored
with no send is dropped once no node knows it and the record is past the store's 72 h expiry.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…n runner test keeps its imports in one group

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…t waits out the hold and answers Sign again before the batch lands

The batch's send is stored before its prompt opens, so the reload holds it for ten minutes as decided in R15-G4;
the test failed at 08d80a7 expecting an immediate re-sign.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…e send held, since it can follow a broadcast

EIP-1193 4100/4900/4901 come from the wallet and can arrive after it broadcast
(a WalletConnect session dropping). Only viem and wagmi's own pre-request errors
now drop the hold; a coded answer settles by nonce like any other.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…Locks are missing or refused, and refuses the run when neither works

The lease is keyed foresight.predict-lease.<flowId>, lasts 90 s, is renewed every 15 s, and is dropped on release and on pagehide. Another tab takes it only once it has run out. RunOwnership gains heldHere(flowId).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…, rather than naming another tab, and the card holds its Discard

The driver reports 'prompt-open' when the claim is refused while this tab holds the run; the popup and the card both show "A wallet prompt from this page is still open. Answer it in your wallet." The card polls while it holds the run, so Discard comes back once the prompt is answered.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…ses, since that popup's own driver still holds the run until it unmounts

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…on test

Recovery and retry show the funding a Resume will spend; Try the rest keeps the
held credits preview. A buy that grows keeps a proportional minimum. Uncertain
sends keep their evidence: nonce-search exhaustion, Discard on a stale unseen
status and the session close no longer release them; a replaced or cancelled
step never reads as success; an interrupted xDAI mint read is redone before the
batch. Storage reads no longer delete expired records; a stored run missing a
planned step is refused. What reaches signing from the indexer is checked
against chain and config: collateral decimals, canonical pools and their
orientation, the parent matching the URL, child bounds and tokens.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
… the popup lets go; mocks expose what session verification reads

A recovered retry keeps the run's saved slippage instead of the global setting.
Discard holds the run until its reconcile reads and deletion finish. Recovery
gets its own credits-price line, since its credits checkbox is hidden. The mock
Seer factory and market expose collateralToken() and wrappedOutcome(), so the
localhost stack passes the on-chain session check.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016rFSsfe3nTwTKF5gcb6ELA
…n the hourly grid

A young or busy session showed one point per option per hour, so a burst of
trades folded into its last price. Each trade inside the drawn range now adds
its own point; the hourly grid still fills quiet stretches.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016rFSsfe3nTwTKF5gcb6ELA
…epic

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016rFSsfe3nTwTKF5gcb6ELA
Ported from the parked 10-11 epic run branches; the behaviour is on the PR, these cases were not pinned.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016rFSsfe3nTwTKF5gcb6ELA
Ported from the parked 10-11 epic run branches; the behaviour is on the PR, these cases were not pinned.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016rFSsfe3nTwTKF5gcb6ELA
Ported from the parked 10-11 epic run branches; the behaviour is on the PR, these cases were not pinned.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016rFSsfe3nTwTKF5gcb6ELA
Ported from the parked 10-11 epic run branches; the behaviour is on the PR, these cases were not pinned.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016rFSsfe3nTwTKF5gcb6ELA
Ported from the parked 10-11 epic run branches; the behaviour is on the PR, these cases were not pinned.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016rFSsfe3nTwTKF5gcb6ELA
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…pend on

Expected values come from MINT_RATE_MARGIN_BPS, BATCH_GAS_SHARE_BPS,
optionsPerBatch, buyBudgetShareBps, MOVED_TOLERANCE_BPS, PRICE_TOLERANCE_TICKS
and QUOTE_DEADLINE_SECONDS instead of literals; batch sizing asserts the chosen
size, not the search order. Refusals assert their departure reason; the Algebra
fake validates readContract too and no longer asserts inside itself. The
replacement cases join orchestrator.resume.test.ts. The getBlock latest-tag
mutant is equivalent and no longer asserted.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@tractorss
tractorss merged commit e9d4491 into master Oct 26, 2026
5 checks passed
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