Skip to content

Instantly share code, notes, and snippets.

@OkoliEvans
Created July 12, 2025 15:42
Show Gist options
  • Save OkoliEvans/0385bbb0fff88b692ef9c95aefc3f041 to your computer and use it in GitHub Desktop.
Save OkoliEvans/0385bbb0fff88b692ef9c95aefc3f041 to your computer and use it in GitHub Desktop.
Apibara sample
import { defineConfig } from "apibara/config";
export default defineConfig({
runtimeConfig: {
myIndexer: {
startingBlock: 820840,
streamUrl: "https://sepolia.starknet.a5a.ch",
contractAddress:
"0x1f103e6694fcbdf2bfbe8db10d7b622bfab12da196ea1f212cb26367196af2c",
},
},
});
import { defineIndexer } from "apibara/indexer";
import { useLogger } from "apibara/plugins";
import { payments } from "../lib/schema";
import { drizzleStorage, useDrizzleStorage } from "@apibara/plugin-drizzle";
import { drizzle } from "@apibara/plugin-drizzle";
import { StarknetStream } from "@apibara/starknet";
import type { ApibaraRuntimeConfig } from "apibara/types";
import { uint256 } from "starknet";
import { confirmDisbursement, validateApiVariables } from "utils/apiAuth";
const PAYMENT_RECEIVED_EVENT_KEY =
"0x0040b634dd7cac6a7e5330478aa1704c7f20133def5f3fd107f3d185844a7b56";
const BYTES_PER_FELT = 31;
interface PaymentReceivedEvent {
sender: string;
token: string;
amount: bigint;
reference: string;
user: string;
}
interface DecodedEvent {
args: {
_tag: "PaymentReceived";
PaymentReceived: PaymentReceivedEvent;
};
}
// ByteArray utilities
function decodeBytes31(felt: string | bigint): string {
try {
let hexValue =
typeof felt === "bigint"
? felt.toString(16)
: felt.startsWith("0x")
? felt.slice(2)
: felt;
// Ensure even length for hex string
if (hexValue.length % 2 !== 0) {
hexValue = "0" + hexValue;
}
// Convert hex to bytes
let result = "";
for (let i = 0; i < hexValue.length; i += 2) {
const byte = parseInt(hexValue.substr(i, 2), 16);
if (byte !== 0) {
result += String.fromCharCode(byte);
}
}
return result;
} catch (error) {
console.warn("Failed to decode bytes31:", error);
return "";
}
}
function decodeByteArray(data: any[]): string {
if (!data || data.length < 3) {
return "";
}
try {
const dataLen = Number(data[0]);
const pendingWordLen = Number(data[data.length - 1]);
const pendingWord = data[data.length - 2];
let result = "";
for (let i = 1; i <= dataLen; i++) {
if (i < data.length - 2 && data[i]) {
result += decodeBytes31(data[i]);
}
}
if (pendingWordLen > 0 && pendingWord) {
const pendingText = decodeBytes31(pendingWord);
result += pendingText.substring(0, pendingWordLen);
}
return result.trim();
} catch (error) {
console.warn("Failed to decode ByteArray:", data, error);
return "";
}
}
function getBytearraySize(data: any[], startIndex: number): number {
if (startIndex >= data.length) return 0;
const dataLen = Number(data[startIndex]);
return 2 + dataLen + 1;
}
function extractByteArray(
data: any[],
startIndex: number
): { value: string; nextIndex: number } {
const size = getBytearraySize(data, startIndex);
const byteArrayData = data.slice(startIndex, startIndex + size);
const value = decodeByteArray(byteArrayData);
return {
value,
nextIndex: startIndex + size,
};
}
// Manual event decoder for PaymentReceived
function decodePaymentReceivedEvent(event: any): DecodedEvent | null {
const { data } = event;
if (!data || data.length < 7) {
throw new Error("Not enough data to decode PaymentReceived event");
}
try {
let currentIndex = 0;
const sender = data[currentIndex++];
if (!sender) {
throw new Error("Missing sender address");
}
const token = data[currentIndex++];
if (!token) {
throw new Error("Missing token address");
}
const amount = uint256.uint256ToBN({
low: data[currentIndex++],
high: data[currentIndex++],
});
const referenceResult = extractByteArray(data, currentIndex);
const reference = referenceResult.value;
currentIndex = referenceResult.nextIndex;
let user = "";
if (currentIndex < data.length) {
const userResult = extractByteArray(data, currentIndex);
user = userResult.value;
}
return {
args: {
_tag: "PaymentReceived",
PaymentReceived: {
sender,
token,
amount,
reference,
user,
},
},
};
} catch (error) {
console.error("Error decoding PaymentReceived event:", error);
return null;
}
}
function isPaymentReceivedEvent(event: any): boolean {
return (
event.keys?.some((key: string) => key === PAYMENT_RECEIVED_EVENT_KEY) ||
false
);
}
function validatePaymentReceivedEvent(payment: PaymentReceivedEvent): boolean {
return !!(
payment.sender &&
payment.token &&
payment.amount &&
payment.reference &&
payment.user
);
}
export default function (runtimeConfig: ApibaraRuntimeConfig) {
const { startingBlock, streamUrl, contractAddress } =
runtimeConfig["myIndexer"];
const db = drizzle({
schema: {
payments,
},
});
return defineIndexer(StarknetStream)({
streamUrl,
finality: "accepted",
startingBlock: BigInt(startingBlock),
filter: {
header: "on_data",
events: [
{
address: contractAddress as `0x${string}`,
},
],
},
plugins: [drizzleStorage({ db })],
async transform({ block }) {
const logger = useLogger();
// Validate API variables
const envVariablesCheck = validateApiVariables();
if (!envVariablesCheck.isValid) {
logger.error("Invalid API configuration", {
missingVars: envVariablesCheck.missingVars,
});
throw new Error(`Missing required environment variables: ${envVariablesCheck.missingVars.join(', ')}`);
}
logger.info(
`Block received: ${block.header.blockNumber}, with events count: ${block.events.length}`
);
for (const event of block.events) {
logger.info("Processing event with keys:", event.keys);
try {
// Skip if not a PaymentReceived event
if (!isPaymentReceivedEvent(event)) {
logger.info("Skipping non-PaymentReceived event");
continue;
}
logger.info("Processing PaymentReceived event");
const decoded = decodePaymentReceivedEvent(event);
if (!decoded) {
logger.warn(
"Failed to decode PaymentReceived event",
event.keys,
event.transactionHash
);
continue;
}
const payment = decoded.args.PaymentReceived;
if(!validatePaymentReceivedEvent(payment)) {
logger.warn("Invalid payment event data", {
transactionHash: event.transactionHash,
payment
});
continue;
}
logger.info("Payment received:", {
amount: payment.amount.toString(),
token: payment.token,
sender: payment.sender,
reference: payment.reference,
user: payment.user,
});
// Make API call to confirm disbursement
const apiResponse = await confirmDisbursement(payment, event.transactionHash, logger);
// Store in database - Use the already instantiated db, not a new one
try {
const { db } = useDrizzleStorage();
await db.insert(payments).values({
eventId: event.transactionHash,
transactionHash: event.transactionHash,
sender: payment.sender as string,
token: payment.token as string,
amount: payment.amount.toString(),
reference: payment.reference,
timestamp: new Date(),
blockNumber: Number(block.header.blockNumber),
status: apiResponse ? "confirmed" : "pending",
});
logger.info("Payment stored in database", {
transactionHash: event.transactionHash,
status: apiResponse ? "confirmed" : "pending",
});
} catch (dbError) {
logger.error("Failed to store event in database:", {
error: dbError.message,
transactionHash: event.transactionHash,
});
}
} catch (error) {
logger.error("Error processing event:", error);
logger.error(
"Event data:",
JSON.stringify({
keys: event.keys,
data: event.data?.slice(0, 10), // Log just first elements to avoid flooding
})
);
}
}
},
});
}
{
"name": "apibara-app",
"version": "0.1.0",
"private": true,
"type": "module",
"scripts": {
"prepare": "apibara prepare",
"dev": "apibara dev",
"start": "apibara start",
"build": "apibara build",
"typecheck": "tsc --noEmit",
"drizzle:generate": "drizzle-kit generate",
"drizzle:migrate": "drizzle-kit migrate"
},
"dependencies": {
"@apibara/plugin-drizzle": "next",
"@apibara/protocol": "next",
"@apibara/starknet": "next",
"@electric-sql/pglite": "^0.2.17",
"@types/express": "^5.0.2",
"apibara": "next",
"drizzle-kit": "^0.29.1",
"drizzle-orm": "^0.40.1",
"pg": "^8.16.0",
"starknet": "^6.24.1"
},
"devDependencies": {
"@types/node": "^20.17.47",
"@types/pg": "^8.15.2",
"typescript": "^5.8.3"
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment