UNPKG

@wuwei-labs/srsly

Version:
267 lines 8.7 kB
/** * 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; [rentalState, offset] = readAddress(raw, offset); [contract, offset] = readAddress(raw, offset); [borrower, offset] = readAddress(raw, offset); [escrow, offset] = readU64(dv, offset); [rate, offset] = readU64(dv, offset); [serviceFee, offset] = readU64(dv, offset); [feeBps, offset] = readU16(dv, offset); [startTime, offset] = readI64(dv, offset); [endTime, 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; [rentalState, offset] = readAddress(raw, offset); [contract, offset] = readAddress(raw, offset); [ownerEarned, offset] = readU64(dv, offset); [escrowRemaining, offset] = readU64(dv, offset); [isFinal, 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; [rentalState, offset] = readAddress(raw, offset); [contract, offset] = readAddress(raw, offset); [borrower, offset] = readAddress(raw, offset); [ownerEarned, offset] = readU64(dv, offset); [feeCollected, offset] = readU64(dv, offset); [refund, offset] = readU64(dv, offset); [newEndTime, 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; [rentalState, offset] = readAddress(raw, offset); [contract, offset] = readAddress(raw, offset); [borrower, offset] = readAddress(raw, offset); [ownerEarned, offset] = readU64(dv, offset); [feeCollected, offset] = readU64(dv, offset); [pointsAwarded, 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; [contract, offset] = readAddress(raw, offset); [owner, offset] = readAddress(raw, offset); [amount, 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; [contract, offset] = readAddress(raw, offset); [feeCollected, offset] = readU64(dv, offset); [discount, offset] = readU64(dv, offset); [discountBps, 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