import { EventEmitter } from 'eventemitter3'; import { SuiGrpcClient } from '@mysten/sui/grpc'; import { Ed25519Keypair } from '@mysten/sui/keypairs/ed25519'; import { T as T2000Error, aD as T2000Options, Q as TransactionSigner, l as PayOptions, m as PayResult, aE as SwapResult, aF as SwapQuoteResult, w as SupportedAsset, r as SendResult, B as BalanceResponse, J as TransactionRecord, e as DepositInfo, aG as PaymentRequest, $ as ZkLoginProof, aH as SuiCoreClient, j as OverlayFeeConfig, aI as SponsoredCoinMergeCache, x as SwapRouteResult, aJ as SendableAsset } from './send-D2nKpwJN.js'; export { aK as CETUS_USDC_SUI_POOL, C as CLOCK_ID, a as COIN_REGISTRY, b as ClassifyBalanceChange, c as ClassifyResult, d as CoinMeta, aL as CoinPage, aM as DEFAULT_GRPC_URL, D as DEFAULT_NETWORK, E as ETH_TYPE, f as ExtractedTransfer, aN as GASLESS_MIN_STABLE_AMOUNT, aO as GASLESS_STABLE_TYPES, G as GAS_RESERVE_MIN, I as IKA_TYPE, K as KNOWN_TARGETS, g as KeypairSigner, L as LABEL_PATTERNS, h as LOFI_TYPE, M as MANIFEST_TYPE, i as MIST_PER_SUI, N as NAVX_TYPE, aP as OPERATION_ASSETS, O as OVERLAY_FEE_RATE, aQ as Operation, P as PREFLIGHT_MAX_AMOUNT, k as PREFLIGHT_OK, n as PreflightResult, aR as SENDABLE_ASSETS, S as STABLE_ASSETS, o as SUI_DECIMALS, p as SUI_TYPE, q as SUPPORTED_ASSETS, aS as SelectAndSplitResult, aT as SerializedCetusRoute, aU as SerializedCetusRoutePath, aV as SerializedRouterDataV3, s as SimulationResult, t as StableAsset, u as SuiHolding, v as SuiRpcTxBlock, y as T2000ErrorCode, z as T2000ErrorData, A as T2000_OVERLAY_FEE_WALLET, F as TOKEN_MAP, H as TransactionLeg, R as TxDirection, U as USDC_DECIMALS, V as USDC_TYPE, W as USDE_TYPE, X as USDSUI_TYPE, Y as USDT_TYPE, Z as WAL_TYPE, _ as WBTC_TYPE, a0 as ZkLoginSigner, aW as addSendToTx, aX as addSwapToTx, aY as assertAllowedAsset, a1 as buildSendTx, a2 as buildSwapTx, a3 as checkPositiveAmount, a4 as checkSuiAddress, a5 as classifyAction, a6 as classifyLabel, a7 as classifyTransaction, aZ as deserializeCetusRoute, a8 as executeTx, a9 as extractAllUserLegs, aa as extractTransferDetails, ab as extractTxCommands, ac as extractTxSender, ad as fallbackLabel, a_ as fetchAllCoins, ae as findSwapRoute, af as formatAssetAmount, ag as formatSui, ah as formatUsd, a$ as getCoinMeta, ai as getDecimals, aj as getDecimalsForCoinType, b0 as getSuiClient, b1 as getSuiGrpcClient, b2 as isAllowedAsset, b3 as isCetusRouteFresh, b4 as isInRegistry, ak as mapMoveAbortCode, al as mapWalletError, am as mistToSui, b5 as normalizeAsset, b6 as normalizeCoinType, an as parseSuiRpcTx, ao as payWithMpp, ap as preflightFail, aq as preflightPay, ar as preflightSend, as as preflightSwap, b7 as queryHistory, b8 as queryTransaction, at as rawToStable, au as rawToUsdc, av as refineLendingLabel, aw as resolveSymbol, ax as resolveTokenType, b9 as selectAndSplitCoin, ba as selectSuiCoin, bb as serializeCetusRoute, bc as simulateTransaction, ay as stableToRaw, az as suiToMist, bd as throwIfSimulationFailed, aA as truncateAddress, aB as usdcToRaw, aC as validateAddress, be as verifyCetusRouteCoinMatch } from './send-D2nKpwJN.js'; import { TransactionObjectArgument, Transaction } from '@mysten/sui/transactions'; import { SuinsClient } from '@mysten/suins'; import '@mysten/sui/client'; import '@cetusprotocol/aggregator-sdk'; import '@mysten/sui/jsonRpc'; interface LimitsConfig { /** Caps any single outbound write (send | swap | pay) by USD value. */ perTxUsd?: number; /** Caps CUMULATIVE outbound spend (all writes) per UTC day, by USD value. */ dailyUsd?: number; } interface DailySpend { /** UTC date `YYYY-MM-DD` the running total applies to. */ date: string; /** USD spent so far on `date`. */ usd: number; } interface LimitsFile { limits?: LimitsConfig; dailySpend?: DailySpend; } declare function readLimitsFile(configDir?: string): LimitsFile; /** * Write the limits + dailySpend keys, PRESERVING any other keys already on * disk (a config file may carry unrelated data). Mode 0600. */ declare function writeLimitsFile(file: LimitsFile, configDir?: string): string; declare function getLimits(configDir?: string): LimitsConfig | undefined; declare function hasLimits(configDir?: string): boolean; /** Merge-set limits (only provided fields change). Returns the written path. */ declare function setLimits(limits: LimitsConfig, configDir?: string): string; /** Remove all limits (keeps the dailySpend ledger). Returns the written path. */ declare function clearLimits(configDir?: string): string; /** Cumulative USD spent so far today (0 when the ledger is from a prior day). */ declare function dailySpentToday(configDir?: string): number; /** Add `usd` to today's running total (resets on UTC date rollover). */ declare function recordDailySpend(usd: number, configDir?: string): void; type LimitKind = 'perTxUsd' | 'dailyUsd'; type LimitOperation = 'send' | 'swap' | 'pay'; declare class LimitExceededError extends Error { readonly code = "LIMIT_EXCEEDED"; readonly operation: LimitOperation; readonly limitKind: LimitKind; readonly limit: number; readonly attempted: number; constructor(params: { operation: LimitOperation; limitKind: LimitKind; limit: number; attempted: number; }); toJSON(): unknown; } /** * Best-effort USD value for an asset+amount pair, for the limit gate. Stables * are 1:1; SUI (and anything else) returns `null` — "unknown, don't gate". * The limits are USD-denominated; SUI-USD gating would need a price lookup and * is intentionally out of scope (matches the prior CLI behavior). */ declare function approxUsdValue(asset: string, amount: number): number | null; /** * Pure gate — no I/O. Returns silently when allowed; throws * `LimitExceededError` when blocked. */ declare function assertLimitConfig(input: { limits: LimitsConfig | undefined; spentTodayUsd: number; operation: LimitOperation; amountUsd: number; force?: boolean; }): void; interface LimitAssertInput { operation: LimitOperation; /** This write's USD value. Pass 0 / non-positive to skip (e.g. SUI sends). */ amountUsd: number; /** When true, skip the gate. */ force?: boolean; } /** * The spending-limit enforcer — reads/writes `~/.t2000/config.json` (or a * `configDir` override for tests). Constructed once by the T2000 agent and * called around every outbound write; also drives the CLI `t2 limit` commands. */ declare class LimitEnforcer { private readonly configDir?; constructor(configDir?: string | undefined); /** Throws `LimitExceededError` when the write exceeds an opted-in cap. */ assert(input: LimitAssertInput): void; /** Add a settled write's USD value to today's cumulative total. */ record(amountUsd: number): void; getLimits(): LimitsConfig | undefined; hasLimits(): boolean; setLimits(limits: LimitsConfig): void; clearLimits(): void; dailySpentToday(): number; } interface T2000Events { balanceChange: (event: { asset: string; previous: number; current: number; cause: string; tx?: string; }) => void; healthWarning: (event: { healthFactor: number; threshold: number; severity: 'warning'; }) => void; healthCritical: (event: { healthFactor: number; threshold: number; severity: 'critical'; }) => void; yield: (event: { earned: number; total: number; apy: number; timestamp: number; }) => void; error: (error: T2000Error) => void; } declare class T2000 extends EventEmitter { private readonly _signer; private readonly _keypair?; private readonly client; private readonly _address; /** Unified spending-limit gate (per-tx + cumulative daily, USD). Shared by * CLI + MCP + programmatic writes — one gate, no bypass (R-0 Finding 1). */ readonly limits: LimitEnforcer; private constructor(); private constructor(); static create(options?: T2000Options): Promise; static fromPrivateKey(privateKey: string, options?: { network?: 'mainnet' | 'testnet'; rpcUrl?: string; }): T2000; static init(options?: { pin?: string; passphrase?: string; keyPath?: string; name?: string; }): Promise<{ agent: T2000; address: string; }>; /** SuiGrpcClient used by this agent — exposed for integrations. */ get suiClient(): SuiGrpcClient; /** Ed25519Keypair used by this agent — exposed for CLI/MCP integrations. */ get keypair(): Ed25519Keypair; /** Transaction signer (works for both keypair and zkLogin). */ get signer(): TransactionSigner; pay(options: PayOptions): Promise; swap(params: { from: string; to: string; amount: number; byAmountIn?: boolean; slippage?: number; force?: boolean; }): Promise; /** * [SPEC_AGENTIC_STACK P1 / SDK F2 — 2026-05-25] * Thin wrapper around the standalone `getSwapQuote()`. Pre-Phase 1 this method * was ~50 LoC duplicating `swap-quote.ts` — missing `serializedRoute` (SPEC 20.2 * fast-path) and the `providers` allow-list (Bug A fix / Pyth-dependent * provider filter for sponsored callers). Routing both API surfaces through * one implementation ensures fixes land for both. */ swapQuote(params: { from: string; to: string; amount: number; byAmountIn?: boolean; providers?: string[]; }): Promise; address(): string; /** * Send `amount` of `asset` to `to` (hex address or SuiNS name). * * [v4.0 Phase A Day 2 — SPEC_AGENT_WALLET_GREENFIELD §A] * * **Breaking changes from v3.x:** * - `asset` is now REQUIRED (no implicit `?? 'USDC'` default). Callers * must specify `'USDC' | 'USDsui' | 'SUI'`. Sending `'USDT'` / * `'USDe'` / `'WAL'` / `'ETH'` / `'NAVX'` / `'GOLD'` now errors * with `INVALID_ASSET` — swap to a stable first. * - USDC + USDsui builds go through `SuiGrpcClient` so the gRPC build * resolver auto-detects `0x2::balance::send_funds` eligibility and * zeros gas at simulate time. Result: **gasless USDC / USDsui sends * from a zero-SUI wallet.** SUI sends stay on the standard gas-paid * path. * * Submission stays on the JSON-RPC client (the rest of the SDK * expects JSON-RPC for read paths, and Sui's docs explicitly support * the "build via gRPC, execute via JSON-RPC" hybrid). */ send(params: { to: string; amount: number; asset: SupportedAsset; force?: boolean; }): Promise; /** * Resolve a recipient string into a canonical 0x address: * 1. **hex** — `0x…` is used directly (no RPC round-trip). * 2. **SuiNS** — `alex.sui` resolves via `suix_resolveNameServiceAddress`. * * Anything else is rejected. (The legacy `contacts.json` alias map was * removed — SuiNS supersedes local contacts.) * * Returns `{ address, suinsName? }` so `send()` can stamp the name source * on the receipt without re-resolving. Throws * `T2000Error('SUINS_NOT_REGISTERED', …)` for well-formed but unregistered * SuiNS names, keeping the SDK error surface `T2000Error`-only. * * Public so MCP's `t2000_send` dryRun preview shares one resolution path * with the live execute path (never resolve the same input two ways). */ resolveRecipient(input: string): Promise<{ address: string; suinsName?: string; }>; balance(): Promise; history(params?: { limit?: number; }): Promise; transactionDetail(digest: string): Promise; deposit(): Promise; /** * [SPEC_AGENTIC_STACK P1 / SDK F2 — 2026-05-25; refreshed S.342 / 2026-05-26] * Preferred alias of `deposit()`. Was introduced to mirror the v3 `t2000 fund` * CLI command; the v4 CLI surface is `t2 fund` (renamed back from the * interim `t2 receive` in S.464). `deposit()` stays as the canonical method name for * back-compat; `fund()` stays as a programmatic alias for audric + other * SDK consumers that prefer the verb. */ fund(): Promise; receive(params?: { amount?: number; currency?: string; memo?: string; label?: string; }): PaymentRequest; exportKey(): string; static fromZkLogin(opts: { ephemeralKeypair: Ed25519Keypair; zkProof: ZkLoginProof; userAddress: string; maxEpoch: number; rpcUrl?: string; }): T2000; private emitBalanceChange; } /** * Canonical 0x-address shape. Loose lower bound (>= 1 hex char) so * pre-v1.0 short addresses still validate; tools that need a full 64-hex * address can additionally check `length === 66`. Case-insensitive on * the `0x` prefix because some upstream callers (and historic tests) * uppercase the prefix as part of address normalization tests; the * normalizer always returns the lowercased canonical form regardless. */ declare const SUI_ADDRESS_REGEX: RegExp; /** * Strict canonical 0x-address shape (66 chars total). Used by the * resolver's "looks like" check to disambiguate "user pasted an * address" vs "user typed a SuiNS name". */ declare const SUI_ADDRESS_STRICT_REGEX: RegExp; /** * Mirrors the pattern in `audric/apps/web-v2/lib/suins-cache.ts` so the * host-side send executor and the engine-side read normalizer agree on * what counts as a SuiNS name. SuiNS allows nested labels (`team.alex.sui`) * but every label must use only `[a-z0-9-]`. */ declare const SUINS_NAME_REGEX: RegExp; declare class InvalidAddressError extends Error { readonly raw: string; constructor(raw: string); } declare class SuinsNotRegisteredError extends Error { readonly name_: string; constructor(name_: string); } declare class SuinsRpcError extends Error { readonly name_: string; constructor(name_: string, detail: string); } /** * Returns true if `value` looks like a SuiNS name (case-insensitive). * Cheap synchronous check — use to avoid an unnecessary RPC round-trip * for inputs that obviously aren't names (0x addresses, contact handles). */ declare function looksLikeSuiNs(value: string): boolean; /** * Resolve a SuiNS name to its on-chain Sui address via the Sui GraphQL * `address(name:)` query. Returns `null` if the name resolves to no address * (= not registered or expired). Throws `SuinsRpcError` on transport failure. * * `ctx.signal` is honored for cancellation. (`ctx.suiRpcUrl` is retained for * call-site back-compat but no longer used — resolution runs against the * canonical GraphQL endpoint via `getSuiGraphQLClient()`.) */ declare function resolveSuinsViaRpc(rawName: string, ctx?: { suiRpcUrl?: string; signal?: AbortSignal; }): Promise; /** * Reverse-resolve a 0x address to its SuiNS name via the Sui GraphQL * `address(address:){ defaultNameRecord { domain } }` query. Returns a * single-element array with the address's **default (primary)** name, or * `[]` when it has none. Throws `SuinsRpcError` on transport failure. * * Behavior note: the legacy JSON-RPC path returned *all* names for the * address; GraphQL exposes the explicitly-configured default record, which is * the one every consumer here actually uses (the LLM `resolve_suins` tool + * card titles only ever render the primary). Returning `[primary]` keeps the * `string[]` contract intact. * * Why this is its own helper (not folded into `normalizeAddressInput`): a * reverse lookup adds a second round-trip per tool call. We don't want every * read tool that takes an `address` to silently double its latency. The lookup * primitive is opt-in via the `resolve_suins` tool; normalizers stay * forward-only. */ declare function resolveAddressToSuinsViaRpc(rawAddress: string, ctx?: { suiRpcUrl?: string; signal?: AbortSignal; }): Promise; interface NormalizedAddress { /** * Canonical 0x-prefixed lowercase hex address. Always set on success. * Tools should use this for any downstream query (BlockVision, NAVI, * positionFetcher, etc.) and for cache keys. */ address: string; /** * The user-facing name when the input was resolved via SuiNS, otherwise * `null`. Tools should stamp this on result data so card titles can * render "Balance · obehi.sui" instead of "Balance · 0x1234…abcd". */ suinsName: string | null; /** * The original input (pre-normalization). Useful for error narration — * "I tried to resolve `Obehi.Sui` and …" reads better than the * post-trim/lowercase form. */ raw: string; } /** * Canonical normalizer. Accepts a 0x address OR a SuiNS name; returns * a structured `NormalizedAddress`. Throws: * - `InvalidAddressError` if the input matches neither shape. * - `SuinsNotRegisteredError` if the SuiNS name is well-formed but * resolves to null (= not registered). * - `SuinsRpcError` if the RPC fails. * * Every read tool (engine) and write helper (SDK `T2000.send()`) that * accepts a user-supplied recipient MUST call this helper before any * downstream lookup. Doing the check inline (1) duplicates the regex, * (2) silently rejects SuiNS names, (3) makes cache keys inconsistent * across tools. */ declare function normalizeAddressInput(value: string, ctx?: { suiRpcUrl?: string; signal?: AbortSignal; }): Promise; /** * Full wallet balance: **every** held coin, not just the curated stables + SUI. * * Uses one `listBalances` (paginated) instead of N `getBalance` calls, then * partitions: stables → priced $1, SUI → Cetus-oracle priced, **everything else * → `tokens[]` amount-only** (`usdValue: null` — no price oracle for arbitrary * tokens; we don't guess). Decimals for non-registry tokens are read on-chain * (the only added call, and only for tokens you actually hold — necessary for a * correct amount). `totalUsd` sums **priced holdings only** so it never * overstates; `tokens` surfaces the rest honestly. */ declare function queryBalance(client: SuiCoreClient, address: string): Promise; declare function generateKeypair(): Ed25519Keypair; declare function keypairFromPrivateKey(privateKey: string): Ed25519Keypair; /** * Save a keypair as a v2 plain Bech32 JSON file with `0o600` perms. * * `_passphrase` is accepted but IGNORED — kept in the signature for * back-compat with v3.x callers. Will be removed when all callers * are off PIN (Phase A Day 5+). */ declare function saveKey(keypair: Ed25519Keypair, _passphrase: string | undefined, keyPath?: string): Promise; /** * Save a Bech32 secret directly as a v2 plain wallet file. Used by any * path where the secret is supplied externally rather than freshly * generated. Kept exported because external tooling may still want it * even though the CLI no longer ships an `--import` flag. */ declare function saveBech32(secret: string, keyPath?: string): Promise; /** * Load a keypair from disk. * * `_passphrase` is accepted but IGNORED — kept for back-compat. The * actual file format determines the load path: * * - **v2 plain JSON** (`{ version: 2, secret: "suiprivkey1..." }`) → * decode + return keypair. * - **Anything else** → throw `WALLET_CORRUPT`. The user moves or * deletes the file and runs `t2 init` to create a fresh wallet. */ declare function loadKey(_passphrase?: string, keyPath?: string): Promise; declare function walletExists(keyPath?: string): Promise; declare function exportPrivateKey(keypair: Ed25519Keypair): string; declare function getAddress(keypair: Ed25519Keypair): string; /** * SPEC 7 v0.4 § Layer 0 — Canonical Write Architecture. * * `composeTx({ steps })` is the single canonical entry-point for every * Audric Enoki-sponsored write. The fragment-appender pattern (Layer 1) * is the implementation; this primitive dispatches each step to its * registered appender from the typed `WRITE_APPENDER_REGISTRY`. * * Single-write and multi-write go through the same code path. A 1-step * `composeTx([{ toolName: 'send_transfer', input: {...} }])` produces * the same shape of result as a 3-step bundle. * * **Why this exists** * * Pre-Layer-0, four parallel write stacks lived across the audric host: * `transactions/prepare` (fat ~600-line route), `services/prepare` * (hand-rolled), `debug-swap` (diagnostic), and PayButton (dapp-kit). * Each one re-implemented merge/split/transfer + hand-maintained the * `allowedAddresses` array Enoki requires. Two production bugs in the * past 60 days came from drift between them — PR-H1 (claim-rewards * self-transfer missing from allowedAddresses) and PR-H4 (borrow/ * withdraw self-transfer same bug). * * Layer 0 collapses 3 of those stacks into one canonical primitive. * PayButton stays out by design (different signer, different trust * model — see `audric-canonical-write.mdc` for the rationale and the * `// CANONICAL-BYPASS:` escape-hatch contract). * * **What composeTx owns** * * - PTB assembly via per-tool wallet-mode appenders (the * `WRITE_APPENDER_REGISTRY`). * - Pre-built `txKindBytes` (`tx.build({ onlyTransactionKind: true })`) * ready for Enoki's `createSponsoredTransaction`. * - Auto-derived `derivedAllowedAddresses` from the assembled PTB's * top-level `transferObjects` calls — eliminates the PR-H1/H4 bug * class permanently. Hand-maintained arrays are now unreachable. * - S.38 Pyth flag plumbing — `sponsoredContext: true` automatically * applies `skipPythUpdate` (borrow/withdraw) and `skipOracle` (repay) * to NAVI appenders so Enoki doesn't reject `tx.gas`-as-argument. * * **What composeTx does NOT own** * * - **Fees** — Audric concern, not @t2000/sdk concern (CLAUDE.md * rule #9). The SDK is fee-free by design as of @t2000/sdk@1.1.0 * (B5 v2). Audric host wraps composeTx with fee insertion in P2.2c. * - **Sponsorship** — caller's job. composeTx returns `txKindBytes` * pre-built for Enoki; the caller calls `createSponsoredTransaction` * with their JWT. * - **Chain-mode coin handoff between steps** — Layer 2 (engine * bundling) ships this. P2.2b is wallet-mode-only: each step fetches * coins independently. Layer 2 will extend by introducing * `inputCoinFromStep: number` to thread upstream output coins. * * **Cross-references** * * - Spec → `spec/SPEC_7_MULTI_WRITE_PTB.md` § "Layer 0: Canonical * Write Architecture" * - Read-side companion → `t2000/.cursor/rules/single-source-of-truth.mdc` * + `audric/.cursor/rules/audric-canonical-portfolio.mdc` * - Write-side rule → `audric/.cursor/rules/audric-canonical-write.mdc` */ /** * Canonical write tools. The 8 tools that can be composed into a PTB. * * History: * - 2026-05-08 (Track B): added `harvest_rewards` (compound macro). * - 2026-05-25 (S.323): removed `volo_stake` / `volo_unstake` — full Volo * cut across SDK/CLI/MCP after the engine cut Volo in S.277. vSUI * remains as a passive token (NAVI reward, Cetus swap target). * * Excluded by design: * - `save_contact` — no on-chain leg (Prisma-only). */ type WriteToolName = 'send_transfer' | 'swap_execute'; /** * [v4.0 Phase A Day 2 — SPEC_AGENT_WALLET_GREENFIELD §A] * `asset` is now REQUIRED (no more silent USDC default). The parameter * type is the wider `SupportedAsset` rather than the narrower * `SendableAsset` so callers that thread a wide-typed asset through * (primarily the engine LLM tool surface) compile without modification. * Runtime narrowing happens via `assertAllowedAsset('send', asset)`, * which throws `INVALID_ASSET` for anything outside the * `['USDC', 'USDsui', 'SUI']` whitelist. USDC + USDsui route through * the gasless `0x2::balance::send_funds` Move call; SUI uses the * standard `transferObjects` path. * * Audric hosts: the audric chat client (`audric-chat-client.tsx`) * already defaults asset to `'USDC'` at the marker layer before * calling `sponsoredTx({ type: 'send' })`, so this signature change * doesn't break the LLM flow. LLM intents like "send 5 WAL" now * surface a clear error instead of silently building a non-gasless * tx. */ interface SendTransferInput { to: string; amount: number; asset: SupportedAsset; } interface SwapExecuteInput { from: string; to: string; amount: number; slippage?: number; byAmountIn?: boolean; /** Cetus provider allow-list. Sponsored callers MUST pass an exclusion list * to remove Pyth-dependent providers — see addSwapToTx JSDoc. composeTx * derives this automatically from `sponsoredContext` if omitted. */ providers?: string[]; /** * [SPEC 20.2 / D-1 (a)] Optional precomputed Cetus route discovered at * `swap_quote` time. Bypasses `findSwapRoute()` (-400-500ms) inside * `addSwapToTx`. Caller (audric prepare-route) is responsible for * coin-type verification (D-2) + freshness check (D-3) before passing. * `addSwapToTx` performs an additional sanity check on `amountIn` + * `byAmountIn` and falls back to fresh discovery on mismatch. */ precomputedRoute?: SwapRouteResult; } /** * Discriminated union mapping `toolName` → `input`. Used to type * `WriteStep` so consumers get autocomplete + compile-time validation * that the input matches the tool. * * **[SPEC 13 Phase 1] `inputCoinFromStep`** — consumer steps may * reference an earlier step's output coin handle by index. When set, * `composeTx`'s orchestration loop threads the producer's * `outputCoin` into this step's appender as the `inputCoin` arg, * bypassing the wallet pre-fetch path. The producer's terminal * `tx.transferObjects([coin], sender)` is suppressed automatically so * the same handle isn't double-consumed. */ type WriteStep = { toolName: 'send_transfer'; input: SendTransferInput; inputCoinFromStep?: number; } | { toolName: 'swap_execute'; input: SwapExecuteInput; inputCoinFromStep?: number; }; interface ComposeTxOptions { sender: string; steps: WriteStep[]; client: SuiCoreClient; /** * S.38 Pyth flag (sponsorship-critical). When true: * - NAVI borrow/withdraw appenders apply `skipPythUpdate: true` * (preserves on-chain price-feed updates, skips the tx.gas-using * Pyth fee payment that Enoki rejects). * - NAVI repay appender applies `skipOracle: true` (debt reduction * has no health-factor risk, full oracle bypass is safe). * - Cetus swap appender applies `getProvidersExcluding([HAEDALPMM, * METASTABLE, OBRIC, STEAMM_OMM, STEAMM_OMM_V2, SEVENK, * HAEDALHMMV2])` — Pyth-dependent providers reference `tx.gas` for * oracle fees, also rejected by Enoki. * - SUI sends fetch coins via `getCoins` (tx.gas belongs to sponsor, * not user) instead of splitting from `tx.gas`. * * Self-funded callers (CLI, MCP, server tasks) leave this `false` / * omit — they pay all oracle/Pyth fees from their own SUI gas. */ sponsoredContext?: boolean; /** * Per-call overlay fee config for Cetus swaps. Audric host passes * `{ rate: 0.001, receiver: T2000_OVERLAY_FEE_WALLET }` to charge the * 0.1% swap overlay. CLI / direct SDK callers omit. Forwarded to * `addSwapToTx`'s `input.overlayFee`. */ overlayFee?: OverlayFeeConfig; } /** Per-step preview returned by each registry appender. Tool-specific shape. */ type StepPreview = { toolName: 'send_transfer'; effectiveAmount: number; recipient: string; asset: SendableAsset; } | { toolName: 'swap_execute'; effectiveAmountIn: number; expectedAmountOut: number; route: SwapRouteResult; }; interface ComposeTxResult { tx: Transaction; /** * Pre-built bytes for Enoki's `createSponsoredTransaction`. Built * with `onlyTransactionKind: true` so the gas coin can be supplied * by the sponsor. */ txKindBytes: Uint8Array; /** * Auto-derived from the assembled PTB's top-level `transferObjects` * commands. Replaces hand-maintained `allowedAddresses` arrays in * audric host's `transactions/prepare` + `services/prepare` — * eliminates the PR-H1/H4 bug class permanently. */ derivedAllowedAddresses: string[]; perStepPreviews: StepPreview[]; } /** * Per-appender context passed into every registry entry. Carries the * RPC client, sender, sponsorship flag, optional per-call overlay * fee config (Cetus swaps), and SPEC 13 Phase 1 chain-mode fields. */ interface AppenderContext { client: SuiCoreClient; sender: string; sponsoredContext: boolean; overlayFee?: OverlayFeeConfig; /** * [SPEC 13 Phase 1] When set, the consumer appender consumes this * coin handle directly instead of pre-fetching from the wallet via * `selectAndSplitCoin` / `selectSuiCoin`. Provided by the * orchestration loop when the step has `inputCoinFromStep` set; the * loop looks up `priorOutputs[step.inputCoinFromStep]` and threads * it through here. * * In chain mode, the consumer consumes the handle IN FULL — the * `input.amount` field is treated as informational (used for preview * math). This matches Cetus's `routerSwap`, NAVI's `deposit`/`repay`, * and the Sui `transferObjects` semantics: each takes a coin object * and consumes its entire balance. */ chainedCoin?: TransactionObjectArgument; /** * [SPEC 13 Phase 1] True when this step's output coin will be * consumed by a downstream step (some later step has * `inputCoinFromStep === currentStepIndex`). Producer appenders MUST * skip their terminal `tx.transferObjects([coin], ctx.sender)` call * when this is true — otherwise the same `TransactionObjectArgument` * gets used twice (once by the consumer, once by the transfer) and * the PTB build fails or the on-chain leg reverts. */ isOutputConsumed?: boolean; /** * Per-PTB merge cache for sponsored coin-object sourcing, shared across * every appender in a single `composeTx` run. Lets multiple legs that * source the same coin type (two SUI swaps, swap USDC + save USDC, etc.) * reuse a single merged primary coin instead of each re-fetching + * re-merging the same coin objects (the second merge of which references * already-consumed coins → Enoki dry-run `ArgumentWithoutValue`). Applies * to ALL coin types, not just SUI. See `SponsoredCoinMergeCache` JSDoc. */ coinMergeCache?: SponsoredCoinMergeCache; } /** * [SPEC 13 Phase 1] Appender return shape. Producers populate * `outputCoin` so the orchestration loop can thread it into a * downstream consumer's `chainedCoin`. The terminal consumer * (`send_transfer`) omits it. * * `swap_execute` is the only dual-mode tool — it accepts `chainedCoin` * AND populates `outputCoin`. (Pre-S.444 the DeFi tools * save/withdraw/borrow/repay were also producers/consumers; removed with NAVI.) */ interface AppenderResult { preview: TPreview; outputCoin?: TransactionObjectArgument; } type AppenderFn = (tx: Transaction, input: TInput, ctx: AppenderContext) => Promise>; /** * Cetus provider exclusion list for sponsored flows. Mirrors the * audric host's `SPONSORED_TX_PROVIDERS` constant — these 7 providers * reference `tx.gas` for Pyth oracle fee payments, which Enoki rejects. * * NOTE: keeping this hardcoded means `findSwapRoute` doesn't need a * dependency on `@cetusprotocol/aggregator-sdk`'s `getProvidersExcluding` * helper — composeTx forwards the literal list to Cetus, Cetus does the * inverse lookup. Result is identical. */ declare const SPONSORED_PYTH_DEPENDENT_PROVIDERS: readonly ["HAEDALPMM", "METASTABLE", "OBRIC", "STEAMM_OMM", "STEAMM_OMM_V2", "SEVENK", "HAEDALHMMV2"]; /** * Get all eligible Cetus provider names except the Pyth-dependent ones, * for sponsored swap context. Computed from the Cetus SDK's * `getAllProviders()` minus the exclusion list. * * [Bug A fix / 2026-05-10] Exported so the engine's `swap_quote` tool can * call this when discovering routes for sponsored hosts. Previously the * engine only exclude these providers at COMPOSE time; now it excludes * at QUOTE time too, so the precomputed route stashed on * `pending_action.cetusRoute` is always sponsor-safe. */ declare function getSponsoredSwapProviders(): Promise; /** * The typed registry. Each entry is a wallet-mode dispatcher that takes * (tx, input, ctx) and returns a per-step preview. Compile-time check * that all `WriteToolName` values have an entry — TypeScript will * fail the build if a tool is missing. * * [S.444] Narrowed to send_transfer + swap_execute after the NAVI/DeFi cut. */ declare const WRITE_APPENDER_REGISTRY: { send_transfer: AppenderFn>; swap_execute: AppenderFn>; }; /** * Walks the assembled PTB's command list and extracts every recipient * address from top-level `TransferObjects` commands. Top-level only — * recipients inside nested Move calls are NOT inspected (Enoki only * cross-checks top-level commands). * * Replaces hand-maintained `allowedAddresses` arrays. Two production * bugs in 60 days came from drift between the array and the actual * PTB recipients (PR-H1 + PR-H4). Computing this from the PTB makes * drift impossible by construction. */ declare function deriveAllowedAddressesFromPtb(tx: Transaction): string[]; /** * Compose a PTB from a list of canonical write steps. Each step * dispatches to its registered fragment-appender; the assembled PTB is * returned alongside pre-built `txKindBytes` ready for Enoki sponsorship * + auto-derived `derivedAllowedAddresses`. * * Single-step: `composeTx({ steps: [{ toolName: 'send_transfer', input: {...} }], ... })` * Multi-step (Layer 2): `composeTx({ steps: [{...}, {...}, {...}], ... })` * * Throws: * - `T2000Error('NO_APPENDER')` — unknown `toolName` * - Any error thrown by the per-step appender (insufficient balance, * asset not supported, route not found, etc.) — propagates as-is. */ declare function composeTx(opts: ComposeTxOptions): Promise; declare function getSwapQuote(params: { walletAddress: string; from: string; to: string; amount: number; byAmountIn?: boolean; /** * [Bug A fix / 2026-05-10] Optional Cetus provider allow-list, forwarded to * `findSwapRoute`. Sponsored callers (Enoki) MUST pass * `getSponsoredSwapProviders()` to remove Pyth-dependent providers * (HAEDALPMM, METASTABLE, OBRIC, STEAMM_OMM/_V2, SEVENK, HAEDALHMMV2). * Those providers cause the Cetus aggregator's internal `routerSwap` to * insert a `tx.splitCoins(tx.gas, ...)` call for the Pyth update fee, * which Enoki rejects with HTTP 400 "Cannot use GasCoin as a transaction * argument" (3-step bundle smoke 2026-05-10). * * Pre-fix: `getSwapQuote` discovered routes against the FULL provider set, * stashed Pyth-dependent routes onto `pending_action.cetusRoute` (SPEC * 20.2 fast-path), and the audric `prepare` route's `swap_execute` * appender used the precomputed route AS-IS — bypassing the providers * filter that `composeTx` correctly applied. Result: every swap whose * best route happened to include a Pyth-dependent provider failed at * Enoki sponsorship. * * Non-sponsored callers (e.g. CLI direct swap) leave this undefined to * keep access to the full provider set including Pyth-dependent pools. */ providers?: string[]; /** * [2.11] On-chain-resolved decimals for `from` / `to`, supplied by callers * that hold a client (e.g. `T2000.swapQuote`). When omitted (standalone / * sponsored callers), falls back to the registry — correct for registry * tokens, a 9-decimal guess for an unknown coin type. */ fromDecimals?: number; toDecimals?: number; }): Promise; /** * SuiNS leaf-subname builders for the `audric.sui` parent NFT. * * Used by SPEC 10 v0.2.1 Phase A.1 — Audric Passport Identity. Lets the audric host * mint and revoke `username.audric.sui` leaves under the parent NFT held by the * dedicated custody address (`0xaca29165…23d11`). * * **Why a dedicated SDK module (not inline in the audric route):** * - Consumers (audric/web's `/api/identity/{reserve,change,release}` routes) can import * the canonical builder shape without re-discovering the `SuinsTransaction` API. * - Single source of truth for the parent NFT ID + parent name. * - Single source of truth for label validation (length / charset / hyphen rules). * * **Signer model — read this before wiring into a route:** * These builders are signed by the **service account** (the parent NFT custody address), * NOT by the user's zkLogin key. Per `audric/.cursor/rules/audric-canonical-write.mdc`, * the SPEC 10 leaf-mint API routes are explicitly documented as a CANONICAL-BYPASS of * the `composeTx` write contract — they are server-to-server, the user's key is never * in the loop, and Enoki sponsors the gas. PTB atomicity requires single-signer, so * leaf mints cannot be bundled with chat-agent writes via composeTx. * * **Reference:** `spec/runbooks/RUNBOOK_audric_sui_parent.md` §1 (parent NFT ID) + * §3 (validated SDK reference shape) + §4 (mainnet smoke test 2026-05-01). */ /** * Parent name registered on SuiNS mainnet. Audric's identity namespace anchor. * * Every leaf created via `buildAddLeafTx` becomes `