Created
July 12, 2025 15:42
-
-
Save OkoliEvans/0385bbb0fff88b692ef9c95aefc3f041 to your computer and use it in GitHub Desktop.
Apibara sample
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { defineConfig } from "apibara/config"; | |
export default defineConfig({ | |
runtimeConfig: { | |
myIndexer: { | |
startingBlock: 820840, | |
streamUrl: "https://sepolia.starknet.a5a.ch", | |
contractAddress: | |
"0x1f103e6694fcbdf2bfbe8db10d7b622bfab12da196ea1f212cb26367196af2c", | |
}, | |
}, | |
}); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | |
}) | |
); | |
} | |
} | |
}, | |
}); | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
{ | |
"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