123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147 |
- import { PublicKey } from "@solana/web3.js";
- import * as assert from "assert";
- import Coder from "../coder";
- const LOG_START_INDEX = "Program log: ".length;
- // Deserialized event.
- export type Event = {
- name: string;
- data: Object;
- };
- export class EventParser {
- private coder: Coder;
- private programId: PublicKey;
- constructor(coder: Coder, programId: PublicKey) {
- this.coder = coder;
- this.programId = programId;
- }
- // Each log given, represents an array of messages emitted by
- // a single transaction, which can execute many different programs across
- // CPI boundaries. However, the subscription is only interested in the
- // events emitted by *this* program. In achieving this, we keep track of the
- // program execution context by parsing each log and looking for a CPI
- // `invoke` call. If one exists, we know a new program is executing. So we
- // push the programId onto a stack and switch the program context. This
- // allows us to track, for a given log, which program was executing during
- // its emission, thereby allowing us to know if a given log event was
- // emitted by *this* program. If it was, then we parse the raw string and
- // emit the event if the string matches the event being subscribed to.
- public parseLogs(logs: string[], callback: (log: Event) => void) {
- const logScanner = new LogScanner(logs);
- const execution = new ExecutionContext(logScanner.next() as string);
- let log = logScanner.next();
- while (log !== null) {
- let [event, newProgram, didPop] = this.handleLog(execution, log);
- if (event) {
- callback(event);
- }
- if (newProgram) {
- execution.push(newProgram);
- }
- if (didPop) {
- execution.pop();
- // Skip the "success" log, which always follows the consumed log.
- logScanner.next();
- }
- log = logScanner.next();
- }
- }
- // Main log handler. Returns a three element array of the event, the
- // next program that was invoked for CPI, and a boolean indicating if
- // a program has completed execution (and thus should be popped off the
- // execution stack).
- private handleLog(
- execution: ExecutionContext,
- log: string
- ): [Event | null, string | null, boolean] {
- // Executing program is this program.
- if (execution.program() === this.programId.toString()) {
- return this.handleProgramLog(log);
- }
- // Executing program is not this program.
- else {
- return [null, ...this.handleSystemLog(log)];
- }
- }
- // Handles logs from *this* program.
- private handleProgramLog(
- log: string
- ): [Event | null, string | null, boolean] {
- // This is a `msg!` log.
- if (log.startsWith("Program log:")) {
- const logStr = log.slice(LOG_START_INDEX);
- const event = this.coder.events.decode(logStr);
- return [event, null, false];
- }
- // System log.
- else {
- return [null, ...this.handleSystemLog(log)];
- }
- }
- // Handles logs when the current program being executing is *not* this.
- private handleSystemLog(log: string): [string | null, boolean] {
- // System component.
- const logStart = log.split(":")[0];
- // Recursive call.
- if (logStart.startsWith(`Program ${this.programId.toString()} invoke`)) {
- return [this.programId.toString(), false];
- }
- // Cpi call.
- else if (logStart.includes("invoke")) {
- return ["cpi", false]; // Any string will do.
- } else {
- // Did the program finish executing?
- if (logStart.match(/^Program (.*) consumed .*$/g) !== null) {
- return [null, true];
- }
- return [null, false];
- }
- }
- }
- // Stack frame execution context, allowing one to track what program is
- // executing for a given log.
- class ExecutionContext {
- stack: string[];
- constructor(log: string) {
- // Assumes the first log in every transaction is an `invoke` log from the
- // runtime.
- const program = /^Program (.*) invoke.*$/g.exec(log)[1];
- this.stack = [program];
- }
- program(): string {
- assert.ok(this.stack.length > 0);
- return this.stack[this.stack.length - 1];
- }
- push(newProgram: string) {
- this.stack.push(newProgram);
- }
- pop() {
- assert.ok(this.stack.length > 0);
- this.stack.pop();
- }
- }
- class LogScanner {
- constructor(public logs: string[]) {}
- next(): string | null {
- if (this.logs.length === 0) {
- return null;
- }
- let l = this.logs[0];
- this.logs = this.logs.slice(1);
- return l;
- }
- }
|