123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748 |
- import camelCase from "camelcase";
- import EventEmitter from "eventemitter3";
- import * as bs58 from "bs58";
- import {
- Account,
- AccountMeta,
- PublicKey,
- ConfirmOptions,
- SystemProgram,
- Transaction,
- TransactionSignature,
- TransactionInstruction,
- SYSVAR_RENT_PUBKEY,
- Commitment,
- } from "@solana/web3.js";
- import Provider from "./provider";
- import {
- Idl,
- IdlAccount,
- IdlInstruction,
- IdlAccountItem,
- IdlStateMethod,
- } from "./idl";
- import { IdlError, ProgramError } from "./error";
- import Coder, {
- ACCOUNT_DISCRIMINATOR_SIZE,
- accountDiscriminator,
- stateDiscriminator,
- accountSize,
- } from "./coder";
- /**
- * Dynamically generated rpc namespace.
- */
- export interface Rpcs {
- [key: string]: RpcFn;
- }
- /**
- * Dynamically generated instruction namespace.
- */
- export interface Ixs {
- [key: string]: IxFn;
- }
- /**
- * Dynamically generated transaction namespace.
- */
- export interface Txs {
- [key: string]: TxFn;
- }
- /**
- * Accounts is a dynamically generated object to fetch any given account
- * of a program.
- */
- export interface Accounts {
- [key: string]: AccountFn;
- }
- /**
- * RpcFn is a single rpc method generated from an IDL.
- */
- export type RpcFn = (...args: any[]) => Promise<TransactionSignature>;
- /**
- * Ix is a function to create a `TransactionInstruction` generated from an IDL.
- */
- export type IxFn = IxProps & ((...args: any[]) => any);
- type IxProps = {
- accounts: (ctx: RpcAccounts) => any;
- };
- /**
- * Tx is a function to create a `Transaction` generate from an IDL.
- */
- export type TxFn = (...args: any[]) => Transaction;
- /**
- * Account is a function returning a deserialized account, given an address.
- */
- export type AccountFn<T = any> = AccountProps & ((address: PublicKey) => T);
- /**
- * Deserialized account owned by a program.
- */
- export type ProgramAccount<T = any> = {
- publicKey: PublicKey;
- account: T;
- };
- /**
- * Non function properties on the acccount namespace.
- */
- type AccountProps = {
- size: number;
- all: (filter?: Buffer) => Promise<ProgramAccount<any>[]>;
- subscribe: (address: PublicKey, commitment?: Commitment) => EventEmitter;
- unsubscribe: (address: PublicKey) => void;
- createInstruction: (account: Account) => Promise<TransactionInstruction>;
- associated: (...args: PublicKey[]) => Promise<any>;
- associatedAddress: (...args: PublicKey[]) => Promise<PublicKey>;
- };
- /**
- * Options for an RPC invocation.
- */
- export type RpcOptions = ConfirmOptions;
- /**
- * RpcContext provides all arguments for an RPC/IX invocation that are not
- * covered by the instruction enum.
- */
- type RpcContext = {
- // Accounts the instruction will use.
- accounts?: RpcAccounts;
- remainingAccounts?: AccountMeta[];
- // Instructions to run *before* the specified rpc instruction.
- instructions?: TransactionInstruction[];
- // Accounts that must sign the transaction.
- signers?: Array<Account>;
- // RpcOptions.
- options?: RpcOptions;
- __private?: { logAccounts: boolean };
- };
- /**
- * Dynamic object representing a set of accounts given to an rpc/ix invocation.
- * The name of each key should match the name for that account in the IDL.
- */
- type RpcAccounts = {
- [key: string]: PublicKey | RpcAccounts;
- };
- export type State = () =>
- | Promise<any>
- | {
- address: () => Promise<PublicKey>;
- rpc: Rpcs;
- instruction: Ixs;
- subscribe: (address: PublicKey, commitment?: Commitment) => EventEmitter;
- unsubscribe: (address: PublicKey) => void;
- };
- // Tracks all subscriptions.
- const subscriptions: Map<string, Subscription> = new Map();
- /**
- * RpcFactory builds an Rpcs object for a given IDL.
- */
- export class RpcFactory {
- /**
- * build dynamically generates RPC methods.
- *
- * @returns an object with all the RPC methods attached.
- */
- public static build(
- idl: Idl,
- coder: Coder,
- programId: PublicKey,
- provider: Provider
- ): [Rpcs, Ixs, Txs, Accounts, State] {
- const idlErrors = parseIdlErrors(idl);
- const rpcs: Rpcs = {};
- const ixFns: Ixs = {};
- const txFns: Txs = {};
- const state = RpcFactory.buildState(
- idl,
- coder,
- programId,
- idlErrors,
- provider
- );
- idl.instructions.forEach((idlIx) => {
- const name = camelCase(idlIx.name);
- // Function to create a raw `TransactionInstruction`.
- const ix = RpcFactory.buildIx(idlIx, coder, programId);
- // Ffnction to create a `Transaction`.
- const tx = RpcFactory.buildTx(idlIx, ix);
- // Function to invoke an RPC against a cluster.
- const rpc = RpcFactory.buildRpc(idlIx, tx, idlErrors, provider);
- rpcs[name] = rpc;
- ixFns[name] = ix;
- txFns[name] = tx;
- });
- const accountFns = idl.accounts
- ? RpcFactory.buildAccounts(idl, coder, programId, provider)
- : {};
- return [rpcs, ixFns, txFns, accountFns, state];
- }
- // Builds the state namespace.
- private static buildState(
- idl: Idl,
- coder: Coder,
- programId: PublicKey,
- idlErrors: Map<number, string>,
- provider: Provider
- ): State | undefined {
- if (idl.state === undefined) {
- return undefined;
- }
- // Fetches the state object from the blockchain.
- const state = async (): Promise<any> => {
- const addr = await programStateAddress(programId);
- const accountInfo = await provider.connection.getAccountInfo(addr);
- if (accountInfo === null) {
- throw new Error(`Account does not exist ${addr.toString()}`);
- }
- // Assert the account discriminator is correct.
- const expectedDiscriminator = await stateDiscriminator(
- idl.state.struct.name
- );
- if (expectedDiscriminator.compare(accountInfo.data.slice(0, 8))) {
- throw new Error("Invalid account discriminator");
- }
- return coder.state.decode(accountInfo.data);
- };
- // Namespace with all rpc functions.
- const rpc: Rpcs = {};
- const ix: Ixs = {};
- idl.state.methods.forEach((m: IdlStateMethod) => {
- const accounts = async (accounts: RpcAccounts): Promise<any> => {
- const keys = await stateInstructionKeys(
- programId,
- provider,
- m,
- accounts
- );
- return keys.concat(RpcFactory.accountsArray(accounts, m.accounts));
- };
- const ixFn = async (...args: any[]): Promise<TransactionInstruction> => {
- const [ixArgs, ctx] = splitArgsAndCtx(m, [...args]);
- return new TransactionInstruction({
- keys: await accounts(ctx.accounts),
- programId,
- data: coder.instruction.encodeState(
- m.name,
- toInstruction(m, ...ixArgs)
- ),
- });
- };
- ixFn["accounts"] = accounts;
- ix[m.name] = ixFn;
- rpc[m.name] = async (...args: any[]): Promise<TransactionSignature> => {
- const [_, ctx] = splitArgsAndCtx(m, [...args]);
- const tx = new Transaction();
- if (ctx.instructions !== undefined) {
- tx.add(...ctx.instructions);
- }
- tx.add(await ix[m.name](...args));
- try {
- const txSig = await provider.send(tx, ctx.signers, ctx.options);
- return txSig;
- } catch (err) {
- let translatedErr = translateError(idlErrors, err);
- if (translatedErr === null) {
- throw err;
- }
- throw translatedErr;
- }
- };
- });
- state["rpc"] = rpc;
- state["instruction"] = ix;
- // Calculates the address of the program's global state object account.
- state["address"] = async (): Promise<PublicKey> =>
- programStateAddress(programId);
- // Subscription singleton.
- let sub: null | Subscription = null;
- // Subscribe to account changes.
- state["subscribe"] = (commitment?: Commitment): EventEmitter => {
- if (sub !== null) {
- return sub.ee;
- }
- const ee = new EventEmitter();
- state["address"]().then((address) => {
- const listener = provider.connection.onAccountChange(
- address,
- (acc) => {
- const account = coder.state.decode(acc.data);
- ee.emit("change", account);
- },
- commitment
- );
- sub = {
- ee,
- listener,
- };
- });
- return ee;
- };
- // Unsubscribe from account changes.
- state["unsubscribe"] = () => {
- if (sub !== null) {
- provider.connection
- .removeAccountChangeListener(sub.listener)
- .then(async () => {
- sub = null;
- })
- .catch(console.error);
- }
- };
- return state;
- }
- // Builds the instuction namespace.
- private static buildIx(
- idlIx: IdlInstruction,
- coder: Coder,
- programId: PublicKey
- ): IxFn {
- if (idlIx.name === "_inner") {
- throw new IdlError("the _inner name is reserved");
- }
- const ix = (...args: any[]): TransactionInstruction => {
- const [ixArgs, ctx] = splitArgsAndCtx(idlIx, [...args]);
- validateAccounts(idlIx.accounts, ctx.accounts);
- validateInstruction(idlIx, ...args);
- const keys = RpcFactory.accountsArray(ctx.accounts, idlIx.accounts);
- if (ctx.remainingAccounts !== undefined) {
- keys.push(...ctx.remainingAccounts);
- }
- if (ctx.__private && ctx.__private.logAccounts) {
- console.log("Outgoing account metas:", keys);
- }
- return new TransactionInstruction({
- keys,
- programId,
- data: coder.instruction.encode(
- idlIx.name,
- toInstruction(idlIx, ...ixArgs)
- ),
- });
- };
- // Utility fn for ordering the accounts for this instruction.
- ix["accounts"] = (accs: RpcAccounts) => {
- return RpcFactory.accountsArray(accs, idlIx.accounts);
- };
- return ix;
- }
- private static accountsArray(
- ctx: RpcAccounts,
- accounts: IdlAccountItem[]
- ): any {
- return accounts
- .map((acc: IdlAccountItem) => {
- // Nested accounts.
- // @ts-ignore
- const nestedAccounts: IdlAccountItem[] | undefined = acc.accounts;
- if (nestedAccounts !== undefined) {
- const rpcAccs = ctx[acc.name] as RpcAccounts;
- return RpcFactory.accountsArray(rpcAccs, nestedAccounts).flat();
- } else {
- const account: IdlAccount = acc as IdlAccount;
- return {
- pubkey: ctx[acc.name],
- isWritable: account.isMut,
- isSigner: account.isSigner,
- };
- }
- })
- .flat();
- }
- // Builds the rpc namespace.
- private static buildRpc(
- idlIx: IdlInstruction,
- txFn: TxFn,
- idlErrors: Map<number, string>,
- provider: Provider
- ): RpcFn {
- const rpc = async (...args: any[]): Promise<TransactionSignature> => {
- const tx = txFn(...args);
- const [_, ctx] = splitArgsAndCtx(idlIx, [...args]);
- try {
- const txSig = await provider.send(tx, ctx.signers, ctx.options);
- return txSig;
- } catch (err) {
- console.log("Translating error", err);
- let translatedErr = translateError(idlErrors, err);
- if (translatedErr === null) {
- throw err;
- }
- throw translatedErr;
- }
- };
- return rpc;
- }
- // Builds the transaction namespace.
- private static buildTx(idlIx: IdlInstruction, ixFn: IxFn): TxFn {
- const txFn = (...args: any[]): Transaction => {
- const [_, ctx] = splitArgsAndCtx(idlIx, [...args]);
- const tx = new Transaction();
- if (ctx.instructions !== undefined) {
- tx.add(...ctx.instructions);
- }
- tx.add(ixFn(...args));
- return tx;
- };
- return txFn;
- }
- // Returns the generated accounts namespace.
- private static buildAccounts(
- idl: Idl,
- coder: Coder,
- programId: PublicKey,
- provider: Provider
- ): Accounts {
- const accountFns: Accounts = {};
- idl.accounts.forEach((idlAccount) => {
- const name = camelCase(idlAccount.name);
- // Fetches the decoded account from the network.
- const accountsNamespace = async (address: PublicKey): Promise<any> => {
- const accountInfo = await provider.connection.getAccountInfo(address);
- if (accountInfo === null) {
- throw new Error(`Account does not exist ${address.toString()}`);
- }
- // Assert the account discriminator is correct.
- const discriminator = await accountDiscriminator(idlAccount.name);
- if (discriminator.compare(accountInfo.data.slice(0, 8))) {
- throw new Error("Invalid account discriminator");
- }
- return coder.accounts.decode(idlAccount.name, accountInfo.data);
- };
- // Returns the size of the account.
- // @ts-ignore
- accountsNamespace["size"] =
- ACCOUNT_DISCRIMINATOR_SIZE + accountSize(idl, idlAccount);
- // Returns an instruction for creating this account.
- // @ts-ignore
- accountsNamespace["createInstruction"] = async (
- account: Account,
- sizeOverride?: number
- ): Promise<TransactionInstruction> => {
- // @ts-ignore
- const size = accountsNamespace["size"];
- return SystemProgram.createAccount({
- fromPubkey: provider.wallet.publicKey,
- newAccountPubkey: account.publicKey,
- space: sizeOverride ?? size,
- lamports: await provider.connection.getMinimumBalanceForRentExemption(
- sizeOverride ?? size
- ),
- programId,
- });
- };
- // Subscribes to all changes to this account.
- // @ts-ignore
- accountsNamespace["subscribe"] = (
- address: PublicKey,
- commitment?: Commitment
- ): EventEmitter => {
- if (subscriptions.get(address.toString())) {
- return subscriptions.get(address.toString()).ee;
- }
- const ee = new EventEmitter();
- const listener = provider.connection.onAccountChange(
- address,
- (acc) => {
- const account = coder.accounts.decode(idlAccount.name, acc.data);
- ee.emit("change", account);
- },
- commitment
- );
- subscriptions.set(address.toString(), {
- ee,
- listener,
- });
- return ee;
- };
- // Unsubscribes to account changes.
- // @ts-ignore
- accountsNamespace["unsubscribe"] = (address: PublicKey) => {
- let sub = subscriptions.get(address.toString());
- if (!sub) {
- console.warn("Address is not subscribed");
- return;
- }
- if (subscriptions) {
- provider.connection
- .removeAccountChangeListener(sub.listener)
- .then(() => {
- subscriptions.delete(address.toString());
- })
- .catch(console.error);
- }
- };
- // Returns all instances of this account type for the program.
- // @ts-ignore
- accountsNamespace["all"] = async (
- filter?: Buffer
- ): Promise<ProgramAccount<any>[]> => {
- let bytes = await accountDiscriminator(idlAccount.name);
- if (filter !== undefined) {
- bytes = Buffer.concat([bytes, filter]);
- }
- // @ts-ignore
- let resp = await provider.connection._rpcRequest("getProgramAccounts", [
- programId.toBase58(),
- {
- commitment: provider.connection.commitment,
- filters: [
- {
- memcmp: {
- offset: 0,
- bytes: bs58.encode(bytes),
- },
- },
- ],
- },
- ]);
- if (resp.error) {
- console.error(resp);
- throw new Error("Failed to get accounts");
- }
- return (
- resp.result
- // @ts-ignore
- .map(({ pubkey, account: { data } }) => {
- data = bs58.decode(data);
- return {
- publicKey: new PublicKey(pubkey),
- account: coder.accounts.decode(idlAccount.name, data),
- };
- })
- );
- };
- // Function returning the associated address. Args are keys to associate.
- // Order matters.
- accountsNamespace["associatedAddress"] = async (
- ...args: PublicKey[]
- ): Promise<PublicKey> => {
- let seeds = [Buffer.from([97, 110, 99, 104, 111, 114])]; // b"anchor".
- args.forEach((arg) => {
- seeds.push(arg.toBuffer());
- });
- const [assoc] = await PublicKey.findProgramAddress(seeds, programId);
- return assoc;
- };
- // Function returning the associated account. Args are keys to associate.
- // Order matters.
- accountsNamespace["associated"] = async (
- ...args: PublicKey[]
- ): Promise<any> => {
- const addr = await accountsNamespace["associatedAddress"](...args);
- return await accountsNamespace(addr);
- };
- accountFns[name] = accountsNamespace;
- });
- return accountFns;
- }
- }
- type Subscription = {
- listener: number;
- ee: EventEmitter;
- };
- function translateError(
- idlErrors: Map<number, string>,
- err: any
- ): Error | null {
- // TODO: don't rely on the error string. web3.js should preserve the error
- // code information instead of giving us an untyped string.
- let components = err.toString().split("custom program error: ");
- if (components.length === 2) {
- try {
- const errorCode = parseInt(components[1]);
- let errorMsg = idlErrors.get(errorCode);
- if (errorMsg === undefined) {
- // Unexpected error code so just throw the untranslated error.
- return null;
- }
- return new ProgramError(errorCode, errorMsg);
- } catch (parseErr) {
- // Unable to parse the error. Just return the untranslated error.
- return null;
- }
- }
- }
- function parseIdlErrors(idl: Idl): Map<number, string> {
- const errors = new Map();
- if (idl.errors) {
- idl.errors.forEach((e) => {
- let msg = e.msg ?? e.name;
- errors.set(e.code, msg);
- });
- }
- return errors;
- }
- function splitArgsAndCtx(
- idlIx: IdlInstruction,
- args: any[]
- ): [any[], RpcContext] {
- let options = {};
- const inputLen = idlIx.args ? idlIx.args.length : 0;
- if (args.length > inputLen) {
- if (args.length !== inputLen + 1) {
- throw new Error("provided too many arguments ${args}");
- }
- options = args.pop();
- }
- return [args, options];
- }
- // Allow either IdLInstruction or IdlStateMethod since the types share fields.
- function toInstruction(idlIx: IdlInstruction | IdlStateMethod, ...args: any[]) {
- if (idlIx.args.length != args.length) {
- throw new Error("Invalid argument length");
- }
- const ix: { [key: string]: any } = {};
- let idx = 0;
- idlIx.args.forEach((ixArg) => {
- ix[ixArg.name] = args[idx];
- idx += 1;
- });
- return ix;
- }
- // Throws error if any account required for the `ix` is not given.
- function validateAccounts(ixAccounts: IdlAccountItem[], accounts: RpcAccounts) {
- ixAccounts.forEach((acc) => {
- // @ts-ignore
- if (acc.accounts !== undefined) {
- // @ts-ignore
- validateAccounts(acc.accounts, accounts[acc.name]);
- } else {
- if (accounts[acc.name] === undefined) {
- throw new Error(`Invalid arguments: ${acc.name} not provided.`);
- }
- }
- });
- }
- // Throws error if any argument required for the `ix` is not given.
- function validateInstruction(ix: IdlInstruction, ...args: any[]) {
- // todo
- }
- // Calculates the deterministic address of the program's "state" account.
- async function programStateAddress(programId: PublicKey): Promise<PublicKey> {
- let [registrySigner, _nonce] = await PublicKey.findProgramAddress(
- [],
- programId
- );
- return PublicKey.createWithSeed(registrySigner, "unversioned", programId);
- }
- // Returns the common keys that are prepended to all instructions targeting
- // the "state" of a program.
- async function stateInstructionKeys(
- programId: PublicKey,
- provider: Provider,
- m: IdlStateMethod,
- accounts: RpcAccounts
- ) {
- if (m.name === "new") {
- // Ctor `new` method.
- const [programSigner, _nonce] = await PublicKey.findProgramAddress(
- [],
- programId
- );
- return [
- {
- pubkey: provider.wallet.publicKey,
- isWritable: false,
- isSigner: true,
- },
- {
- pubkey: await programStateAddress(programId),
- isWritable: true,
- isSigner: false,
- },
- { pubkey: programSigner, isWritable: false, isSigner: false },
- {
- pubkey: SystemProgram.programId,
- isWritable: false,
- isSigner: false,
- },
- { pubkey: programId, isWritable: false, isSigner: false },
- {
- pubkey: SYSVAR_RENT_PUBKEY,
- isWritable: false,
- isSigner: false,
- },
- ];
- } else {
- validateAccounts(m.accounts, accounts);
- return [
- {
- pubkey: await programStateAddress(programId),
- isWritable: true,
- isSigner: false,
- },
- ];
- }
- }
|