IOTA

IOTA is a decentralized blockchain infrastructure featuring a Move-based Layer 1 protocol and an Ethereum Virtual Machine (EVM) Layer 2, designed for real-world asset tokenization, digital identity, and enterprise use cases with exceptional scalability and energy efficiency.

Core Concepts

IOTA integration leverages Move-based concepts similar to Sui, optimized for IOTA's dual-layer architecture supporting both Move smart contracts on L1 and EVM compatibility on L2. This enables diverse use cases including real-world asset tokenization, supply chain tracking, and DeFi applications.

Processors

IOTA integration offers several processor types, listed in order of common usage:

User-Defined Processors

Generate type-safe processors from your smart contract ABIs using Sentio's code generation tools. These provide the best developer experience with full type safety and auto-completion.

// Example: Generated from your custom DEX contract
import { dex } from './types/iota/0xYourPackage.dex'

dex.bind({
  network: IotaNetwork.MAIN_NET,
  startCheckpoint: 1000000n
}).onEventSwapEvent(async (evt, ctx) => {
  // Fully typed event data
  const { amount_in, amount_out, user } = evt.data_decoded
  // Process swap...
})

Built-in System Processors

Sentio SDK pre-generates type-safe processors from IOTA's core system modules, providing ready-to-use bindings for system-level monitoring:

// Validator staking system
import { validator, validator_set } from '@sentio/sdk/iota/builtin/0x3'

// IOTA system module
import { iota_system } from '@sentio/sdk/iota/builtin/0x3'

These processors are generated by Sentio SDK from IOTA's system packages and provide fully-typed interfaces for monitoring staking, validator operations, epoch changes, and other system activities.

IotaObjectProcessor

Monitors a specific object by ID and its dynamic fields.

  • Use Case: Track state of a specific pool, NFT, or singleton object
  • Binding: IotaObjectProcessor.bind({ objectId: '0x...', network: IotaNetwork.MAIN_NET, startCheckpoint: 1000000n })

IotaObjectTypeProcessor

Monitors ALL objects of a specific Move type across the network.

  • Use Case: Track all NFTs in a collection, all pools of a DEX
  • Binding: IotaObjectTypeProcessor.bind({ objectType: '0x...::nft::Token', network: IotaNetwork.MAIN_NET })
  • Supports generic type T with @typemove/iota for type-safe decoding

IotaAddressProcessor

Monitors all objects owned by an address and transactions sent to it.

  • Use Case: Portfolio tracking, wallet monitoring, treasury management
  • Binding: IotaAddressProcessor.bind({ address: '0x...', network: IotaNetwork.MAIN_NET, startCheckpoint: 1000000n })

IotaModulesProcessor

Binds to a package address for manual event/call processing.

  • Note: Consider using code generation for type-safe processors instead
  • Binding: IotaModulesProcessor.bind({ address: '0x...', network: IotaNetwork.MAIN_NET })
  • Advanced Use: Rarely needed as user-defined processors handle most use cases better

IotaGlobalProcessor

Processes all transactions network-wide with optional filtering.

  • Warning: Can be resource-intensive; use specific processors when possible
  • Binding: IotaGlobalProcessor.bind({ network: IotaNetwork.MAIN_NET })
  • Advanced Use: Only for chain-wide analytics requiring global transaction monitoring

Handlers

Handlers vary depending on the processor type:

  • IotaModulesProcessor/IotaGlobalProcessor:

    • onMoveEvent(handler(event, ctx), filter): Triggered when a specific Move event matching the filter (type string) is emitted.
    • onEntryFunctionCall(handler(call, ctx), filter): Triggered when an entry function matching the filter (e.g., 0x...::module::function_name) is called.
    • onTransactionBlock(handler(tx, ctx), filter?): Triggered for transaction blocks matching the filter (e.g., involving specific addresses).
    • onObjectChange(handler(changes, ctx), typeFilter) (IotaGlobalProcessor only): Triggered when objects matching the typeFilter are changed (created, mutated, deleted) within any transaction block.
  • IotaAddressProcessor/IotaObjectProcessor / IotaObjectTypeProcessor:

    • onTimeInterval(handler(objects | self, dynamicFields, ctx), intervalMinutes?, backfillIntervalMinutes?, type?, fetchConfig?): Periodically fetches and processes objects based on time intervals.
      • IotaAddressProcessor: handler(objects: IotaMoveObject[], ctx: IotaAddressContext)
      • IotaObjectProcessor: handler(self: IotaMoveObject, dynamicFields: IotaMoveObject[], ctx: IotaObjectContext)
      • IotaObjectTypeProcessor: handler(self: TypedIotaMoveObject<T>, dynamicFields: IotaMoveObject[], ctx: IotaObjectContext)
    • onCheckpointInterval(handler(...), interval?, backfillInterval?, type?, fetchConfig?): Similar to onTimeInterval but based on checkpoint intervals.
    • onTransactionBlock(handler(tx, ctx), filter?) (IotaAddressProcessor only): Handles transaction blocks sent to the bound address.
    • onObjectChange(handler(changes, ctx)) (IotaObjectTypeProcessor only): Processes changes specific to the bound object type.

