-
Notifications
You must be signed in to change notification settings - Fork 1.4k
/
Copy pathevent.ts
147 lines (132 loc) · 4.37 KB
/
event.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
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;
}
}