anchor/ts/src/program/event.ts

170 lines
5.2 KiB
TypeScript
Raw Normal View History

2021-05-08 14:52:26 -07:00
import { PublicKey } from "@solana/web3.js";
import * as base64 from "base64-js";
import * as assert from "assert";
import Coder, { eventDiscriminator } from "../coder";
import { Idl } from "../idl";
2021-05-08 14:52:26 -07:00
const LOG_START_INDEX = "Program log: ".length;
// Deserialized event.
export type Event = {
name: string;
data: Object;
};
export class EventParser {
2021-05-08 14:52:26 -07:00
private coder: Coder;
private programId: PublicKey;
// Maps base64 encoded event discriminator to event name.
private discriminators: Map<string, string>;
2021-05-08 14:52:26 -07:00
constructor(coder: Coder, programId: PublicKey, idl: Idl) {
2021-05-08 14:52:26 -07:00
this.coder = coder;
this.programId = programId;
this.discriminators = new Map<string, string>(
idl.events === undefined
? []
: idl.events.map((e) => [
base64.fromByteArray(eventDiscriminator(e.name)),
e.name,
])
);
2021-05-08 14:52:26 -07:00
}
// 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) {
2021-05-08 14:52:26 -07:00
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();
2021-05-08 14:52:26 -07:00
}
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);
2021-05-08 14:52:26 -07:00
}
// Executing program is not this program.
else {
return [null, ...this.handleSystemLog(log)];
2021-05-08 14:52:26 -07:00
}
}
// Handles logs from *this* program.
private handleProgramLog(
log: string
): [Event | null, string | null, boolean] {
2021-05-08 14:52:26 -07:00
// This is a `msg!` log.
if (log.startsWith("Program log:")) {
const logStr = log.slice(LOG_START_INDEX);
const logArr = Buffer.from(base64.toByteArray(logStr));
const disc = base64.fromByteArray(logArr.slice(0, 8));
2021-05-08 14:52:26 -07:00
// Only deserialize if the discriminator implies a proper event.
let event = null;
let eventName = this.discriminators.get(disc);
if (eventName !== undefined) {
event = {
name: eventName,
data: this.coder.events.decode(eventName, logArr.slice(8)),
};
2021-05-08 14:52:26 -07:00
}
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];
2021-05-08 14:52:26 -07:00
}
// 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];
2021-05-08 14:52:26 -07:00
}
}
}
// 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;
}
}