Context (ctx)

Handlers receive a context object specific to the processor and handler type (IotaContext, IotaAddressContext, IotaObjectContext, IotaObjectChangeContext) providing:

  • Chain information: network, checkpoint.
  • Source details: address (package or account), moduleName, objectId.
  • Transaction/Event details: transaction, eventIndex, timestamp.
  • Helper methods: IOTA client (ctx.client) for interacting with the RPC, coder for decoding Move data.
  • Standard SDK outputs: ctx.meter.Counter('...'), ctx.eventLogger.emit('...'), ctx.exporter.iota_Object(...).

Fetch Configuration (fetchConfig)

  • Transaction-based handlers (onMoveEvent, onEntryFunctionCall, onTransactionBlock) support MoveFetchConfig to include resourceChanges, allEvents, or inputs.
interface MoveFetchConfig {
    allEvents: boolean;            // Fetch all events for the transaction
    includeFailedTransaction?: boolean; // Include failed transactions
    inputs: boolean;               // Fetch transaction input arguments
    resourceChanges: boolean;      // Fetch resource changes
    resourceConfig?: ResourceConfig; // Specific configuration for resource fetching
    supportMultisigFunc?: boolean;   // Support for multisig functions
}
  • Interval-based handlers (onTimeInterval, onCheckpointInterval) support MoveAccountFetchConfig (mainly owned: true/false).
interface MoveAccountFetchConfig {
    owned: boolean;
}

Getting Started Example (Monitoring Validator Metrics)

First, create an IOTA processor project: yarn sentio create -c iota <project-name>. You can refer to the CLI Reference to learn details.

import { validator, validator_set } from '@sentio/sdk/iota/builtin/0x3'

import { IotaNetwork } from '@sentio/sdk/iota'

/**
 * IOTA Validator Metrics Processor
 *
 * This processor tracks validator staking activities, rewards, and performance metrics.
 * It captures:
 * - Staking/unstaking events from delegators
 * - Validator epoch rewards and performance scores
 * - Real-time exchange rates for calculating delegator returns
 */

// Track when delegators stake tokens with a validator
validator.bind({ network: IotaNetwork.MAIN_NET }).onEventStakingRequestEvent(
  async (evt, ctx) => {
    // Extract staking details
    const validator_address = evt.data_decoded.validator_address
    const delegator_address = evt.data_decoded.staker_address
    const amount = evt.data_decoded.amount.scaleDown(9) // Convert from 9 decimals to human-readable

    // Log staking event for tracking delegator positions
    ctx.eventLogger.emit('stake_action', {
      action: 'stake',
      amount, // Positive amount for staking
      pool: evt.data_decoded.pool_id,
      validator: validator_address,
      delegator: delegator_address
    })
  },
  { allEvents: true }
)

