state.ts 6.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217
  1. import EventEmitter from "eventemitter3";
  2. import {
  3. PublicKey,
  4. SystemProgram,
  5. Transaction,
  6. TransactionSignature,
  7. TransactionInstruction,
  8. SYSVAR_RENT_PUBKEY,
  9. Commitment,
  10. } from "@solana/web3.js";
  11. import Provider from "../../provider";
  12. import { Idl, IdlStateMethod } from "../../idl";
  13. import Coder, { stateDiscriminator } from "../../coder";
  14. import { RpcNamespace, InstructionNamespace } from "./";
  15. import {
  16. Subscription,
  17. translateError,
  18. toInstruction,
  19. validateAccounts,
  20. } from "../common";
  21. import { Accounts, splitArgsAndCtx } from "../context";
  22. import InstructionNamespaceFactory from "./instruction";
  23. export type StateNamespace = () =>
  24. | Promise<any>
  25. | {
  26. address: () => Promise<PublicKey>;
  27. rpc: RpcNamespace;
  28. instruction: InstructionNamespace;
  29. subscribe: (commitment?: Commitment) => EventEmitter;
  30. unsubscribe: () => void;
  31. };
  32. export default class StateFactory {
  33. // Builds the state namespace.
  34. public static build(
  35. idl: Idl,
  36. coder: Coder,
  37. programId: PublicKey,
  38. idlErrors: Map<number, string>,
  39. provider: Provider
  40. ): StateNamespace | undefined {
  41. if (idl.state === undefined) {
  42. return undefined;
  43. }
  44. // Fetches the state object from the blockchain.
  45. const state = async (): Promise<any> => {
  46. const addr = await programStateAddress(programId);
  47. const accountInfo = await provider.connection.getAccountInfo(addr);
  48. if (accountInfo === null) {
  49. throw new Error(`Account does not exist ${addr.toString()}`);
  50. }
  51. // Assert the account discriminator is correct.
  52. const expectedDiscriminator = await stateDiscriminator(
  53. idl.state.struct.name
  54. );
  55. if (expectedDiscriminator.compare(accountInfo.data.slice(0, 8))) {
  56. throw new Error("Invalid account discriminator");
  57. }
  58. return coder.state.decode(accountInfo.data);
  59. };
  60. // Namespace with all rpc functions.
  61. const rpc: RpcNamespace = {};
  62. const ix: InstructionNamespace = {};
  63. idl.state.methods.forEach((m: IdlStateMethod) => {
  64. const accounts = async (accounts: Accounts): Promise<any> => {
  65. const keys = await stateInstructionKeys(
  66. programId,
  67. provider,
  68. m,
  69. accounts
  70. );
  71. return keys.concat(
  72. InstructionNamespaceFactory.accountsArray(accounts, m.accounts)
  73. );
  74. };
  75. const ixFn = async (...args: any[]): Promise<TransactionInstruction> => {
  76. const [ixArgs, ctx] = splitArgsAndCtx(m, [...args]);
  77. return new TransactionInstruction({
  78. keys: await accounts(ctx.accounts),
  79. programId,
  80. data: coder.instruction.encodeState(
  81. m.name,
  82. toInstruction(m, ...ixArgs)
  83. ),
  84. });
  85. };
  86. ixFn["accounts"] = accounts;
  87. ix[m.name] = ixFn;
  88. rpc[m.name] = async (...args: any[]): Promise<TransactionSignature> => {
  89. const [, ctx] = splitArgsAndCtx(m, [...args]);
  90. const tx = new Transaction();
  91. if (ctx.instructions !== undefined) {
  92. tx.add(...ctx.instructions);
  93. }
  94. tx.add(await ix[m.name](...args));
  95. try {
  96. const txSig = await provider.send(tx, ctx.signers, ctx.options);
  97. return txSig;
  98. } catch (err) {
  99. let translatedErr = translateError(idlErrors, err);
  100. if (translatedErr === null) {
  101. throw err;
  102. }
  103. throw translatedErr;
  104. }
  105. };
  106. });
  107. state["rpc"] = rpc;
  108. state["instruction"] = ix;
  109. // Calculates the address of the program's global state object account.
  110. state["address"] = async (): Promise<PublicKey> =>
  111. programStateAddress(programId);
  112. // Subscription singleton.
  113. let sub: null | Subscription = null;
  114. // Subscribe to account changes.
  115. state["subscribe"] = (commitment?: Commitment): EventEmitter => {
  116. if (sub !== null) {
  117. return sub.ee;
  118. }
  119. const ee = new EventEmitter();
  120. state["address"]().then((address) => {
  121. const listener = provider.connection.onAccountChange(
  122. address,
  123. (acc) => {
  124. const account = coder.state.decode(acc.data);
  125. ee.emit("change", account);
  126. },
  127. commitment
  128. );
  129. sub = {
  130. ee,
  131. listener,
  132. };
  133. });
  134. return ee;
  135. };
  136. // Unsubscribe from account changes.
  137. state["unsubscribe"] = () => {
  138. if (sub !== null) {
  139. provider.connection
  140. .removeAccountChangeListener(sub.listener)
  141. .then(async () => {
  142. sub = null;
  143. })
  144. .catch(console.error);
  145. }
  146. };
  147. return state;
  148. }
  149. }
  150. // Calculates the deterministic address of the program's "state" account.
  151. async function programStateAddress(programId: PublicKey): Promise<PublicKey> {
  152. let [registrySigner] = await PublicKey.findProgramAddress([], programId);
  153. return PublicKey.createWithSeed(registrySigner, "unversioned", programId);
  154. }
  155. // Returns the common keys that are prepended to all instructions targeting
  156. // the "state" of a program.
  157. async function stateInstructionKeys(
  158. programId: PublicKey,
  159. provider: Provider,
  160. m: IdlStateMethod,
  161. accounts: Accounts
  162. ) {
  163. if (m.name === "new") {
  164. // Ctor `new` method.
  165. const [programSigner] = await PublicKey.findProgramAddress([], programId);
  166. return [
  167. {
  168. pubkey: provider.wallet.publicKey,
  169. isWritable: false,
  170. isSigner: true,
  171. },
  172. {
  173. pubkey: await programStateAddress(programId),
  174. isWritable: true,
  175. isSigner: false,
  176. },
  177. { pubkey: programSigner, isWritable: false, isSigner: false },
  178. {
  179. pubkey: SystemProgram.programId,
  180. isWritable: false,
  181. isSigner: false,
  182. },
  183. { pubkey: programId, isWritable: false, isSigner: false },
  184. {
  185. pubkey: SYSVAR_RENT_PUBKEY,
  186. isWritable: false,
  187. isSigner: false,
  188. },
  189. ];
  190. } else {
  191. validateAccounts(m.accounts, accounts);
  192. return [
  193. {
  194. pubkey: await programStateAddress(programId),
  195. isWritable: true,
  196. isSigner: false,
  197. },
  198. ];
  199. }
  200. }