Helius Integration

Helius provides enhanced RPC services, webhooks, and APIs for Solana. It simplifies indexing with real-time notifications and enriched data.

Services Overview

Text
┌─────────────────────────────────────────────────────────────────┐
│                      Helius Services                            │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│  ┌───────────────┐  ┌───────────────┐  ┌───────────────┐        │
│  │   Enhanced    │  │   Webhooks    │  │  DAS API      │        │
│  │   RPC         │  │               │  │  (NFT data)   │        │
│  │               │  │  • Account    │  │               │        │
│  │  • Fast       │  │  • Program    │  │  • Metadata   │        │
│  │  • Reliable   │  │  • NFT        │  │  • Ownership  │        │
│  │  • Archive    │  │  • Token      │  │  • Collection │        │
│  └───────────────┘  └───────────────┘  └───────────────┘        │
│                                                                 │
│  ┌───────────────┐  ┌───────────────┐  ┌───────────────┐        │
│  │  Transaction  │  │   Priority    │  │   Name        │        │
│  │  History      │  │   Fees        │  │   Service     │        │
│  │               │  │               │  │               │        │
│  │  • Parsed     │  │  • Dynamic    │  │  • .sol       │        │
│  │  • Enriched   │  │  • Estimates  │  │  • Lookup     │        │
│  └───────────────┘  └───────────────┘  └───────────────┘        │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

Webhooks

Setup Webhook

TypeScript
import axios from "axios";

interface WebhookConfig {
  apiKey: string;
  webhookURL: string;
  transactionTypes?: string[];
  accountAddresses?: string[];
  webhookType: "enhanced" | "raw" | "discord";
}

async function createWebhook(config: WebhookConfig) {
  const response = await axios.post(
    "https://api.helius.xyz/v0/webhooks",
    {
      webhookURL: config.webhookURL,
      transactionTypes: config.transactionTypes || ["Any"],
      accountAddresses: config.accountAddresses || [],
      webhookType: config.webhookType,
      authHeader: "x-webhook-secret", // Optional auth header
    },
    {
      headers: {
        "Content-Type": "application/json",
      },
      params: {
        "api-key": config.apiKey,
      },
    },
  );

  return response.data;
}

// Create a webhook for NFT sales
const webhook = await createWebhook({
  apiKey: "your-api-key",
  webhookURL: "https://your-server.com/webhook/nft-sales",
  transactionTypes: ["NFT_SALE"],
  webhookType: "enhanced",
});

console.log("Webhook ID:", webhook.webhookID);

Webhook Handler

TypeScript
import express from "express";
import crypto from "crypto";

const app = express();
app.use(express.json());

// Webhook types
interface EnhancedTransaction {
  signature: string;
  slot: number;
  timestamp: number;
  type: string;
  fee: number;
  feePayer: string;
  description: string;
  source: string;
  nativeTransfers: NativeTransfer[];
  tokenTransfers: TokenTransfer[];
  accountData: AccountData[];
  events: {
    nft?: NFTEvent;
    swap?: SwapEvent;
    compressed?: CompressedEvent;
  };
}

interface NativeTransfer {
  fromUserAccount: string;
  toUserAccount: string;
  amount: number;
}

interface TokenTransfer {
  fromUserAccount: string;
  toUserAccount: string;
  fromTokenAccount: string;
  toTokenAccount: string;
  tokenAmount: number;
  mint: string;
  tokenStandard: string;
}

interface NFTEvent {
  description: string;
  type: string;
  source: string;
  amount: number;
  fee: number;
  feePayer: string;
  signature: string;
  slot: number;
  timestamp: number;
  saleType: string;
  buyer: string;
  seller: string;
  nfts: Array<{
    mint: string;
    tokenStandard: string;
  }>;
}

// Verify webhook signature
function verifyWebhook(
  payload: string,
  signature: string,
  secret: string,
): boolean {
  const expectedSig = crypto
    .createHmac("sha256", secret)
    .update(payload)
    .digest("hex");
  return crypto.timingSafeEqual(
    Buffer.from(signature),
    Buffer.from(expectedSig),
  );
}

