Skill: sentioxyz/sentio-processor (version 1.0.1)

--- name: sentio-processor description: Use when initializing Sentio projects, writing blockchain processor code, adding contracts/ABIs, testing processors, or deploying to the Sentio platform. Triggers on sentio CLI usage, processor development, multi-chain indexing, blockchain data analytics with Sentio SDK, DeFi protocols, points systems, or store entities. --- # Managing Sentio Processors ## Overview Sentio SDK is a TypeScript blockchain data indexing platform. Processors handle on-chain events, transactions, and state changes across Ethereum, Aptos, Sui, Solana, Starknet, Bitcoin, Cosmos, Fuel, and IOTA. **Reference examples:** See [sentioxyz/sentio-processors](https://github.com/sentioxyz/sentio-processors) for 120+ production processors. ## When to Use - Creating/initializing a Sentio indexing project - Adding contracts or ABIs to a project - Writing processor code for any supported chain - Testing processors with `TestProcessorServer` - Deploying processors with `sentio upload` - Configuring `sentio.yaml` - Multi-chain or multi-contract setups - DeFi protocols (DEX, lending, staking, points systems) ## Progressive Disclosure This skill follows a layered approach: 1. **This file** — CLI commands, core patterns, all handler types, metrics/events/store overview, best practices 2. **[references/advanced-patterns.md](references/advanced-patterns.md)** — Multi-contract binding, GlobalProcessor, lazy caching, view calls, partition keys, baseLabels 3. **[references/defi-patterns.md](references/defi-patterns.md)** — Price feeds, DEX/AMM, lending protocols, TVL tracking, Aptos/Sui DEX helpers 4. **[references/store-and-points.md](references/store-and-points.md)** — Store schema advanced features (relationships, timeseries, indexes, iterators, filter operators) 5. **[references/position-tracking-templates.md](references/position-tracking-templates.md)** — 10 protocol-specific points/position tracking templates (Simple Holding, Aave, Morpho, Vault/LP, Uni V3, Pendle, Compound, NFT, Uni V2, Uni V4) 6. **[references/production-examples.md](references/production-examples.md)** — 7 complete production processor examples (Uniswap, AAVE, Cetus, LiquidSwap, Lombard points, Scallop) ## Project Lifecycle ``` sentio create → sentio add → sentio gen → write processor → sentio test → sentio upload → sentio processor status ``` ### 1. Initialize Project ```bash npx @sentio/cli create <project-name> --chain-type <type> --chain-id <id> ``` | Flag | Description | Default | |------|-------------|---------| | `--chain-type` | `eth`, `aptos`, `sui`, `solana`, `iota`, `fuel`, `starknet`, `raw` | `eth` | | `--chain-id` | EVM chain ID (only for eth) | `1` | | `--subproject` | Monorepo mode (no root dependencies) | false | ### 2. Add Contracts & Generate Types ```bash sentio add <address> --chain <chain> --name <Name> # Downloads ABI, updates sentio.yaml sentio gen # Generate TypeScript bindings sentio build # Full: gen + typecheck + bundle ``` | Flag | Description | |------|-------------| | `-c, --chain` | Chain ID (`1`, `56`) or name (`aptos_mainnet`, `sui_mainnet`) | | `-n, --name` | Contract/module display name | | `--api-key` | Explorer API key (Etherscan) | ### 3. Login & Deploy ```bash sentio login # OAuth browser flow (prod) sentio login --api-key <key> # API key auth sentio upload # Build + deploy sentio upload --skip-build # Deploy only sentio upload --continue-from <ver> # Hot-swap without re-indexing sentio upload --checkpoint "1:18000000" # Rollback to specific block sentio upload --num-workers=4 # Parallel workers for compute-heavy processors ``` ### 4. Verify Processor is Running After upload, use `sentio processor status --project <owner>/<slug>` to check the processor status. If the processor is in `ERROR` state, use `sentio processor logs --project <owner>/<slug> --level ERROR` to get error details, diagnose the issue, fix the code, and re-upload. ## package.json Recommend **yarn** over npm. With npm you may hit `BaseContract` / `DeferredTopicFilter` type conflicts (TS2344 due to duplicate ethers). ```json { "name": "project-name", "type": "module", "scripts": { "build": "sentio build", "gen": "sentio gen", "upload": "sentio upload", "test": "sentio test" }, "dependencies": { "@sentio/sdk": "^3.4.0" }, "devDependencies": { "@sentio/cli": "^3.4.0", "typescript": "^5.4.5" } } ``` If using npm and hitting ethers type conflicts, add: ```json "overrides": { "ethers": "npm:@sentio/ethers@6.13.1-patch.4" } ``` ## sentio.yaml Quick Reference ```yaml project: owner/project-name # Required host: prod # prod | test | staging | local contracts: - chain: "1" # EVM: numeric IDs ("1", "56", "137", "42161") address: "0x..." name: "USDC" - chain: aptos_mainnet # Move: aptos_mainnet, sui_mainnet, iota_mainnet address: "0x1" - chain: sui_mainnet address: "0xdee9..." name: "deepbook" networkOverrides: # Redirect chain ID to custom RPC - chain: '2390' host: "https://rpc.tac.build" variables: # Runtime env vars - key: API_KEY value: xxx isSecret: true debug: false numWorkers: 1 ``` ## Processor Patterns by Chain **Proxy contracts:** Use the **implementation contract's ABI** while binding to the **proxy address**. ### Ethereum / EVM ```typescript import { Counter, Gauge } from '@sentio/sdk' import { EthChainId } from '@sentio/chain' import { ERC20Processor } from '@sentio/sdk/eth/builtin' // Or generated: import { MyContractProcessor } from './types/eth/mycontract.js' const transfers = Counter.register('transfers') ERC20Processor.bind({ address: [REDACTED_SECRET], network: EthChainId.ETHEREUM, startBlock: 6082465, }) .onEventTransfer(async (event, ctx) => { transfers.add(ctx, 1) ctx.eventLogger.emit('Transfer', { distinctId: event.args.from, from: event.args.from, to: event.args.to, amount: event.args.value.scaleDown(6), }) }) .onTimeInterval(async (block, ctx) => {}, 60, 240) ``` **Handlers:** `onEvent*`, `onBlockInterval`, `onTimeInterval`, `onTransaction`, `onTrace`, `onCallXxx` #### Event Filtering Filter events to only process matching ones (e.g., only mints): ```typescript const mintFilter = ERC20Processor.filters.Transfer( [REDACTED_SECRET], // from = null address (mint) null // any recipient ); ERC20Processor.bind({ address: TOKEN, network: NETWORK }) .onEventTransfer(async (event, ctx) => { // only mint events reach here }, mintFilter); ``` #### Generic Event Handler Catches any event from the contract: ```typescript .onEvent(async (event, ctx) => { // event.eventName to determine which event }); ``` #### Function Call Handler (`onCallXxx`) Triggers on internal function calls (trace-based). Access args and return values: ```typescript MyContractProcessor.bind({ address: CONTRACT, network: NETWORK }) .onCallDeposit(async (call, ctx) => { // call.args, call.returnValue, call.error }); ``` **GlobalProcessor:** `GlobalProcessor.bind({ network }).onBlockInterval(handler)` ### Aptos ```typescript import { AptosNetwork } from '@sentio/sdk/aptos' import { coin } from '@sentio/sdk/aptos/builtin/0x1' coin.bind({ network: AptosNetwork.MAIN_NET }) .onEventWithdrawEvent(async (evt, ctx) => { ctx.meter.Counter('withdrawals').add(1) ctx.eventLogger.emit('Withdraw', { distinctId: evt.guid.account_address, amount: evt.data_decoded.amount, }) }) ``` **Handlers:** `onEvent*`, `onEntry*`, `onTransaction`, `onTimeInterval`, `onVersionInterval` **Resources:** `AptosResourcesProcessor.bind({ address }).onResourceChange(handler, typeString)` ### Sui ```typescript import { SuiNetwork, SuiObjectTypeProcessor } from '@sentio/sdk/sui' import { pool } from './types/sui/deepbook.js' pool.bind({ network: SuiNetwork.MAIN_NET }) .onEventSwap(async (evt, ctx) => { ctx.meter.Counter('swaps').add(1) }) SuiObjectTypeProcessor.bind({ objectType: pool.Pool.type() }) .onObjectChange((changes, ctx) => { ctx.meter.Counter('pool_updates').add(changes.length) }) ``` **Handlers:** `onEvent*`, `onEntryFunctionCall`, `onObjectChange`, `onTimeInterval` **Note:** Use `startCheckpoint: 8500000n` (BigInt) instead of `startBlock` for Sui. ### Solana ```typescript SolanaGlobalProcessor.bind({ name: 'my-program', instructionCoder: new Anchor.BorshInstructionCoder(idl), }).onInstruction('transfer', async (instruction, ctx, accounts) => { ctx.meter.Counter('transfers').add(1) }) ``` ### Starknet ```typescript StarknetProcessor.bind({ address: '0x...', network: StarknetChainId.STARKNET_MAINNET }) .onEvent('Transfer', async (events, ctx) => { ctx.meter.Counter('transfers').add(events.length) }) ``` ## Factory Pattern (ProcessorTemplate) For dynamically-created contracts (e.g., DEX pair factory): ```typescript import { UniswapV2PairProcessorTemplate } from "./types/eth/uniswapv2pair.js"; import { UniswapV2FactoryProcessor } from "./types/eth/uniswapv2factory.js"; // 1. Define template with handlers const pairTemplate = new UniswapV2PairProcessorTemplate() .onEventSwap(async (event, ctx) => { ctx.meter.Counter("swap_volume").add(event.args.amount0Out); }) .onEventSync(async (event, ctx) => { ctx.meter.Gauge("reserve0").record(event.args.reserve0); }); // 2. Bind factory and dynamically bind new pairs UniswapV2FactoryProcessor.bind({ address: FACTORY_ADDRESS, network: NETWORK, }) .onEventPairCreated(async (event, ctx) => { pairTemplate.bind( { address: event.args.pair, startBlock: ctx.blockNumber }, ctx ); }); ``` ## Metrics & Events **CRITICAL — Metric Naming:** Names MUST NOT end with reserved suffixes: `_count`, `_sum`, `_avg`, `_min`, `_max`, `_last`. These are appended automatically by Sentio. Error: `COUNTER name "X" is invalid, cannot use [_count _sum _avg _min _max _last] as the suffix: invalid meta` ``` ❌ Counter.register('transfer_count') → invalid suffix _count ❌ ctx.meter.Counter('pool_sum') → invalid suffix _sum ✅ Counter.register('transfers') ✅ ctx.meter.Counter('pools_created') ``` ```typescript // Global registration (recommended) const counter = Counter.register('name', { unit: 'token' }) counter.add(ctx, value, { label: 'value' }) // Inline via context ctx.meter.Counter('name').add(value, { label: 'value' }) ctx.meter.Gauge('name').record(value, { label: 'value' }) // Sparse gauge (high cardinality — many pools/tokens) const vol = Gauge.register("vol", { sparse: true, aggregationConfig: { intervalInMinutes: [60], discardOrigin: true, // only keep aggregated vol_count, vol_sum }, }) // Event logging ctx.eventLogger.emit('EventName', { distinctId: address, // Primary entity ID (used for user-level analytics) message: 'description', // ... arbitrary key-value attributes // IMPORTANT: Numeric attributes used as time series metrics must be number or BigDecimal. // Passing string/undefined/null for numeric fields causes "invalid time series data" errors. }) // Exporter (export to external systems via webhook) ctx.exporter.emit({ key: "value" }) ``` **Label cardinality warning:** ~10,000 unique series limit per metric. NEVER use wallet addresses, tx hashes, or token IDs as labels. Use event logs or entities for high-cardinality data instead. ## Store (Database) API Define entities in `schema.graphql`, generated by `sentio gen`: ```graphql type AccountSnapshot @entity { id: ID! timestampMilli: BigInt! balance: BigInt! } ``` ```typescript import { AccountSnapshot } from "./schema/store.js" await ctx.store.get(AccountSnapshot, id) await ctx.store.upsert(new AccountSnapshot({ id, timestampMilli, balance })) await ctx.store.list(AccountSnapshot, []) // List all await ctx.store.delete(AccountSnapshot, id) ``` **Critical:** Enable sequential execution when using store to prevent race conditions: ```typescript import { GLOBAL_CONFIG } from "@sentio/runtime" GLOBAL_CONFIG.execution = { sequential: true } ``` See [references/store-and-points.md](references/store-and-points.md) for points system patterns. ## Price Feeds ```typescript import { getPriceByType, token } from "@sentio/sdk/utils" const info = await token.getERC20TokenInfo(EthChainId.ETHEREUM, tokenAddr) const price = await getPriceByType(EthChainId.ETHEREUM, tokenAddr, ctx.timestamp) || 0 const usdValue = amount.scaleDown(info.decimal).multipliedBy(price) ``` See [references/defi-patterns.md](references/defi-patterns.md) for caching patterns and DeFi examples. ## Testing ```typescript import { TestProcessorServer, firstCounterValue } from '@sentio/sdk/testing' const service = new TestProcessorServer(() => import('./processor.js')) before(async () => { await service.start() }) // Ethereum const resp = await service.eth.testLog(mockTransferLog('0x...', { from, to, value })) assert.equal(firstCounterValue(resp.result, 'transfers'), 1n) ``` | Chain | Facet | Key Methods | |-------|-------|-------------| | Ethereum | `service.eth` | `testLog()`, `testBlock()`, `testTransaction()`, `testTrace()` | | Aptos | `service.aptos` | `testEvent()`, `testCall()`, `testResourceChange()` | | Sui | `service.sui` | `testEvent()`, `testObjectChange()` | | Solana | `service.solana` | `testInstruction()` | ```bash sentio test # All tests sentio test --test-name-pattern="swap" # Filter by name ``` ## Project Structure ``` my-project/ sentio.yaml # Project config schema.graphql # Store entity definitions (optional) package.json # @sentio/sdk + @sentio/cli deps tsconfig.json abis/{chain}/ # Contract ABIs src/ processor.ts # Main processor code processor.test.ts # Tests types/ # Auto-generated (sentio gen) schema/ # Auto-generated store entities dist/lib.js # Bundled output (sentio build) ``` ## Best Practices 1. **Use `sequential: true`** when handlers share state via entity store 2. **Do NOT use global variables** for persistent state — use `ctx.store` 3. **Use `startBlock`** to skip irrelevant history and reduce backfill cost 4. **Prefer Metrics over Logs/Entities** for aggregated numerical data (lower cost) 5. **Never use addresses/hashes as metric labels** — ~10k series limit per metric 6. **Use event filters** (`Processor.filters`) to reduce unnecessary handler executions 7. **Increase backfill intervals** to the largest acceptable value for cost reduction 8. **Use `listIterator()`** instead of `list()` for large entity datasets 9. **Use `Promise.all`** for parallel contract calls and entity processing 10. **Use `BigDecimal`** and `scaleDown()` for precision — avoid floating point 11. **Filter null addresses** with `isNullAddress()` for mint/burn events 12. **Skip self-transfers** — `if (from == to) return;` ## Common Mistakes | Mistake | Fix | |---------|-----| | Metric name ends with `_count`, `_sum`, `_avg`, `_min`, `_max`, `_last` | Use a different name: `transfers` not `transfer_count`, `pools_created` not `pool_count` | | "invalid time series data" in eventLogger | Ensure numeric fields are `number` or `BigDecimal`, not `string`/`undefined`/`null`; use `\|\| 0` for fallible prices | | Writing processor before `sentio gen` | Always `sentio add` then `sentio gen` first | | Wrong import path for generated types | Use `./types/{chain}/{name}.js` (`.js` extension for ESM) | | Forgetting `.scaleDown(decimals)` | `.scaleDown(18)` for ETH, `.scaleDown(6)` for USDC | | Not setting `startBlock` / `startCheckpoint` | Processor starts from genesis without it | | Store entities without sequential execution | Add `GLOBAL_CONFIG.execution = { sequential: true }` | | Deploying without login | Run `sentio login` first | | Caching resolved values instead of Promises | Cache the `Promise` to prevent duplicate concurrent RPC calls |


Security scan verdicts — ClawScan: clean | static analysis: clean | VirusTotal: clean | SkillSpector: suspicious (MEDIUM)