Skip to main content

Intent System

Early Stage

The Intent system is a prototype. 1inch and LiFi have been migrated; stargate, wormhole, and uniswap-v4 are Phase 2. The handler contract and dispatch flow described here are stable, but the migration is not complete.

The Intent system is dappTerminal's confirm-before-execute UX pattern. On-chain commands produce a reviewable Intent object — a pre-resolved execution plan — before any signing occurs. The user inspects the plan, types confirm or cancel, and only then does the terminal execute.


What "Intent" Means Here

An Intent is not the conventional blockchain intent pattern. There are no solvers, no ERC-7683 cross-chain intents, no UniswapX-style declarative goal fulfillment, and no off-chain intent relay.

An Intent here is a pre-resolved, step-by-step execution plan held by the CLI until the user confirms. It contains:

  • steps — ordered list of UI steps, each with a label, description, and status (pending / running / complete / skipped / failed)
  • expectedBalanceChanges — token amounts in/out displayed in the preview card
  • execute closure — the async function that runs the on-chain operations when the user confirms

The analogy is a purchase receipt you review before clicking Pay.


Where It Fits in the Layer Stack

The Intent system spans three layers. See System Layers for the full layer map.

LayerRole in Intent flow
Layer 1 — CLI / Terminal UIOwns confirm/cancel UX; renders the preview card; stores pendingIntent on the tab; calls executeIntent on confirm
Layer 3 — Plugin LayerIntentHandler<T> functions in handlers.ts build and return the Intent
Layer 2 — Core RuntimeDispatches handler results; detects non-void returns; routes to preview or direct execution

The CLI layer owns the UX state machine (pending → confirmed → executing). Plugin handlers are pure: they build an Intent and return it. They do not call back into CLI internals.


Handler Types

Plugin handlers live in src/plugins/*/handlers.ts. See Plugin System: Handlers for the broader handler context.

// Display-only commands (quote, price, gas preview) — always return void
CommandHandler<T>Promise<Intent | void>

// On-chain execution commands — always return an Intent
IntentHandler<T>Promise<Intent>

IntentHandler<T> is a stricter subtype of CommandHandler<T>. Because Promise<Intent> is assignable to Promise<Intent | void>, IntentHandlers satisfy the existing handler contract without any changes to the plugin loader or command registry.

Display-only handlers (quote, price, limit order preview) return void and continue to satisfy CommandHandler<T>Promise<void> is assignable to Promise<Intent | void>.


Dispatch Flow

1. handler(data, ctx) → intentResult

2. if (intentResult is Intent):
a. renderIntentPreview()
→ styled preview card added to history item
b. tab.pendingIntent = { intent: intentResult, historyTimestamp }
c. async: fetch current balances
→ re-render preview with balance projections

3. User types "confirm"
→ executeIntent(pendingIntent.intent)
→ runs execute closure
→ setStepStatus callbacks animate each step in real-time
→ appendLinks adds explorer links on completion

4. User types "cancel"
→ pendingIntent cleared
→ normal prompt restored

If the handler returns void (display-only), step 2 is skipped — the CLI renders output directly and no pending intent is set.


Handler Contract

Handlers receive ExecutionContext & CLIContext. CLIContext provides:

Property / MethodPurpose
updateHistory / updateStyledHistoryDisplay-only output (used by void handlers)
addHistoryLinksAppend links to a history entry
fetchTokenBalanceRead a token balance for the connected wallet
activeTabIdCurrent tab identifier
walletAddressConnected wallet address
chainIdActive chain ID

Handlers do not call back into CLI internals to trigger transaction flows. An IntentHandler expresses its entire on-chain work inside the execute closure, which receives wagmi-compatible callbacks:

interface ExecuteCallbacks {
setStepStatus(stepId: string, status: StepStatus): void
appendLinks(links: HistoryLink[]): void
walletAddress: string
}

// Inside the Intent's execute closure:
execute: async (callbacks) => {
callbacks.setStepStatus('approve', 'running')
const hash = await sendTransaction(tx)
callbacks.setStepStatus('approve', 'complete')
callbacks.appendLinks([{ label: 'View on Explorer', href: explorerUrl(hash) }])
}

Migration Status

PluginStatus
1inch✅ Migrated to IntentHandler<T>
LiFi✅ Migrated to IntentHandler<T>
Stargate📋 Phase 2
Wormhole📋 Phase 2
Uniswap v4📋 Phase 2

To migrate a Phase 2 plugin:

  1. Type the handler as IntentHandler<T> instead of CommandHandler<T>
  2. Build and return an Intent (steps, expectedBalanceChanges, execute closure)
  3. Replace ctx.updateHistory(...) loading states with intent steps

No changes to the dispatch layer or runtime are required — the Promise<Intent | void> contract already accommodates it.


Relationship to Execution Engine

The Intent system directly addresses Blocker #1 from the Execution Engine hardening roadmap:

1. No handler return channel — Handler contract returns Promise<void>, so post-handler structured chaining data is not available for the next pipeline step.

By changing the handler contract to Promise<Intent | void>, the Intent system opens a structured return channel from handler to CLI. The Intent object is the first concrete instance of the HandlerResult concept described in the Execution Engine Phase 0 plan.

The Intent system is not a full implementation of HandlerResult (it does not yet carry pipelineOutput for DSL chaining), but it resolves the blocker for the confirm-before-execute use case and establishes the pattern for the full HandlerResult contract.


Relationship to Plugin System

IntentHandler<T> slots directly into the existing plugin structure — it lives in handlers.ts alongside existing handlers. The plugin loader, fiber registration, and command registry require no changes.

src/plugins/[protocol]/
├── index.ts # Plugin metadata & initialize()
├── commands.ts # G_p command implementations
├── types.ts # Protocol-specific types
├── handlers.ts # CommandHandler<T> and IntentHandler<T> functions ← here
└── ARCHITECTURE.md

The stricter IntentHandler<T> type is enforced at the handler definition site, not at the dispatch site. This means the constraint is expressed in plugin code (where the protocol author works) rather than in shared runtime infrastructure.