// Track when delegators unstake tokens from a validator
validator.bind({ network: IotaNetwork.MAIN_NET }).onEventUnstakingRequestEvent(
  async (evt, ctx) => {
    // Extract unstaking details
    const validator_address = evt.data_decoded.validator_address
    const delegator_address = evt.data_decoded.staker_address
    const principal = evt.data_decoded.principal_amount.scaleDown(9) // Original staked amount
    const reward = evt.data_decoded.reward_amount.scaleDown(9) // Rewards earned
    const total_amount = principal.plus(reward) // Total withdrawal

    // Log unstaking event with negative principal to calculate net positions
    ctx.eventLogger.emit('stake_action', {
      action: 'unstake',
      amount: -principal,  // Negative principal for net position calculation
      reward: reward, // Rewards earned (always positive)
      total_withdrawn: total_amount, // Total amount returned to delegator,
      pool: evt.data_decoded.pool_id,
      validator: validator_address,
      delegator: delegator_address,
      stake_activation_epoch: evt.data_decoded.stake_activation_epoch.toString(), // When stake was activated
      unstaking_epoch: evt.data_decoded.unstaking_epoch.toString() // When unstaking was requested
    })
  },
  { allEvents: true }
)

/**
 * Track validator performance metrics at each epoch
 * This event fires at the end of each epoch (approximately every 24 hours)
 * and provides comprehensive validator statistics including:
 * - Total stake and voting power
 * - Rewards earned during the epoch
 * - Pool token exchange rate (for calculating delegator returns)
 * - Performance score (tallying rule global score)
 */
validator_set.bind({ network: IotaNetwork.MAIN_NET }).onEventValidatorEpochInfoEventV1(
  async (evt, ctx) => {
    try {
      // Extract validator performance metrics
      const validator_address = evt.data_decoded.validator_address
      const epoch = evt.data_decoded.epoch
      const stake = evt.data_decoded.stake.scaleDown(9) // Total stake with validator
      const voting_power = evt.data_decoded.voting_power // Consensus voting weight
      const commission_rate = evt.data_decoded.commission_rate // Fee charged to delegators (basis points)
      const pool_staking_reward = evt.data_decoded.pool_staking_reward.scaleDown(9) // Rewards earned this epoch
      const pool_token_exchange_rate = evt.data_decoded.pool_token_exchange_rate // Exchange rate for pool tokens
      const reference_gas_survey_quote = evt.data_decoded.reference_gas_survey_quote // Gas price reference
      const tallying_rule_global_score = evt.data_decoded.tallying_rule_global_score // Validator performance score

      // Calculate exchange rate for pool tokens to IOTA
      // This rate increases over time as rewards accumulate
      const exchange_rate = Number((pool_token_exchange_rate as any).numerator || 1) / Number((pool_token_exchange_rate as any).denominator || 1)

      // Record metrics for dashboard visualization
      ctx.meter.Gauge('validator_stake').record(stake, { validator: validator_address }) // Total staked amount
      ctx.meter.Gauge('validator_voting_power').record(voting_power, { validator: validator_address }) // Network influence
      ctx.meter.Gauge('validator_epoch_rewards').record(pool_staking_reward, { validator: validator_address }) // Epoch rewards
      ctx.meter.Gauge('validator_commission').record(commission_rate, { validator: validator_address }) // Commission rate
      ctx.meter.Gauge('validator_exchange_rate').record(exchange_rate, { validator: validator_address }) // Pool token value
      ctx.meter.Gauge('validator_performance_score').record(tallying_rule_global_score, { validator: validator_address }) // Performance metric

      // Log comprehensive epoch snapshot for historical analysis
      ctx.eventLogger.emit('validator_epoch_info', {
        validator: validator_address,
        epoch: epoch.toString(),
        stake,
        voting_power: voting_power.toString(),
        commission_rate,
        pool_staking_reward,
        exchange_rate,
        reference_gas_survey_quote: reference_gas_survey_quote.toString(),
        tallying_rule_global_score: tallying_rule_global_score.toString()
      })
    } catch (error) {
      console.error('Error processing validator epoch info:', error)
    }
  },
  { allEvents: true }
)

Best Practices

  1. Start Checkpoint: Always specify a startCheckpoint to avoid processing the entire chain history unnecessarily.

  2. Scaling Decimals: IOTA uses 9 decimal places. Use .scaleDown(9) to convert from smallest units to human-readable values.

  3. Error Handling: Wrap handler logic in try-catch blocks to prevent processor crashes from unexpected data.

  4. Efficient Filtering: Use specific filters in handlers to reduce processing overhead:

    .onMoveEvent(handler, '0x3::validator::StakingRequestEvent')
  5. Resource Management: Use fetchConfig judiciously - only fetch what you need (events, inputs, resource changes).

Resources