app.post("/webhook/transactions", async (req, res) => {
  const signature = req.headers["x-webhook-secret"] as string;
  const payload = JSON.stringify(req.body);

  // Verify signature if configured
  if (process.env.WEBHOOK_SECRET) {
    if (!verifyWebhook(payload, signature, process.env.WEBHOOK_SECRET)) {
      return res.status(401).json({ error: "Invalid signature" });
    }
  }

  const transactions: EnhancedTransaction[] = req.body;

  for (const tx of transactions) {
    console.log(`Processing ${tx.type}:`, tx.signature);

    switch (tx.type) {
      case "NFT_SALE":
        await handleNFTSale(tx);
        break;
      case "SWAP":
        await handleSwap(tx);
        break;
      case "TRANSFER":
        await handleTransfer(tx);
        break;
      default:
        console.log("Unhandled transaction type:", tx.type);
    }
  }

  res.status(200).json({ received: true });
});

async function handleNFTSale(tx: EnhancedTransaction) {
  const nftEvent = tx.events.nft;
  if (!nftEvent) return;

  console.log("NFT Sale:", {
    mint: nftEvent.nfts[0]?.mint,
    price: nftEvent.amount / 1e9, // Convert lamports to SOL
    buyer: nftEvent.buyer,
    seller: nftEvent.seller,
    marketplace: nftEvent.source,
  });

  // Store in database
  await db.query(
    `INSERT INTO nft_sales (signature, mint, price_lamports, buyer, seller, marketplace, timestamp)
     VALUES ($1, $2, $3, $4, $5, $6, $7)`,
    [
      tx.signature,
      nftEvent.nfts[0]?.mint,
      nftEvent.amount,
      nftEvent.buyer,
      nftEvent.seller,
      nftEvent.source,
      new Date(tx.timestamp * 1000),
    ],
  );
}

DAS API (Digital Asset Standard)

TypeScript
import axios from "axios";

const HELIUS_URL = "https://mainnet.helius-rpc.com/?api-key=YOUR_API_KEY";

// Get asset by ID
async function getAsset(assetId: string) {
  const response = await axios.post(HELIUS_URL, {
    jsonrpc: "2.0",
    id: "my-id",
    method: "getAsset",
    params: { id: assetId },
  });

  return response.data.result;
}

// Get assets by owner
async function getAssetsByOwner(
  owner: string,
  options?: {
    page?: number;
    limit?: number;
    sortBy?: { sortBy: string; sortDirection: string };
    displayOptions?: { showCollectionMetadata?: boolean };
  },
) {
  const response = await axios.post(HELIUS_URL, {
    jsonrpc: "2.0",
    id: "my-id",
    method: "getAssetsByOwner",
    params: {
      ownerAddress: owner,
      page: options?.page ?? 1,
      limit: options?.limit ?? 1000,
      sortBy: options?.sortBy,
      displayOptions: options?.displayOptions,
    },
  });

  return response.data.result;
}

// Get assets by collection
async function getAssetsByGroup(
  groupKey: string,
  groupValue: string,
  options?: { page?: number; limit?: number },
) {
  const response = await axios.post(HELIUS_URL, {
    jsonrpc: "2.0",
    id: "my-id",
    method: "getAssetsByGroup",
    params: {
      groupKey, // "collection"
      groupValue, // collection address
      page: options?.page ?? 1,
      limit: options?.limit ?? 1000,
    },
  });

  return response.data.result;
}

// Search assets
async function searchAssets(params: {
  ownerAddress?: string;
  creatorAddress?: string;
  grouping?: [string, string];
  burnt?: boolean;
  frozen?: boolean;
  compressed?: boolean;
  page?: number;
  limit?: number;
}) {
  const response = await axios.post(HELIUS_URL, {
    jsonrpc: "2.0",
    id: "my-id",
    method: "searchAssets",
    params,
  });

  return response.data.result;
}

// Example: Get all NFTs owned by wallet
async function getAllNFTs(wallet: string) {
  const assets: any[] = [];
  let page = 1;
  let hasMore = true;

  while (hasMore) {
    const result = await getAssetsByOwner(wallet, {
      page,
      limit: 1000,
      displayOptions: { showCollectionMetadata: true },
    });

    assets.push(...result.items);
    hasMore = result.items.length === 1000;
    page++;
  }

  return assets;
}

Enhanced Transactions API

TypeScript
// Parse a transaction
async function parseTransaction(signature: string, apiKey: string) {
  const response = await axios.get(
    `https://api.helius.xyz/v0/transactions/?api-key=${apiKey}`,
    {
      params: { transactions: [signature] },
    },
  );

  return response.data[0];
}

