program.ts 7.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269
  1. import { PublicKey } from "@solana/web3.js";
  2. import { inflate } from "pako";
  3. import Provider from "./provider";
  4. import { RpcFactory } from "./rpc";
  5. import { Idl, idlAddress, decodeIdlAccount } from "./idl";
  6. import Coder, { eventDiscriminator } from "./coder";
  7. import { Rpcs, Ixs, Txs, Accounts, State } from "./rpc";
  8. import { getProvider } from "./";
  9. import * as base64 from "base64-js";
  10. import * as assert from "assert";
  11. /**
  12. * Program is the IDL deserialized representation of a Solana program.
  13. */
  14. export class Program {
  15. /**
  16. * Address of the program.
  17. */
  18. readonly programId: PublicKey;
  19. /**
  20. * IDL describing this program's interface.
  21. */
  22. readonly idl: Idl;
  23. /**
  24. * Async functions to invoke instructions against a Solana priogram running
  25. * on a cluster.
  26. */
  27. readonly rpc: Rpcs;
  28. /**
  29. * Async functions to fetch deserialized program accounts from a cluster.
  30. */
  31. readonly account: Accounts;
  32. /**
  33. * Functions to build `TransactionInstruction` objects.
  34. */
  35. readonly instruction: Ixs;
  36. /**
  37. * Functions to build `Transaction` objects.
  38. */
  39. readonly transaction: Txs;
  40. /**
  41. * Coder for serializing rpc requests.
  42. */
  43. readonly coder: Coder;
  44. /**
  45. * Object with state account accessors and rpcs.
  46. */
  47. readonly state: State;
  48. /**
  49. * Wallet and network provider.
  50. */
  51. readonly provider: Provider;
  52. public constructor(idl: Idl, programId: PublicKey, provider?: Provider) {
  53. this.idl = idl;
  54. this.programId = programId;
  55. this.provider = provider ?? getProvider();
  56. // Build the serializer.
  57. const coder = new Coder(idl);
  58. // Build the dynamic RPC functions.
  59. const [rpcs, ixs, txs, accounts, state] = RpcFactory.build(
  60. idl,
  61. coder,
  62. programId,
  63. this.provider
  64. );
  65. this.rpc = rpcs;
  66. this.instruction = ixs;
  67. this.transaction = txs;
  68. this.account = accounts;
  69. this.coder = coder;
  70. this.state = state;
  71. }
  72. /**
  73. * Generates a Program client by fetching the IDL from chain.
  74. */
  75. public static async at(programId: PublicKey, provider?: Provider) {
  76. const idl = await Program.fetchIdl(programId, provider);
  77. return new Program(idl, programId, provider);
  78. }
  79. /**
  80. * Fetches an idl from the blockchain.
  81. */
  82. public static async fetchIdl(programId: PublicKey, provider?: Provider) {
  83. provider = provider ?? getProvider();
  84. const address = await idlAddress(programId);
  85. const accountInfo = await provider.connection.getAccountInfo(address);
  86. // Chop off account discriminator.
  87. let idlAccount = decodeIdlAccount(accountInfo.data.slice(8));
  88. const inflatedIdl = inflate(idlAccount.data);
  89. return JSON.parse(decodeUtf8(inflatedIdl));
  90. }
  91. /**
  92. * Invokes the given callback everytime the given event is emitted.
  93. */
  94. public addEventListener<T>(
  95. eventName: string,
  96. callback: (event: T, slot: number) => void
  97. ): Promise<void> {
  98. // Values shared across log handlers.
  99. const thisProgramStr = this.programId.toString();
  100. const discriminator = eventDiscriminator(eventName);
  101. const logStartIndex = "Program log: ".length;
  102. // Handles logs when the current program being executing is *not* this.
  103. const handleSystemLog = (log: string): [string | null, boolean] => {
  104. // System component.
  105. const logStart = log.split(":")[0];
  106. // Recursive call.
  107. if (logStart.startsWith(`Program ${this.programId.toString()} invoke`)) {
  108. return [this.programId.toString(), false];
  109. }
  110. // Cpi call.
  111. else if (logStart.includes("invoke")) {
  112. return ["cpi", false]; // Any string will do.
  113. } else {
  114. // Did the program finish executing?
  115. if (logStart.match(/^Program (.*) consumed .*$/g) !== null) {
  116. return [null, true];
  117. }
  118. return [null, false];
  119. }
  120. };
  121. // Handles logs from *this* program.
  122. const handleProgramLog = (
  123. log: string
  124. ): [T | null, string | null, boolean] => {
  125. // This is a `msg!` log.
  126. if (log.startsWith("Program log:")) {
  127. const logStr = log.slice(logStartIndex);
  128. const logArr = Buffer.from(base64.toByteArray(logStr));
  129. const disc = logArr.slice(0, 8);
  130. // Only deserialize if the discriminator implies a proper event.
  131. let event = null;
  132. if (disc.equals(discriminator)) {
  133. event = this.coder.events.decode(eventName, logArr.slice(8));
  134. }
  135. return [event, null, false];
  136. }
  137. // System log.
  138. else {
  139. return [null, ...handleSystemLog(log)];
  140. }
  141. };
  142. // Main log handler. Returns a three element array of the event, the
  143. // next program that was invoked for CPI, and a boolean indicating if
  144. // a program has completed execution (and thus should be popped off the
  145. // execution stack).
  146. const handleLog = (
  147. execution: ExecutionContext,
  148. log: string
  149. ): [T | null, string | null, boolean] => {
  150. // Executing program is this program.
  151. if (execution.program() === thisProgramStr) {
  152. return handleProgramLog(log);
  153. }
  154. // Executing program is not this program.
  155. else {
  156. return [null, ...handleSystemLog(log)];
  157. }
  158. };
  159. // Each log given, represents an array of messages emitted by
  160. // a single transaction, which can execute many different programs across
  161. // CPI boundaries. However, the subscription is only interested in the
  162. // events emitted by *this* program. In achieving this, we keep track of the
  163. // program execution context by parsing each log and looking for a CPI
  164. // `invoke` call. If one exists, we know a new program is executing. So we
  165. // push the programId onto a stack and switch the program context. This
  166. // allows us to track, for a given log, which program was executing during
  167. // its emission, thereby allowing us to know if a given log event was
  168. // emitted by *this* program. If it was, then we parse the raw string and
  169. // emit the event if the string matches the event being subscribed to.
  170. //
  171. // @ts-ignore
  172. return this.provider.connection.onLogs(this.programId, (logs, ctx) => {
  173. if (logs.err) {
  174. console.error(logs);
  175. return;
  176. }
  177. const logScanner = new LogScanner(logs.logs);
  178. const execution = new ExecutionContext(logScanner.next() as string);
  179. let log = logScanner.next();
  180. while (log !== null) {
  181. let [event, newProgram, didPop] = handleLog(execution, log);
  182. if (event) {
  183. callback(event, ctx.slot);
  184. }
  185. if (newProgram) {
  186. execution.push(newProgram);
  187. }
  188. if (didPop) {
  189. execution.pop();
  190. }
  191. log = logScanner.next();
  192. }
  193. });
  194. }
  195. public async removeEventListener(listener: number): Promise<void> {
  196. // @ts-ignore
  197. return this.provider.connection.removeOnLogsListener(listener);
  198. }
  199. }
  200. // Stack frame execution context, allowing one to track what program is
  201. // executing for a given log.
  202. class ExecutionContext {
  203. stack: string[];
  204. constructor(log: string) {
  205. // Assumes the first log in every transaction is an `invoke` log from the
  206. // runtime.
  207. const program = /^Program (.*) invoke.*$/g.exec(log)[1];
  208. this.stack = [program];
  209. }
  210. program(): string {
  211. assert.ok(this.stack.length > 0);
  212. return this.stack[this.stack.length - 1];
  213. }
  214. push(newProgram: string) {
  215. this.stack.push(newProgram);
  216. }
  217. pop() {
  218. assert.ok(this.stack.length > 0);
  219. this.stack.pop();
  220. }
  221. }
  222. class LogScanner {
  223. constructor(public logs: string[]) {}
  224. next(): string | null {
  225. if (this.logs.length === 0) {
  226. return null;
  227. }
  228. let l = this.logs[0];
  229. this.logs = this.logs.slice(1);
  230. return l;
  231. }
  232. }
  233. function decodeUtf8(array: Uint8Array): string {
  234. const decoder =
  235. typeof TextDecoder === "undefined"
  236. ? new (require("util").TextDecoder)("utf-8") // Node.
  237. : new TextDecoder("utf-8"); // Browser.
  238. return decoder.decode(array);
  239. }