Skip to main content

Overview

The ank-exec module provides stateless execution helpers for calling protocols. It defines transaction types and execution callbacks that allow the engine (or other callers) to handle balances, fees, and metrics.

Design

The exec layer is stateless: it only looks up the target protocol and calls Protocol::execute, returning the ExecOutcome. Any stateful concerns (fees, balance mutations, metrics, rollback, etc.) must be implemented by the caller via the callback supplied to *_with_cb functions.

Core Types

Tx

pub struct Tx {
    pub to: String,
    pub action: Action,
    pub gas_limit: Option<u64>,
}
A single protocol call to be executed by the engine/runner. The exec layer does not interpret the action; it is passed through to the target protocol.
to
String
required
Target protocol id (e.g., "aave-v3", "uniswap-v3")
action
Action
required
Opaque action payload. Each protocol parses and validates its own actions. When ts-bindings are enabled, this is exported as any to TypeScript.
gas_limit
Option<u64>
Optional gas limit for precharge/refund flows. If Some(limit), callers can precharge limit * gas_price at ExecStage::PreTx and refund the unused portion after execution. If None, callers typically post-charge by outcome.gas_used at ExecStage::PostTx.

TxBundle

pub struct TxBundle {
    pub txs: Vec<Tx>,
}
A sequence of transactions to be executed for a single user.
txs
Vec<Tx>
required
Ordered list of transactions

ExecStage

pub enum ExecStage {
    PreTx,
    PostTx,
}
Execution phases observed by the callback passed to the *_with_cb helpers. The callback is invoked:
  • once with ExecStage::PreTx before calling Protocol::execute
  • once with ExecStage::PostTx after obtaining the ExecOutcome
PreTx
variant
Pre-execution hook — ideal for preflight checks and precharge of gas
PostTx
variant
Post-execution hook — ideal for applying deltas and charging/refunding fees

Execution Functions

execute_tx_with_cb

pub fn execute_tx_with_cb<F>(
    protocols: &mut IndexMap<String, Box<dyn Protocol>>,
    ts: Timestamp,
    user: UserId,
    tx: &Tx,
    cb: &mut F,
) -> Result<ExecOutcome>
where
    F: FnMut(ExecStage, &Tx, Option<&ExecOutcome>) -> anyhow::Result<()>
Execute a single tx with a user-supplied callback. The callback can mutate external state (e.g., balances, metrics, fee buckets) by capturing environment by mutable reference.
protocols
&mut IndexMap<String, Box<dyn Protocol>>
required
Map of protocol instances
ts
Timestamp
required
Current simulation timestamp
user
UserId
required
User identifier
tx
&Tx
required
Transaction to execute
cb
&mut F
required
Callback invoked at PreTx and PostTx stages
return
Result<ExecOutcome>
Execution outcome or error from either the protocol or callback

execute_bundle_with_cb

pub fn execute_bundle_with_cb<F>(
    protocols: &mut IndexMap<String, Box<dyn Protocol>>,
    ts: Timestamp,
    user: UserId,
    bundle: &TxBundle,
    cb: F,
) -> Result<Vec<ExecOutcome>>
where
    F: FnMut(ExecStage, &Tx, Option<&ExecOutcome>) -> anyhow::Result<()>
Execute all txs in a bundle with the same callback. Stops on the first error (either from Protocol::execute or from the callback).
protocols
&mut IndexMap<String, Box<dyn Protocol>>
required
Map of protocol instances
ts
Timestamp
required
Current simulation timestamp
user
UserId
required
User identifier
bundle
&TxBundle
required
Bundle of transactions to execute
cb
F
required
Callback invoked at PreTx and PostTx stages for each transaction
return
Result<Vec<ExecOutcome>>
Vector of execution outcomes (one per transaction)

execute_bundle

pub fn execute_bundle(
    protocols: &mut IndexMap<String, Box<dyn Protocol>>,
    ts: Timestamp,
    user: UserId,
    bundle: &TxBundle,
) -> Result<Vec<ExecOutcome>>
Convenience helper that executes a bundle without callbacks. This is suitable when the caller does not need to track balances, fees, or any side effects and only wants raw ExecOutcome values.
protocols
&mut IndexMap<String, Box<dyn Protocol>>
required
Map of protocol instances
ts
Timestamp
required
Current simulation timestamp
user
UserId
required
User identifier
bundle
&TxBundle
required
Bundle of transactions to execute
return
Result<Vec<ExecOutcome>>
Vector of execution outcomes (one per transaction)

Usage Example: Execution with Callbacks

use indexmap::IndexMap;
use ank_accounting::{Timestamp, UserId};
use ank_protocol::{Protocol, ExecOutcome};
use ank_exec::{Tx, ExecStage, execute_tx_with_cb};

let mut protocols = proto_map();
let ts: Timestamp = 0;
let user: UserId = 1;
let tx = Tx {
    to: "uniswap-v3".into(),
    action: ank_protocol::Action::Custom(serde_json::json!({})),
    gas_limit: None,
};

let mut cb = |stage: ExecStage, _tx: &Tx, out: Option<&ExecOutcome>| -> anyhow::Result<()> {
    match stage {
        ExecStage::PreTx => {
            // Preflight checks / precharge gas
        }
        ExecStage::PostTx => {
            let _out = out.unwrap();
            // Apply deltas / charge fees / update metrics
        }
    }
    Ok(())
};

let outcome = execute_tx_with_cb(&mut protocols, ts, user, &tx, &mut cb)?;

Gas Semantics

  • If Tx::gas_limit is Some, callers typically precharge gas_limit * gas_price at ExecStage::PreTx and refund on ExecStage::PostTx using outcome.gas_used
  • If gas_limit is None, callers often post-charge by outcome.gas_used in PostTx
The exact fee logic is implemented by the caller (typically the Engine) via the execution callback.

Build docs developers (and LLMs) love