Skip to content

Instantly share code, notes, and snippets.

@patcito
Last active April 23, 2026 15:45
Show Gist options
  • Select an option

  • Save patcito/a70ef01aeca8e2f2dc6fe20205a8cf9b to your computer and use it in GitHub Desktop.

Select an option

Save patcito/a70ef01aeca8e2f2dc6fe20205a8cf9b to your computer and use it in GitHub Desktop.
FE rounds 7 + 8 status — what shipped + what's open

Shipped today

Lot of stuff landed. All of this is either live in dev or rolling right now.

1. That 0xea70c7ed revert

Decoded it — it's ftPositionManagerMinEquity(), the protocol's per-position equity floor. 5x on $0.10 gives you $0.10 of equity backing $0.40 of borrow, which is below what ftDNMM will let you open. That's why the order failed.

Added a stable code for it: ORDER_BELOW_MIN_EQUITY. Shows up on the POST response (code) and the poll response (errorCode). Friendly message: "position equity below PM minimum (bump up sellAmount or lower leverage)". Keep equity ≥ $1 on dev and you won't hit it.

2. /account/orders returns failed orders now

I wired the indexer to proxy to the executor's GET /leverage/orders. Failed orders show up in the Order History table with the revert reason:

{
  "status": "failed",
  "errorMessage": "ORDER_REVERTED: Error(string): Return amount is not enough",
  "errorCode": "ORDER_REVERTED"
}

So you see every order — pending, filled, failed — not just fills.

3. Position.margin populated

Was hardcoded "0". Now returns notional / leverage in USD. Falls back to "0" only if mark or leverage aren't available — honest zero, not a fabricated number.

4. Liquidated positions dropped from /positions

Shorts with liquidationPriceReason="user_debt_unavailable" (on-chain debt cleared) and longs with "user_collateral_unavailable" are filtered out. No more ghost rows from positions that already got closed out.

5. Funding fixes

  • rate is a plain numeric string ("0.0009"). No more % suffix — you can append it at render time.
  • total=0 for fresh users (count was joining on exposures only, so a parked USDC balance blew it up to 72).
  • size scaled by base decimals (no more 132458992489623049 wS values).

6. Trade history price + fee salvage

Old rows (from before the scaling fix shipped) with raw-wei price or fee auto-salvage at read time:

  • Price: when stored price is > 100× the current mark, substitute notional / mark.
  • Fee: when fee > notional * 10, divide by decimals to get the human value.

New rows index correctly from the start. No DB migration, fix is pure read-path.

7. order_pending WS frame (new)

Fires the instant POST /mm/leverage/orders accepts the order — BEFORE the tx even goes on-chain. You get the frame ~50-100ms after the POST returns.

{
  "type": "order_pending",
  "address": "0x…",
  "chainId": 146,
  "data": {
    "orderId": "lev_…",
    "status": "pending",
    "orderType": "open|close|swap",
    "createdAt": 1776944355,
    "invalidate": ["order"]
  }
}

Dispatch on type like the other account-channel frames. Insert the row into Open Orders immediately; the order_filled or order_failed frame updates it in place via orderId when the tx resolves.

8. balance_update carries the full snapshot

You said you'd rather not refetch REST after every balance_update. Fair. Every frame now embeds data.balances: AccountBalance[] — exact same shape as GET /mm/spot/account/balance:

{
  "type": "balance_update",
  "address": "0x…",
  "chainId": 146,
  "data": {
    "asset": "0x…", "eventSig": "Borrow", "blockNumber": 12345,
    "timestamp": 1776944355, "invalidate": true,
    "balances": [
      { "chainId": 146, "network": "Sonic",
        "token": "wS", "tokenAddress": "0x…",
        "totalBalance": "1.5", "usdValue": "0.065",
        "pnlPct": "+2.4%" }
    ]
  }
}

Replace your balances state directly from the frame. Skip the REST call entirely.

Graceful degrade: if the snapshot build times out (>2s), the frame falls back to signal-only (no balances field). Your hook should handle both — when balances present, replace state; when absent, treat as invalidation. Same handler either way.

9. Sub-second latency on on-chain events — newHeads push

This is the big one you pushed for. The indexer now subscribes to the chain's WebSocket newHeads stream and runs the collector the instant a new head lands. Before: 2-4s floor (chain block + 1s collector poll + 1s projector poll). Now: block time + WS hop, roughly 100-300ms to balance_update arrival.

Pure-poll fallback kicks in automatically if the WS endpoint drops — the collector degrades in-place without a process restart. Dev is using the Alchemy wss endpoint.

Combined with the rich balance_update above, a Withdraw should land in your table within ~500ms end-to-end (block + WS + projector + fan-out + render). Not 3-4s anymore.

10. Session nonce — I was wrong last time

Correction: session flow is already 0 wallet popups per order. The ephemeral delegate key signs SessionCall locally in useSessionManager.signSessionCall. If you're seeing 3 popups on TP + SL, it means delegateKeyRef.current is empty when signSessionCall fires and the fallback walletClient.signTypedData path kicks in — that's 1 popup per order because the wallet's actually signing. Check localStorage for the key session_<addr>_dk — if it disappears mid-flow you've got a session-restore bug, not a signature-count one.

The real nonce issue is separate: executor keys idempotency on (sessionId, nonce). While Order A is pending at nonce N, submitting Order B with the same nonce just dedupes back to A. That breaks OCO (TP + SL pending together) and limit-stacking.

Two fixes, I can ship the second one this week if you want it:

  1. Contract-side: non-sequential nonces. Bitmap of used nonces within a sliding window (say 64 slots). Clean, but needs a redeploy.
  2. Executor-side OCO. Relax idempotency: allow multiple pending orders at the same (sessionId, nonce) when they're tagged mutually-exclusive. When one fires, the session nonce advances → the others auto-invalidate → executor marks them cancelled. That's literally OCO for free. Flag it on submit with ocoGroupId and we're done. ~100 lines, no contract work.

Keep sigType: "session" for everything. Do NOT route limits to EIP-712 — that would actually add popups.

11. Still open

Item Status What I need
/account/summary aggregate Not shipped Confirm the field list (perpBalance, crossMarginRatio, maintenanceMargin, unrealizedPnl, crossAccountLeverage) and I'll ship
OCO / nonce lock See #10 above Say go and I'll do option 2
SL trigger-price watcher Not shipped Priority? It's its own little service
Relayer PM.approveBorrow / approveEngine Out of my lane SC + relayer team call
Dev oracle price staleness Not a code bug Operator thing — dev oracle router isn't being pushed fresh prices

If anything above looks wrong after dev settles, send me the exact request (URL + response for REST, or raw frame for WS) and I'll trace it. The rich balance_update + order_pending shapes are new — tell me the moment they don't look right and I'll fix same day.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment