state.ts 7.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283
  1. import EventEmitter from "eventemitter3";
  2. import camelCase from "camelcase";
  3. import {
  4. PublicKey,
  5. SystemProgram,
  6. Commitment,
  7. AccountMeta,
  8. } from "@solana/web3.js";
  9. import Provider, { getProvider } from "../../provider.js";
  10. import { Idl, IdlInstruction, IdlStateMethod, IdlTypeDef } from "../../idl.js";
  11. import { BorshCoder, Coder } from "../../coder/index.js";
  12. import {
  13. RpcNamespace,
  14. InstructionNamespace,
  15. TransactionNamespace,
  16. } from "./index.js";
  17. import { Subscription, validateAccounts, parseIdlErrors } from "../common.js";
  18. import {
  19. findProgramAddressSync,
  20. createWithSeedSync,
  21. } from "../../utils/pubkey.js";
  22. import { Accounts } from "../context.js";
  23. import InstructionNamespaceFactory from "./instruction.js";
  24. import RpcNamespaceFactory from "./rpc.js";
  25. import TransactionNamespaceFactory from "./transaction.js";
  26. import { IdlTypes, TypeDef } from "./types.js";
  27. export default class StateFactory {
  28. public static build<IDL extends Idl>(
  29. idl: IDL,
  30. coder: Coder,
  31. programId: PublicKey,
  32. provider?: Provider
  33. ): StateClient<IDL> | undefined {
  34. if (idl.state === undefined) {
  35. return undefined;
  36. }
  37. return new StateClient(idl, programId, provider, coder as BorshCoder);
  38. }
  39. }
  40. type NullableMethods<IDL extends Idl> = IDL["state"] extends undefined
  41. ? IdlInstruction[]
  42. : NonNullable<IDL["state"]>["methods"];
  43. /**
  44. * A client for the program state. Similar to the base [[Program]] client,
  45. * one can use this to send transactions and read accounts for the state
  46. * abstraction.
  47. */
  48. export class StateClient<IDL extends Idl> {
  49. /**
  50. * [[RpcNamespace]] for all state methods.
  51. */
  52. readonly rpc: RpcNamespace<IDL, NullableMethods<IDL>[number]>;
  53. /**
  54. * [[InstructionNamespace]] for all state methods.
  55. */
  56. readonly instruction: InstructionNamespace<IDL, NullableMethods<IDL>[number]>;
  57. /**
  58. * [[TransactionNamespace]] for all state methods.
  59. */
  60. readonly transaction: TransactionNamespace<IDL, NullableMethods<IDL>[number]>;
  61. /**
  62. * Returns the program ID owning the state.
  63. */
  64. get programId(): PublicKey {
  65. return this._programId;
  66. }
  67. private _programId: PublicKey;
  68. private _address: PublicKey;
  69. private _idl: IDL;
  70. private _sub: Subscription | null;
  71. constructor(
  72. idl: IDL,
  73. programId: PublicKey,
  74. /**
  75. * Returns the client's wallet and network provider.
  76. */
  77. public readonly provider: Provider = getProvider(),
  78. /**
  79. * Returns the coder. Note that we use BorshCoder and not `Coder` because
  80. * the deprecated state abstraction only applies to Anchor programs.
  81. */
  82. public readonly coder: BorshCoder = new BorshCoder(idl)
  83. ) {
  84. this._idl = idl;
  85. this._programId = programId;
  86. this._address = programStateAddress(programId);
  87. this._sub = null;
  88. // Build namespaces.
  89. const [instruction, transaction, rpc] = ((): [
  90. InstructionNamespace<IDL, NullableMethods<IDL>[number]>,
  91. TransactionNamespace<IDL, NullableMethods<IDL>[number]>,
  92. RpcNamespace<IDL, NullableMethods<IDL>[number]>
  93. ] => {
  94. let instruction: InstructionNamespace = {};
  95. let transaction: TransactionNamespace = {};
  96. let rpc: RpcNamespace = {};
  97. idl.state?.methods.forEach(
  98. <I extends NullableMethods<IDL>[number]>(m: I) => {
  99. // Build instruction method.
  100. const ixItem = InstructionNamespaceFactory.build<IDL, I>(
  101. m,
  102. (ixName, ix) => coder.instruction.encodeState(ixName, ix),
  103. programId
  104. );
  105. ixItem["accounts"] = (accounts) => {
  106. const keys = stateInstructionKeys(programId, provider, m, accounts);
  107. return keys.concat(
  108. InstructionNamespaceFactory.accountsArray(
  109. accounts,
  110. m.accounts,
  111. m.name
  112. )
  113. );
  114. };
  115. // Build transaction method.
  116. const txItem = TransactionNamespaceFactory.build(m, ixItem);
  117. // Build RPC method.
  118. const rpcItem = RpcNamespaceFactory.build(
  119. m,
  120. txItem,
  121. parseIdlErrors(idl),
  122. provider
  123. );
  124. // Attach them all to their respective namespaces.
  125. const name = camelCase(m.name);
  126. instruction[name] = ixItem;
  127. transaction[name] = txItem;
  128. rpc[name] = rpcItem;
  129. }
  130. );
  131. return [
  132. instruction as InstructionNamespace<IDL, NullableMethods<IDL>[number]>,
  133. transaction as TransactionNamespace<IDL, NullableMethods<IDL>[number]>,
  134. rpc as RpcNamespace<IDL, NullableMethods<IDL>[number]>,
  135. ];
  136. })();
  137. this.instruction = instruction;
  138. this.transaction = transaction;
  139. this.rpc = rpc;
  140. }
  141. /**
  142. * Returns the deserialized state account.
  143. */
  144. async fetch(): Promise<
  145. TypeDef<
  146. IDL["state"] extends undefined
  147. ? IdlTypeDef
  148. : NonNullable<IDL["state"]>["struct"],
  149. IdlTypes<IDL>
  150. >
  151. > {
  152. const addr = this.address();
  153. const accountInfo = await this.provider.connection.getAccountInfo(addr);
  154. if (accountInfo === null) {
  155. throw new Error(`Account does not exist ${addr.toString()}`);
  156. }
  157. // Assert the account discriminator is correct.
  158. const state = this._idl.state;
  159. if (!state) {
  160. throw new Error("State is not specified in IDL.");
  161. }
  162. const expectedDiscriminator = await this.coder.state.discriminator(state.struct.name);
  163. const discriminator = this.coder.state.header.parseDiscriminator(
  164. accountInfo.data
  165. );
  166. if (discriminator.compare(expectedDiscriminator)) {
  167. throw new Error("Invalid state discriminator");
  168. }
  169. return this.coder.state.decode(accountInfo.data);
  170. }
  171. /**
  172. * Returns the state address.
  173. */
  174. address(): PublicKey {
  175. return this._address;
  176. }
  177. /**
  178. * Returns an `EventEmitter` with a `"change"` event that's fired whenever
  179. * the state account cahnges.
  180. */
  181. subscribe(commitment?: Commitment): EventEmitter {
  182. if (this._sub !== null) {
  183. return this._sub.ee;
  184. }
  185. const ee = new EventEmitter();
  186. const listener = this.provider.connection.onAccountChange(
  187. this.address(),
  188. (acc) => {
  189. const account = this.coder.state.decode(acc.data);
  190. ee.emit("change", account);
  191. },
  192. commitment
  193. );
  194. this._sub = {
  195. ee,
  196. listener,
  197. };
  198. return ee;
  199. }
  200. /**
  201. * Unsubscribes to state changes.
  202. */
  203. unsubscribe() {
  204. if (this._sub !== null) {
  205. this.provider.connection
  206. .removeAccountChangeListener(this._sub.listener)
  207. .then(async () => {
  208. this._sub = null;
  209. })
  210. .catch(console.error);
  211. }
  212. }
  213. }
  214. // Calculates the deterministic address of the program's "state" account.
  215. function programStateAddress(programId: PublicKey): PublicKey {
  216. let [registrySigner] = findProgramAddressSync([], programId);
  217. return createWithSeedSync(registrySigner, "unversioned", programId);
  218. }
  219. // Returns the common keys that are prepended to all instructions targeting
  220. // the "state" of a program.
  221. function stateInstructionKeys<M extends IdlStateMethod>(
  222. programId: PublicKey,
  223. provider: Provider,
  224. m: M,
  225. accounts: Accounts<M["accounts"][number]>
  226. ): AccountMeta[] {
  227. if (m.name === "new") {
  228. // Ctor `new` method.
  229. const [programSigner] = findProgramAddressSync([], programId);
  230. return [
  231. {
  232. pubkey: provider.wallet.publicKey,
  233. isWritable: false,
  234. isSigner: true,
  235. },
  236. {
  237. pubkey: programStateAddress(programId),
  238. isWritable: true,
  239. isSigner: false,
  240. },
  241. { pubkey: programSigner, isWritable: false, isSigner: false },
  242. {
  243. pubkey: SystemProgram.programId,
  244. isWritable: false,
  245. isSigner: false,
  246. },
  247. { pubkey: programId, isWritable: false, isSigner: false },
  248. ];
  249. } else {
  250. validateAccounts(m.accounts, accounts);
  251. return [
  252. {
  253. pubkey: programStateAddress(programId),
  254. isWritable: true,
  255. isSigner: false,
  256. },
  257. ];
  258. }
  259. }