rpc.ts 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483
  1. import camelCase from "camelcase";
  2. import {
  3. Account,
  4. AccountMeta,
  5. PublicKey,
  6. ConfirmOptions,
  7. SystemProgram,
  8. Transaction,
  9. TransactionSignature,
  10. TransactionInstruction,
  11. } from "@solana/web3.js";
  12. import { sha256 } from "crypto-hash";
  13. import {
  14. Idl,
  15. IdlAccount,
  16. IdlInstruction,
  17. IdlTypeDef,
  18. IdlType,
  19. IdlField,
  20. IdlEnumVariant,
  21. IdlAccountItem,
  22. } from "./idl";
  23. import { IdlError, ProgramError } from "./error";
  24. import Coder from "./coder";
  25. import { getProvider } from "./";
  26. /**
  27. * Number of bytes of the account discriminator.
  28. */
  29. const ACCOUNT_DISCRIMINATOR_SIZE = 8;
  30. /**
  31. * Rpcs is a dynamically generated object with rpc methods attached.
  32. */
  33. export interface Rpcs {
  34. [key: string]: RpcFn;
  35. }
  36. /**
  37. * Ixs is a dynamically generated object with ix functions attached.
  38. */
  39. export interface Ixs {
  40. [key: string]: IxFn;
  41. }
  42. export interface Txs {
  43. [key: string]: TxFn;
  44. }
  45. /**
  46. * Accounts is a dynamically generated object to fetch any given account
  47. * of a program.
  48. */
  49. export interface Accounts {
  50. [key: string]: AccountFn;
  51. }
  52. /**
  53. * RpcFn is a single rpc method generated from an IDL.
  54. */
  55. export type RpcFn = (...args: any[]) => Promise<TransactionSignature>;
  56. /**
  57. * Ix is a function to create a `TransactionInstruction` generated from an IDL.
  58. */
  59. export type IxFn = (...args: any[]) => TransactionInstruction;
  60. /**
  61. * Tx is a function to create a `Transaction` generate from an IDL.
  62. */
  63. export type TxFn = (...args: any[]) => Transaction;
  64. /**
  65. * Account is a function returning a deserialized account, given an address.
  66. */
  67. export type AccountFn<T = any> = (address: PublicKey) => T;
  68. /**
  69. * Options for an RPC invocation.
  70. */
  71. export type RpcOptions = ConfirmOptions;
  72. /**
  73. * RpcContext provides all arguments for an RPC/IX invocation that are not
  74. * covered by the instruction enum.
  75. */
  76. type RpcContext = {
  77. // Accounts the instruction will use.
  78. accounts?: RpcAccounts;
  79. remainingAccounts?: AccountMeta[];
  80. // Instructions to run *before* the specified rpc instruction.
  81. instructions?: TransactionInstruction[];
  82. // Accounts that must sign the transaction.
  83. signers?: Array<Account>;
  84. // RpcOptions.
  85. options?: RpcOptions;
  86. __private?: { logAccounts: boolean };
  87. };
  88. /**
  89. * Dynamic object representing a set of accounts given to an rpc/ix invocation.
  90. * The name of each key should match the name for that account in the IDL.
  91. */
  92. type RpcAccounts = {
  93. [key: string]: PublicKey | RpcAccounts;
  94. };
  95. /**
  96. * RpcFactory builds an Rpcs object for a given IDL.
  97. */
  98. export class RpcFactory {
  99. /**
  100. * build dynamically generates RPC methods.
  101. *
  102. * @returns an object with all the RPC methods attached.
  103. */
  104. public static build(
  105. idl: Idl,
  106. coder: Coder,
  107. programId: PublicKey
  108. ): [Rpcs, Ixs, Txs, Accounts] {
  109. const idlErrors = parseIdlErrors(idl);
  110. const rpcs: Rpcs = {};
  111. const ixFns: Ixs = {};
  112. const txFns: Txs = {};
  113. idl.instructions.forEach((idlIx) => {
  114. // Function to create a raw `TransactionInstruction`.
  115. const ix = RpcFactory.buildIx(idlIx, coder, programId);
  116. // Ffnction to create a `Transaction`.
  117. const tx = RpcFactory.buildTx(idlIx, ix);
  118. // Function to invoke an RPC against a cluster.
  119. const rpc = RpcFactory.buildRpc(idlIx, tx, idlErrors);
  120. const name = camelCase(idlIx.name);
  121. rpcs[name] = rpc;
  122. ixFns[name] = ix;
  123. txFns[name] = tx;
  124. });
  125. const accountFns = idl.accounts
  126. ? RpcFactory.buildAccounts(idl, coder, programId)
  127. : {};
  128. return [rpcs, ixFns, txFns, accountFns];
  129. }
  130. private static buildIx(
  131. idlIx: IdlInstruction,
  132. coder: Coder,
  133. programId: PublicKey
  134. ): IxFn {
  135. if (idlIx.name === "_inner") {
  136. throw new IdlError("the _inner name is reserved");
  137. }
  138. const ix = (...args: any[]): TransactionInstruction => {
  139. const [ixArgs, ctx] = splitArgsAndCtx(idlIx, [...args]);
  140. validateAccounts(idlIx.accounts, ctx.accounts);
  141. validateInstruction(idlIx, ...args);
  142. const keys = RpcFactory.accountsArray(ctx.accounts, idlIx.accounts);
  143. if (ctx.remainingAccounts !== undefined) {
  144. keys.push(...ctx.remainingAccounts);
  145. }
  146. if (ctx.__private && ctx.__private.logAccounts) {
  147. console.log("Outoing account metas:", keys);
  148. }
  149. return new TransactionInstruction({
  150. keys,
  151. programId,
  152. data: coder.instruction.encode(toInstruction(idlIx, ...ixArgs)),
  153. });
  154. };
  155. // Utility fn for ordering the accounts for this instruction.
  156. ix["accounts"] = (accs: RpcAccounts) => {
  157. return RpcFactory.accountsArray(accs, idlIx.accounts);
  158. };
  159. return ix;
  160. }
  161. private static accountsArray(
  162. ctx: RpcAccounts,
  163. accounts: IdlAccountItem[]
  164. ): any {
  165. return accounts
  166. .map((acc: IdlAccountItem) => {
  167. // Nested accounts.
  168. // @ts-ignore
  169. const nestedAccounts: IdlAccountItem[] | undefined = acc.accounts;
  170. if (nestedAccounts !== undefined) {
  171. const rpcAccs = ctx[acc.name] as RpcAccounts;
  172. return RpcFactory.accountsArray(rpcAccs, nestedAccounts).flat();
  173. } else {
  174. const account: IdlAccount = acc as IdlAccount;
  175. return {
  176. pubkey: ctx[acc.name],
  177. isWritable: account.isMut,
  178. isSigner: account.isSigner,
  179. };
  180. }
  181. })
  182. .flat();
  183. }
  184. private static buildRpc(
  185. idlIx: IdlInstruction,
  186. txFn: TxFn,
  187. idlErrors: Map<number, string>
  188. ): RpcFn {
  189. const rpc = async (...args: any[]): Promise<TransactionSignature> => {
  190. const tx = txFn(...args);
  191. const [_, ctx] = splitArgsAndCtx(idlIx, [...args]);
  192. const provider = getProvider();
  193. if (provider === null) {
  194. throw new Error("Provider not found");
  195. }
  196. try {
  197. const txSig = await provider.send(tx, ctx.signers, ctx.options);
  198. return txSig;
  199. } catch (err) {
  200. let translatedErr = translateError(idlErrors, err);
  201. if (translatedErr === null) {
  202. throw err;
  203. }
  204. throw translatedErr;
  205. }
  206. };
  207. return rpc;
  208. }
  209. private static buildTx(idlIx: IdlInstruction, ixFn: IxFn): TxFn {
  210. const txFn = (...args: any[]): Transaction => {
  211. const [_, ctx] = splitArgsAndCtx(idlIx, [...args]);
  212. const tx = new Transaction();
  213. if (ctx.instructions !== undefined) {
  214. tx.add(...ctx.instructions);
  215. }
  216. tx.add(ixFn(...args));
  217. return tx;
  218. };
  219. return txFn;
  220. }
  221. private static buildAccounts(
  222. idl: Idl,
  223. coder: Coder,
  224. programId: PublicKey
  225. ): Accounts {
  226. const accountFns: Accounts = {};
  227. idl.accounts.forEach((idlAccount) => {
  228. const accountFn = async (address: PublicKey): Promise<any> => {
  229. const provider = getProvider();
  230. if (provider === null) {
  231. throw new Error("Provider not set");
  232. }
  233. const accountInfo = await provider.connection.getAccountInfo(address);
  234. if (accountInfo === null) {
  235. throw new Error(`Entity does not exist ${address}`);
  236. }
  237. // Assert the account discriminator is correct.
  238. const expectedDiscriminator = Buffer.from(
  239. (
  240. await sha256(`account:${idlAccount.name}`, {
  241. outputFormat: "buffer",
  242. })
  243. ).slice(0, 8)
  244. );
  245. const discriminator = accountInfo.data.slice(0, 8);
  246. if (expectedDiscriminator.compare(discriminator)) {
  247. throw new Error("Invalid account discriminator");
  248. }
  249. // Chop off the discriminator before decoding.
  250. const data = accountInfo.data.slice(8);
  251. return coder.accounts.decode(idlAccount.name, data);
  252. };
  253. const name = camelCase(idlAccount.name);
  254. accountFns[name] = accountFn;
  255. const size = ACCOUNT_DISCRIMINATOR_SIZE + accountSize(idl, idlAccount);
  256. // @ts-ignore
  257. accountFns[name]["size"] = size;
  258. // @ts-ignore
  259. accountFns[name]["createInstruction"] = async (
  260. account: Account,
  261. sizeOverride?: number
  262. ): Promise<TransactionInstruction> => {
  263. const provider = getProvider();
  264. return SystemProgram.createAccount({
  265. fromPubkey: provider.wallet.publicKey,
  266. newAccountPubkey: account.publicKey,
  267. space: sizeOverride ?? size,
  268. lamports: await provider.connection.getMinimumBalanceForRentExemption(
  269. sizeOverride ?? size
  270. ),
  271. programId,
  272. });
  273. };
  274. });
  275. return accountFns;
  276. }
  277. }
  278. function translateError(
  279. idlErrors: Map<number, string>,
  280. err: any
  281. ): Error | null {
  282. // TODO: don't rely on the error string. web3.js should preserve the error
  283. // code information instead of giving us an untyped string.
  284. let components = err.toString().split("custom program error: ");
  285. if (components.length === 2) {
  286. try {
  287. const errorCode = parseInt(components[1]);
  288. let errorMsg = idlErrors.get(errorCode);
  289. if (errorMsg === undefined) {
  290. // Unexpected error code so just throw the untranslated error.
  291. return null;
  292. }
  293. return new ProgramError(errorCode, errorMsg);
  294. } catch (parseErr) {
  295. // Unable to parse the error. Just return the untranslated error.
  296. return null;
  297. }
  298. }
  299. }
  300. function parseIdlErrors(idl: Idl): Map<number, string> {
  301. const errors = new Map();
  302. if (idl.errors) {
  303. idl.errors.forEach((e) => {
  304. let msg = e.msg ?? e.name;
  305. errors.set(e.code, msg);
  306. });
  307. }
  308. return errors;
  309. }
  310. function splitArgsAndCtx(
  311. idlIx: IdlInstruction,
  312. args: any[]
  313. ): [any[], RpcContext] {
  314. let options = {};
  315. const inputLen = idlIx.args ? idlIx.args.length : 0;
  316. if (args.length > inputLen) {
  317. if (args.length !== inputLen + 1) {
  318. throw new Error("provided too many arguments ${args}");
  319. }
  320. options = args.pop();
  321. }
  322. return [args, options];
  323. }
  324. function toInstruction(idlIx: IdlInstruction, ...args: any[]) {
  325. if (idlIx.args.length != args.length) {
  326. throw new Error("Invalid argument length");
  327. }
  328. const ix: { [key: string]: any } = {};
  329. let idx = 0;
  330. idlIx.args.forEach((ixArg) => {
  331. ix[ixArg.name] = args[idx];
  332. idx += 1;
  333. });
  334. // JavaScript representation of the rust enum variant.
  335. const name = camelCase(idlIx.name);
  336. const ixVariant: { [key: string]: any } = {};
  337. ixVariant[name] = ix;
  338. return ixVariant;
  339. }
  340. // Throws error if any account required for the `ix` is not given.
  341. function validateAccounts(ixAccounts: IdlAccountItem[], accounts: RpcAccounts) {
  342. ixAccounts.forEach((acc) => {
  343. // @ts-ignore
  344. if (acc.accounts !== undefined) {
  345. // @ts-ignore
  346. validateAccounts(acc.accounts, accounts[acc.name]);
  347. } else {
  348. if (accounts[acc.name] === undefined) {
  349. throw new Error(`Invalid arguments: ${acc.name} not provided.`);
  350. }
  351. }
  352. });
  353. }
  354. // Throws error if any argument required for the `ix` is not given.
  355. function validateInstruction(ix: IdlInstruction, ...args: any[]) {
  356. // todo
  357. }
  358. function accountSize(idl: Idl, idlAccount: IdlTypeDef): number | undefined {
  359. if (idlAccount.type.kind === "enum") {
  360. let variantSizes = idlAccount.type.variants.map(
  361. (variant: IdlEnumVariant) => {
  362. if (variant.fields === undefined) {
  363. return 0;
  364. }
  365. // @ts-ignore
  366. return (
  367. variant.fields
  368. // @ts-ignore
  369. .map((f: IdlField | IdlType) => {
  370. // @ts-ignore
  371. if (f.name === undefined) {
  372. throw new Error("Tuple enum variants not yet implemented.");
  373. }
  374. // @ts-ignore
  375. return typeSize(idl, f.type);
  376. })
  377. .reduce((a: number, b: number) => a + b)
  378. );
  379. }
  380. );
  381. return Math.max(...variantSizes) + 1;
  382. }
  383. if (idlAccount.type.fields === undefined) {
  384. return 0;
  385. }
  386. return idlAccount.type.fields
  387. .map((f) => typeSize(idl, f.type))
  388. .reduce((a, b) => a + b);
  389. }
  390. // Returns the size of the type in bytes. For variable length types, just return
  391. // 1. Users should override this value in such cases.
  392. function typeSize(idl: Idl, ty: IdlType): number {
  393. switch (ty) {
  394. case "bool":
  395. return 1;
  396. case "u8":
  397. return 1;
  398. case "i8":
  399. return 1;
  400. case "u16":
  401. return 2;
  402. case "u32":
  403. return 4;
  404. case "u64":
  405. return 8;
  406. case "i64":
  407. return 8;
  408. case "bytes":
  409. return 1;
  410. case "string":
  411. return 1;
  412. case "publicKey":
  413. return 32;
  414. default:
  415. // @ts-ignore
  416. if (ty.vec !== undefined) {
  417. return 1;
  418. }
  419. // @ts-ignore
  420. if (ty.option !== undefined) {
  421. // @ts-ignore
  422. return 1 + typeSize(ty.option);
  423. }
  424. // @ts-ignore
  425. if (ty.defined !== undefined) {
  426. // @ts-ignore
  427. const filtered = idl.types.filter((t) => t.name === ty.defined);
  428. if (filtered.length !== 1) {
  429. throw new IdlError(`Type not found: ${JSON.stringify(ty)}`);
  430. }
  431. let typeDef = filtered[0];
  432. return accountSize(idl, typeDef);
  433. }
  434. throw new Error(`Invalid type ${JSON.stringify(ty)}`);
  435. }
  436. }