// Get transaction history
async function getTransactionHistory(
  address: string,
  apiKey: string,
  options?: {
    before?: string;
    until?: string;
    limit?: number;
    type?: string;
  },
) {
  const response = await axios.get(
    `https://api.helius.xyz/v0/addresses/${address}/transactions`,
    {
      params: {
        "api-key": apiKey,
        ...options,
      },
    },
  );

  return response.data;
}

// Get all transactions for an address
async function getAllTransactions(address: string, apiKey: string) {
  const transactions: any[] = [];
  let before: string | undefined;

  while (true) {
    const batch = await getTransactionHistory(address, apiKey, {
      before,
      limit: 100,
    });

    if (batch.length === 0) break;

    transactions.push(...batch);
    before = batch[batch.length - 1].signature;

    // Rate limiting
    await new Promise((r) => setTimeout(r, 100));
  }

  return transactions;
}

Priority Fee API

TypeScript
interface PriorityFeeEstimate {
  priorityFeeLevels: {
    min: number;
    low: number;
    medium: number;
    high: number;
    veryHigh: number;
    unsafeMax: number;
  };
}

async function getPriorityFeeEstimate(
  accountKeys: string[],
  apiKey: string,
): Promise<PriorityFeeEstimate> {
  const response = await axios.post(
    `https://mainnet.helius-rpc.com/?api-key=${apiKey}`,
    {
      jsonrpc: "2.0",
      id: "1",
      method: "getPriorityFeeEstimate",
      params: [
        {
          accountKeys,
          options: {
            includeAllPriorityFeeLevels: true,
          },
        },
      ],
    },
  );

  return response.data.result;
}

// Usage in transaction
async function sendWithOptimalFee(
  connection: Connection,
  transaction: Transaction,
  signer: Keypair,
  apiKey: string,
) {
  // Get accounts from transaction
  const accountKeys = transaction
    .compileMessage()
    .accountKeys.map((k) => k.toBase58());

  // Get fee estimate
  const feeEstimate = await getPriorityFeeEstimate(accountKeys, apiKey);

  // Add priority fee instruction
  const priorityFee = ComputeBudgetProgram.setComputeUnitPrice({
    microLamports: feeEstimate.priorityFeeLevels.medium,
  });

  transaction.add(priorityFee);

  // Send transaction
  const signature = await connection.sendTransaction(transaction, [signer]);
  return signature;
}

Complete Indexer with Helius

TypeScript
import axios from "axios";
import express from "express";
import { Pool } from "pg";

interface HeliusIndexerConfig {
  apiKey: string;
  webhookSecret: string;
  databaseUrl: string;
  programId: string;
}

class HeliusIndexer {
  private app: express.Application;
  private db: Pool;

  constructor(private config: HeliusIndexerConfig) {
    this.app = express();
    this.app.use(express.json());
    this.db = new Pool({ connectionString: config.databaseUrl });

    this.setupRoutes();
  }

  private setupRoutes() {
    // Health check
    this.app.get("/health", (req, res) => {
      res.json({ status: "ok" });
    });

    // Main webhook endpoint
    this.app.post("/webhook", async (req, res) => {
      try {
        const transactions = req.body;
        await this.processTransactions(transactions);
        res.json({ success: true });
      } catch (error) {
        console.error("Webhook error:", error);
        res.status(500).json({ error: "Processing failed" });
      }
    });

    // Query endpoints
    this.app.get("/api/events", async (req, res) => {
      const { type, limit = 100, offset = 0 } = req.query;

      let query = "SELECT * FROM events";
      const params: any[] = [];

      if (type) {
        params.push(type);
        query += ` WHERE event_type = $${params.length}`;
      }

      params.push(limit, offset);
      query += ` ORDER BY timestamp DESC LIMIT $${params.length - 1} OFFSET $${params.length}`;

      const result = await this.db.query(query, params);
      res.json(result.rows);
    });

    this.app.get("/api/accounts/:pubkey", async (req, res) => {
      const { pubkey } = req.params;
      const result = await this.db.query(
        "SELECT * FROM accounts WHERE pubkey = $1",
        [pubkey],
      );

      if (result.rows.length === 0) {
        return res.status(404).json({ error: "Account not found" });
      }

      res.json(result.rows[0]);
    });
  }

