Skip to content

Solana Adapters

This guide covers how to build adapters for Solana-based protocols using Subsquid's Solana processor.

Overview

Solana adapters use Subsquid's data processing framework to efficiently index on-chain data from the Solana blockchain. The adapter scans blocks, filters instructions, and extracts relevant data for downstream processing.

Setting up the Data Source

Importing Dependencies

First, import the necessary components:

// Import core types and classes from Subsquid's Solana processor package
import { DataSourceBuilder, SolanaRpcClient } from '@subsquid/solana-stream';
 
// Import instruction definitions for the programs you want to listen to
import * as tokenProgram from './abi/TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA';
 
// Import shared types and config validation from Absinthe's common package
import { validateEnv } from './utils/validateEnv';
import { AbsintheApiClient, HOURS_TO_MS } from '@absinthe/common';
import { SplTransfersProcessor } from './BatchProcessor';

Environment Configuration

Load and validate your environment configuration:

// Load and validate environment variables (including ABS_CONFIG)
const env = validateEnv();
const { splTransfersProtocol, baseConfig } = env;

Building the Processor

Create the Solana data source processor:

export const processor = new DataSourceBuilder()
  // Set the Subsquid gateway URL for this chain (for fast block data)
  .setGateway(splTransfersProtocol.gatewayUrl)
 
  // Set the fallback RPC endpoint (for on-demand data, e.g., account states)
  .setRpc(
    splTransfersProtocol.rpcUrl == null
      ? undefined
      : {
          client: new SolanaRpcClient({
            url: splTransfersProtocol.rpcUrl,
            // rateLimit: 100 // requests per sec
          }),
          strideConcurrency: 10,
        },
  )
 
  // Define the block range to scan, based on config
  .setBlockRange({ from: splTransfersProtocol.fromBlock })
 
  // Specify which fields to extract for blocks, transactions, instructions, and token balances
  .setFields({
    block: {
      timestamp: true,
    },
    transaction: {
      signatures: true,
    },
    instruction: {
      programId: true,
      accounts: true,
      data: true,
    },
    tokenBalance: {
      preAmount: true,
      postAmount: true,
      preOwner: true,
      postOwner: true,
    },
  })
 
  // Listen for specific instructions from the Token program
  .addInstruction({
    where: {
      programId: [tokenProgram.programId], // executed by the SPL Token program
      d1: [tokenProgram.instructions.transfer.d1], // first 8 bytes equal to transfer discriminator
      isCommitted: true, // were successfully committed
    },
    include: {
      innerInstructions: true, // inner instructions
      transaction: true, // transaction that executed the given instruction
      transactionTokenBalances: true, // all token balance records of executed transaction
    },
  })
  .build();

Setting up the API Client

Configure the Absinthe API client for sending processed data:

// Instantiate the Absinthe API client for sending processed data
const apiClient = new AbsintheApiClient({
  baseUrl: baseConfig.absintheApiUrl,
  apiKey: baseConfig.absintheApiKey,
});

Chain Configuration

Prepare chain metadata for downstream use:

const chainConfig = {
  chainArch: splTransfersProtocol.chainArch,
  networkId: splTransfersProtocol.chainId,
  chainShortName: splTransfersProtocol.chainShortName,
  chainName: splTransfersProtocol.chainName,
};

Running the Processor

Finally, instantiate and run your custom processor:

// Calculate window duration for balance flushing
const WINDOW_DURATION_MS = baseConfig.balanceFlushIntervalHours * HOURS_TO_MS;
 
// Instantiate your custom processor class
const splTransfersProcessor = new SplTransfersProcessor(
  splTransfersProtocol,
  WINDOW_DURATION_MS,
  apiClient,
  env.baseConfig,
  chainConfig,
);
 
// Start the processor
splTransfersProcessor.run();

Key Components

DataSourceBuilder

The core engine that scans blocks, filters instructions, and extracts data from the Solana blockchain.

SolanaRpcClient

Provides fallback RPC functionality for on-demand data retrieval, such as account states.

Field Selection

Configure which fields to extract from:

  • Blocks: timestamp and other block header fields
  • Transactions: signatures and transaction metadata
  • Instructions: program ID, accounts, and instruction data
  • Token Balances: pre/post amounts and owners

Instruction Filtering

Use discriminators to filter for specific instruction types from target programs. This ensures you only process relevant on-chain activities.

Understanding the SplTransfersProcessor Implementation

The SplTransfersProcessor class is the core business logic that handles the actual processing of Solana SPL token transfer data. Here's what it does:

Class Overview

The processor is designed to monitor SPL token transfers on Solana and convert them into standardized transaction records for the Absinthe platform.

Key Responsibilities

1. Batch Processing Architecture
  • Uses Subsquid's batch processor to efficiently handle blocks of data
  • Processes multiple blocks in batches to optimize performance
  • Maintains protocol state across batch processing cycles
2. SPL Token Transfer Detection
  • Monitors instructions from the SPL Token program
  • Specifically filters for transfer instruction discriminators
  • Extracts token balance changes from transaction token balance records
3. Token Filtering and Tracking
  • Only processes transfers for tracked tokens (configured in TRACKED_TOKENS)
  • Currently tracks USDC(Mint) transfers (EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v)
4. Transaction Normalization
  • Converts raw Solana token balance changes into standardized transaction records
  • Calculates display amounts using token decimals (e.g., USDC has 6 decimals)
  • Extracts relevant metadata: transaction hash, block number, timestamp, user address

5. Data Flow Processing The processor follows this flow for each batch: Initialize protocol states for tracking For each block in the batch: Scan all instructions Filter for SPL token transfer instructions Extract token balance changes Convert to standardized transaction format Send processed transactions to Absinthe API

6. State Management
  • Maintains protocol state per contract address
  • Accumulates transactions within each batch
  • Handles balance windows for time-weighted calculations
7. Error Handling and Logging
  • Comprehensive error handling for batch processing failures
  • Detailed logging for debugging and monitoring
  • Graceful failure recovery to prevent data loss

Token Balance Processing Logic

The processor examines token balance records from each transaction to identify:

  • Net changes: Difference between pre and post amounts
  • Token metadata: Mint address, decimals, owner
  • Value calculations: Converting raw amounts to human-readable values

Only processes balances where the mint address matches tracked tokens and there's an actual balance change (pre ≠ post).

Integration with Absinthe Platform

The processed transactions are formatted according to Absinthe's schema and include:

  • Event classification (transaction type)
  • User identification (token owner)
  • Value calculations in USD
  • Gas and fee information
  • Blockchain metadata (block, transaction hash)

This standardized format allows the Absinthe platform to consistently handle transaction data across different blockchain protocols.