@lifi/compose-spec
Version:
Public wire-format types and schemas for Compose flows
186 lines (171 loc) • 6.78 kB
text/typescript
/**
* 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;