granite-indexer-sdk
v1.0.1
Published
Granite stacks indexer SDK
Readme
Stacks Events Indexer SDK
A clean, intuitive TypeScript SDK for indexing Stacks blockchain events with flexible persistence.
🚀 Quick Start
npm install @granite/granite-indexer-sdkimport { StacksEventsIndexer, MemoryPersistenceAdapter } from '@granite/granite-indexer-sdk';
// 1. Create persistence (or implement your own)
const persistence = new MemoryPersistenceAdapter();
// 2. Create indexer
const indexer = new StacksEventsIndexer(persistence, {
rpcUrls: ['https://api.testnet.hiro.so']
});
// 3. Add contracts to monitor
await indexer.addContract('ST1...contract-id', 'My Contract', ['defi']);
// 4. Index events
const result = await indexer.indexContract('ST1...contract-id');
console.log(`Processed ${result.eventsProcessed} events`);🎯 How It Works
1. Add Contracts
Tell the SDK which contracts to monitor:
await indexer.addContract(
'ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM.pyth-oracle-v1',
);2. Index Events
The SDK fetches and processes blockchain events:
// Index a specific contract
const result = await indexer.indexContract('ST1...contract-id');
// Or index all enabled contracts
const allResults = await indexer.indexAllContracts();3. Get Processed Events
Events are automatically parsed and stored via your persistence layer:
// Example event structure
{
contractId: 'ST1...contract-id',
txId: '0x123...',
eventIndex: 0,
topic: 'mint', // Event type (mint, transfer, burn, etc.)
blockHeight: 156789,
blockTime: 1640995200,
senderAddress: 'ST1...',
parsedData: { // Automatically parsed Clarity data
amount: 1000,
recipient: 'ST1...'
}
}🏗️ Architecture
┌─────────────────┐ ┌──────────────────┐ ┌─────────────────┐
│ Your App │ │ Indexer SDK │ │ Stacks RPC │
│ (Consumer) │ │ │ │ │
│ ┌─────────────┐ │ │ ┌──────────────┐ │ │ ┌─────────────┐ │
│ │ MongoDB │◄├────┤ │ Event Parser │ │◄───┤ │ Events │ │
│ │ Persistence │ │ │ │ │ │ │ │ │ │
│ └─────────────┘ │ │ └──────────────┘ │ │ └─────────────┘ │
│ │ │ │ │ │
│ │ │ ┌──────────────┐ │ │ ┌─────────────┐ │
│ ◄├────┤ │ Batch │ │ │ │ Multi-RPC │ │
│ │ │ │ Processor │ │ │ │ Failover │ │
│ │ │ └──────────────┘ │ │ └─────────────┘ │
└─────────────────┘ └──────────────────┘ └─────────────────┘🔧 Core Features
Smart Event Processing
- ✅ Duplicate Detection - Never process the same event twice
- ✅ Batch Processing - Efficient handling of large event volumes
- ✅ Auto-parsing - Clarity values converted to JavaScript objects
- ✅ Error Recovery - Robust retry logic for failed operations
Multi-RPC Support
- ✅ Automatic Failover - Switch between RPC endpoints seamlessly
- ✅ Health Monitoring - Continuous endpoint health checks
- ✅ Best Endpoint Selection - Always use the highest block height
Flexible Persistence
- ✅ Interface-based - Implement any storage backend
- ✅ Memory Adapter - Included for testing and development
- ✅ Easy Integration - Simple interface to implement
💾 Custom Persistence
Implement your own storage by creating a persistence adapter:
import { PersistenceAdapter, StacksEvent } from '@granite/granite-indexer-sdk';
class MyMongoPersistence implements PersistenceAdapter {
async saveEvents(events: StacksEvent[]): Promise<void> {
// Save events to your MongoDB
await EventModel.insertMany(events);
}
async checkEventsExist(keys: EventKey[]): Promise<boolean[]> {
// Check if events already exist (for duplicate detection)
const existing = await EventModel.find({
txIdEventIndex: { $in: keys.map(k => k.txIdEventIndex) }
});
// Return boolean array indicating which events exist
}
async getEnabledContracts(): Promise<string[]> {
// Return list of contract IDs to monitor
const contracts = await ContractModel.find({ enabled: true });
return contracts.map(c => c.contractId);
}
// ... implement other required methods
}
// Use with your indexer
const indexer = new StacksEventsIndexer(new MyMongoPersistence(), config);📊 Event Types
The SDK automatically parses different event types:
// Mint events
{
topic: 'mint',
parsedData: {
amount: 1000,
recipient: 'ST1...'
}
}
// Transfer events
{
topic: 'transfer',
parsedData: {
amount: 500,
sender: 'ST1...',
recipient: 'ST2...'
}
}
// Custom contract events
{
topic: 'price-update',
parsedData: {
symbol: 'BTC',
price: 50000,
timestamp: 1640995200
}
}⚙️ Configuration
const indexer = new StacksEventsIndexer(persistence, {
rpcUrls: [
'https://api.testnet.hiro.so',
'https://api.hiro.so' // Multiple RPCs for failover
],
apiKeys: ['key1', 'key2'], // Optional: for rate limiting
batchSize: 25, // Events per batch (default: 25)
concurrentLimit: 5, // Parallel contract processing (default: 10)
retryAttempts: 3, // API retry attempts (default: 3)
retryDelay: 1000 // Retry delay in ms (default: 1000)
});📈 Monitoring
// Check indexer status
const status = await indexer.getStatus();
console.log(status);
/*
{
isProcessing: false,
rpcEndpoints: [
{ url: 'https://api.testnet.hiro.so', height: 156789, healthy: true }
],
enabledContracts: ['ST1...contract1', 'ST2...contract2'],
config: { batchSize: 25, ... }
}
*/
// Check if currently processing
if (indexer.isCurrentlyProcessing()) {
console.log('Indexer is busy, try again later');
}🎮 Real-World Usage
DeFi Protocol Monitoring
// Monitor DEX events
await indexer.addContract('ST1...dex-contract');
const result = await indexer.indexContract('ST1...dex-contract');
result.newEvents.forEach(event => {
if (event.topic === 'swap') {
console.log(`Swap: ${event.parsedData.amount} tokens`);
}
});NFT Collection Tracking
// Monitor NFT mints and transfers
await indexer.addContract('ST1...nft-contract');
const result = await indexer.indexAllContracts();
Object.values(result).forEach(contractResult => {
contractResult.newEvents.forEach(event => {
if (event.topic === 'mint') {
console.log(`New NFT minted: ${event.parsedData.tokenId}`);
}
});
});Oracle Price Feeds
// Monitor price updates
await indexer.addContract('ST1...oracle-contract');
const result = await indexer.indexContract('ST1...oracle-contract');
result.newEvents.forEach(event => {
if (event.topic === 'price-update') {
console.log(`${event.parsedData.symbol}: $${event.parsedData.price}`);
}
});🔄 Continuous Processing
For production apps, set up continuous processing:
// Process every 5 minutes
setInterval(async () => {
try {
const results = await indexer.indexAllContracts();
const totalEvents = Object.values(results)
.reduce((sum, r) => sum + r.eventsProcessed, 0);
if (totalEvents > 0) {
console.log(`Processed ${totalEvents} new events`);
// Send notifications, update databases, etc.
}
} catch (error) {
console.error('Processing failed:', error);
}
}, 5 * 60 * 1000);