state.ts 7.7 KB

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