Repository Public
1// MANDATE_LIVE — the narrow, server-side interface for an automated strategy runtime (the perp-agent engine). Nothing here is2// reachable over HTTP: the owner's session cannot place a passkey-less order, only code running inside the API process can.3//4// What a caller can rely on (docs/v6/DERIVATIVE-MANDATE.md):5// - it holds no key and cannot widen anything: every order is checked by the API's risk engine against the venue's real state,6// then by the isolated signer against the mandate the owner approved with his passkey (positions and exposure included);7// - `placeMandateOrder` is idempotent on `ctx.clientId` and returns the order as RECONCILED with the venue; a state of8// `unknown` means the venue gave no usable answer — it resolves only by reconciliation (call `mandateOrder` later);9// - a refusal is a thrown `UserError` with a stable `code`; the order is then stored `rejected` with the reason;10// - stopping never needs anything: `cancelMandateOrder` works while the venue authorization lives, and reduce-only orders pass11// while halted, when limits are used up, and for 24 h after the mandate is revoked or expires (wind-down).12import type { OrderRequest, Position } from '@spw/derivatives';13import { UserError } from '../config.ts';14import type { LiveService, MandateOrderCtx } from './exec.ts';15import { live } from './live.ts';1617export type { MandateOrderCtx } from './exec.ts';18export type MandateOrderResult = Awaited<ReturnType<LiveService['placeMandateOrder']>>;19export type MandateAccountState = Awaited<ReturnType<LiveService['mandateAccountState']>>;20export type MandateLiveOrder = MandateOrderResult['order'];2122/**23 * Place ONE order under the account's derivative mandate — no passkey.24 *25 * `req`: a complete OrderRequest. `price` is required (limit price; for `market`, the worst acceptable price). `req.clientId`26 * is ignored: the idempotency key is `ctx.clientId` (8–100 chars of [A-Za-z0-9._:-], unique per account).27 * `ctx.strategySha256` and `ctx.agentId` must be those the mandate was approved for (else `mandate_mismatch`).28 *29 * Returns `{ order, idempotent, estimate }`; `order.state` ∈ open | partially_filled | filled | cancelled | rejected | unknown30 * (+ submitted/signed transiently), `order.final`, `order.filled`, `order.avgFillPrice`, `order.reason`, `order.source`.31 * A venue refusal is NOT an exception: it is `order.state === 'rejected'` with the venue's words in `order.reason`.32 *33 * Throws UserError (`code`, `status`):34 * 400 bad_request · 403 mainnet_disabled | mandate_mainnet_disabled · 404 unknown_account | unknown_instrument ·35 * 409 idempotency (the key belongs to a manual order) · 422 mode_read_only | authorization_expired | mandate_none |36 * mandate_mismatch | mandate_wind_down | mandate_floor_equity | halted | <risk engine code> | signer_<code>37 * (signer_position_exceeded, signer_gross_exceeded, signer_orders_day, signer_orders_hour, signer_turnover, signer_price,38 * signer_no_price, signer_floor_equity, signer_leverage_setting, signer_venue_unreachable, signer_venue_stale,39 * signer_venue_inconsistent, signer_unpriced_position, signer_halted, signer_wind_down, signer_revoked, signer_expired, …) ·40 * 503 asset_table_mismatch | venue_unavailable | signer_unreachable | mandate_unavailable.41 */42export function placeMandateOrder(accountId: string, req: OrderRequest, ctx: MandateOrderCtx, svc: LiveService = live()): Promise<MandateOrderResult> {43 return svc.placeMandateOrder(accountId, req, ctx);44}4546/** Withdraw a mandate order from the venue (no passkey). Returns the order as the venue reports it afterwards. */47export async function cancelMandateOrder(accountId: string, clientId: string, svc: LiveService = live()) {48 return svc.cancelOrder(accountId, clientId);49}5051/**52 * Set the leverage (and margin mode) of one instrument under the mandate — no passkey; at most the mandate's leverage for that53 * instrument. Needed once before the first order when the venue's setting is above it (a new account is at 20× cross): the54 * signer refuses any order that can add exposure while the setting is higher (`signer_leverage_setting`).55 */56export function setMandateLeverage(accountId: string, body: { instrument: string; leverage: number; marginMode: 'cross' | 'isolated' }, ctx: Pick<MandateOrderCtx, 'strategySha256' | 'agentId'>, svc: LiveService = live()) {57 return svc.setMandateLeverage(accountId, body, ctx);58}5960/** The stored order of an idempotency key, reconciled with the venue now; null when no such order exists. */61export async function mandateOrder(accountId: string, clientId: string, svc: LiveService = live()): Promise<MandateLiveOrder | null> {62 try { return (await svc.placeMandateOrderReplay(accountId, clientId))?.order ?? null; }63 catch (e) { if (e instanceof UserError && e.code === 'unknown_order') return null; throw e; }64}6566/**67 * The venue's own account state, positions and open orders (source: 'venue'; `error` non-null = the venue could not be read —68 * treat the account as unreadable, never as empty), the mandate in force, and whether an order may be placed right now.69 */70export function mandateAccountState(accountId: string, svc: LiveService = live()): Promise<MandateAccountState> {71 return svc.mandateAccountState(accountId);72}7374/** The venue's positions of the account. Throws `venue_unavailable` (503) when the venue cannot be read. */75export async function mandatePositions(accountId: string, svc: LiveService = live()): Promise<Position[]> {76 const s = await svc.mandateAccountState(accountId);77 if (s.error !== null) throw new UserError('venue_unavailable', s.error, 503);78 return s.positions;79}80