event.ts 4.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147
  1. import { PublicKey } from "@solana/web3.js";
  2. import * as assert from "assert";
  3. import Coder from "../coder";
  4. const LOG_START_INDEX = "Program log: ".length;
  5. // Deserialized event.
  6. export type Event = {
  7. name: string;
  8. data: Object;
  9. };
  10. export class EventParser {
  11. private coder: Coder;
  12. private programId: PublicKey;
  13. constructor(coder: Coder, programId: PublicKey) {
  14. this.coder = coder;
  15. this.programId = programId;
  16. }
  17. // Each log given, represents an array of messages emitted by
  18. // a single transaction, which can execute many different programs across
  19. // CPI boundaries. However, the subscription is only interested in the
  20. // events emitted by *this* program. In achieving this, we keep track of the
  21. // program execution context by parsing each log and looking for a CPI
  22. // `invoke` call. If one exists, we know a new program is executing. So we
  23. // push the programId onto a stack and switch the program context. This
  24. // allows us to track, for a given log, which program was executing during
  25. // its emission, thereby allowing us to know if a given log event was
  26. // emitted by *this* program. If it was, then we parse the raw string and
  27. // emit the event if the string matches the event being subscribed to.
  28. public parseLogs(logs: string[], callback: (log: Event) => void) {
  29. const logScanner = new LogScanner(logs);
  30. const execution = new ExecutionContext(logScanner.next() as string);
  31. let log = logScanner.next();
  32. while (log !== null) {
  33. let [event, newProgram, didPop] = this.handleLog(execution, log);
  34. if (event) {
  35. callback(event);
  36. }
  37. if (newProgram) {
  38. execution.push(newProgram);
  39. }
  40. if (didPop) {
  41. execution.pop();
  42. // Skip the "success" log, which always follows the consumed log.
  43. logScanner.next();
  44. }
  45. log = logScanner.next();
  46. }
  47. }
  48. // Main log handler. Returns a three element array of the event, the
  49. // next program that was invoked for CPI, and a boolean indicating if
  50. // a program has completed execution (and thus should be popped off the
  51. // execution stack).
  52. private handleLog(
  53. execution: ExecutionContext,
  54. log: string
  55. ): [Event | null, string | null, boolean] {
  56. // Executing program is this program.
  57. if (execution.program() === this.programId.toString()) {
  58. return this.handleProgramLog(log);
  59. }
  60. // Executing program is not this program.
  61. else {
  62. return [null, ...this.handleSystemLog(log)];
  63. }
  64. }
  65. // Handles logs from *this* program.
  66. private handleProgramLog(
  67. log: string
  68. ): [Event | null, string | null, boolean] {
  69. // This is a `msg!` log.
  70. if (log.startsWith("Program log:")) {
  71. const logStr = log.slice(LOG_START_INDEX);
  72. const event = this.coder.events.decode(logStr);
  73. return [event, null, false];
  74. }
  75. // System log.
  76. else {
  77. return [null, ...this.handleSystemLog(log)];
  78. }
  79. }
  80. // Handles logs when the current program being executing is *not* this.
  81. private handleSystemLog(log: string): [string | null, boolean] {
  82. // System component.
  83. const logStart = log.split(":")[0];
  84. // Recursive call.
  85. if (logStart.startsWith(`Program ${this.programId.toString()} invoke`)) {
  86. return [this.programId.toString(), false];
  87. }
  88. // Cpi call.
  89. else if (logStart.includes("invoke")) {
  90. return ["cpi", false]; // Any string will do.
  91. } else {
  92. // Did the program finish executing?
  93. if (logStart.match(/^Program (.*) consumed .*$/g) !== null) {
  94. return [null, true];
  95. }
  96. return [null, false];
  97. }
  98. }
  99. }
  100. // Stack frame execution context, allowing one to track what program is
  101. // executing for a given log.
  102. class ExecutionContext {
  103. stack: string[];
  104. constructor(log: string) {
  105. // Assumes the first log in every transaction is an `invoke` log from the
  106. // runtime.
  107. const program = /^Program (.*) invoke.*$/g.exec(log)[1];
  108. this.stack = [program];
  109. }
  110. program(): string {
  111. assert.ok(this.stack.length > 0);
  112. return this.stack[this.stack.length - 1];
  113. }
  114. push(newProgram: string) {
  115. this.stack.push(newProgram);
  116. }
  117. pop() {
  118. assert.ok(this.stack.length > 0);
  119. this.stack.pop();
  120. }
  121. }
  122. class LogScanner {
  123. constructor(public logs: string[]) {}
  124. next(): string | null {
  125. if (this.logs.length === 0) {
  126. return null;
  127. }
  128. let l = this.logs[0];
  129. this.logs = this.logs.slice(1);
  130. return l;
  131. }
  132. }