@wuwei-labs/srsly
Version:
TypeScript SDK for SRSLY
267 lines • 8.7 kB
JavaScript
/**
* Anchor event log parser for SRSLY audit events.
*
* Discriminators are read from the IDL (precomputed by Anchor's build).
* Borsh decoding is manual — no external borsh dependency needed.
*/
import { getAddressDecoder } from '@solana/kit';
import idl from '../idl/srsly.json';
/** Convert byte array to hex string */
function toHex(bytes) {
return Array.from(bytes)
.map(b => b.toString(16).padStart(2, '0'))
.join('');
}
// Build discriminator → event name map from IDL
const DISC_MAP = new Map();
for (const evt of idl.events ?? []) {
const hex = toHex(evt.discriminator);
DISC_MAP.set(hex, evt.name);
}
const addressDecoder = getAddressDecoder();
/** Decode base64 string to Uint8Array (browser + Node compatible via atob) */
function decodeBase64(str) {
const binary = atob(str);
const bytes = new Uint8Array(binary.length);
for (let i = 0; i < binary.length; i++)
bytes[i] = binary.charCodeAt(i);
return bytes;
}
// ── Borsh deserialization helpers ──
function readAddress(buf, offset) {
const bytes = buf.slice(offset, offset + 32);
const address = addressDecoder.decode(bytes);
return [address, offset + 32];
}
function readU64(buf, offset) {
const lo = buf.getUint32(offset, true);
const hi = buf.getUint32(offset + 4, true);
return [BigInt(lo) + (BigInt(hi) << 32n), offset + 8];
}
function readI64(buf, offset) {
const value = buf.getBigInt64(offset, true);
return [value, offset + 8];
}
function readU16(buf, offset) {
return [buf.getUint16(offset, true), offset + 2];
}
function readBool(buf, offset) {
return [buf.getUint8(offset) !== 0, offset + 1];
}
// ── Event decoders ──
function decodeRentalAccepted(raw, dv, offset, slot, signature) {
let rentalState, contract, borrower;
let escrow, rate, serviceFee;
let feeBps;
let startTime, endTime;
[] = readAddress(raw, offset);
[] = readAddress(raw, offset);
[] = readAddress(raw, offset);
[] = readU64(dv, offset);
[] = readU64(dv, offset);
[] = readU64(dv, offset);
[] = readU16(dv, offset);
[] = readI64(dv, offset);
[] = readI64(dv, offset);
return {
type: 'RentalAccepted',
rentalState,
contract,
borrower,
escrow,
rate,
serviceFee,
feeBps,
startTime,
endTime,
slot,
signature,
};
}
function decodeRentalSettled(raw, dv, offset, slot, signature) {
let rentalState, contract;
let ownerEarned, escrowRemaining;
let isFinal;
[] = readAddress(raw, offset);
[] = readAddress(raw, offset);
[] = readU64(dv, offset);
[] = readU64(dv, offset);
[] = readBool(dv, offset);
return {
type: 'RentalSettled',
rentalState,
contract,
ownerEarned,
escrowRemaining,
isFinal,
slot,
signature,
};
}
function decodeRentalCancelled(raw, dv, offset, slot, signature) {
let rentalState, contract, borrower;
let ownerEarned, feeCollected, refund;
let newEndTime;
[] = readAddress(raw, offset);
[] = readAddress(raw, offset);
[] = readAddress(raw, offset);
[] = readU64(dv, offset);
[] = readU64(dv, offset);
[] = readU64(dv, offset);
[] = readI64(dv, offset);
return {
type: 'RentalCancelled',
rentalState,
contract,
borrower,
ownerEarned,
feeCollected,
refund,
newEndTime,
slot,
signature,
};
}
function decodeRentalClosed(raw, dv, offset, slot, signature) {
let rentalState, contract, borrower;
let ownerEarned, feeCollected, pointsAwarded;
[] = readAddress(raw, offset);
[] = readAddress(raw, offset);
[] = readAddress(raw, offset);
[] = readU64(dv, offset);
[] = readU64(dv, offset);
[] = readU64(dv, offset);
return {
type: 'RentalClosed',
rentalState,
contract,
borrower,
ownerEarned,
feeCollected,
pointsAwarded,
slot,
signature,
};
}
function decodeOwnerPaid(raw, dv, offset, slot, signature) {
let contract, owner;
let amount;
[] = readAddress(raw, offset);
[] = readAddress(raw, offset);
[] = readU64(dv, offset);
return {
type: 'OwnerPaid',
contract,
owner,
amount,
slot,
signature,
};
}
function decodeFeesPaid(raw, dv, offset, slot, signature) {
let contract;
let feeCollected, discount;
let discountBps;
[] = readAddress(raw, offset);
[] = readU64(dv, offset);
[] = readU64(dv, offset);
[] = readU16(dv, offset);
return {
type: 'FeesPaid',
contract,
feeCollected,
discount,
discountBps,
slot,
signature,
};
}
// Sanity check: timestamps should be between 2024 and 2030
const MIN_TS = 1704067200n; // 2024-01-01
const MAX_TS = 1893456000n; // 2030-01-01
function isPlausibleEvent(event) {
if (event.type === 'RentalAccepted') {
return (event.startTime >= MIN_TS &&
event.startTime <= MAX_TS &&
event.endTime >= MIN_TS &&
event.endTime <= MAX_TS &&
event.endTime > event.startTime);
}
if (event.type === 'RentalCancelled') {
return event.newEndTime >= MIN_TS && event.newEndTime <= MAX_TS;
}
// For other events, check amounts aren't astronomically large (> 1 trillion stardust = 10k ATLAS)
if (event.type === 'RentalSettled') {
return event.ownerEarned < 1000000000000000n;
}
return true;
}
const DECODERS = {
RentalAccepted: decodeRentalAccepted,
RentalSettled: decodeRentalSettled,
RentalCancelled: decodeRentalCancelled,
RentalClosed: decodeRentalClosed,
OwnerPaid: decodeOwnerPaid,
FeesPaid: decodeFeesPaid,
};
/**
* Parse Anchor events from transaction log messages.
*
* Anchor emits events as `Program data: <base64>` log lines where the data
* starts with an 8-byte discriminator followed by borsh-serialized fields.
*
* Discriminators are matched against the IDL — no runtime hashing needed.
*/
export function parseAnchorEvents(logs, programId, slot, signature) {
const events = [];
// Track execution context as a stack of program IDs.
// `Program data:` lines belong to the program on top of the stack.
// This correctly handles CPI chains (thread → SRSLY → token → ...).
const execStack = [];
const invokeRe = /^Program (\w+) invoke \[(\d+)\]$/;
for (const log of logs) {
const invokeMatch = log.match(invokeRe);
if (invokeMatch) {
execStack.push(invokeMatch[1]);
continue;
}
if (log.startsWith('Program ') && (log.endsWith(' success') || log.endsWith(' failed'))) {
execStack.pop();
continue;
}
// Only parse `Program data:` when our program is on top of the stack
const currentProgram = execStack[execStack.length - 1];
if (currentProgram !== programId || !log.startsWith('Program data: '))
continue;
const base64Data = log.slice('Program data: '.length);
let raw;
try {
raw = decodeBase64(base64Data);
}
catch {
continue;
}
if (raw.length < 8)
continue;
const discHex = toHex(raw.slice(0, 8));
const eventName = DISC_MAP.get(discHex);
if (!eventName)
continue;
const decoder = DECODERS[eventName];
if (!decoder)
continue;
try {
const dv = new DataView(raw.buffer, raw.byteOffset, raw.byteLength);
const event = decoder(raw, dv, 8, slot, signature);
// Validate: reject events from old program versions with mismatched layouts
if (!isPlausibleEvent(event))
continue;
events.push(event);
}
catch {
// Skip malformed events
}
}
return events;
}
//# sourceMappingURL=events.js.map