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
| Practice | Description |
|---|---|
| Idempotent handlers | Handle duplicate webhook deliveries |
| Signature verification | Validate webhook authenticity |
| Rate limit awareness | Respect API rate limits |
| Backfill strategy | Use history API for gaps |
| Error handling | Implement retries with backoff |
Next: GraphQL Queries - Building GraphQL APIs for indexed data.