  private async processTransactions(transactions: any[]) {
    const client = await this.db.connect();

    try {
      await client.query("BEGIN");

      for (const tx of transactions) {
        // Store transaction
        await client.query(
          `INSERT INTO transactions (signature, type, timestamp, fee, fee_payer, description)
           VALUES ($1, $2, $3, $4, $5, $6)
           ON CONFLICT (signature) DO NOTHING`,
          [
            tx.signature,
            tx.type,
            new Date(tx.timestamp * 1000),
            tx.fee,
            tx.feePayer,
            tx.description,
          ],
        );

        // Store token transfers
        for (const transfer of tx.tokenTransfers || []) {
          await client.query(
            `INSERT INTO token_transfers 
             (signature, mint, from_address, to_address, amount, timestamp)
             VALUES ($1, $2, $3, $4, $5, $6)`,
            [
              tx.signature,
              transfer.mint,
              transfer.fromUserAccount,
              transfer.toUserAccount,
              transfer.tokenAmount,
              new Date(tx.timestamp * 1000),
            ],
          );
        }

        // Store account data changes
        for (const account of tx.accountData || []) {
          if (account.nativeBalanceChange !== 0) {
            await client.query(
              `INSERT INTO balance_changes (signature, account, change, timestamp)
               VALUES ($1, $2, $3, $4)`,
              [
                tx.signature,
                account.account,
                account.nativeBalanceChange,
                new Date(tx.timestamp * 1000),
              ],
            );
          }
        }

        // Handle specific event types
        if (tx.events?.nft) {
          await this.processNFTEvent(client, tx);
        }
        if (tx.events?.swap) {
          await this.processSwapEvent(client, tx);
        }
      }

      await client.query("COMMIT");
    } catch (error) {
      await client.query("ROLLBACK");
      throw error;
    } finally {
      client.release();
    }
  }

  private async processNFTEvent(client: any, tx: any) {
    const nft = tx.events.nft;

    await client.query(
      `INSERT INTO nft_events (signature, type, mint, buyer, seller, price, marketplace, timestamp)
       VALUES ($1, $2, $3, $4, $5, $6, $7, $8)`,
      [
        tx.signature,
        nft.type,
        nft.nfts[0]?.mint,
        nft.buyer,
        nft.seller,
        nft.amount,
        nft.source,
        new Date(tx.timestamp * 1000),
      ],
    );
  }

  private async processSwapEvent(client: any, tx: any) {
    const swap = tx.events.swap;

    await client.query(
      `INSERT INTO swap_events 
       (signature, native_input, native_output, token_inputs, token_outputs, source, timestamp)
       VALUES ($1, $2, $3, $4, $5, $6, $7)`,
      [
        tx.signature,
        JSON.stringify(swap.nativeInput),
        JSON.stringify(swap.nativeOutput),
        JSON.stringify(swap.tokenInputs),
        JSON.stringify(swap.tokenOutputs),
        swap.source,
        new Date(tx.timestamp * 1000),
      ],
    );
  }

  async setupWebhook() {
    const response = await axios.post(
      `https://api.helius.xyz/v0/webhooks?api-key=${this.config.apiKey}`,
      {
        webhookURL: `${process.env.BASE_URL}/webhook`,
        transactionTypes: ["Any"],
        accountAddresses: [this.config.programId],
        webhookType: "enhanced",
      },
    );

    console.log("Webhook created:", response.data.webhookID);
    return response.data;
  }

  start(port: number) {
    this.app.listen(port, () => {
      console.log(`Helius indexer listening on port ${port}`);
    });
  }
}

// Usage
const indexer = new HeliusIndexer({
  apiKey: process.env.HELIUS_API_KEY!,
  webhookSecret: process.env.WEBHOOK_SECRET!,
  databaseUrl: process.env.DATABASE_URL!,
  programId: "YourProgramId...",
});

await indexer.setupWebhook();
indexer.start(3000);

Best Practices

PracticeDescription
Idempotent handlersHandle duplicate webhook deliveries
Signature verificationValidate webhook authenticity
Rate limit awarenessRespect API rate limits
Backfill strategyUse history API for gaps
Error handlingImplement retries with backoff

Next: GraphQL Queries - Building GraphQL APIs for indexed data.