-
Notifications
You must be signed in to change notification settings - Fork 132
[simplex]: Post operator limit orders to the HyperFX orderbook #1270
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
12 commits
Select commit
Hold shift + click to select a range
e9fe399
Post operator limit orders to the HyperFX orderbook
dharjeezy 77422fe
Derive a limit order's rate and side from the amounts the operator gives
dharjeezy 5806db8
Keep a limit order alive when the orderbook could not decide it
dharjeezy 1f0e92f
Record why the protocol fee needs no adjustment in the limit order path
dharjeezy 0c10643
Require the orderbook and give a limit order one clock
dharjeezy 5134316
Take limit order amounts in whole tokens
dharjeezy 068176f
Make limit orders part of every running filler
dharjeezy a24e50f
Hand the binary the limit order controller unconditionally
dharjeezy 2bfbeca
Give the desktop end to end configs an orderbook section
dharjeezy a804b77
Give the packaged smoke test an orderbook section
dharjeezy e7b3028
Post a limit order in the one fill shape there is
Wizdave97 8f2eb40
Key a limit order's posting by its calldata, as every bid is
Wizdave97 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
83 changes: 83 additions & 0 deletions
83
...ex/docs/ai/changelog/2026-09-15-limit-orders-posted-to-the-hyperfx-orderbook.md
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,83 @@ | ||
| # 2026-09-15 — Limit orders posted to the HyperFX orderbook | ||
|
|
||
| The operator can now create limit orders, and simplex advertises them on the HyperFX orderbook. | ||
| An operator states a limit order as what simplex takes in and what it pays out for that, for example | ||
| 10,000 USDC in for 139,000,000 cNGN out. The rate and the side of the book follow from those two | ||
| amounts, so an order is directional by construction: that one prices USDC to cNGN swaps and can | ||
| never price cNGN to USDC, which needs its own order. An operator can hold as many at once as they | ||
| like, and an incoming order is matched against all of them. | ||
|
|
||
| Orders live in a new `limit_orders` table in `bids.db` behind `SimplexDataStore.limitOrders`, and the | ||
| orderbook entry is a derived copy that expires and is reposted. Amounts and prices are decimal | ||
| strings at 1e18 everywhere they cross the orderbook boundary, whatever decimals the tokens use on | ||
| their own chains. | ||
|
|
||
| ## The API | ||
|
|
||
| `Simplex.limitOrders` and four routes on the operator server: | ||
|
|
||
| ``` | ||
| GET /api/limit-orders?status=&chain=&book= | ||
| GET /api/limit-orders/:id | ||
| POST /api/limit-orders { fillChain, tokenIn, amountIn, tokenOut, amountOut, acceptedSources, ttlSecs? } | ||
| DELETE /api/limit-orders/:id | ||
| ``` | ||
|
|
||
| `amountIn` and `amountOut` are whole tokens, as decimal strings: `"1000"`, `"1500.25"`. Nobody | ||
| creating an order should have to know an asset's decimals, let alone that the orderbook normalises | ||
| everything to 1e18, so the handler scales them while it validates. Anything with more than 18 decimal | ||
| places, or that is not a plain decimal, is refused with the amount named. | ||
|
|
||
| `acceptedSources` is required and non-empty: it names the source chains the order accepts swaps | ||
| from, and the orderbook refuses an order that declares none. A create emits `limit-order:posted` or | ||
| `limit-order:rejected`, a cancel emits `limit-order:cancelled`. | ||
|
|
||
| A request is validated against `serverInfo` and `books` before anything is stored, so the rejection | ||
| codes the orderbook reserves for bad requests (`TTL_TOO_SHORT`, `MIN_ORDER_SIZE`, `UNSUPPORTED_PAIR`) | ||
| should never come back. The order is stored before it is posted, so a posting that fails leaves a | ||
| row carrying the reason rather than a request that vanished. | ||
|
|
||
| ## What gets signed | ||
|
|
||
| `ContractInteractionService.prepareLimitOrderUserOp` builds a `fillOrder` UserOperation for a | ||
| synthetic same-chain order at the operator's rate. It is a price commitment, not a transaction: | ||
| nothing ever submits it, the orderbook only verifies the solver signature over the userOpHash and | ||
| reads the amounts out of the calldata. So the gas fields are fixed rather than estimated, and | ||
| `paymasterAndData` carries the accepted-source declaration instead of a paymaster. | ||
|
|
||
| `FillOptions` takes one quote per leg, so the order's single leg carries `outputs[0]` — what the | ||
| operator pays — and `inputs[0]`, the whole input they want for it. That pair is the rate, since the | ||
| order's own output amount is zero as the orderbook requires. `validUntil` is a TTL in seconds from | ||
| the orderbook's receipt, not a block number. | ||
|
|
||
| There is one `fillOrder` shape, the one the gateway speaks, so the op is built without choosing a | ||
| version: the encoder has no other to offer. | ||
|
|
||
| The derived rate is rounded in simplex's favour, and so is the input the op is built from, so | ||
| neither the stored rate nor a repost quotes better than the two amounts the operator gave. `REPLAYED` and `ORDER_EXISTS` are answered once by bumping | ||
| `orderNonce`, which changes the commitment and the userOpHash; the orderbook remembers every hash it | ||
| has accepted, so a fresh nonce is the only way past. | ||
|
|
||
| ## Config | ||
|
|
||
| ```toml | ||
| [orderbook] | ||
| url = "https://orderbook.hyperbridge.network/graphql" | ||
| defaultTtlSecs = 900 | ||
| reconcileIntervalSecs = 300 | ||
| requestTimeoutMs = 10000 | ||
| ``` | ||
|
|
||
| The section is required. Simplex prices every fill from the operator's limit orders and those | ||
| live on the orderbook, so there is no configuration without one. | ||
|
|
||
| `ttlSecs` is the only clock. It is the TTL written into the posting and the life of the order | ||
| itself: `expiresAt` is derived from it when the order is created, and nothing renews it. When it | ||
| runs out the posting lapses and the order is done. | ||
|
|
||
| The limit orders themselves are not configured here. They are inventory the | ||
| operator opens and closes while the filler runs, so they live in `bids.db` and are created over the | ||
| API. | ||
|
|
||
| Pricing still comes from the pair curves; matching incoming orders against limit orders, and drawing | ||
| `remaining` down on a fill, land separately. |
31 changes: 31 additions & 0 deletions
31
...simplex/docs/ai/changelog/2026-09-16-review-fixes-for-posting-and-cancelling.md
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,31 @@ | ||
| # 2026-09-16 — Review fixes for posting and cancelling | ||
|
|
||
| ## A failure is not a refusal | ||
|
|
||
| `post` marked an order `rejected` whichever way the orderbook answered, and `rejected` is terminal: | ||
| nothing looks at that row again. One `TIMEOUT`, one `DATABASE_UNAVAILABLE` or one dropped connection | ||
| during a create and the operator's order was dead with nothing retrying it. A retryable failure now | ||
| leaves the row `open` with no commitment and the reason in `lastError`, which is the shape | ||
| reconciliation already knows how to post again. A refusal still retires the order. | ||
|
|
||
| ## A cancel that got no answer says so | ||
|
|
||
| `signAndCancel` turned an unreachable orderbook into `UNKNOWN_ORDER`, which is the orderbook's | ||
| considered answer that the entry is already gone. `cancel` reads that as "already gone" and clears | ||
| the commitment, so a `cancelOrder` that merely timed out left a live entry nothing here owned. | ||
| `CancelOrderResult` has a `failed` variant now, and a cancel that fails keeps the commitment on the | ||
| row for reconciliation to find. | ||
|
|
||
| ## `ORDER_EXISTS` is not `REPLAYED` | ||
|
|
||
| `REPLAYED` is an op hash the orderbook remembers for an order that is gone, and a new nonce is the | ||
| only way past it. `ORDER_EXISTS` is a live entry sitting at the commitment we just tried, so bumping | ||
| the nonce puts a second entry behind one liability and records only the second. The client can read | ||
| `order(solver, commitment)` now, and a live entry that is ours is taken as the posting it already is. | ||
|
|
||
| ## The declared chains are checked before posting | ||
|
|
||
| `serverInfo.chains` lists every chain the orderbook serves. A fill chain or accepted source it does | ||
| not serve is refused on the way in, naming the ones it does, rather than coming back as | ||
| `UNSUPPORTED_SOURCE_CHAIN` against a row already stored. It answers half of that code: whether the | ||
| input symbol is registered on a chain is the server's own config and still cannot be checked here. |
32 changes: 32 additions & 0 deletions
32
...26-09-16-the-protocol-fee-is-already-out-of-an-order-by-the-time-we-price-it.md
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,32 @@ | ||
| # 2026-09-16 — The protocol fee is already out of an order by the time we price it | ||
|
|
||
| Decided: the matcher prices against `order.inputs[0].amount` as it arrives, with no fee adjustment | ||
| anywhere in the limit order path, and `bookPrice` is stored next to `price` rather than reconciled | ||
| with it. | ||
|
|
||
| `IntentGatewayV2.placeOrder` takes `protocolFeeBps` off the **input**: it escrows `X(1-f)` under a | ||
| commitment computed over the reduced inputs, and emits `inputs: reducedInputs`. So the order our | ||
| scanner reconstructs is already net of the fee, which is what `inputNet` in `fx.ts` is named for. | ||
|
|
||
| The orderbook shades the other side. `crates/core/src/haircut.rs` applies the fee to what the solver | ||
| delivers and quotes every price on that, so `bookPrice = amountOut(1-f) / amountIn`, where `price`, | ||
| the rate the op is signed at, is `amountOut / amountIn`. | ||
|
|
||
| Both are right, for different readers: | ||
|
|
||
| - the solver receives `X(1-f)` and pays `X(1-f) · price`, so it gets exactly the rate it signed and | ||
| the fee never touches it; | ||
| - the swapper pays `X` and receives `X(1-f) · price`, so their rate is `price(1-f)`, which is | ||
| `bookPrice`. | ||
|
|
||
| They are two sides of one trade rather than two estimates of one number, which is why both are kept | ||
| on the row and neither is derived from the other at read time. | ||
|
|
||
| What makes this worth recording is the failure it hides. If the gateway ever emitted gross inputs, | ||
| `inputNet` would quietly become gross, every payout would be `1/(1-f)` too large, and nothing here | ||
| would notice: the matcher takes the event at its word, and the golden vectors fix the op rather than | ||
| the event. The invariant to hold onto is that `OrderPlaced` carries reduced inputs. | ||
|
|
||
| Rejected: subtracting the fee ourselves before pricing. It would double-count today, and it would put | ||
| a second copy of the gateway's fee schedule in the filler, which is the thing the reduced event | ||
| exists to avoid. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,2 +1,8 @@ | ||
| /** Orders the engine evaluates concurrently when the config does not say. */ | ||
| export const DEFAULT_MAX_CONCURRENT_ORDERS = 5 | ||
|
|
||
| /** How long an orderbook request waits before it is treated as unreachable. */ | ||
| export const DEFAULT_ORDERBOOK_TIMEOUT_MS = 10_000 | ||
|
|
||
| /** The orderbook simplex posts to when the config does not name another. */ | ||
| export const DEFAULT_ORDERBOOK_URL = "https://orderbook.hyperbridge.network/graphql" |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.