UNPKG

@lifi/compose-spec

Version:

Public wire-format types and schemas for Compose flows

186 lines (171 loc) 6.78 kB
/** * Canonical wire-format types for the standalone `POST /simulate` endpoint. * * `/simulate` takes a raw, pre-encoded transaction (a `to` address, hex `data`, * optional native `value`), funds a sender, runs it in one `eth_call`, and * reports the watched balances before and after, their signed deltas, and the * inner-call gas. Unlike `POST /compose`, its response body is **un-enveloped**: * `{ status, block, timestamp, ... }` lives at the top level (no `{ data }` * wrapper). * * These are the single source of truth for the request/response shapes. The * server-side Zod schemas in `@lifi/api-schemas` (`src/routes/simulate.ts`) * validate the same contract and are kept in lockstep by a compile-time * conformance assertion (`src/routes/simulate.typecheck.ts`). * * Mirrors the hand-authored style of `src/compile.ts`. */ /** * Maximum number of watched `(token, owner)` pairs accepted by `POST /simulate`. * * The limit comes from the VM's hard register budget (~123 usable registers, * checked on the final ISA layout). Each pair costs two result registers — a * before-read and an after-read — that are both live at the closing abi-encode, * so the compiler cannot coalesce them; that `2 × N` is the cost that scales. * Token and owner address literals do not scale it, since constant-propagation * merges identical-valued literals into one register. The cap leaves headroom * for the inner-call / gas-measurement / abi-encode scaffolding. */ export const SIMULATE_MAX_TRACKED_BALANCES = 40; /** * Maximum number of funding `requirements` accepted by `POST /simulate`. * * Each requirement triggers a slot-discovery cycle (access-list probe + sentinel * verification + RPC round trips) in slot-finder, so an unbounded array lets a * single request amplify into arbitrary upstream RPC work. Bounded here at the * trust boundary and re-enforced server-side in `rawSimulation.ts`. */ export const SIMULATE_MAX_REQUIREMENTS = 40; /** A `(token, owner)` pair whose balance is watched across the simulation. */ export interface TrackedBalance { /** Token to watch (use the zero address for native balance). */ readonly token: string; /** Account whose balance of `token` is watched. */ readonly owner: string; } /** * Funding instruction: seed an ERC-20 balance on a wallet before simulation. * Amounts accept `bigint | string` — the SDK serialises `bigint` to a decimal * string; the string side covers values already stringified. */ export interface Erc20BalanceRequirement { readonly type: "Erc20Balance"; readonly wallet: string; readonly token: string; readonly balance: bigint | string; } /** Funding instruction: seed a native balance on a wallet before simulation. */ export interface NativeBalanceRequirement { readonly type: "NativeBalance"; readonly wallet: string; readonly balance: bigint | string; } /** Funding instruction: seed an ERC-20 allowance before simulation. */ export interface Erc20AllowanceRequirement { readonly type: "Erc20Allowance"; readonly owner: string; readonly spender: string; readonly token: string; readonly allowance: bigint | string; } /** * The funding-instruction union applied before a simulation. Three variants, * discriminated by `type`. */ export type SlotFinderRequirement = | Erc20BalanceRequirement | NativeBalanceRequirement | Erc20AllowanceRequirement; /** Request body for `POST /simulate`. */ export interface SimulateRequest { /** EVM chain id. */ readonly chainId: number; /** * Sender of the simulated transaction. The VM bytecode is injected here so * the inner call carries `msg.sender == from`. */ readonly from: string; /** Target contract of the raw transaction. */ readonly to: string; /** Pre-encoded transaction calldata (`0x`-prefixed hex). */ readonly data: string; /** * Native value (wei) sent with the inner call. Accepts `bigint | string`; * defaults to `"0"` server-side when omitted. */ readonly value?: bigint | string; /** * Block number to simulate against. Omitted ⇒ the chain head. Named tags * (e.g. "latest") are rejected with HTTP 400. */ readonly block?: number; /** Funding instructions applied before simulation (max 40). */ readonly requirements?: readonly SlotFinderRequirement[]; /** Balances to read before and after the transaction (1–40 pairs). */ readonly trackedBalances: readonly TrackedBalance[]; } /** * One watched balance in a simulation response. `amount` is a decimal string; * for `deltas` it is signed (`after - before`). */ export interface SimulateBalanceEntry { readonly token: string; readonly owner: string; readonly amount: string; } /** * HTTP 200, `status: "ok"`: the simulation ran successfully. Balance arrays are * ordered to match the request `trackedBalances`. `gasUsed` is inner-call * execution gas only. * * Note: the array element types are intentionally mutable (`SimulateBalanceEntry[]`, * not `readonly SimulateBalanceEntry[]`) so they stay structurally identical to * the Zod-inferred wire types, which `z.array(...)` infers as mutable `T[]`. The * bidirectional conformance assertion in `@lifi/api-schemas` relies on this. */ export interface SimulateOkResult { readonly status: "ok"; readonly block: number; readonly timestamp: number; readonly balancesBefore: SimulateBalanceEntry[]; readonly balancesAfter: SimulateBalanceEntry[]; readonly deltas: SimulateBalanceEntry[]; readonly gasUsed: string; } /** Decoded revert diagnostics, when slot-finder can parse the revert reason. */ export interface SimulateRevertDecodeResult { readonly errorCandidates?: { readonly decodedErrorSignature: string; readonly decodedParams: string[]; }[]; readonly error?: string; } /** * HTTP 200, `status: "revert"`: the simulation ran but the transaction reverted * on-chain. A revert is a *successful simulation*, not a transport error. */ export interface SimulateRevertResult { readonly status: "revert"; readonly block: number; readonly timestamp: number; readonly revertReason?: string; readonly code?: number; readonly rawErrorBytes?: string; readonly decodeResult?: SimulateRevertDecodeResult; } /** * HTTP 422, `status: "error"`: the request was well-formed but the simulation * could not be set up or run. `message` is intentionally generic. */ export interface SimulateSetupErrorResult { readonly status: "error"; readonly message: string; } /** * Discriminated result of `POST /simulate`. `switch (result.status)` narrows to * one member: `ok` and `revert` come back with HTTP 200, `error` with HTTP 422. */ export type SimulateResult = | SimulateOkResult | SimulateRevertResult | SimulateSetupErrorResult;