Armani Ferrante 4 роки тому
батько
коміт
06b40b7811

+ 1 - 1
ts/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@project-serum/anchor",
-  "version": "0.5.0",
+  "version": "0.5.1-beta.1",
   "description": "Anchor client",
   "main": "dist/cjs/index.js",
   "module": "dist/esm/index.js",

+ 1 - 1
ts/src/coder.ts

@@ -1,6 +1,6 @@
 import camelCase from "camelcase";
 import { snakeCase } from "snake-case";
-import { Layout, seq } from "buffer-layout";
+import { Layout } from "buffer-layout";
 import * as sha256 from "js-sha256";
 import * as borsh from "@project-serum/borsh";
 import {

+ 2 - 2
ts/src/index.ts

@@ -1,12 +1,12 @@
 import BN from "bn.js";
 import * as web3 from "@solana/web3.js";
 import Provider, { NodeWallet as Wallet } from "./provider";
-import { Program } from "./program";
 import Coder from "./coder";
 import { Idl } from "./idl";
 import workspace from "./workspace";
 import utils from "./utils";
-import { ProgramAccount } from "./rpc";
+import { Program } from "./program";
+import { ProgramAccount } from "./program/namespace";
 
 let _provider: Provider | null = null;
 

+ 0 - 269
ts/src/program.ts

@@ -1,269 +0,0 @@
-import { PublicKey } from "@solana/web3.js";
-import { inflate } from "pako";
-import Provider from "./provider";
-import { RpcFactory } from "./rpc";
-import { Idl, idlAddress, decodeIdlAccount } from "./idl";
-import Coder, { eventDiscriminator } from "./coder";
-import { Rpcs, Ixs, Txs, Accounts, State } from "./rpc";
-import { getProvider } from "./";
-import * as base64 from "base64-js";
-import * as assert from "assert";
-
-/**
- * Program is the IDL deserialized representation of a Solana program.
- */
-export class Program {
-  /**
-   * Address of the program.
-   */
-  readonly programId: PublicKey;
-
-  /**
-   * IDL describing this program's interface.
-   */
-  readonly idl: Idl;
-
-  /**
-   * Async functions to invoke instructions against a Solana priogram running
-   * on a cluster.
-   */
-  readonly rpc: Rpcs;
-
-  /**
-   * Async functions to fetch deserialized program accounts from a cluster.
-   */
-  readonly account: Accounts;
-
-  /**
-   * Functions to build `TransactionInstruction` objects.
-   */
-  readonly instruction: Ixs;
-
-  /**
-   * Functions to build `Transaction` objects.
-   */
-  readonly transaction: Txs;
-
-  /**
-   * Coder for serializing rpc requests.
-   */
-  readonly coder: Coder;
-
-  /**
-   * Object with state account accessors and rpcs.
-   */
-  readonly state: State;
-
-  /**
-   * Wallet and network provider.
-   */
-  readonly provider: Provider;
-
-  public constructor(idl: Idl, programId: PublicKey, provider?: Provider) {
-    this.idl = idl;
-    this.programId = programId;
-    this.provider = provider ?? getProvider();
-
-    // Build the serializer.
-    const coder = new Coder(idl);
-
-    // Build the dynamic RPC functions.
-    const [rpcs, ixs, txs, accounts, state] = RpcFactory.build(
-      idl,
-      coder,
-      programId,
-      this.provider
-    );
-    this.rpc = rpcs;
-    this.instruction = ixs;
-    this.transaction = txs;
-    this.account = accounts;
-    this.coder = coder;
-    this.state = state;
-  }
-
-  /**
-   * Generates a Program client by fetching the IDL from chain.
-   */
-  public static async at(programId: PublicKey, provider?: Provider) {
-    const idl = await Program.fetchIdl(programId, provider);
-    return new Program(idl, programId, provider);
-  }
-
-  /**
-   * Fetches an idl from the blockchain.
-   */
-  public static async fetchIdl(programId: PublicKey, provider?: Provider) {
-    provider = provider ?? getProvider();
-    const address = await idlAddress(programId);
-    const accountInfo = await provider.connection.getAccountInfo(address);
-    // Chop off account discriminator.
-    let idlAccount = decodeIdlAccount(accountInfo.data.slice(8));
-    const inflatedIdl = inflate(idlAccount.data);
-    return JSON.parse(decodeUtf8(inflatedIdl));
-  }
-
-  /**
-   * Invokes the given callback everytime the given event is emitted.
-   */
-  public addEventListener<T>(
-    eventName: string,
-    callback: (event: T, slot: number) => void
-  ): Promise<void> {
-    // Values shared across log handlers.
-    const thisProgramStr = this.programId.toString();
-    const discriminator = eventDiscriminator(eventName);
-    const logStartIndex = "Program log: ".length;
-
-    // Handles logs when the current program being executing is *not* this.
-    const handleSystemLog = (log: string): [string | null, boolean] => {
-      // System component.
-      const logStart = log.split(":")[0];
-      // Recursive call.
-      if (logStart.startsWith(`Program ${this.programId.toString()} invoke`)) {
-        return [this.programId.toString(), false];
-      }
-      // Cpi call.
-      else if (logStart.includes("invoke")) {
-        return ["cpi", false]; // Any string will do.
-      } else {
-        // Did the program finish executing?
-        if (logStart.match(/^Program (.*) consumed .*$/g) !== null) {
-          return [null, true];
-        }
-        return [null, false];
-      }
-    };
-
-    // Handles logs from *this* program.
-    const handleProgramLog = (
-      log: string
-    ): [T | null, string | null, boolean] => {
-      // This is a `msg!` log.
-      if (log.startsWith("Program log:")) {
-        const logStr = log.slice(logStartIndex);
-        const logArr = Buffer.from(base64.toByteArray(logStr));
-        const disc = logArr.slice(0, 8);
-        // Only deserialize if the discriminator implies a proper event.
-        let event = null;
-        if (disc.equals(discriminator)) {
-          event = this.coder.events.decode(eventName, logArr.slice(8));
-        }
-        return [event, null, false];
-      }
-      // System log.
-      else {
-        return [null, ...handleSystemLog(log)];
-      }
-    };
-
-    // Main log handler. Returns a three element array of the event, the
-    // next program that was invoked for CPI, and a boolean indicating if
-    // a program has completed execution (and thus should be popped off the
-    // execution stack).
-    const handleLog = (
-      execution: ExecutionContext,
-      log: string
-    ): [T | null, string | null, boolean] => {
-      // Executing program is this program.
-      if (execution.program() === thisProgramStr) {
-        return handleProgramLog(log);
-      }
-      // Executing program is not this program.
-      else {
-        return [null, ...handleSystemLog(log)];
-      }
-    };
-
-    // Each log given, represents an array of messages emitted by
-    // a single transaction, which can execute many different programs across
-    // CPI boundaries. However, the subscription is only interested in the
-    // events emitted by *this* program. In achieving this, we keep track of the
-    // program execution context by parsing each log and looking for a CPI
-    // `invoke` call. If one exists, we know a new program is executing. So we
-    // push the programId onto a stack and switch the program context. This
-    // allows us to track, for a given log, which program was executing during
-    // its emission, thereby allowing us to know if a given log event was
-    // emitted by *this* program. If it was, then we parse the raw string and
-    // emit the event if the string matches the event being subscribed to.
-    //
-    // @ts-ignore
-    return this.provider.connection.onLogs(this.programId, (logs, ctx) => {
-      if (logs.err) {
-        console.error(logs);
-        return;
-      }
-
-      const logScanner = new LogScanner(logs.logs);
-      const execution = new ExecutionContext(logScanner.next() as string);
-
-      let log = logScanner.next();
-      while (log !== null) {
-        let [event, newProgram, didPop] = handleLog(execution, log);
-        if (event) {
-          callback(event, ctx.slot);
-        }
-        if (newProgram) {
-          execution.push(newProgram);
-        }
-        if (didPop) {
-          execution.pop();
-        }
-        log = logScanner.next();
-      }
-    });
-  }
-
-  public async removeEventListener(listener: number): Promise<void> {
-    // @ts-ignore
-    return this.provider.connection.removeOnLogsListener(listener);
-  }
-}
-
-// Stack frame execution context, allowing one to track what program is
-// executing for a given log.
-class ExecutionContext {
-  stack: string[];
-
-  constructor(log: string) {
-    // Assumes the first log in every transaction is an `invoke` log from the
-    // runtime.
-    const program = /^Program (.*) invoke.*$/g.exec(log)[1];
-    this.stack = [program];
-  }
-
-  program(): string {
-    assert.ok(this.stack.length > 0);
-    return this.stack[this.stack.length - 1];
-  }
-
-  push(newProgram: string) {
-    this.stack.push(newProgram);
-  }
-
-  pop() {
-    assert.ok(this.stack.length > 0);
-    this.stack.pop();
-  }
-}
-
-class LogScanner {
-  constructor(public logs: string[]) {}
-
-  next(): string | null {
-    if (this.logs.length === 0) {
-      return null;
-    }
-    let l = this.logs[0];
-    this.logs = this.logs.slice(1);
-    return l;
-  }
-}
-
-function decodeUtf8(array: Uint8Array): string {
-  const decoder =
-    typeof TextDecoder === "undefined"
-      ? new (require("util").TextDecoder)("utf-8") // Node.
-      : new TextDecoder("utf-8"); // Browser.
-  return decoder.decode(array);
-}

+ 79 - 0
ts/src/program/common.ts

@@ -0,0 +1,79 @@
+import EventEmitter from "eventemitter3";
+import { Idl, IdlInstruction, IdlAccountItem, IdlStateMethod } from "../idl";
+import { ProgramError } from "../error";
+import { RpcAccounts } from "./context";
+
+export type Subscription = {
+  listener: number;
+  ee: EventEmitter;
+};
+
+export 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;
+}
+
+// Allow either IdLInstruction or IdlStateMethod since the types share fields.
+export 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.
+export 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.`);
+      }
+    }
+  });
+}
+
+export 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;
+    }
+  }
+}

+ 57 - 0
ts/src/program/context.ts

@@ -0,0 +1,57 @@
+import {
+  Account,
+  AccountMeta,
+  PublicKey,
+  ConfirmOptions,
+  TransactionInstruction,
+} from "@solana/web3.js";
+import { IdlInstruction } from "../idl";
+
+/**
+ * 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;
+  // All accounts to pass into an instruction *after* the main `accounts`.
+  remainingAccounts?: AccountMeta[];
+  // Accounts that must sign the transaction.
+  signers?: Array<Account>;
+  // Instructions to run *before* the specified rpc instruction.
+  instructions?: TransactionInstruction[];
+  // RpcOptions.
+  options?: RpcOptions;
+  // Private namespace for dev.
+  __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.
+ */
+export type RpcAccounts = {
+  [key: string]: PublicKey | RpcAccounts;
+};
+
+/**
+ * Options for an RPC invocation.
+ */
+export type RpcOptions = ConfirmOptions;
+
+export 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];
+}

+ 149 - 0
ts/src/program/event.ts

@@ -0,0 +1,149 @@
+import { PublicKey } from "@solana/web3.js";
+import * as base64 from "base64-js";
+import * as assert from "assert";
+import Coder, { eventDiscriminator } from "../coder";
+
+const LOG_START_INDEX = "Program log: ".length;
+
+export class EventParser<T> {
+  private coder: Coder;
+  private programId: PublicKey;
+  private eventName: string;
+  private discriminator: Buffer;
+
+  constructor(coder: Coder, programId: PublicKey, eventName: string) {
+    this.coder = coder;
+    this.programId = programId;
+    this.eventName = eventName;
+    this.discriminator = eventDiscriminator(eventName);
+  }
+
+  // Each log given, represents an array of messages emitted by
+  // a single transaction, which can execute many different programs across
+  // CPI boundaries. However, the subscription is only interested in the
+  // events emitted by *this* program. In achieving this, we keep track of the
+  // program execution context by parsing each log and looking for a CPI
+  // `invoke` call. If one exists, we know a new program is executing. So we
+  // push the programId onto a stack and switch the program context. This
+  // allows us to track, for a given log, which program was executing during
+  // its emission, thereby allowing us to know if a given log event was
+  // emitted by *this* program. If it was, then we parse the raw string and
+  // emit the event if the string matches the event being subscribed to.
+  public parseLogs(logs: string[], callback: (log: T) => void) {
+    const logScanner = new LogScanner(logs);
+    const execution = new ExecutionContext(logScanner.next() as string);
+
+    let log = logScanner.next();
+    while (log !== null) {
+      let [event, newProgram, didPop] = this.handleLog(execution, log);
+      if (event) {
+        callback(event);
+      }
+      if (newProgram) {
+        execution.push(newProgram);
+      }
+      if (didPop) {
+        execution.pop();
+      }
+      log = logScanner.next();
+    }
+  }
+
+  // Handles logs when the current program being executing is *not* this.
+  private handleSystemLog(log: string): [string | null, boolean] {
+    // System component.
+    const logStart = log.split(":")[0];
+    // Recursive call.
+    if (logStart.startsWith(`Program ${this.programId.toString()} invoke`)) {
+      return [this.programId.toString(), false];
+    }
+    // Cpi call.
+    else if (logStart.includes("invoke")) {
+      return ["cpi", false]; // Any string will do.
+    } else {
+      // Did the program finish executing?
+      if (logStart.match(/^Program (.*) consumed .*$/g) !== null) {
+        return [null, true];
+      }
+      return [null, false];
+    }
+  }
+
+  // Handles logs from *this* program.
+  private handleProgramLog(log: string): [T | null, string | null, boolean] {
+    // This is a `msg!` log.
+    if (log.startsWith("Program log:")) {
+      const logStr = log.slice(LOG_START_INDEX);
+      const logArr = Buffer.from(base64.toByteArray(logStr));
+      const disc = logArr.slice(0, 8);
+      // Only deserialize if the discriminator implies a proper event.
+      let event = null;
+      if (disc.equals(this.discriminator)) {
+        event = this.coder.events.decode(this.eventName, logArr.slice(8));
+      }
+      return [event, null, false];
+    }
+    // System log.
+    else {
+      return [null, ...this.handleSystemLog(log)];
+    }
+  }
+
+  // Main log handler. Returns a three element array of the event, the
+  // next program that was invoked for CPI, and a boolean indicating if
+  // a program has completed execution (and thus should be popped off the
+  // execution stack).
+  private handleLog(
+    execution: ExecutionContext,
+    log: string
+  ): [T | null, string | null, boolean] {
+    // Executing program is this program.
+    if (execution.program() === this.programId.toString()) {
+      return this.handleProgramLog(log);
+    }
+    // Executing program is not this program.
+    else {
+      return [null, ...this.handleSystemLog(log)];
+    }
+  }
+}
+
+// Stack frame execution context, allowing one to track what program is
+// executing for a given log.
+class ExecutionContext {
+  stack: string[];
+
+  constructor(log: string) {
+    // Assumes the first log in every transaction is an `invoke` log from the
+    // runtime.
+    const program = /^Program (.*) invoke.*$/g.exec(log)[1];
+    this.stack = [program];
+  }
+
+  program(): string {
+    assert.ok(this.stack.length > 0);
+    return this.stack[this.stack.length - 1];
+  }
+
+  push(newProgram: string) {
+    this.stack.push(newProgram);
+  }
+
+  pop() {
+    assert.ok(this.stack.length > 0);
+    this.stack.pop();
+  }
+}
+
+class LogScanner {
+  constructor(public logs: string[]) {}
+
+  next(): string | null {
+    if (this.logs.length === 0) {
+      return null;
+    }
+    let l = this.logs[0];
+    this.logs = this.logs.slice(1);
+    return l;
+  }
+}

+ 131 - 0
ts/src/program/index.ts

@@ -0,0 +1,131 @@
+import { inflate } from "pako";
+import { PublicKey } from "@solana/web3.js";
+import Provider from "../provider";
+import { Idl, idlAddress, decodeIdlAccount } from "../idl";
+import Coder from "../coder";
+import NamespaceFactory, { Rpcs, Ixs, Txs, Accounts, State } from "./namespace";
+import { getProvider } from "../";
+import { decodeUtf8 } from "../utils";
+import { EventParser } from "./event";
+
+/**
+ * Program is the IDL deserialized representation of a Solana program.
+ */
+export class Program {
+  /**
+   * Address of the program.
+   */
+  readonly programId: PublicKey;
+
+  /**
+   * IDL describing this program's interface.
+   */
+  readonly idl: Idl;
+
+  /**
+   * Async functions to invoke instructions against a Solana priogram running
+   * on a cluster.
+   */
+  readonly rpc: Rpcs;
+
+  /**
+   * Async functions to fetch deserialized program accounts from a cluster.
+   */
+  readonly account: Accounts;
+
+  /**
+   * Functions to build `TransactionInstruction` objects.
+   */
+  readonly instruction: Ixs;
+
+  /**
+   * Functions to build `Transaction` objects.
+   */
+  readonly transaction: Txs;
+
+  /**
+   * Coder for serializing rpc requests.
+   */
+  readonly coder: Coder;
+
+  /**
+   * Object with state account accessors and rpcs.
+   */
+  readonly state: State;
+
+  /**
+   * Wallet and network provider.
+   */
+  readonly provider: Provider;
+
+  public constructor(idl: Idl, programId: PublicKey, provider?: Provider) {
+    this.idl = idl;
+    this.programId = programId;
+    this.provider = provider ?? getProvider();
+
+    // Build the serializer.
+    const coder = new Coder(idl);
+
+    // Build the dynamic namespaces.
+    const [rpcs, ixs, txs, accounts, state] = NamespaceFactory.build(
+      idl,
+      coder,
+      programId,
+      this.provider
+    );
+    this.rpc = rpcs;
+    this.instruction = ixs;
+    this.transaction = txs;
+    this.account = accounts;
+    this.coder = coder;
+    this.state = state;
+  }
+
+  /**
+   * Generates a Program client by fetching the IDL from chain.
+   */
+  public static async at(programId: PublicKey, provider?: Provider) {
+    const idl = await Program.fetchIdl(programId, provider);
+    return new Program(idl, programId, provider);
+  }
+
+  /**
+   * Fetches an idl from the blockchain.
+   */
+  public static async fetchIdl(programId: PublicKey, provider?: Provider) {
+    provider = provider ?? getProvider();
+    const address = await idlAddress(programId);
+    const accountInfo = await provider.connection.getAccountInfo(address);
+    // Chop off account discriminator.
+    let idlAccount = decodeIdlAccount(accountInfo.data.slice(8));
+    const inflatedIdl = inflate(idlAccount.data);
+    return JSON.parse(decodeUtf8(inflatedIdl));
+  }
+
+  /**
+   * Invokes the given callback everytime the given event is emitted.
+   */
+  public addEventListener<T>(
+    eventName: string,
+    callback: (event: T, slot: number) => void
+  ): number {
+    const eventParser = new EventParser<T>(
+      this.coder,
+      this.programId,
+      eventName
+    );
+    return this.provider.connection.onLogs(this.programId, (logs, ctx) => {
+      if (logs.err) {
+        console.error(logs);
+        return;
+      }
+      eventParser.parseLogs(logs.logs, (event) => {
+        callback(event, ctx.slot);
+      });
+    });
+  }
+
+  public async removeEventListener(listener: number): Promise<void> {
+    return this.provider.connection.removeOnLogsListener(listener);
+  }
+}

+ 225 - 0
ts/src/program/namespace/account.ts

@@ -0,0 +1,225 @@
+import camelCase from "camelcase";
+import EventEmitter from "eventemitter3";
+import * as bs58 from "bs58";
+import {
+  Account,
+  PublicKey,
+  SystemProgram,
+  TransactionInstruction,
+  Commitment,
+} from "@solana/web3.js";
+import Provider from "../../provider";
+import { Idl } from "../../idl";
+import Coder, {
+  ACCOUNT_DISCRIMINATOR_SIZE,
+  accountDiscriminator,
+  accountSize,
+} from "../../coder";
+import { Subscription } from "../common";
+
+/**
+ * Accounts is a dynamically generated object to fetch any given account
+ * of a program.
+ */
+export interface Accounts {
+  [key: string]: AccountFn;
+}
+
+/**
+ * Account is a function returning a deserialized account, given an address.
+ */
+export type AccountFn<T = any> = AccountProps & ((address: PublicKey) => 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>;
+};
+
+/**
+ * Deserialized account owned by a program.
+ */
+export type ProgramAccount<T = any> = {
+  publicKey: PublicKey;
+  account: T;
+};
+
+// Tracks all subscriptions.
+const subscriptions: Map<string, Subscription> = new Map();
+
+export default class AccountNamespace {
+  // Returns the generated accounts namespace.
+  public static build(
+    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;
+  }
+}

+ 63 - 0
ts/src/program/namespace/index.ts

@@ -0,0 +1,63 @@
+import camelCase from "camelcase";
+import { PublicKey } from "@solana/web3.js";
+import Coder from "../../coder";
+import Provider from "../../provider";
+import { Idl } from "../../idl";
+import { parseIdlErrors } from "../common";
+import StateNamespace, { State } from "./state";
+import InstructionNamespace, { Ixs } from "./instruction";
+import TransactionNamespace, { Txs } from "./transaction";
+import RpcNamespace, { Rpcs } from "./rpc";
+import AccountNamespace, { Accounts } from "./account";
+
+// Re-exports.
+export { State } from "./state";
+export { Ixs } from "./instruction";
+export { Txs, TxFn } from "./transaction";
+export { Rpcs, RpcFn } from "./rpc";
+export { Accounts, AccountFn, ProgramAccount } from "./account";
+
+export default class NamespaceFactory {
+  /**
+   * 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 = StateNamespace.build(
+      idl,
+      coder,
+      programId,
+      idlErrors,
+      provider
+    );
+
+    idl.instructions.forEach((idlIx) => {
+      const ix = InstructionNamespace.build(idlIx, coder, programId);
+      const tx = TransactionNamespace.build(idlIx, ix);
+      const rpc = RpcNamespace.build(idlIx, tx, idlErrors, provider);
+
+      const name = camelCase(idlIx.name);
+
+      ixFns[name] = ix;
+      txFns[name] = tx;
+      rpcs[name] = rpc;
+    });
+
+    const accountFns = idl.accounts
+      ? AccountNamespace.build(idl, coder, programId, provider)
+      : {};
+
+    return [rpcs, ixFns, txFns, accountFns, state];
+  }
+}

+ 100 - 0
ts/src/program/namespace/instruction.ts

@@ -0,0 +1,100 @@
+import { PublicKey, TransactionInstruction } from "@solana/web3.js";
+import { IdlAccount, IdlInstruction, IdlAccountItem } from "../../idl";
+import { IdlError } from "../../error";
+import Coder from "../../coder";
+import { toInstruction, validateAccounts } from "../common";
+import { RpcAccounts, splitArgsAndCtx } from "../context";
+
+/**
+ * Dynamically generated instruction namespace.
+ */
+export interface Ixs {
+  [key: string]: IxFn;
+}
+
+/**
+ * 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;
+};
+
+export default class InstructionNamespace {
+  // Builds the instuction namespace.
+  public static build(
+    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 = InstructionNamespace.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 InstructionNamespace.accountsArray(accs, idlIx.accounts);
+    };
+
+    return ix;
+  }
+
+  public 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 InstructionNamespace.accountsArray(
+            rpcAccs,
+            nestedAccounts
+          ).flat();
+        } else {
+          const account: IdlAccount = acc as IdlAccount;
+          return {
+            pubkey: ctx[acc.name],
+            isWritable: account.isMut,
+            isSigner: account.isSigner,
+          };
+        }
+      })
+      .flat();
+  }
+}
+
+// Throws error if any argument required for the `ix` is not given.
+function validateInstruction(ix: IdlInstruction, ...args: any[]) {
+  // todo
+}

+ 46 - 0
ts/src/program/namespace/rpc.ts

@@ -0,0 +1,46 @@
+import { TransactionSignature } from "@solana/web3.js";
+import Provider from "../../provider";
+import { IdlInstruction } from "../../idl";
+import { translateError } from "../common";
+import { splitArgsAndCtx } from "../context";
+import { TxFn } from "./transaction";
+
+/**
+ * Dynamically generated rpc namespace.
+ */
+export interface Rpcs {
+  [key: string]: RpcFn;
+}
+
+/**
+ * RpcFn is a single rpc method generated from an IDL.
+ */
+export type RpcFn = (...args: any[]) => Promise<TransactionSignature>;
+
+export default class RpcNamespace {
+  // Builds the rpc namespace.
+  public static build(
+    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;
+  }
+}

+ 223 - 0
ts/src/program/namespace/state.ts

@@ -0,0 +1,223 @@
+import EventEmitter from "eventemitter3";
+import {
+  PublicKey,
+  SystemProgram,
+  Transaction,
+  TransactionSignature,
+  TransactionInstruction,
+  SYSVAR_RENT_PUBKEY,
+  Commitment,
+} from "@solana/web3.js";
+import Provider from "../../provider";
+import { Idl, IdlStateMethod } from "../../idl";
+import Coder, { stateDiscriminator } from "../../coder";
+import { Rpcs, Ixs } from "./";
+import {
+  Subscription,
+  translateError,
+  toInstruction,
+  validateAccounts,
+} from "../common";
+import { RpcAccounts, splitArgsAndCtx } from "../context";
+import InstructionNamespace from "./instruction";
+
+export type State = () =>
+  | Promise<any>
+  | {
+      address: () => Promise<PublicKey>;
+      rpc: Rpcs;
+      instruction: Ixs;
+      subscribe: (address: PublicKey, commitment?: Commitment) => EventEmitter;
+      unsubscribe: (address: PublicKey) => void;
+    };
+
+export default class StateNamespace {
+  // Builds the state namespace.
+  public static build(
+    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(
+          InstructionNamespace.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;
+  }
+}
+
+// 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,
+      },
+    ];
+  }
+}

+ 33 - 0
ts/src/program/namespace/transaction.ts

@@ -0,0 +1,33 @@
+import { Transaction } from "@solana/web3.js";
+import { IdlInstruction } from "../../idl";
+import { splitArgsAndCtx } from "../context";
+import { IxFn } from "./instruction";
+
+/**
+ * Dynamically generated transaction namespace.
+ */
+export interface Txs {
+  [key: string]: TxFn;
+}
+
+/**
+ * Tx is a function to create a `Transaction` generate from an IDL.
+ */
+export type TxFn = (...args: any[]) => Transaction;
+
+export default class TransactionNamespace {
+  // Builds the transaction namespace.
+  public static build(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;
+  }
+}

+ 0 - 748
ts/src/rpc.ts

@@ -1,748 +0,0 @@
-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,
-      },
-    ];
-  }
-}

+ 8 - 0
ts/src/utils.ts

@@ -69,6 +69,14 @@ async function getMultipleAccounts(
   });
 }
 
+export function decodeUtf8(array: Uint8Array): string {
+  const decoder =
+    typeof TextDecoder === "undefined"
+      ? new (require("util").TextDecoder)("utf-8") // Node.
+      : new TextDecoder("utf-8"); // Browser.
+  return decoder.decode(array);
+}
+
 const utils = {
   bs58,
   sha256,