[simplex]: Post operator limit orders to the HyperFX orderbook - #1270
Conversation
d5904a5 to
407f570
Compare
Wizdave97
left a comment
There was a problem hiding this comment.
Reviewed against #1267 and the orderbook's own source at polytope-labs/hyperfx-orderbook@main.
The wire format is right. I checked every boundary against the server rather than against the issue text:
tests/fixtures/orderbook-schema.graphqlis byte-identical to the orderbook'sschema.graphql, andorderbook-userops.jsonto itsfixtures/userops.json.- The EIP-712
CancelOrder/Heartbeatfield order and the two-field domain matchcrates/core/src/messages.rsexactly. - The posted op satisfies
crates/core/src/verify.rs:session = 0,output.assets[0].amount = 0, FillOptions v2,source == destination == fillChain, nonce =bidNonceKey(commitment, 0) << 64, commitment-prefixed 97-byte signature. The declaration round-trips throughcrates/core/src/paymaster.rs. schema.test.tsvalidating every sent document and pinning all three enums against the published schema is the right call — it catches exactly the class of drift nothing else here would.
Worth recording somewhere, because it isn't obvious and it does hold: the gateway takes protocolFeeBps off the input (IntentGatewayV2.sol:356, with the commitment computed over the reduced inputs) while the orderbook shades the output (crates/core/src/haircut.rs). For a linear fee the algebra coincides, so bookPrice and what simplex actually pays agree end to end. The "Pricing basis" paragraph in #1267 is correct.
Comments below, the first being the one I'd block on.
cba02ea to
00cb33c
Compare
00cb33c to
182a52e
Compare
|
All seven actioned. Four here in Two landed in #1271 rather than here, because they need the fill path and the The contract deviation is called out in the description. Your On the fee basis: I could not reproduce "the algebra coincides" at first, because I assumed |
Wizdave97
left a comment
There was a problem hiding this comment.
Re-reviewed at 11ea8c4c. All seven comments are addressed, and e434064b is a better fix than I proposed on two of them:
- Retryable failures now keep the row
openwithlastErrorset, so reconciliation reposts it. A timeout no longer retires an order the operator still wants. - Transport failures are their own
kind: "failed"onCancelOrderResultrather than being relabelledUNKNOWN_ORDER, andcancel()now keeps the commitment on the row when it could not confirm the entry is gone — which is the part that actually prevents orphaning. ORDER_EXISTSlooks the entry up through the neworderAtquery and adopts it asunchanged, falling back to the nonce bump only when the lookup fails. Exactly what §2 asks for.serverInfo.chainsis validated for bothfillChainand every declared source, with the right caveat that it only answers half ofUNSUPPORTED_SOURCE_CHAIN.- The
:idroute returns fills viawithFills, andlimit-order:resized/limit-order:fillednow exist.
The create-API shape still differs from the one #1267 published ({tokenIn, amountIn, tokenOut, amountOut} rather than {book, side, price, size}). I still think the implementation's shape is the better one — just worth a line in the PR description or an edit to the issue so the contract has one published form.
No further comments from me on this one.
cf839a1 to
19afa7b
Compare
19afa7b to
f09aebf
Compare
4f59e76 to
483e6d0
Compare
2bc78b2 to
aa9de76
Compare
aa9de76 to
02cbfdd
Compare
`fillOrder` takes one quote per leg: the input beside each output is the most escrow the solver takes for it. A limit order's op carries the one leg it is, so its take is the whole input the operator wants for what they are paying — the rate itself, since the order's own output amount is zero, as the orderbook requires. The op is still never executed, and there is no longer a version to choose for it: `encodeFillOrder` has one shape and `FillOptionsVersion` is gone with the versioned encoder.
A posting is a bid's op in form, and SolverAccount now derives a bid's nonce key from its calldata as well as its order and session key. The posting follows the same rule, so the orderbook's nonce binding and the account's are one derivation.
02cbfdd to
8f2eb40
Compare
An operator can now create, list, fetch and cancel limit orders, and simplex advertises each one on the HyperFX orderbook. A limit order is what simplex offers to pay, stored in a new
limit_orderstable inbids.db, and the orderbook entry is a derived copy built from a signedfillOrderUserOperation that nothing ever submits. Every request is validated againstserverInfoandbooksbefore it is stored, so the rejection codes the orderbook keeps for bad requests should never come back.Pricing incoming orders against these limit orders, and drawing
remainingdown on a fill, land separately.The create request deviates from #1267 deliberately. The issue publishes
{ book, side, fillChain, price, size, acceptedSources, ttlSecs, expiresAt? }; this takes{ fillChain, tokenIn, amountIn, tokenOut, amountOut, acceptedSources, ttlSecs, expiresAt? }and derives the book, the side and the rate from the two amounts. An operator states what they take in and what they pay out, so the direction falls out of the request instead of being a separate field they can contradict, and there is no question of which price they meant. The issue needs updating, or this noting when it merges.Review fixes are in the last two commits. A retryable
OrderSubmissionFailedno longer retires the order: it staysopenwith the reason on it for reconciliation to post again, where before one timeout killed it permanently. AcancelOrderthat never got an answer reads asfailedrather thanUNKNOWN_ORDER, so it no longer clears a commitment whose entry may still be live.ORDER_EXISTSis answered by readingorder(solver, commitment)and treating a live entry of ours as the posting it already is, instead of bumping the nonce and leaving two entries behind one liability. AndserverInfo.chainsis checked before posting, so a mistyped source chain is refused on the way in.Two of that review's points are answered in #1271 rather than here, because they need code this PR does not have: the missing
limitOrderResizedandlimitOrderFilledevents, which come out of the fill path, andGET /api/limit-orders/:idcarrying the fills that consumed the order, which reads thelimitOrderIdbids only carry from there on.docs/ai/decisions/2026-09-16-the-protocol-fee-is-already-out-of-an-order-by-the-time-we-price-it.mdrecords the fee basis:placeOrderemitsreducedInputs, so what the matcher prices is already net, the solver gets the rate it signed, andbookPriceis the same trade seen from the swapper's side. The invariant worth keeping is that the event carries reduced inputs; gross ones would make every payout1/(1-f)too large with nothing failing.Stacked on #1268, which it needs for
encodePhantomBidDeclaration.Part of #1267.