rpc.ts 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724
  1. import camelCase from "camelcase";
  2. import EventEmitter from "eventemitter3";
  3. import * as bs58 from "bs58";
  4. import {
  5. Account,
  6. AccountMeta,
  7. PublicKey,
  8. ConfirmOptions,
  9. SystemProgram,
  10. Transaction,
  11. TransactionSignature,
  12. TransactionInstruction,
  13. SYSVAR_RENT_PUBKEY,
  14. Commitment,
  15. } from "@solana/web3.js";
  16. import Provider from "./provider";
  17. import {
  18. Idl,
  19. IdlAccount,
  20. IdlInstruction,
  21. IdlAccountItem,
  22. IdlStateMethod,
  23. } from "./idl";
  24. import { IdlError, ProgramError } from "./error";
  25. import Coder, {
  26. ACCOUNT_DISCRIMINATOR_SIZE,
  27. accountDiscriminator,
  28. stateDiscriminator,
  29. accountSize,
  30. } from "./coder";
  31. /**
  32. * Dynamically generated rpc namespace.
  33. */
  34. export interface Rpcs {
  35. [key: string]: RpcFn;
  36. }
  37. /**
  38. * Dynamically generated instruction namespace.
  39. */
  40. export interface Ixs {
  41. [key: string]: IxFn;
  42. }
  43. /**
  44. * Dynamically generated transaction namespace.
  45. */
  46. export interface Txs {
  47. [key: string]: TxFn;
  48. }
  49. /**
  50. * Accounts is a dynamically generated object to fetch any given account
  51. * of a program.
  52. */
  53. export interface Accounts {
  54. [key: string]: AccountFn;
  55. }
  56. /**
  57. * RpcFn is a single rpc method generated from an IDL.
  58. */
  59. export type RpcFn = (...args: any[]) => Promise<TransactionSignature>;
  60. /**
  61. * Ix is a function to create a `TransactionInstruction` generated from an IDL.
  62. */
  63. export type IxFn = IxProps & ((...args: any[]) => any);
  64. type IxProps = {
  65. accounts: (ctx: RpcAccounts) => any;
  66. };
  67. /**
  68. * Tx is a function to create a `Transaction` generate from an IDL.
  69. */
  70. export type TxFn = (...args: any[]) => Transaction;
  71. /**
  72. * Account is a function returning a deserialized account, given an address.
  73. */
  74. export type AccountFn<T = any> = AccountProps & ((address: PublicKey) => T);
  75. /**
  76. * Deserialized account owned by a program.
  77. */
  78. export type ProgramAccount<T = any> = {
  79. publicKey: PublicKey;
  80. account: T;
  81. };
  82. /**
  83. * Non function properties on the acccount namespace.
  84. */
  85. type AccountProps = {
  86. size: number;
  87. all: (filter?: Buffer) => Promise<ProgramAccount<any>[]>;
  88. subscribe: (address: PublicKey, commitment?: Commitment) => EventEmitter;
  89. unsubscribe: (address: PublicKey) => void;
  90. createInstruction: (account: Account) => Promise<TransactionInstruction>;
  91. };
  92. /**
  93. * Options for an RPC invocation.
  94. */
  95. export type RpcOptions = ConfirmOptions;
  96. /**
  97. * RpcContext provides all arguments for an RPC/IX invocation that are not
  98. * covered by the instruction enum.
  99. */
  100. type RpcContext = {
  101. // Accounts the instruction will use.
  102. accounts?: RpcAccounts;
  103. remainingAccounts?: AccountMeta[];
  104. // Instructions to run *before* the specified rpc instruction.
  105. instructions?: TransactionInstruction[];
  106. // Accounts that must sign the transaction.
  107. signers?: Array<Account>;
  108. // RpcOptions.
  109. options?: RpcOptions;
  110. __private?: { logAccounts: boolean };
  111. };
  112. /**
  113. * Dynamic object representing a set of accounts given to an rpc/ix invocation.
  114. * The name of each key should match the name for that account in the IDL.
  115. */
  116. type RpcAccounts = {
  117. [key: string]: PublicKey | RpcAccounts;
  118. };
  119. export type State = () =>
  120. | Promise<any>
  121. | {
  122. address: () => Promise<PublicKey>;
  123. rpc: Rpcs;
  124. instruction: Ixs;
  125. subscribe: (address: PublicKey, commitment?: Commitment) => EventEmitter;
  126. unsubscribe: (address: PublicKey) => void;
  127. };
  128. // Tracks all subscriptions.
  129. const subscriptions: Map<string, Subscription> = new Map();
  130. /**
  131. * RpcFactory builds an Rpcs object for a given IDL.
  132. */
  133. export class RpcFactory {
  134. /**
  135. * build dynamically generates RPC methods.
  136. *
  137. * @returns an object with all the RPC methods attached.
  138. */
  139. public static build(
  140. idl: Idl,
  141. coder: Coder,
  142. programId: PublicKey,
  143. provider: Provider
  144. ): [Rpcs, Ixs, Txs, Accounts, State] {
  145. const idlErrors = parseIdlErrors(idl);
  146. const rpcs: Rpcs = {};
  147. const ixFns: Ixs = {};
  148. const txFns: Txs = {};
  149. const state = RpcFactory.buildState(
  150. idl,
  151. coder,
  152. programId,
  153. idlErrors,
  154. provider
  155. );
  156. idl.instructions.forEach((idlIx) => {
  157. const name = camelCase(idlIx.name);
  158. // Function to create a raw `TransactionInstruction`.
  159. const ix = RpcFactory.buildIx(idlIx, coder, programId);
  160. // Ffnction to create a `Transaction`.
  161. const tx = RpcFactory.buildTx(idlIx, ix);
  162. // Function to invoke an RPC against a cluster.
  163. const rpc = RpcFactory.buildRpc(idlIx, tx, idlErrors, provider);
  164. rpcs[name] = rpc;
  165. ixFns[name] = ix;
  166. txFns[name] = tx;
  167. });
  168. const accountFns = idl.accounts
  169. ? RpcFactory.buildAccounts(idl, coder, programId, provider)
  170. : {};
  171. return [rpcs, ixFns, txFns, accountFns, state];
  172. }
  173. // Builds the state namespace.
  174. private static buildState(
  175. idl: Idl,
  176. coder: Coder,
  177. programId: PublicKey,
  178. idlErrors: Map<number, string>,
  179. provider: Provider
  180. ): State | undefined {
  181. if (idl.state === undefined) {
  182. return undefined;
  183. }
  184. // Fetches the state object from the blockchain.
  185. const state = async (): Promise<any> => {
  186. const addr = await programStateAddress(programId);
  187. const accountInfo = await provider.connection.getAccountInfo(addr);
  188. if (accountInfo === null) {
  189. throw new Error(`Account does not exist ${addr.toString()}`);
  190. }
  191. // Assert the account discriminator is correct.
  192. const expectedDiscriminator = await stateDiscriminator(
  193. idl.state.struct.name
  194. );
  195. if (expectedDiscriminator.compare(accountInfo.data.slice(0, 8))) {
  196. throw new Error("Invalid account discriminator");
  197. }
  198. return coder.state.decode(accountInfo.data);
  199. };
  200. // Namespace with all rpc functions.
  201. const rpc: Rpcs = {};
  202. const ix: Ixs = {};
  203. idl.state.methods.forEach((m: IdlStateMethod) => {
  204. const accounts = async (accounts: RpcAccounts): Promise<any> => {
  205. const keys = await stateInstructionKeys(
  206. programId,
  207. provider,
  208. m,
  209. accounts
  210. );
  211. return keys.concat(RpcFactory.accountsArray(accounts, m.accounts));
  212. };
  213. const ixFn = async (...args: any[]): Promise<TransactionInstruction> => {
  214. const [ixArgs, ctx] = splitArgsAndCtx(m, [...args]);
  215. return new TransactionInstruction({
  216. keys: await accounts(ctx.accounts),
  217. programId,
  218. data: coder.instruction.encodeState(
  219. m.name,
  220. toInstruction(m, ...ixArgs)
  221. ),
  222. });
  223. };
  224. ixFn["accounts"] = accounts;
  225. ix[m.name] = ixFn;
  226. rpc[m.name] = async (...args: any[]): Promise<TransactionSignature> => {
  227. const [_, ctx] = splitArgsAndCtx(m, [...args]);
  228. const tx = new Transaction();
  229. if (ctx.instructions !== undefined) {
  230. tx.add(...ctx.instructions);
  231. }
  232. tx.add(await ix[m.name](...args));
  233. try {
  234. const txSig = await provider.send(tx, ctx.signers, ctx.options);
  235. return txSig;
  236. } catch (err) {
  237. let translatedErr = translateError(idlErrors, err);
  238. if (translatedErr === null) {
  239. throw err;
  240. }
  241. throw translatedErr;
  242. }
  243. };
  244. });
  245. state["rpc"] = rpc;
  246. state["instruction"] = ix;
  247. // Calculates the address of the program's global state object account.
  248. state["address"] = async (): Promise<PublicKey> =>
  249. programStateAddress(programId);
  250. // Subscription singleton.
  251. let sub: null | Subscription = null;
  252. // Subscribe to account changes.
  253. state["subscribe"] = (commitment?: Commitment): EventEmitter => {
  254. if (sub !== null) {
  255. return sub.ee;
  256. }
  257. const ee = new EventEmitter();
  258. state["address"]().then((address) => {
  259. const listener = provider.connection.onAccountChange(
  260. address,
  261. (acc) => {
  262. const account = coder.state.decode(acc.data);
  263. ee.emit("change", account);
  264. },
  265. commitment
  266. );
  267. sub = {
  268. ee,
  269. listener,
  270. };
  271. });
  272. return ee;
  273. };
  274. // Unsubscribe from account changes.
  275. state["unsubscribe"] = () => {
  276. if (sub !== null) {
  277. provider.connection
  278. .removeAccountChangeListener(sub.listener)
  279. .then(async () => {
  280. sub = null;
  281. })
  282. .catch(console.error);
  283. }
  284. };
  285. return state;
  286. }
  287. // Builds the instuction namespace.
  288. private static buildIx(
  289. idlIx: IdlInstruction,
  290. coder: Coder,
  291. programId: PublicKey
  292. ): IxFn {
  293. if (idlIx.name === "_inner") {
  294. throw new IdlError("the _inner name is reserved");
  295. }
  296. const ix = (...args: any[]): TransactionInstruction => {
  297. const [ixArgs, ctx] = splitArgsAndCtx(idlIx, [...args]);
  298. validateAccounts(idlIx.accounts, ctx.accounts);
  299. validateInstruction(idlIx, ...args);
  300. const keys = RpcFactory.accountsArray(ctx.accounts, idlIx.accounts);
  301. if (ctx.remainingAccounts !== undefined) {
  302. keys.push(...ctx.remainingAccounts);
  303. }
  304. if (ctx.__private && ctx.__private.logAccounts) {
  305. console.log("Outgoing account metas:", keys);
  306. }
  307. return new TransactionInstruction({
  308. keys,
  309. programId,
  310. data: coder.instruction.encode(
  311. idlIx.name,
  312. toInstruction(idlIx, ...ixArgs)
  313. ),
  314. });
  315. };
  316. // Utility fn for ordering the accounts for this instruction.
  317. ix["accounts"] = (accs: RpcAccounts) => {
  318. return RpcFactory.accountsArray(accs, idlIx.accounts);
  319. };
  320. return ix;
  321. }
  322. private static accountsArray(
  323. ctx: RpcAccounts,
  324. accounts: IdlAccountItem[]
  325. ): any {
  326. return accounts
  327. .map((acc: IdlAccountItem) => {
  328. // Nested accounts.
  329. // @ts-ignore
  330. const nestedAccounts: IdlAccountItem[] | undefined = acc.accounts;
  331. if (nestedAccounts !== undefined) {
  332. const rpcAccs = ctx[acc.name] as RpcAccounts;
  333. return RpcFactory.accountsArray(rpcAccs, nestedAccounts).flat();
  334. } else {
  335. const account: IdlAccount = acc as IdlAccount;
  336. return {
  337. pubkey: ctx[acc.name],
  338. isWritable: account.isMut,
  339. isSigner: account.isSigner,
  340. };
  341. }
  342. })
  343. .flat();
  344. }
  345. // Builds the rpc namespace.
  346. private static buildRpc(
  347. idlIx: IdlInstruction,
  348. txFn: TxFn,
  349. idlErrors: Map<number, string>,
  350. provider: Provider
  351. ): RpcFn {
  352. const rpc = async (...args: any[]): Promise<TransactionSignature> => {
  353. const tx = txFn(...args);
  354. const [_, ctx] = splitArgsAndCtx(idlIx, [...args]);
  355. try {
  356. const txSig = await provider.send(tx, ctx.signers, ctx.options);
  357. return txSig;
  358. } catch (err) {
  359. console.log("Translating error", err);
  360. let translatedErr = translateError(idlErrors, err);
  361. if (translatedErr === null) {
  362. throw err;
  363. }
  364. throw translatedErr;
  365. }
  366. };
  367. return rpc;
  368. }
  369. // Builds the transaction namespace.
  370. private static buildTx(idlIx: IdlInstruction, ixFn: IxFn): TxFn {
  371. const txFn = (...args: any[]): Transaction => {
  372. const [_, ctx] = splitArgsAndCtx(idlIx, [...args]);
  373. const tx = new Transaction();
  374. if (ctx.instructions !== undefined) {
  375. tx.add(...ctx.instructions);
  376. }
  377. tx.add(ixFn(...args));
  378. return tx;
  379. };
  380. return txFn;
  381. }
  382. // Returns the generated accounts namespace.
  383. private static buildAccounts(
  384. idl: Idl,
  385. coder: Coder,
  386. programId: PublicKey,
  387. provider: Provider
  388. ): Accounts {
  389. const accountFns: Accounts = {};
  390. idl.accounts.forEach((idlAccount) => {
  391. const name = camelCase(idlAccount.name);
  392. // Fetches the decoded account from the network.
  393. const accountsNamespace = async (address: PublicKey): Promise<any> => {
  394. const accountInfo = await provider.connection.getAccountInfo(address);
  395. if (accountInfo === null) {
  396. throw new Error(`Account does not exist ${address.toString()}`);
  397. }
  398. // Assert the account discriminator is correct.
  399. const discriminator = await accountDiscriminator(idlAccount.name);
  400. if (discriminator.compare(accountInfo.data.slice(0, 8))) {
  401. throw new Error("Invalid account discriminator");
  402. }
  403. return coder.accounts.decode(idlAccount.name, accountInfo.data);
  404. };
  405. // Returns the size of the account.
  406. // @ts-ignore
  407. accountsNamespace["size"] =
  408. ACCOUNT_DISCRIMINATOR_SIZE + accountSize(idl, idlAccount);
  409. // Returns an instruction for creating this account.
  410. // @ts-ignore
  411. accountsNamespace["createInstruction"] = async (
  412. account: Account,
  413. sizeOverride?: number
  414. ): Promise<TransactionInstruction> => {
  415. // @ts-ignore
  416. const size = accountsNamespace["size"];
  417. return SystemProgram.createAccount({
  418. fromPubkey: provider.wallet.publicKey,
  419. newAccountPubkey: account.publicKey,
  420. space: sizeOverride ?? size,
  421. lamports: await provider.connection.getMinimumBalanceForRentExemption(
  422. sizeOverride ?? size
  423. ),
  424. programId,
  425. });
  426. };
  427. // Subscribes to all changes to this account.
  428. // @ts-ignore
  429. accountsNamespace["subscribe"] = (
  430. address: PublicKey,
  431. commitment?: Commitment
  432. ): EventEmitter => {
  433. if (subscriptions.get(address.toString())) {
  434. return subscriptions.get(address.toString()).ee;
  435. }
  436. const ee = new EventEmitter();
  437. const listener = provider.connection.onAccountChange(
  438. address,
  439. (acc) => {
  440. const account = coder.accounts.decode(idlAccount.name, acc.data);
  441. ee.emit("change", account);
  442. },
  443. commitment
  444. );
  445. subscriptions.set(address.toString(), {
  446. ee,
  447. listener,
  448. });
  449. return ee;
  450. };
  451. // Unsubscribes to account changes.
  452. // @ts-ignore
  453. accountsNamespace["unsubscribe"] = (address: PublicKey) => {
  454. let sub = subscriptions.get(address.toString());
  455. if (!sub) {
  456. console.warn("Address is not subscribed");
  457. return;
  458. }
  459. if (subscriptions) {
  460. provider.connection
  461. .removeAccountChangeListener(sub.listener)
  462. .then(() => {
  463. subscriptions.delete(address.toString());
  464. })
  465. .catch(console.error);
  466. }
  467. };
  468. // Returns all instances of this account type for the program.
  469. // @ts-ignore
  470. accountsNamespace["all"] = async (
  471. filter?: Buffer
  472. ): Promise<ProgramAccount<any>[]> => {
  473. let bytes = await accountDiscriminator(idlAccount.name);
  474. if (filter !== undefined) {
  475. bytes = Buffer.concat([bytes, filter]);
  476. }
  477. // @ts-ignore
  478. let resp = await provider.connection._rpcRequest("getProgramAccounts", [
  479. programId.toBase58(),
  480. {
  481. commitment: provider.connection.commitment,
  482. filters: [
  483. {
  484. memcmp: {
  485. offset: 0,
  486. bytes: bs58.encode(bytes),
  487. },
  488. },
  489. ],
  490. },
  491. ]);
  492. if (resp.error) {
  493. console.error(resp);
  494. throw new Error("Failed to get accounts");
  495. }
  496. return (
  497. resp.result
  498. // @ts-ignore
  499. .map(({ pubkey, account: { data } }) => {
  500. data = bs58.decode(data);
  501. return {
  502. publicKey: new PublicKey(pubkey),
  503. account: coder.accounts.decode(idlAccount.name, data),
  504. };
  505. })
  506. );
  507. };
  508. accountFns[name] = accountsNamespace;
  509. });
  510. return accountFns;
  511. }
  512. }
  513. type Subscription = {
  514. listener: number;
  515. ee: EventEmitter;
  516. };
  517. function translateError(
  518. idlErrors: Map<number, string>,
  519. err: any
  520. ): Error | null {
  521. // TODO: don't rely on the error string. web3.js should preserve the error
  522. // code information instead of giving us an untyped string.
  523. let components = err.toString().split("custom program error: ");
  524. if (components.length === 2) {
  525. try {
  526. const errorCode = parseInt(components[1]);
  527. let errorMsg = idlErrors.get(errorCode);
  528. if (errorMsg === undefined) {
  529. // Unexpected error code so just throw the untranslated error.
  530. return null;
  531. }
  532. return new ProgramError(errorCode, errorMsg);
  533. } catch (parseErr) {
  534. // Unable to parse the error. Just return the untranslated error.
  535. return null;
  536. }
  537. }
  538. }
  539. function parseIdlErrors(idl: Idl): Map<number, string> {
  540. const errors = new Map();
  541. if (idl.errors) {
  542. idl.errors.forEach((e) => {
  543. let msg = e.msg ?? e.name;
  544. errors.set(e.code, msg);
  545. });
  546. }
  547. return errors;
  548. }
  549. function splitArgsAndCtx(
  550. idlIx: IdlInstruction,
  551. args: any[]
  552. ): [any[], RpcContext] {
  553. let options = {};
  554. const inputLen = idlIx.args ? idlIx.args.length : 0;
  555. if (args.length > inputLen) {
  556. if (args.length !== inputLen + 1) {
  557. throw new Error("provided too many arguments ${args}");
  558. }
  559. options = args.pop();
  560. }
  561. return [args, options];
  562. }
  563. // Allow either IdLInstruction or IdlStateMethod since the types share fields.
  564. function toInstruction(idlIx: IdlInstruction | IdlStateMethod, ...args: any[]) {
  565. if (idlIx.args.length != args.length) {
  566. throw new Error("Invalid argument length");
  567. }
  568. const ix: { [key: string]: any } = {};
  569. let idx = 0;
  570. idlIx.args.forEach((ixArg) => {
  571. ix[ixArg.name] = args[idx];
  572. idx += 1;
  573. });
  574. return ix;
  575. }
  576. // Throws error if any account required for the `ix` is not given.
  577. function validateAccounts(ixAccounts: IdlAccountItem[], accounts: RpcAccounts) {
  578. ixAccounts.forEach((acc) => {
  579. // @ts-ignore
  580. if (acc.accounts !== undefined) {
  581. // @ts-ignore
  582. validateAccounts(acc.accounts, accounts[acc.name]);
  583. } else {
  584. if (accounts[acc.name] === undefined) {
  585. throw new Error(`Invalid arguments: ${acc.name} not provided.`);
  586. }
  587. }
  588. });
  589. }
  590. // Throws error if any argument required for the `ix` is not given.
  591. function validateInstruction(ix: IdlInstruction, ...args: any[]) {
  592. // todo
  593. }
  594. // Calculates the deterministic address of the program's "state" account.
  595. async function programStateAddress(programId: PublicKey): Promise<PublicKey> {
  596. let [registrySigner, _nonce] = await PublicKey.findProgramAddress(
  597. [],
  598. programId
  599. );
  600. return PublicKey.createWithSeed(registrySigner, "unversioned", programId);
  601. }
  602. // Returns the common keys that are prepended to all instructions targeting
  603. // the "state" of a program.
  604. async function stateInstructionKeys(
  605. programId: PublicKey,
  606. provider: Provider,
  607. m: IdlStateMethod,
  608. accounts: RpcAccounts
  609. ) {
  610. if (m.name === "new") {
  611. // Ctor `new` method.
  612. const [programSigner, _nonce] = await PublicKey.findProgramAddress(
  613. [],
  614. programId
  615. );
  616. return [
  617. {
  618. pubkey: provider.wallet.publicKey,
  619. isWritable: false,
  620. isSigner: true,
  621. },
  622. {
  623. pubkey: await programStateAddress(programId),
  624. isWritable: true,
  625. isSigner: false,
  626. },
  627. { pubkey: programSigner, isWritable: false, isSigner: false },
  628. {
  629. pubkey: SystemProgram.programId,
  630. isWritable: false,
  631. isSigner: false,
  632. },
  633. { pubkey: programId, isWritable: false, isSigner: false },
  634. {
  635. pubkey: SYSVAR_RENT_PUBKEY,
  636. isWritable: false,
  637. isSigner: false,
  638. },
  639. ];
  640. } else {
  641. validateAccounts(m.accounts, accounts);
  642. return [
  643. {
  644. pubkey: await programStateAddress(programId),
  645. isWritable: true,
  646. isSigner: false,
  647. },
  648. ];
  649. }
  650. }