Browse Source

ts: Reorganize program namespaces into well typed elements (#322)

Armani Ferrante 4 years ago
parent
commit
2f780e0d27

+ 3 - 2
CHANGELOG.md

@@ -19,9 +19,10 @@ incremented for features.
 
 ## Breaking Changes
 
+* ts: Retrieving deserialized accounts from the `<program>.account.<my-account>` and `<program>.state` namespaces now require explicitly invoking the `fetch` API. For example, `program.account.myAccount(<adddress>)` and `program.state()` is now `program.account.myAccount.fetch(<address>)` and `program.state.fetch()` ([#322](https://github.com/project-serum/anchor/pull/322)).
 * lang: `#[account(associated)]` now requires `init` to be provided to create an associated account. If not provided, then the address will be assumed to exist, and a constraint will be added to ensure its correctness ([#318](https://github.com/project-serum/anchor/pull/318)).
-* lang, ts: Change account discriminator pre-image of the `#[state]` account discriminator to be namespaced by "state:". This change should only be noticed by library maintainers ([#320](https://github.com/project-serum/anchor/pull/320)).
-* lang, ts: Change domain delimiters for the pre-image of the instruciton sighash to be a single colon `:` to be consistent with accounts. This change should only be noticed by library maintainers.
+* lang, ts: Change account discriminator pre-image of the `#[state]` account discriminator to be namespaced by "state:" ([#320](https://github.com/project-serum/anchor/pull/320)).
+* lang, ts: Change domain delimiters for the pre-image of the instruciton sighash to be a single colon `:` to be consistent with accounts ([#321](https://github.com/project-serum/anchor/pull/321)).
 
 ## [0.6.0] - 2021-05-23
 

+ 2 - 2
examples/cashiers-check/tests/cashiers-check.js

@@ -63,7 +63,7 @@ describe("cashiers-check", () => {
       ],
     });
 
-    const checkAccount = await program.account.check(check.publicKey);
+    const checkAccount = await program.account.check.fetch(check.publicKey);
     assert.ok(checkAccount.from.equals(god));
     assert.ok(checkAccount.to.equals(receiver));
     assert.ok(checkAccount.amount.eq(new anchor.BN(100)));
@@ -91,7 +91,7 @@ describe("cashiers-check", () => {
       },
     });
 
-    const checkAccount = await program.account.check(check.publicKey);
+    const checkAccount = await program.account.check.fetch(check.publicKey);
     assert.ok(checkAccount.burned === true);
 
     let vaultAccount = await serumCmn.getTokenAccount(

+ 2 - 2
examples/chat/tests/chat.js

@@ -25,7 +25,7 @@ describe("chat", () => {
       signers: [chatRoom],
     });
 
-    const chat = await program.account.chatRoom(chatRoom.publicKey);
+    const chat = await program.account.chatRoom.fetch(chatRoom.publicKey);
     const name = new TextDecoder("utf-8").decode(new Uint8Array(chat.name));
     assert.ok(name.startsWith("Test Chat")); // [u8; 280] => trailing zeros.
     assert.ok(chat.messages.length === 33607);
@@ -76,7 +76,7 @@ describe("chat", () => {
     }
 
     // Check the chat room state is as expected.
-    const chat = await program.account.chatRoom(chatRoom.publicKey);
+    const chat = await program.account.chatRoom.fetch(chatRoom.publicKey);
     const name = new TextDecoder("utf-8").decode(new Uint8Array(chat.name));
     assert.ok(name.startsWith("Test Chat")); // [u8; 280] => trailing zeros.
     assert.ok(chat.messages.length === 33607);

+ 2 - 2
examples/composite/tests/composite.js

@@ -41,8 +41,8 @@ describe("composite", () => {
       }
     );
 
-    const dummyAAccount = await program.account.dummyA(dummyA.publicKey);
-    const dummyBAccount = await program.account.dummyB(dummyB.publicKey);
+    const dummyAAccount = await program.account.dummyA.fetch(dummyA.publicKey);
+    const dummyBAccount = await program.account.dummyB.fetch(dummyB.publicKey);
 
     assert.ok(dummyAAccount.data.eq(new anchor.BN(1234)));
     assert.ok(dummyBAccount.data.eq(new anchor.BN(4321)));

+ 2 - 2
examples/interface/tests/interface.js

@@ -10,7 +10,7 @@ describe("interface", () => {
   it("Is initialized!", async () => {
     await counter.state.rpc.new(counterAuth.programId);
 
-    const stateAccount = await counter.state();
+    const stateAccount = await counter.state.fetch();
     assert.ok(stateAccount.count.eq(new anchor.BN(0)));
     assert.ok(stateAccount.authProgram.equals(counterAuth.programId));
   });
@@ -39,7 +39,7 @@ describe("interface", () => {
         authProgram: counterAuth.programId,
       },
     });
-    const stateAccount = await counter.state();
+    const stateAccount = await counter.state.fetch();
     assert.ok(stateAccount.count.eq(new anchor.BN(3)));
   });
 });

+ 17 - 17
examples/lockup/tests/lockup.js

@@ -37,7 +37,7 @@ describe("Lockup and Registry", () => {
     });
 
     lockupAddress = await lockup.state.address();
-    const lockupAccount = await lockup.state();
+    const lockupAccount = await lockup.state.fetch();
 
     assert.ok(lockupAccount.authority.equals(provider.wallet.publicKey));
     assert.ok(lockupAccount.whitelist.length === WHITELIST_SIZE);
@@ -63,7 +63,7 @@ describe("Lockup and Registry", () => {
       },
     });
 
-    let lockupAccount = await lockup.state();
+    let lockupAccount = await lockup.state.fetch();
     assert.ok(lockupAccount.authority.equals(newAuthority.publicKey));
 
     await lockup.state.rpc.setAuthority(provider.wallet.publicKey, {
@@ -73,7 +73,7 @@ describe("Lockup and Registry", () => {
       signers: [newAuthority],
     });
 
-    lockupAccount = await lockup.state();
+    lockupAccount = await lockup.state.fetch();
     assert.ok(lockupAccount.authority.equals(provider.wallet.publicKey));
   });
 
@@ -97,7 +97,7 @@ describe("Lockup and Registry", () => {
 
     await lockup.state.rpc.whitelistAdd(entries[0], { accounts });
 
-    let lockupAccount = await lockup.state();
+    let lockupAccount = await lockup.state.fetch();
 
     assert.ok(lockupAccount.whitelist.length === 1);
     assert.deepEqual(lockupAccount.whitelist, [entries[0]]);
@@ -106,7 +106,7 @@ describe("Lockup and Registry", () => {
       await lockup.state.rpc.whitelistAdd(entries[k], { accounts });
     }
 
-    lockupAccount = await lockup.state();
+    lockupAccount = await lockup.state.fetch();
 
     assert.deepEqual(lockupAccount.whitelist, entries);
 
@@ -129,7 +129,7 @@ describe("Lockup and Registry", () => {
         authority: provider.wallet.publicKey,
       },
     });
-    let lockupAccount = await lockup.state();
+    let lockupAccount = await lockup.state.fetch();
     assert.deepEqual(lockupAccount.whitelist, entries.slice(1));
   });
 
@@ -185,7 +185,7 @@ describe("Lockup and Registry", () => {
       }
     );
 
-    vestingAccount = await lockup.account.vesting(vesting.publicKey);
+    vestingAccount = await lockup.account.vesting.fetch(vesting.publicKey);
 
     assert.ok(vestingAccount.beneficiary.equals(provider.wallet.publicKey));
     assert.ok(vestingAccount.mint.equals(mint));
@@ -246,7 +246,7 @@ describe("Lockup and Registry", () => {
       },
     });
 
-    vestingAccount = await lockup.account.vesting(vesting.publicKey);
+    vestingAccount = await lockup.account.vesting.fetch(vesting.publicKey);
     assert.ok(vestingAccount.outstanding.eq(new anchor.BN(0)));
 
     const vaultAccount = await serumCmn.getTokenAccount(
@@ -287,7 +287,7 @@ describe("Lockup and Registry", () => {
       accounts: { lockupProgram: lockup.programId },
     });
 
-    const state = await registry.state();
+    const state = await registry.state.fetch();
     assert.ok(state.lockupProgram.equals(lockup.programId));
 
     // Should not allow a second initializatoin.
@@ -324,7 +324,7 @@ describe("Lockup and Registry", () => {
       }
     );
 
-    registrarAccount = await registry.account.registrar(registrar.publicKey);
+    registrarAccount = await registry.account.registrar.fetch(registrar.publicKey);
 
     assert.ok(registrarAccount.authority.equals(provider.wallet.publicKey));
     assert.equal(registrarAccount.nonce, nonce);
@@ -385,7 +385,7 @@ describe("Lockup and Registry", () => {
 
     let txSigs = await provider.sendAll(allTxs);
 
-    memberAccount = await registry.account.member(member.publicKey);
+    memberAccount = await registry.account.member.fetch(member.publicKey);
 
     assert.ok(memberAccount.registrar.equals(registrar.publicKey));
     assert.ok(memberAccount.beneficiary.equals(provider.wallet.publicKey));
@@ -516,7 +516,7 @@ describe("Lockup and Registry", () => {
       }
     );
 
-    const vendorAccount = await registry.account.rewardVendor(
+    const vendorAccount = await registry.account.rewardVendor.fetch(
       unlockedVendor.publicKey
     );
 
@@ -531,7 +531,7 @@ describe("Lockup and Registry", () => {
     assert.ok(vendorAccount.rewardEventQCursor === 0);
     assert.deepEqual(vendorAccount.kind, rewardKind);
 
-    const rewardQAccount = await registry.account.rewardQueue(
+    const rewardQAccount = await registry.account.rewardQueue.fetch(
       rewardQ.publicKey
     );
     assert.ok(rewardQAccount.head === 1);
@@ -571,7 +571,7 @@ describe("Lockup and Registry", () => {
     let tokenAccount = await serumCmn.getTokenAccount(provider, token);
     assert.ok(tokenAccount.amount.eq(new anchor.BN(200)));
 
-    const memberAccount = await registry.account.member(member.publicKey);
+    const memberAccount = await registry.account.member.fetch(member.publicKey);
     assert.ok(memberAccount.rewardsCursor == 1);
   });
 
@@ -635,7 +635,7 @@ describe("Lockup and Registry", () => {
       }
     );
 
-    const vendorAccount = await registry.account.rewardVendor(
+    const vendorAccount = await registry.account.rewardVendor.fetch(
       lockedVendor.publicKey
     );
 
@@ -653,7 +653,7 @@ describe("Lockup and Registry", () => {
       JSON.stringify(lockedRewardKind)
     );
 
-    const rewardQAccount = await registry.account.rewardQueue(
+    const rewardQAccount = await registry.account.rewardQueue.fetch(
       rewardQ.publicKey
     );
     assert.ok(rewardQAccount.head === 2);
@@ -727,7 +727,7 @@ describe("Lockup and Registry", () => {
       ],
     });
 
-    const lockupAccount = await lockup.account.vesting(
+    const lockupAccount = await lockup.account.vesting.fetch(
       vendoredVesting.publicKey
     );
 

+ 9 - 9
examples/misc/tests/misc.js

@@ -11,7 +11,7 @@ describe("misc", () => {
   it("Can allocate extra space for a state constructor", async () => {
     const tx = await program.state.rpc.new();
     const addr = await program.state.address();
-    const state = await program.state();
+    const state = await program.state.fetch();
     const accountInfo = await program.provider.connection.getAccountInfo(addr);
     assert.ok(state.v.equals(Buffer.from([])));
     assert.ok(accountInfo.data.length === 99);
@@ -32,7 +32,7 @@ describe("misc", () => {
         instructions: [await program.account.data.createInstruction(data)],
       }
     );
-    const dataAccount = await program.account.data(data.publicKey);
+    const dataAccount = await program.account.data.fetch(data.publicKey);
     assert.ok(dataAccount.udata.eq(new anchor.BN(1234)));
     assert.ok(dataAccount.idata.eq(new anchor.BN(22)));
   });
@@ -47,7 +47,7 @@ describe("misc", () => {
       signers: [data],
       instructions: [await program.account.dataU16.createInstruction(data)],
     });
-    const dataAccount = await program.account.dataU16(data.publicKey);
+    const dataAccount = await program.account.dataU16.fetch(data.publicKey);
     assert.ok(dataAccount.data === 99);
   });
 
@@ -110,7 +110,7 @@ describe("misc", () => {
         authority: program.provider.wallet.publicKey,
       },
     });
-    let stateAccount = await misc2Program.state();
+    let stateAccount = await misc2Program.state.fetch();
     assert.ok(stateAccount.data.eq(oldData));
     assert.ok(stateAccount.auth.equals(program.provider.wallet.publicKey));
     const newData = new anchor.BN(2134);
@@ -121,7 +121,7 @@ describe("misc", () => {
         misc2Program: misc2Program.programId,
       },
     });
-    stateAccount = await misc2Program.state();
+    stateAccount = await misc2Program.state.fetch();
     assert.ok(stateAccount.data.eq(newData));
     assert.ok(stateAccount.auth.equals(program.provider.wallet.publicKey));
   });
@@ -145,7 +145,7 @@ describe("misc", () => {
     );
     await assert.rejects(
       async () => {
-        await program.account.testData(associatedAccount);
+        await program.account.testData.fetch(associatedAccount);
       },
       (err) => {
         assert.ok(
@@ -234,7 +234,7 @@ describe("misc", () => {
       instructions: [await program.account.dataI8.createInstruction(data)],
       signers: [data],
     });
-    const dataAccount = await program.account.dataI8(data.publicKey);
+    const dataAccount = await program.account.dataI8.fetch(data.publicKey);
     assert.ok(dataAccount.data === -3);
   });
 
@@ -250,14 +250,14 @@ describe("misc", () => {
       instructions: [await program.account.dataI16.createInstruction(data)],
       signers: [data],
     });
-    const dataAccount = await program.account.dataI16(data.publicKey);
+    const dataAccount = await program.account.dataI16.fetch(data.publicKey);
     assert.ok(dataAccount.data === -2048);
 
     dataPubkey = data.publicKey;
   });
 
   it("Can use base58 strings to fetch an account", async () => {
-    const dataAccount = await program.account.dataI16(dataPubkey.toString());
+    const dataAccount = await program.account.dataI16.fetch(dataPubkey.toString());
     assert.ok(dataAccount.data === -2048);
   });
 });

+ 3 - 3
examples/multisig/tests/multisig.js

@@ -39,7 +39,7 @@ describe("multisig", () => {
       signers: [multisig],
     });
 
-    let multisigAccount = await program.account.multisig(multisig.publicKey);
+    let multisigAccount = await program.account.multisig.fetch(multisig.publicKey);
 
     assert.equal(multisigAccount.nonce, nonce);
     assert.ok(multisigAccount.threshold.eq(new anchor.BN(2)));
@@ -81,7 +81,7 @@ describe("multisig", () => {
       signers: [transaction, ownerA],
     });
 
-    const txAccount = await program.account.transaction(transaction.publicKey);
+    const txAccount = await program.account.transaction.fetch(transaction.publicKey);
 
     assert.ok(txAccount.programId.equals(pid));
     assert.deepEqual(txAccount.accounts, accounts);
@@ -124,7 +124,7 @@ describe("multisig", () => {
         }),
     });
 
-    multisigAccount = await program.account.multisig(multisig.publicKey);
+    multisigAccount = await program.account.multisig.fetch(multisig.publicKey);
 
     assert.equal(multisigAccount.nonce, nonce);
     assert.ok(multisigAccount.threshold.eq(new anchor.BN(2)));

+ 4 - 4
examples/tutorial/basic-1/tests/basic-1.js

@@ -43,7 +43,7 @@ describe("basic-1", () => {
     // #endregion code-separated
 
     // Fetch the newly created account from the cluster.
-    const account = await program.account.myAccount(myAccount.publicKey);
+    const account = await program.account.myAccount.fetch(myAccount.publicKey);
 
     // Check it's state was initialized.
     assert.ok(account.data.eq(new anchor.BN(1234)));
@@ -81,7 +81,7 @@ describe("basic-1", () => {
     });
 
     // Fetch the newly created account from the cluster.
-    const account = await program.account.myAccount(myAccount.publicKey);
+    const account = await program.account.myAccount.fetch(myAccount.publicKey);
 
     // Check it's state was initialized.
     assert.ok(account.data.eq(new anchor.BN(1234)));
@@ -108,7 +108,7 @@ describe("basic-1", () => {
     // #endregion code-simplified
 
     // Fetch the newly created account from the cluster.
-    const account = await program.account.myAccount(myAccount.publicKey);
+    const account = await program.account.myAccount.fetch(myAccount.publicKey);
 
     // Check it's state was initialized.
     assert.ok(account.data.eq(new anchor.BN(1234)));
@@ -133,7 +133,7 @@ describe("basic-1", () => {
     });
 
     // Fetch the newly updated account.
-    const account = await program.account.myAccount(myAccount.publicKey);
+    const account = await program.account.myAccount.fetch(myAccount.publicKey);
 
     // Check it's state was mutated.
     assert.ok(account.data.eq(new anchor.BN(4321)));

+ 2 - 2
examples/tutorial/basic-2/tests/basic-2.js

@@ -23,7 +23,7 @@ describe('basic-2', () => {
       instructions: [await program.account.counter.createInstruction(counter)],
     })
 
-    let counterAccount = await program.account.counter(counter.publicKey)
+    let counterAccount = await program.account.counter.fetch(counter.publicKey)
 
     assert.ok(counterAccount.authority.equals(provider.wallet.publicKey))
     assert.ok(counterAccount.count.toNumber() === 0)
@@ -37,7 +37,7 @@ describe('basic-2', () => {
       },
     })
 
-    const counterAccount = await program.account.counter(counter.publicKey)
+    const counterAccount = await program.account.counter.fetch(counter.publicKey)
 
     assert.ok(counterAccount.authority.equals(provider.wallet.publicKey))
     assert.ok(counterAccount.count.toNumber() == 1)

+ 1 - 1
examples/tutorial/basic-3/tests/basic-3.js

@@ -41,7 +41,7 @@ describe("basic-3", () => {
     });
 
     // Check the state updated.
-    puppetAccount = await puppet.account.puppet(newPuppetAccount.publicKey);
+    puppetAccount = await puppet.account.puppet.fetch(newPuppetAccount.publicKey);
     assert.ok(puppetAccount.data.eq(new anchor.BN(111)));
   });
 });

+ 2 - 2
examples/tutorial/basic-4/tests/basic-4.js

@@ -21,7 +21,7 @@ describe("basic-4", () => {
 
     // Fetch the state struct from the network.
     // #region accessor
-    const state = await program.state();
+    const state = await program.state.fetch();
     // #endregion accessor
 
     assert.ok(state.count.eq(new anchor.BN(0)));
@@ -35,7 +35,7 @@ describe("basic-4", () => {
       },
     });
     // #endregion instruction
-    const state = await program.state();
+    const state = await program.state.fetch();
     assert.ok(state.count.eq(new anchor.BN(1)));
   });
 });

+ 9 - 9
examples/zero-copy/tests/zero-copy.js

@@ -17,7 +17,7 @@ describe("zero-copy", () => {
         authority: program.provider.wallet.publicKey,
       },
     });
-    const state = await program.state();
+    const state = await program.state.fetch();
     assert.ok(state.authority.equals(program.provider.wallet.publicKey));
     assert.ok(state.events.length === 250);
     state.events.forEach((event, idx) => {
@@ -36,7 +36,7 @@ describe("zero-copy", () => {
         authority: program.provider.wallet.publicKey,
       },
     });
-    const state = await program.state();
+    const state = await program.state.fetch();
     assert.ok(state.authority.equals(program.provider.wallet.publicKey));
     assert.ok(state.events.length === 250);
     state.events.forEach((event, idx) => {
@@ -60,7 +60,7 @@ describe("zero-copy", () => {
       instructions: [await program.account.foo.createInstruction(foo)],
       signers: [foo],
     });
-    const account = await program.account.foo(foo.publicKey);
+    const account = await program.account.foo.fetch(foo.publicKey);
     assert.ok(
       JSON.stringify(account.authority.toBuffer()) ===
         JSON.stringify(program.provider.wallet.publicKey.toBuffer())
@@ -81,7 +81,7 @@ describe("zero-copy", () => {
       },
     });
 
-    const account = await program.account.foo(foo.publicKey);
+    const account = await program.account.foo.fetch(foo.publicKey);
 
     assert.ok(
       JSON.stringify(account.authority.toBuffer()) ===
@@ -103,7 +103,7 @@ describe("zero-copy", () => {
       },
     });
 
-    const account = await program.account.foo(foo.publicKey);
+    const account = await program.account.foo.fetch(foo.publicKey);
 
     assert.ok(
       JSON.stringify(account.authority.toBuffer()) ===
@@ -172,7 +172,7 @@ describe("zero-copy", () => {
       ],
       signers: [eventQ],
     });
-    const account = await program.account.eventQ(eventQ.publicKey);
+    const account = await program.account.eventQ.fetch(eventQ.publicKey);
     assert.ok(account.events.length === 25000);
     account.events.forEach((event) => {
       assert.ok(event.from.equals(new PublicKey()));
@@ -189,7 +189,7 @@ describe("zero-copy", () => {
       },
     });
     // Verify update.
-    let account = await program.account.eventQ(eventQ.publicKey);
+    let account = await program.account.eventQ.fetch(eventQ.publicKey);
     assert.ok(account.events.length === 25000);
     account.events.forEach((event, idx) => {
       if (idx === 0) {
@@ -209,7 +209,7 @@ describe("zero-copy", () => {
       },
     });
     // Verify update.
-    account = await program.account.eventQ(eventQ.publicKey);
+    account = await program.account.eventQ.fetch(eventQ.publicKey);
     assert.ok(account.events.length === 25000);
     account.events.forEach((event, idx) => {
       if (idx === 0) {
@@ -232,7 +232,7 @@ describe("zero-copy", () => {
       },
     });
     // Verify update.
-    account = await program.account.eventQ(eventQ.publicKey);
+    account = await program.account.eventQ.fetch(eventQ.publicKey);
     assert.ok(account.events.length === 25000);
     account.events.forEach((event, idx) => {
       if (idx === 0) {

+ 1 - 1
ts/package.json

@@ -18,7 +18,7 @@
     "lint:fix": "prettier src/** -w",
     "watch": "tsc -p tsconfig.cjs.json --watch",
     "prepublishOnly": "yarn build",
-    "docs": "typedoc --excludePrivate --includeVersion --out ../docs/src/.vuepress/dist/ts/ src/index.ts"
+    "docs": "typedoc --excludePrivate --includeVersion --out ../docs/src/.vuepress/dist/ts/ --readme none src/index.ts"
   },
   "dependencies": {
     "@project-serum/borsh": "^0.2.2",

+ 0 - 571
ts/src/coder.ts

@@ -1,571 +0,0 @@
-import camelCase from "camelcase";
-import * as base64 from "base64-js";
-import { snakeCase } from "snake-case";
-import { Layout } from "buffer-layout";
-import * as sha256 from "js-sha256";
-import * as borsh from "@project-serum/borsh";
-import {
-  Idl,
-  IdlField,
-  IdlTypeDef,
-  IdlEnumVariant,
-  IdlType,
-  IdlStateMethod,
-} from "./idl";
-import { IdlError } from "./error";
-import { Event } from "./program/event";
-
-/**
- * Number of bytes of the account discriminator.
- */
-export const ACCOUNT_DISCRIMINATOR_SIZE = 8;
-/**
- * Namespace for state method function signatures.
- */
-export const SIGHASH_STATE_NAMESPACE = "state";
-/**
- * Namespace for global instruction function signatures (i.e. functions
- * that aren't namespaced by the state or any of its trait implementations).
- */
-export const SIGHASH_GLOBAL_NAMESPACE = "global";
-
-/**
- * Coder provides a facade for encoding and decoding all IDL related objects.
- */
-export default class Coder {
-  /**
-   * Instruction coder.
-   */
-  readonly instruction: InstructionCoder;
-
-  /**
-   * Account coder.
-   */
-  readonly accounts: AccountsCoder;
-
-  /**
-   * Types coder.
-   */
-  readonly types: TypesCoder;
-
-  /**
-   * Coder for state structs.
-   */
-  readonly state: StateCoder;
-
-  /**
-   * Coder for events.
-   */
-  readonly events: EventCoder;
-
-  constructor(idl: Idl) {
-    this.instruction = new InstructionCoder(idl);
-    this.accounts = new AccountsCoder(idl);
-    this.types = new TypesCoder(idl);
-    this.events = new EventCoder(idl);
-    if (idl.state) {
-      this.state = new StateCoder(idl);
-    }
-  }
-
-  public sighash(nameSpace: string, ixName: string): Buffer {
-    return sighash(nameSpace, ixName);
-  }
-}
-
-/**
- * Encodes and decodes program instructions.
- */
-class InstructionCoder {
-  /**
-   * Instruction args layout. Maps namespaced method
-   */
-  private ixLayout: Map<string, Layout>;
-
-  public constructor(idl: Idl) {
-    this.ixLayout = InstructionCoder.parseIxLayout(idl);
-  }
-
-  /**
-   * Encodes a program instruction.
-   */
-  public encode(ixName: string, ix: any) {
-    return this._encode(SIGHASH_GLOBAL_NAMESPACE, ixName, ix);
-  }
-
-  /**
-   * Encodes a program state instruction.
-   */
-  public encodeState(ixName: string, ix: any) {
-    return this._encode(SIGHASH_STATE_NAMESPACE, ixName, ix);
-  }
-
-  private _encode(nameSpace: string, ixName: string, ix: any): Buffer {
-    const buffer = Buffer.alloc(1000); // TODO: use a tighter buffer.
-    const methodName = camelCase(ixName);
-    const len = this.ixLayout.get(methodName).encode(ix, buffer);
-    const data = buffer.slice(0, len);
-    return Buffer.concat([sighash(nameSpace, ixName), data]);
-  }
-
-  private static parseIxLayout(idl: Idl): Map<string, Layout> {
-    const stateMethods = idl.state ? idl.state.methods : [];
-
-    const ixLayouts = stateMethods
-      .map((m: IdlStateMethod) => {
-        let fieldLayouts = m.args.map((arg: IdlField) => {
-          return IdlCoder.fieldLayout(arg, idl.types);
-        });
-        const name = camelCase(m.name);
-        return [name, borsh.struct(fieldLayouts, name)];
-      })
-      .concat(
-        idl.instructions.map((ix) => {
-          let fieldLayouts = ix.args.map((arg: IdlField) =>
-            IdlCoder.fieldLayout(arg, idl.types)
-          );
-          const name = camelCase(ix.name);
-          return [name, borsh.struct(fieldLayouts, name)];
-        })
-      );
-    // @ts-ignore
-    return new Map(ixLayouts);
-  }
-}
-
-/**
- * Encodes and decodes account objects.
- */
-class AccountsCoder {
-  /**
-   * Maps account type identifier to a layout.
-   */
-  private accountLayouts: Map<string, Layout>;
-
-  public constructor(idl: Idl) {
-    if (idl.accounts === undefined) {
-      this.accountLayouts = new Map();
-      return;
-    }
-    const layouts: [string, Layout][] = idl.accounts.map((acc) => {
-      return [acc.name, IdlCoder.typeDefLayout(acc, idl.types)];
-    });
-
-    this.accountLayouts = new Map(layouts);
-  }
-
-  public async encode<T = any>(
-    accountName: string,
-    account: T
-  ): Promise<Buffer> {
-    const buffer = Buffer.alloc(1000); // TODO: use a tighter buffer.
-    const layout = this.accountLayouts.get(accountName);
-    const len = layout.encode(account, buffer);
-    let accountData = buffer.slice(0, len);
-    let discriminator = await accountDiscriminator(accountName);
-    return Buffer.concat([discriminator, accountData]);
-  }
-
-  public decode<T = any>(accountName: string, ix: Buffer): T {
-    // Chop off the discriminator before decoding.
-    const data = ix.slice(8);
-    const layout = this.accountLayouts.get(accountName);
-    return layout.decode(data);
-  }
-}
-
-/**
- * Encodes and decodes user defined types.
- */
-class TypesCoder {
-  /**
-   * Maps account type identifier to a layout.
-   */
-  private layouts: Map<string, Layout>;
-
-  public constructor(idl: Idl) {
-    if (idl.types === undefined) {
-      this.layouts = new Map();
-      return;
-    }
-    const layouts = idl.types.map((acc) => {
-      return [acc.name, IdlCoder.typeDefLayout(acc, idl.types)];
-    });
-
-    // @ts-ignore
-    this.layouts = new Map(layouts);
-  }
-
-  public encode<T = any>(accountName: string, account: T): Buffer {
-    const buffer = Buffer.alloc(1000); // TODO: use a tighter buffer.
-    const layout = this.layouts.get(accountName);
-    const len = layout.encode(account, buffer);
-    return buffer.slice(0, len);
-  }
-
-  public decode<T = any>(accountName: string, ix: Buffer): T {
-    const layout = this.layouts.get(accountName);
-    return layout.decode(ix);
-  }
-}
-
-class EventCoder {
-  /**
-   * Maps account type identifier to a layout.
-   */
-  private layouts: Map<string, Layout>;
-
-  /**
-   * Maps base64 encoded event discriminator to event name.
-   */
-  private discriminators: Map<string, string>;
-
-  public constructor(idl: Idl) {
-    if (idl.events === undefined) {
-      this.layouts = new Map();
-      return;
-    }
-    const layouts = idl.events.map((event) => {
-      let eventTypeDef: IdlTypeDef = {
-        name: event.name,
-        type: {
-          kind: "struct",
-          fields: event.fields.map((f) => {
-            return { name: f.name, type: f.type };
-          }),
-        },
-      };
-      return [event.name, IdlCoder.typeDefLayout(eventTypeDef, idl.types)];
-    });
-    // @ts-ignore
-    this.layouts = new Map(layouts);
-
-    this.discriminators = new Map<string, string>(
-      idl.events === undefined
-        ? []
-        : idl.events.map((e) => [
-            base64.fromByteArray(eventDiscriminator(e.name)),
-            e.name,
-          ])
-    );
-  }
-
-  public decode(log: string): Event | null {
-    const logArr = Buffer.from(base64.toByteArray(log));
-    const disc = base64.fromByteArray(logArr.slice(0, 8));
-
-    // Only deserialize if the discriminator implies a proper event.
-    const eventName = this.discriminators.get(disc);
-    if (eventName === undefined) {
-      return null;
-    }
-
-    const layout = this.layouts.get(eventName);
-    const data = layout.decode(logArr.slice(8));
-    return { data, name: eventName };
-  }
-}
-
-class StateCoder {
-  private layout: Layout;
-
-  public constructor(idl: Idl) {
-    if (idl.state === undefined) {
-      throw new Error("Idl state not defined.");
-    }
-    this.layout = IdlCoder.typeDefLayout(idl.state.struct, idl.types);
-  }
-
-  public async encode<T = any>(name: string, account: T): Promise<Buffer> {
-    const buffer = Buffer.alloc(1000); // TODO: use a tighter buffer.
-    const len = this.layout.encode(account, buffer);
-
-    const disc = await stateDiscriminator(name);
-    const accData = buffer.slice(0, len);
-
-    return Buffer.concat([disc, accData]);
-  }
-
-  public decode<T = any>(ix: Buffer): T {
-    // Chop off discriminator.
-    const data = ix.slice(8);
-    return this.layout.decode(data);
-  }
-}
-
-class IdlCoder {
-  public static fieldLayout(field: IdlField, types?: IdlTypeDef[]): Layout {
-    const fieldName =
-      field.name !== undefined ? camelCase(field.name) : undefined;
-    switch (field.type) {
-      case "bool": {
-        return borsh.bool(fieldName);
-      }
-      case "u8": {
-        return borsh.u8(fieldName);
-      }
-      case "i8": {
-        return borsh.i8(fieldName);
-      }
-      case "u16": {
-        return borsh.u16(fieldName);
-      }
-      case "i16": {
-        return borsh.i16(fieldName);
-      }
-      case "u32": {
-        return borsh.u32(fieldName);
-      }
-      case "i32": {
-        return borsh.i32(fieldName);
-      }
-      case "u64": {
-        return borsh.u64(fieldName);
-      }
-      case "i64": {
-        return borsh.i64(fieldName);
-      }
-      case "u128": {
-        return borsh.u128(fieldName);
-      }
-      case "i128": {
-        return borsh.i128(fieldName);
-      }
-      case "bytes": {
-        return borsh.vecU8(fieldName);
-      }
-      case "string": {
-        return borsh.str(fieldName);
-      }
-      case "publicKey": {
-        return borsh.publicKey(fieldName);
-      }
-      default: {
-        // @ts-ignore
-        if (field.type.vec) {
-          return borsh.vec(
-            IdlCoder.fieldLayout(
-              {
-                name: undefined,
-                // @ts-ignore
-                type: field.type.vec,
-              },
-              types
-            ),
-            fieldName
-          );
-          // @ts-ignore
-        } else if (field.type.option) {
-          return borsh.option(
-            IdlCoder.fieldLayout(
-              {
-                name: undefined,
-                // @ts-ignore
-                type: field.type.option,
-              },
-              types
-            ),
-            fieldName
-          );
-          // @ts-ignore
-        } else if (field.type.defined) {
-          // User defined type.
-          if (types === undefined) {
-            throw new IdlError("User defined types not provided");
-          }
-          // @ts-ignore
-          const filtered = types.filter((t) => t.name === field.type.defined);
-          if (filtered.length !== 1) {
-            throw new IdlError(`Type not found: ${JSON.stringify(field)}`);
-          }
-          return IdlCoder.typeDefLayout(filtered[0], types, fieldName);
-          // @ts-ignore
-        } else if (field.type.array) {
-          // @ts-ignore
-          let arrayTy = field.type.array[0];
-          // @ts-ignore
-          let arrayLen = field.type.array[1];
-          let innerLayout = IdlCoder.fieldLayout(
-            {
-              name: undefined,
-              type: arrayTy,
-            },
-            types
-          );
-          return borsh.array(innerLayout, arrayLen, fieldName);
-        } else {
-          throw new Error(`Not yet implemented: ${field}`);
-        }
-      }
-    }
-  }
-
-  public static typeDefLayout(
-    typeDef: IdlTypeDef,
-    types: IdlTypeDef[],
-    name?: string
-  ): Layout {
-    if (typeDef.type.kind === "struct") {
-      const fieldLayouts = typeDef.type.fields.map((field) => {
-        const x = IdlCoder.fieldLayout(field, types);
-        return x;
-      });
-      return borsh.struct(fieldLayouts, name);
-    } else if (typeDef.type.kind === "enum") {
-      let variants = typeDef.type.variants.map((variant: IdlEnumVariant) => {
-        const name = camelCase(variant.name);
-        if (variant.fields === undefined) {
-          return borsh.struct([], name);
-        }
-        // @ts-ignore
-        const fieldLayouts = variant.fields.map((f: IdlField | IdlType) => {
-          // @ts-ignore
-          if (f.name === undefined) {
-            throw new Error("Tuple enum variants not yet implemented.");
-          }
-          // @ts-ignore
-          return IdlCoder.fieldLayout(f, types);
-        });
-        return borsh.struct(fieldLayouts, name);
-      });
-
-      if (name !== undefined) {
-        // Buffer-layout lib requires the name to be null (on construction)
-        // when used as a field.
-        return borsh.rustEnum(variants).replicate(name);
-      }
-
-      return borsh.rustEnum(variants, name);
-    } else {
-      throw new Error(`Unknown type kint: ${typeDef}`);
-    }
-  }
-}
-
-// Calculates unique 8 byte discriminator prepended to all anchor accounts.
-export async function accountDiscriminator(name: string): Promise<Buffer> {
-  // @ts-ignore
-  return Buffer.from(sha256.digest(`account:${name}`)).slice(0, 8);
-}
-
-// Calculates unique 8 byte discriminator prepended to all anchor state accounts.
-export async function stateDiscriminator(name: string): Promise<Buffer> {
-  // @ts-ignore
-  return Buffer.from(sha256.digest(`state:${name}`)).slice(0, 8);
-}
-
-export function eventDiscriminator(name: string): Buffer {
-  // @ts-ignore
-  return Buffer.from(sha256.digest(`event:${name}`)).slice(0, 8);
-}
-
-// Returns the size of the type in bytes. For variable length types, just return
-// 1. Users should override this value in such cases.
-function typeSize(idl: Idl, ty: IdlType): number {
-  switch (ty) {
-    case "bool":
-      return 1;
-    case "u8":
-      return 1;
-    case "i8":
-      return 1;
-    case "i16":
-      return 2;
-    case "u16":
-      return 2;
-    case "u32":
-      return 4;
-    case "i32":
-      return 4;
-    case "u64":
-      return 8;
-    case "i64":
-      return 8;
-    case "u128":
-      return 16;
-    case "i128":
-      return 16;
-    case "bytes":
-      return 1;
-    case "string":
-      return 1;
-    case "publicKey":
-      return 32;
-    default:
-      // @ts-ignore
-      if (ty.vec !== undefined) {
-        return 1;
-      }
-      // @ts-ignore
-      if (ty.option !== undefined) {
-        // @ts-ignore
-        return 1 + typeSize(idl, ty.option);
-      }
-      // @ts-ignore
-      if (ty.defined !== undefined) {
-        // @ts-ignore
-        const filtered = idl.types.filter((t) => t.name === ty.defined);
-        if (filtered.length !== 1) {
-          throw new IdlError(`Type not found: ${JSON.stringify(ty)}`);
-        }
-        let typeDef = filtered[0];
-
-        return accountSize(idl, typeDef);
-      }
-      // @ts-ignore
-      if (ty.array !== undefined) {
-        // @ts-ignore
-        let arrayTy = ty.array[0];
-        // @ts-ignore
-        let arraySize = ty.array[1];
-        // @ts-ignore
-        return typeSize(idl, arrayTy) * arraySize;
-      }
-      throw new Error(`Invalid type ${JSON.stringify(ty)}`);
-  }
-}
-
-export function accountSize(
-  idl: Idl,
-  idlAccount: IdlTypeDef
-): number | undefined {
-  if (idlAccount.type.kind === "enum") {
-    let variantSizes = idlAccount.type.variants.map(
-      (variant: IdlEnumVariant) => {
-        if (variant.fields === undefined) {
-          return 0;
-        }
-        // @ts-ignore
-        return (
-          variant.fields
-            // @ts-ignore
-            .map((f: IdlField | IdlType) => {
-              // @ts-ignore
-              if (f.name === undefined) {
-                throw new Error("Tuple enum variants not yet implemented.");
-              }
-              // @ts-ignore
-              return typeSize(idl, f.type);
-            })
-            .reduce((a: number, b: number) => a + b)
-        );
-      }
-    );
-    return Math.max(...variantSizes) + 1;
-  }
-  if (idlAccount.type.fields === undefined) {
-    return 0;
-  }
-  return idlAccount.type.fields
-    .map((f) => typeSize(idl, f.type))
-    .reduce((a, b) => a + b);
-}
-
-// Not technically sighash, since we don't include the arguments, as Rust
-// doesn't allow function overloading.
-function sighash(nameSpace: string, ixName: string): Buffer {
-  let name = snakeCase(ixName);
-  let preimage = `${nameSpace}:${name}`;
-  // @ts-ignore
-  return Buffer.from(sha256.digest(preimage)).slice(0, 8);
-}

+ 55 - 0
ts/src/coder/accounts.ts

@@ -0,0 +1,55 @@
+import { Layout } from "buffer-layout";
+import { Idl } from "../idl";
+import { IdlCoder } from "./idl";
+import { sha256 } from "js-sha256";
+
+/**
+ * Number of bytes of the account discriminator.
+ */
+export const ACCOUNT_DISCRIMINATOR_SIZE = 8;
+
+/**
+ * Encodes and decodes account objects.
+ */
+export class AccountsCoder {
+  /**
+   * Maps account type identifier to a layout.
+   */
+  private accountLayouts: Map<string, Layout>;
+
+  public constructor(idl: Idl) {
+    if (idl.accounts === undefined) {
+      this.accountLayouts = new Map();
+      return;
+    }
+    const layouts: [string, Layout][] = idl.accounts.map((acc) => {
+      return [acc.name, IdlCoder.typeDefLayout(acc, idl.types)];
+    });
+
+    this.accountLayouts = new Map(layouts);
+  }
+
+  public async encode<T = any>(
+    accountName: string,
+    account: T
+  ): Promise<Buffer> {
+    const buffer = Buffer.alloc(1000); // TODO: use a tighter buffer.
+    const layout = this.accountLayouts.get(accountName);
+    const len = layout.encode(account, buffer);
+    let accountData = buffer.slice(0, len);
+    let discriminator = await accountDiscriminator(accountName);
+    return Buffer.concat([discriminator, accountData]);
+  }
+
+  public decode<T = any>(accountName: string, ix: Buffer): T {
+    // Chop off the discriminator before decoding.
+    const data = ix.slice(8);
+    const layout = this.accountLayouts.get(accountName);
+    return layout.decode(data);
+  }
+}
+
+// Calculates unique 8 byte discriminator prepended to all anchor accounts.
+export async function accountDiscriminator(name: string): Promise<Buffer> {
+  return Buffer.from(sha256.digest(`account:${name}`)).slice(0, 8);
+}

+ 113 - 0
ts/src/coder/common.ts

@@ -0,0 +1,113 @@
+import { snakeCase } from "snake-case";
+import { sha256 } from "js-sha256";
+import { Idl, IdlField, IdlTypeDef, IdlEnumVariant, IdlType } from "../idl";
+import { IdlError } from "../error";
+
+export function accountSize(
+  idl: Idl,
+  idlAccount: IdlTypeDef
+): number | undefined {
+  if (idlAccount.type.kind === "enum") {
+    let variantSizes = idlAccount.type.variants.map(
+      (variant: IdlEnumVariant) => {
+        if (variant.fields === undefined) {
+          return 0;
+        }
+        return (
+          variant.fields
+            // @ts-ignore
+            .map((f: IdlField | IdlType) => {
+              // @ts-ignore
+              if (f.name === undefined) {
+                throw new Error("Tuple enum variants not yet implemented.");
+              }
+              // @ts-ignore
+              return typeSize(idl, f.type);
+            })
+            .reduce((a: number, b: number) => a + b)
+        );
+      }
+    );
+    return Math.max(...variantSizes) + 1;
+  }
+  if (idlAccount.type.fields === undefined) {
+    return 0;
+  }
+  return idlAccount.type.fields
+    .map((f) => typeSize(idl, f.type))
+    .reduce((a, b) => a + b);
+}
+
+// Returns the size of the type in bytes. For variable length types, just return
+// 1. Users should override this value in such cases.
+function typeSize(idl: Idl, ty: IdlType): number {
+  switch (ty) {
+    case "bool":
+      return 1;
+    case "u8":
+      return 1;
+    case "i8":
+      return 1;
+    case "i16":
+      return 2;
+    case "u16":
+      return 2;
+    case "u32":
+      return 4;
+    case "i32":
+      return 4;
+    case "u64":
+      return 8;
+    case "i64":
+      return 8;
+    case "u128":
+      return 16;
+    case "i128":
+      return 16;
+    case "bytes":
+      return 1;
+    case "string":
+      return 1;
+    case "publicKey":
+      return 32;
+    default:
+      // @ts-ignore
+      if (ty.vec !== undefined) {
+        return 1;
+      }
+      // @ts-ignore
+      if (ty.option !== undefined) {
+        // @ts-ignore
+        return 1 + typeSize(idl, ty.option);
+      }
+      // @ts-ignore
+      if (ty.defined !== undefined) {
+        // @ts-ignore
+        const filtered = idl.types.filter((t) => t.name === ty.defined);
+        if (filtered.length !== 1) {
+          throw new IdlError(`Type not found: ${JSON.stringify(ty)}`);
+        }
+        let typeDef = filtered[0];
+
+        return accountSize(idl, typeDef);
+      }
+      // @ts-ignore
+      if (ty.array !== undefined) {
+        // @ts-ignore
+        let arrayTy = ty.array[0];
+        // @ts-ignore
+        let arraySize = ty.array[1];
+        // @ts-ignore
+        return typeSize(idl, arrayTy) * arraySize;
+      }
+      throw new Error(`Invalid type ${JSON.stringify(ty)}`);
+  }
+}
+
+// Not technically sighash, since we don't include the arguments, as Rust
+// doesn't allow function overloading.
+export function sighash(nameSpace: string, ixName: string): Buffer {
+  let name = snakeCase(ixName);
+  let preimage = `${nameSpace}:${name}`;
+  return Buffer.from(sha256.digest(preimage)).slice(0, 8);
+}

+ 67 - 0
ts/src/coder/event.ts

@@ -0,0 +1,67 @@
+import * as base64 from "base64-js";
+import { Layout } from "buffer-layout";
+import { sha256 } from "js-sha256";
+import { Idl, IdlTypeDef } from "../idl";
+import { Event } from "../program/event";
+import { IdlCoder } from "./idl";
+
+export class EventCoder {
+  /**
+   * Maps account type identifier to a layout.
+   */
+  private layouts: Map<string, Layout>;
+
+  /**
+   * Maps base64 encoded event discriminator to event name.
+   */
+  private discriminators: Map<string, string>;
+
+  public constructor(idl: Idl) {
+    if (idl.events === undefined) {
+      this.layouts = new Map();
+      return;
+    }
+    const layouts = idl.events.map((event) => {
+      let eventTypeDef: IdlTypeDef = {
+        name: event.name,
+        type: {
+          kind: "struct",
+          fields: event.fields.map((f) => {
+            return { name: f.name, type: f.type };
+          }),
+        },
+      };
+      return [event.name, IdlCoder.typeDefLayout(eventTypeDef, idl.types)];
+    });
+    // @ts-ignore
+    this.layouts = new Map(layouts);
+
+    this.discriminators = new Map<string, string>(
+      idl.events === undefined
+        ? []
+        : idl.events.map((e) => [
+            base64.fromByteArray(eventDiscriminator(e.name)),
+            e.name,
+          ])
+    );
+  }
+
+  public decode(log: string): Event | null {
+    const logArr = Buffer.from(base64.toByteArray(log));
+    const disc = base64.fromByteArray(logArr.slice(0, 8));
+
+    // Only deserialize if the discriminator implies a proper event.
+    const eventName = this.discriminators.get(disc);
+    if (eventName === undefined) {
+      return null;
+    }
+
+    const layout = this.layouts.get(eventName);
+    const data = layout.decode(logArr.slice(8));
+    return { data, name: eventName };
+  }
+}
+
+export function eventDiscriminator(name: string): Buffer {
+  return Buffer.from(sha256.digest(`event:${name}`)).slice(0, 8);
+}

+ 154 - 0
ts/src/coder/idl.ts

@@ -0,0 +1,154 @@
+import camelCase from "camelcase";
+import { Layout } from "buffer-layout";
+import * as borsh from "@project-serum/borsh";
+import { IdlField, IdlTypeDef, IdlEnumVariant, IdlType } from "../idl";
+import { IdlError } from "../error";
+
+export class IdlCoder {
+  public static fieldLayout(field: IdlField, types?: IdlTypeDef[]): Layout {
+    const fieldName =
+      field.name !== undefined ? camelCase(field.name) : undefined;
+    switch (field.type) {
+      case "bool": {
+        return borsh.bool(fieldName);
+      }
+      case "u8": {
+        return borsh.u8(fieldName);
+      }
+      case "i8": {
+        return borsh.i8(fieldName);
+      }
+      case "u16": {
+        return borsh.u16(fieldName);
+      }
+      case "i16": {
+        return borsh.i16(fieldName);
+      }
+      case "u32": {
+        return borsh.u32(fieldName);
+      }
+      case "i32": {
+        return borsh.i32(fieldName);
+      }
+      case "u64": {
+        return borsh.u64(fieldName);
+      }
+      case "i64": {
+        return borsh.i64(fieldName);
+      }
+      case "u128": {
+        return borsh.u128(fieldName);
+      }
+      case "i128": {
+        return borsh.i128(fieldName);
+      }
+      case "bytes": {
+        return borsh.vecU8(fieldName);
+      }
+      case "string": {
+        return borsh.str(fieldName);
+      }
+      case "publicKey": {
+        return borsh.publicKey(fieldName);
+      }
+      default: {
+        // @ts-ignore
+        if (field.type.vec) {
+          return borsh.vec(
+            IdlCoder.fieldLayout(
+              {
+                name: undefined,
+                // @ts-ignore
+                type: field.type.vec,
+              },
+              types
+            ),
+            fieldName
+          );
+          // @ts-ignore
+        } else if (field.type.option) {
+          return borsh.option(
+            IdlCoder.fieldLayout(
+              {
+                name: undefined,
+                // @ts-ignore
+                type: field.type.option,
+              },
+              types
+            ),
+            fieldName
+          );
+          // @ts-ignore
+        } else if (field.type.defined) {
+          // User defined type.
+          if (types === undefined) {
+            throw new IdlError("User defined types not provided");
+          }
+          // @ts-ignore
+          const filtered = types.filter((t) => t.name === field.type.defined);
+          if (filtered.length !== 1) {
+            throw new IdlError(`Type not found: ${JSON.stringify(field)}`);
+          }
+          return IdlCoder.typeDefLayout(filtered[0], types, fieldName);
+          // @ts-ignore
+        } else if (field.type.array) {
+          // @ts-ignore
+          let arrayTy = field.type.array[0];
+          // @ts-ignore
+          let arrayLen = field.type.array[1];
+          let innerLayout = IdlCoder.fieldLayout(
+            {
+              name: undefined,
+              type: arrayTy,
+            },
+            types
+          );
+          return borsh.array(innerLayout, arrayLen, fieldName);
+        } else {
+          throw new Error(`Not yet implemented: ${field}`);
+        }
+      }
+    }
+  }
+
+  public static typeDefLayout(
+    typeDef: IdlTypeDef,
+    types: IdlTypeDef[],
+    name?: string
+  ): Layout {
+    if (typeDef.type.kind === "struct") {
+      const fieldLayouts = typeDef.type.fields.map((field) => {
+        const x = IdlCoder.fieldLayout(field, types);
+        return x;
+      });
+      return borsh.struct(fieldLayouts, name);
+    } else if (typeDef.type.kind === "enum") {
+      let variants = typeDef.type.variants.map((variant: IdlEnumVariant) => {
+        const name = camelCase(variant.name);
+        if (variant.fields === undefined) {
+          return borsh.struct([], name);
+        }
+        // @ts-ignore
+        const fieldLayouts = variant.fields.map((f: IdlField | IdlType) => {
+          // @ts-ignore
+          if (f.name === undefined) {
+            throw new Error("Tuple enum variants not yet implemented.");
+          }
+          // @ts-ignore
+          return IdlCoder.fieldLayout(f, types);
+        });
+        return borsh.struct(fieldLayouts, name);
+      });
+
+      if (name !== undefined) {
+        // Buffer-layout lib requires the name to be null (on construction)
+        // when used as a field.
+        return borsh.rustEnum(variants).replicate(name);
+      }
+
+      return borsh.rustEnum(variants, name);
+    } else {
+      throw new Error(`Unknown type kint: ${typeDef}`);
+    }
+  }
+}

+ 62 - 0
ts/src/coder/index.ts

@@ -0,0 +1,62 @@
+import { Idl } from "../idl";
+import { InstructionCoder } from "./instruction";
+import { AccountsCoder } from "./accounts";
+import { TypesCoder } from "./types";
+import { EventCoder } from "./event";
+import { StateCoder } from "./state";
+import { sighash } from "./common";
+
+export { accountSize } from "./common";
+export { TypesCoder } from "./types";
+export { InstructionCoder } from "./instruction";
+export {
+  AccountsCoder,
+  accountDiscriminator,
+  ACCOUNT_DISCRIMINATOR_SIZE,
+} from "./accounts";
+export { EventCoder, eventDiscriminator } from "./event";
+export { StateCoder, stateDiscriminator } from "./state";
+
+/**
+ * Coder provides a facade for encoding and decoding all IDL related objects.
+ */
+export default class Coder {
+  /**
+   * Instruction coder.
+   */
+  readonly instruction: InstructionCoder;
+
+  /**
+   * Account coder.
+   */
+  readonly accounts: AccountsCoder;
+
+  /**
+   * Types coder.
+   */
+  readonly types: TypesCoder;
+
+  /**
+   * Coder for state structs.
+   */
+  readonly state: StateCoder;
+
+  /**
+   * Coder for events.
+   */
+  readonly events: EventCoder;
+
+  constructor(idl: Idl) {
+    this.instruction = new InstructionCoder(idl);
+    this.accounts = new AccountsCoder(idl);
+    this.types = new TypesCoder(idl);
+    this.events = new EventCoder(idl);
+    if (idl.state) {
+      this.state = new StateCoder(idl);
+    }
+  }
+
+  public sighash(nameSpace: string, ixName: string): Buffer {
+    return sighash(nameSpace, ixName);
+  }
+}

+ 76 - 0
ts/src/coder/instruction.ts

@@ -0,0 +1,76 @@
+import camelCase from "camelcase";
+import { Layout } from "buffer-layout";
+import * as borsh from "@project-serum/borsh";
+import { Idl, IdlField, IdlStateMethod } from "../idl";
+import { IdlCoder } from "./idl";
+import { sighash } from "./common";
+
+/**
+ * Namespace for state method function signatures.
+ */
+export const SIGHASH_STATE_NAMESPACE = "state";
+/**
+ * Namespace for global instruction function signatures (i.e. functions
+ * that aren't namespaced by the state or any of its trait implementations).
+ */
+export const SIGHASH_GLOBAL_NAMESPACE = "global";
+
+/**
+ * Encodes and decodes program instructions.
+ */
+export class InstructionCoder {
+  /**
+   * Instruction args layout. Maps namespaced method
+   */
+  private ixLayout: Map<string, Layout>;
+
+  public constructor(idl: Idl) {
+    this.ixLayout = InstructionCoder.parseIxLayout(idl);
+  }
+
+  /**
+   * Encodes a program instruction.
+   */
+  public encode(ixName: string, ix: any) {
+    return this._encode(SIGHASH_GLOBAL_NAMESPACE, ixName, ix);
+  }
+
+  /**
+   * Encodes a program state instruction.
+   */
+  public encodeState(ixName: string, ix: any) {
+    return this._encode(SIGHASH_STATE_NAMESPACE, ixName, ix);
+  }
+
+  private _encode(nameSpace: string, ixName: string, ix: any): Buffer {
+    const buffer = Buffer.alloc(1000); // TODO: use a tighter buffer.
+    const methodName = camelCase(ixName);
+    const len = this.ixLayout.get(methodName).encode(ix, buffer);
+    const data = buffer.slice(0, len);
+    return Buffer.concat([sighash(nameSpace, ixName), data]);
+  }
+
+  private static parseIxLayout(idl: Idl): Map<string, Layout> {
+    const stateMethods = idl.state ? idl.state.methods : [];
+
+    const ixLayouts = stateMethods
+      .map((m: IdlStateMethod) => {
+        let fieldLayouts = m.args.map((arg: IdlField) => {
+          return IdlCoder.fieldLayout(arg, idl.types);
+        });
+        const name = camelCase(m.name);
+        return [name, borsh.struct(fieldLayouts, name)];
+      })
+      .concat(
+        idl.instructions.map((ix) => {
+          let fieldLayouts = ix.args.map((arg: IdlField) =>
+            IdlCoder.fieldLayout(arg, idl.types)
+          );
+          const name = camelCase(ix.name);
+          return [name, borsh.struct(fieldLayouts, name)];
+        })
+      );
+    // @ts-ignore
+    return new Map(ixLayouts);
+  }
+}

+ 36 - 0
ts/src/coder/state.ts

@@ -0,0 +1,36 @@
+import { Layout } from "buffer-layout";
+import { sha256 } from "js-sha256";
+import { Idl } from "../idl";
+import { IdlCoder } from "./idl";
+
+export class StateCoder {
+  private layout: Layout;
+
+  public constructor(idl: Idl) {
+    if (idl.state === undefined) {
+      throw new Error("Idl state not defined.");
+    }
+    this.layout = IdlCoder.typeDefLayout(idl.state.struct, idl.types);
+  }
+
+  public async encode<T = any>(name: string, account: T): Promise<Buffer> {
+    const buffer = Buffer.alloc(1000); // TODO: use a tighter buffer.
+    const len = this.layout.encode(account, buffer);
+
+    const disc = await stateDiscriminator(name);
+    const accData = buffer.slice(0, len);
+
+    return Buffer.concat([disc, accData]);
+  }
+
+  public decode<T = any>(ix: Buffer): T {
+    // Chop off discriminator.
+    const data = ix.slice(8);
+    return this.layout.decode(data);
+  }
+}
+
+// Calculates unique 8 byte discriminator prepended to all anchor state accounts.
+export async function stateDiscriminator(name: string): Promise<Buffer> {
+  return Buffer.from(sha256.digest(`state:${name}`)).slice(0, 8);
+}

+ 38 - 0
ts/src/coder/types.ts

@@ -0,0 +1,38 @@
+import { Layout } from "buffer-layout";
+import { Idl } from "../idl";
+import { IdlCoder } from "./idl";
+
+/**
+ * Encodes and decodes user defined types.
+ */
+export class TypesCoder {
+  /**
+   * Maps account type identifier to a layout.
+   */
+  private layouts: Map<string, Layout>;
+
+  public constructor(idl: Idl) {
+    if (idl.types === undefined) {
+      this.layouts = new Map();
+      return;
+    }
+    const layouts = idl.types.map((acc) => {
+      return [acc.name, IdlCoder.typeDefLayout(acc, idl.types)];
+    });
+
+    // @ts-ignore
+    this.layouts = new Map(layouts);
+  }
+
+  public encode<T = any>(accountName: string, account: T): Buffer {
+    const buffer = Buffer.alloc(1000); // TODO: use a tighter buffer.
+    const layout = this.layouts.get(accountName);
+    const len = layout.encode(account, buffer);
+    return buffer.slice(0, len);
+  }
+
+  public decode<T = any>(accountName: string, ix: Buffer): T {
+    const layout = this.layouts.get(accountName);
+    return layout.decode(ix);
+  }
+}

+ 2 - 2
ts/src/idl.ts

@@ -29,8 +29,6 @@ export type IdlInstruction = {
   args: IdlField[];
 };
 
-// IdlStateMethods are similar to instructions, except they only allow
-// for a single account, the state account.
 export type IdlState = {
   struct: IdlTypeDef;
   methods: IdlStateMethod[];
@@ -80,6 +78,8 @@ export type IdlType =
   | "i32"
   | "u64"
   | "i64"
+  | "u128"
+  | "i128"
   | "bytes"
   | "string"
   | "publicKey"

+ 43 - 2
ts/src/index.ts

@@ -1,21 +1,46 @@
 import BN from "bn.js";
 import * as web3 from "@solana/web3.js";
 import Provider, { NodeWallet as Wallet } from "./provider";
-import Coder from "./coder";
+import Coder, {
+  InstructionCoder,
+  EventCoder,
+  StateCoder,
+  TypesCoder,
+} from "./coder";
 import { Idl } from "./idl";
 import workspace from "./workspace";
 import utils from "./utils";
 import { Program } from "./program";
 import { Address } from "./program/common";
-import { ProgramAccount } from "./program/namespace";
+import { Event } from "./program/event";
+import {
+  ProgramAccount,
+  AccountNamespace,
+  AccountClient,
+  StateClient,
+  RpcNamespace,
+  RpcFn,
+  SimulateNamespace,
+  SimulateFn,
+  TransactionNamespace,
+  TransactionFn,
+  InstructionNamespace,
+  InstructionFn,
+} from "./program/namespace";
 import { Context, Accounts } from "./program/context";
 
 let _provider: Provider | null = null;
 
+/**
+ * Sets the default provider on the client.
+ */
 function setProvider(provider: Provider) {
   _provider = provider;
 }
 
+/**
+ * Returns the default provider being used by the client.
+ */
 function getProvider(): Provider {
   if (_provider === null) {
     return Provider.local();
@@ -26,10 +51,26 @@ function getProvider(): Provider {
 export {
   workspace,
   Program,
+  AccountNamespace,
+  AccountClient,
+  StateClient,
+  RpcNamespace,
+  RpcFn,
+  SimulateNamespace,
+  SimulateFn,
+  TransactionNamespace,
+  TransactionFn,
+  InstructionNamespace,
+  InstructionFn,
   ProgramAccount,
   Context,
   Accounts,
   Coder,
+  InstructionCoder,
+  EventCoder,
+  StateCoder,
+  TypesCoder,
+  Event,
   setProvider,
   getProvider,
   Provider,

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

@@ -1,10 +1,8 @@
 import EventEmitter from "eventemitter3";
-import * as bs58 from "bs58";
 import { PublicKey } from "@solana/web3.js";
 import { Idl, IdlInstruction, IdlAccountItem, IdlStateMethod } from "../idl";
 import { ProgramError } from "../error";
 import { Accounts } from "./context";
-import Provider from "../provider";
 
 export type Subscription = {
   listener: number;

+ 36 - 30
ts/src/program/index.ts

@@ -8,7 +8,7 @@ import NamespaceFactory, {
   InstructionNamespace,
   TransactionNamespace,
   AccountNamespace,
-  StateNamespace,
+  StateClient,
   SimulateNamespace,
 } from "./namespace";
 import { getProvider } from "../";
@@ -28,27 +28,29 @@ import { Address, translateAddress } from "./common";
  * changes, and listen to events.
  *
  * In addition to field accessors and methods, the object provides a set of
- * dynamically generated properties (internally referred to as namespaces) that
- * map one-to-one to program instructions and accounts. These namespaces
- * generally can be used as follows:
+ * dynamically generated properties, also known as namespaces, that
+ * map one-to-one to program methods and accounts. These namespaces generally
+ *  can be used as follows:
+ *
+ * ## Usage
  *
  * ```javascript
- * program.<namespace>.<program-specific-field>
+ * program.<namespace>.<program-specific-method>
  * ```
  *
  * API specifics are namespace dependent. The examples used in the documentation
  * below will refer to the two counter examples found
- * [here](https://project-serum.github.io/anchor/ts/#examples).
+ * [here](https://github.com/project-serum/anchor#examples).
  */
 export class Program {
   /**
-   * Async methods to send signed transactions invoking *non*-state methods
-   * on an Anchor program.
+   * Async methods to send signed transactions to *non*-state methods on the
+   * program, returning a [[TransactionSignature]].
    *
-   * ## rpc
+   * ## Usage
    *
    * ```javascript
-   * program.rpc.<method>(...args, ctx);
+   * rpc.<method>(...args, ctx);
    * ```
    *
    * ## Parameters
@@ -74,32 +76,32 @@ export class Program {
   readonly rpc: RpcNamespace;
 
   /**
-   * Async functions to fetch deserialized program accounts from a cluster.
+   * The namespace provides handles to an [[AccountClient]] object for each
+   * account in the program.
    *
-   * ## account
+   * ## Usage
    *
    * ```javascript
-   * program.account.<account>(address);
+   * program.account.<account-client>
    * ```
    *
-   * ## Parameters
-   *
-   * 1. `address` - The [[Address]] of the account.
-   *
    * ## Example
    *
-   * To fetch a `Counter` object from the above example,
+   * To fetch a `Counter` account from the above example,
    *
    * ```javascript
-   * const counter = await program.account.counter(address);
+   * const counter = await program.account.counter.fetch(address);
    * ```
+   *
+   * For the full API, see the [[AccountClient]] reference.
    */
   readonly account: AccountNamespace;
 
   /**
-   * Functions to build [[TransactionInstruction]] objects for program methods.
+   * The namespace provides functions to build [[TransactionInstruction]]
+   * objects for each method of a program.
    *
-   * ## instruction
+   * ## Usage
    *
    * ```javascript
    * program.instruction.<method>(...args, ctx);
@@ -127,9 +129,10 @@ export class Program {
   readonly instruction: InstructionNamespace;
 
   /**
-   * Functions to build [[Transaction]] objects.
+   * The namespace provides functions to build [[Transaction]] objects for each
+   * method of a program.
    *
-   * ## transaction
+   * ## Usage
    *
    * ```javascript
    * program.transaction.<method>(...args, ctx);
@@ -157,8 +160,9 @@ export class Program {
   readonly transaction: TransactionNamespace;
 
   /**
-   * Async functions to simulate instructions against an Anchor program,
-   * returning a list of deserialized events *and* raw program logs.
+   * The namespace provides functions to simulate transactions for each method
+   * of a program, returning a list of deserialized events *and* raw program
+   * logs.
    *
    * One can use this to read data calculated from a program on chain, by
    * emitting an event in the program and reading the emitted event client side
@@ -182,7 +186,7 @@ export class Program {
    * To simulate the `increment` method above,
    *
    * ```javascript
-   * const tx = await program.simulate.increment({
+   * const events = await program.simulate.increment({
    *   accounts: {
    *     counter,
    *   },
@@ -192,9 +196,11 @@ export class Program {
   readonly simulate: SimulateNamespace;
 
   /**
-   * Object with state account accessors and rpcs.
+   * A client for the program state. Similar to the base [[Program]] client,
+   * one can use this to send transactions and read accounts for the state
+   * abstraction.
    */
-  readonly state: StateNamespace;
+  readonly state: StateClient;
 
   /**
    * Address of the program.
@@ -249,15 +255,15 @@ export class Program {
       instruction,
       transaction,
       account,
-      state,
       simulate,
+      state,
     ] = NamespaceFactory.build(idl, this._coder, programId, this._provider);
     this.rpc = rpc;
     this.instruction = instruction;
     this.transaction = transaction;
     this.account = account;
-    this.state = state;
     this.simulate = simulate;
+    this.state = state;
   }
 
   /**

+ 230 - 182
ts/src/program/namespace/account.ts

@@ -9,55 +9,16 @@ import {
   Commitment,
 } from "@solana/web3.js";
 import Provider from "../../provider";
-import { Idl } from "../../idl";
+import { Idl, IdlTypeDef } from "../../idl";
 import Coder, {
   ACCOUNT_DISCRIMINATOR_SIZE,
   accountDiscriminator,
   accountSize,
 } from "../../coder";
 import { Subscription, Address, translateAddress } from "../common";
-
-/**
- * Accounts is a dynamically generated object to fetch any given account
- * of a program.
- */
-export interface AccountNamespace {
-  [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: Address, commitment?: Commitment) => EventEmitter;
-  unsubscribe: (address: Address) => void;
-  createInstruction: (signer: Signer) => Promise<TransactionInstruction>;
-  associated: (...args: PublicKey[]) => Promise<any>;
-  associatedAddress: (...args: PublicKey[]) => Promise<PublicKey>;
-};
-
-/**
- * @hidden
- *
- * 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();
+import { getProvider } from "../../";
 
 export default class AccountFactory {
-  // Returns the generated accounts namespace.
   public static build(
     idl: Idl,
     coder: Coder,
@@ -68,163 +29,250 @@ export default class AccountFactory {
 
     idl.accounts.forEach((idlAccount) => {
       const name = camelCase(idlAccount.name);
+      accountFns[name] = new AccountClient(
+        idl,
+        idlAccount,
+        programId,
+        provider,
+        coder
+      );
+    });
 
-      // Fetches the decoded account from the network.
-      const accountsNamespace = async (address: Address): Promise<any> => {
-        const accountInfo = await provider.connection.getAccountInfo(
-          translateAddress(address)
-        );
-        if (accountInfo === null) {
-          throw new Error(`Account does not exist ${address.toString()}`);
-        }
+    return accountFns;
+  }
+}
 
-        // 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");
-        }
+/**
+ * The namespace provides handles to an [[AccountClient]] object for each
+ * account in a program.
+ *
+ * ## Usage
+ *
+ * ```javascript
+ * account.<account-client>
+ * ```
+ *
+ * ## Example
+ *
+ * To fetch a `Counter` account from the above example,
+ *
+ * ```javascript
+ * const counter = await program.account.counter.fetch(address);
+ * ```
+ *
+ * For the full API, see the [[AccountClient]] reference.
+ */
+export interface AccountNamespace {
+  [key: string]: AccountClient;
+}
 
-        return coder.accounts.decode(idlAccount.name, accountInfo.data);
-      };
+export class AccountClient {
+  /**
+   * Returns the number of bytes in this account.
+   */
+  get size(): number {
+    return this._size;
+  }
+  private _size: number;
 
-      // 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 (
-        signer: Signer,
-        sizeOverride?: number
-      ): Promise<TransactionInstruction> => {
-        // @ts-ignore
-        const size = accountsNamespace["size"];
-
-        return SystemProgram.createAccount({
-          fromPubkey: provider.wallet.publicKey,
-          newAccountPubkey: signer.publicKey,
-          space: sizeOverride ?? size,
-          lamports: await provider.connection.getMinimumBalanceForRentExemption(
-            sizeOverride ?? size
-          ),
-          programId,
-        });
-      };
+  /**
+   * Returns the program ID owning all accounts.
+   */
+  get programId(): PublicKey {
+    return this._programId;
+  }
+  private _programId: PublicKey;
 
-      // Subscribes to all changes to this account.
-      // @ts-ignore
-      accountsNamespace["subscribe"] = (
-        address: Address,
-        commitment?: Commitment
-      ): EventEmitter => {
-        if (subscriptions.get(address.toString())) {
-          return subscriptions.get(address.toString()).ee;
-        }
-
-        const ee = new EventEmitter();
-        address = translateAddress(address);
-        const listener = provider.connection.onAccountChange(
-          address,
-          (acc) => {
-            const account = coder.accounts.decode(idlAccount.name, acc.data);
-            ee.emit("change", account);
-          },
-          commitment
-        );
+  /**
+   * Returns the cleint's wallet and network provider.
+   */
+  get provider(): Provider {
+    return this._provider;
+  }
+  private _provider: Provider;
 
-        subscriptions.set(address.toString(), {
-          ee,
-          listener,
-        });
+  /**
+   * Returns the coder.
+   */
+  get coder(): Coder {
+    return this._coder;
+  }
+  private _coder: Coder;
 
-        return ee;
-      };
+  private _idlAccount: IdlTypeDef;
 
-      // Unsubscribes to account changes.
-      // @ts-ignore
-      accountsNamespace["unsubscribe"] = (address: Address) => {
-        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);
-        }
-      };
+  constructor(
+    idl: Idl,
+    idlAccount: IdlTypeDef,
+    programId: PublicKey,
+    provider?: Provider,
+    coder?: Coder
+  ) {
+    this._idlAccount = idlAccount;
+    this._programId = programId;
+    this._provider = provider ?? getProvider();
+    this._coder = coder ?? new Coder(idl);
+    this._size = ACCOUNT_DISCRIMINATOR_SIZE + accountSize(idl, idlAccount);
+  }
+
+  /**
+   * Returns a deserialized account.
+   *
+   * @param address The address of the account to fetch.
+   */
+  async fetch(address: Address): Promise<Object> {
+    const accountInfo = await this._provider.connection.getAccountInfo(
+      translateAddress(address)
+    );
+    if (accountInfo === null) {
+      throw new Error(`Account does not exist ${address.toString()}`);
+    }
+
+    // Assert the account discriminator is correct.
+    const discriminator = await accountDiscriminator(this._idlAccount.name);
+    if (discriminator.compare(accountInfo.data.slice(0, 8))) {
+      throw new Error("Invalid account discriminator");
+    }
+
+    return this._coder.accounts.decode(this._idlAccount.name, accountInfo.data);
+  }
+
+  /**
+   * Returns all instances of this account type for the program.
+   */
+  async all(filter?: Buffer): Promise<ProgramAccount<any>[]> {
+    let bytes = await accountDiscriminator(this._idlAccount.name);
+    if (filter !== undefined) {
+      bytes = Buffer.concat([bytes, filter]);
+    }
 
-      // 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(),
+    let resp = await this._provider.connection.getProgramAccounts(
+      this._programId,
+      {
+        commitment: this._provider.connection.commitment,
+        filters: [
           {
-            commitment: provider.connection.commitment,
-            filters: [
-              {
-                memcmp: {
-                  offset: 0,
-                  bytes: bs58.encode(bytes),
-                },
-              },
-            ],
+            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),
-              };
-            })
-        );
+        ],
+      }
+    );
+    return resp.map(({ pubkey, account }) => {
+      return {
+        publicKey: pubkey,
+        account: this._coder.accounts.decode(
+          this._idlAccount.name,
+          account.data
+        ),
       };
+    });
+  }
 
-      // Function returning the associated address. Args are keys to associate.
-      // Order matters.
-      accountsNamespace["associatedAddress"] = async (
-        ...args: Address[]
-      ): Promise<PublicKey> => {
-        let seeds = [Buffer.from([97, 110, 99, 104, 111, 114])]; // b"anchor".
-        args.forEach((arg) => {
-          seeds.push(translateAddress(arg).toBuffer());
-        });
-        const [assoc] = await PublicKey.findProgramAddress(seeds, programId);
-        return assoc;
-      };
+  /**
+   * Returns an `EventEmitter` emitting a "change" event whenever the account
+   * changes.
+   */
+  subscribe(address: Address, commitment?: Commitment): EventEmitter {
+    if (subscriptions.get(address.toString())) {
+      return subscriptions.get(address.toString()).ee;
+    }
 
-      // Function returning the associated account. Args are keys to associate.
-      // Order matters.
-      accountsNamespace["associated"] = async (
-        ...args: Address[]
-      ): Promise<any> => {
-        const addr = await accountsNamespace["associatedAddress"](...args);
-        return await accountsNamespace(addr);
-      };
+    const ee = new EventEmitter();
+    address = translateAddress(address);
+    const listener = this._provider.connection.onAccountChange(
+      address,
+      (acc) => {
+        const account = this._coder.accounts.decode(
+          this._idlAccount.name,
+          acc.data
+        );
+        ee.emit("change", account);
+      },
+      commitment
+    );
 
-      accountFns[name] = accountsNamespace;
+    subscriptions.set(address.toString(), {
+      ee,
+      listener,
     });
 
-    return accountFns;
+    return ee;
+  }
+
+  /**
+   * Unsubscribes from the account at the given address.
+   */
+  unsubscribe(address: Address) {
+    let sub = subscriptions.get(address.toString());
+    if (!sub) {
+      console.warn("Address is not subscribed");
+      return;
+    }
+    if (subscriptions) {
+      this._provider.connection
+        .removeAccountChangeListener(sub.listener)
+        .then(() => {
+          subscriptions.delete(address.toString());
+        })
+        .catch(console.error);
+    }
+  }
+
+  /**
+   * Returns an instruction for creating this account.
+   */
+  async createInstruction(
+    signer: Signer,
+    sizeOverride?: number
+  ): Promise<TransactionInstruction> {
+    const size = this.size;
+
+    return SystemProgram.createAccount({
+      fromPubkey: this._provider.wallet.publicKey,
+      newAccountPubkey: signer.publicKey,
+      space: sizeOverride ?? size,
+      lamports: await this._provider.connection.getMinimumBalanceForRentExemption(
+        sizeOverride ?? size
+      ),
+      programId: this._programId,
+    });
+  }
+
+  /**
+   * Function returning the associated account. Args are keys to associate.
+   * Order matters.
+   */
+  async associated(...args: PublicKey[]): Promise<any> {
+    const addr = await this.associatedAddress(...args);
+    return await this.fetch(addr);
+  }
+
+  /**
+   * Function returning the associated address. Args are keys to associate.
+   * Order matters.
+   */
+  async associatedAddress(...args: PublicKey[]): Promise<PublicKey> {
+    let seeds = [Buffer.from([97, 110, 99, 104, 111, 114])]; // b"anchor".
+    args.forEach((arg) => {
+      seeds.push(translateAddress(arg).toBuffer());
+    });
+    const [assoc] = await PublicKey.findProgramAddress(seeds, this._programId);
+    return assoc;
   }
 }
+
+/**
+ * @hidden
+ *
+ * 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();

+ 18 - 20
ts/src/program/namespace/index.ts

@@ -3,21 +3,21 @@ import { PublicKey } from "@solana/web3.js";
 import Coder from "../../coder";
 import Provider from "../../provider";
 import { Idl } from "../../idl";
-import { parseIdlErrors } from "../common";
-import StateFactory, { StateNamespace } from "./state";
+import StateFactory, { StateClient } from "./state";
 import InstructionFactory, { InstructionNamespace } from "./instruction";
 import TransactionFactory, { TransactionNamespace } from "./transaction";
 import RpcFactory, { RpcNamespace } from "./rpc";
 import AccountFactory, { AccountNamespace } from "./account";
 import SimulateFactory, { SimulateNamespace } from "./simulate";
+import { parseIdlErrors } from "../common";
 
 // Re-exports.
-export { StateNamespace } from "./state";
-export { InstructionNamespace } from "./instruction";
-export { TransactionNamespace, TxFn } from "./transaction";
+export { StateClient } from "./state";
+export { InstructionNamespace, InstructionFn } from "./instruction";
+export { TransactionNamespace, TransactionFn } from "./transaction";
 export { RpcNamespace, RpcFn } from "./rpc";
-export { AccountNamespace, AccountFn, ProgramAccount } from "./account";
-export { SimulateNamespace } from "./simulate";
+export { AccountNamespace, AccountClient, ProgramAccount } from "./account";
+export { SimulateNamespace, SimulateFn } from "./simulate";
 
 export default class NamespaceFactory {
   /**
@@ -33,26 +33,24 @@ export default class NamespaceFactory {
     InstructionNamespace,
     TransactionNamespace,
     AccountNamespace,
-    StateNamespace,
-    SimulateNamespace
+    SimulateNamespace,
+    StateClient
   ] {
-    const idlErrors = parseIdlErrors(idl);
-
     const rpc: RpcNamespace = {};
     const instruction: InstructionNamespace = {};
     const transaction: TransactionNamespace = {};
     const simulate: SimulateNamespace = {};
 
-    const state = StateFactory.build(
-      idl,
-      coder,
-      programId,
-      idlErrors,
-      provider
-    );
+    const idlErrors = parseIdlErrors(idl);
+
+    const state = StateFactory.build(idl, coder, programId, provider);
 
     idl.instructions.forEach((idlIx) => {
-      const ixItem = InstructionFactory.build(idlIx, coder, programId);
+      const ixItem = InstructionFactory.build(
+        idlIx,
+        (ixName: string, ix: any) => coder.instruction.encode(ixName, ix),
+        programId
+      );
       const txItem = TransactionFactory.build(idlIx, ixItem);
       const rpcItem = RpcFactory.build(idlIx, txItem, idlErrors, provider);
       const simulateItem = SimulateFactory.build(
@@ -77,6 +75,6 @@ export default class NamespaceFactory {
       ? AccountFactory.build(idl, coder, programId, provider)
       : {};
 
-    return [rpc, instruction, transaction, account, state, simulate];
+    return [rpc, instruction, transaction, account, simulate, state];
   }
 }

+ 49 - 27
ts/src/program/namespace/instruction.ts

@@ -1,7 +1,6 @@
 import { PublicKey, TransactionInstruction } from "@solana/web3.js";
 import { IdlAccount, IdlInstruction, IdlAccountItem } from "../../idl";
 import { IdlError } from "../../error";
-import Coder from "../../coder";
 import {
   toInstruction,
   validateAccounts,
@@ -10,28 +9,12 @@ import {
 } from "../common";
 import { Accounts, splitArgsAndCtx } from "../context";
 
-/**
- * Dynamically generated instruction namespace.
- */
-export interface InstructionNamespace {
-  [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: Accounts) => any;
-};
-
 export default class InstructionNamespaceFactory {
-  // Builds the instuction namespace.
   public static build(
     idlIx: IdlInstruction,
-    coder: Coder,
+    encodeFn: InstructionEncodeFn,
     programId: PublicKey
-  ): IxFn {
+  ): InstructionFn {
     if (idlIx.name === "_inner") {
       throw new IdlError("the _inner name is reserved");
     }
@@ -41,10 +24,7 @@ export default class InstructionNamespaceFactory {
       validateAccounts(idlIx.accounts, ctx.accounts);
       validateInstruction(idlIx, ...args);
 
-      const keys = InstructionNamespaceFactory.accountsArray(
-        ctx.accounts,
-        idlIx.accounts
-      );
+      const keys = ix.accounts(ctx.accounts);
 
       if (ctx.remainingAccounts !== undefined) {
         keys.push(...ctx.remainingAccounts);
@@ -56,10 +36,7 @@ export default class InstructionNamespaceFactory {
       return new TransactionInstruction({
         keys,
         programId,
-        data: coder.instruction.encode(
-          idlIx.name,
-          toInstruction(idlIx, ...ixArgs)
-        ),
+        data: encodeFn(idlIx.name, toInstruction(idlIx, ...ixArgs)),
       });
     };
 
@@ -96,6 +73,51 @@ export default class InstructionNamespaceFactory {
   }
 }
 
+/**
+ * The namespace provides functions to build [[TransactionInstruction]]
+ * objects for each method of a program.
+ *
+ * ## Usage
+ *
+ * ```javascript
+ * instruction.<method>(...args, ctx);
+ * ```
+ *
+ * ## Parameters
+ *
+ * 1. `args` - The positional arguments for the program. The type and number
+ *    of these arguments depend on the program being used.
+ * 2. `ctx`  - [[Context]] non-argument parameters to pass to the method.
+ *    Always the last parameter in the method call.
+ *
+ * ## Example
+ *
+ * To create an instruction for the `increment` method above,
+ *
+ * ```javascript
+ * const tx = await program.instruction.increment({
+ *   accounts: {
+ *     counter,
+ *   },
+ * });
+ * ```
+ */
+export interface InstructionNamespace {
+  [key: string]: InstructionFn;
+}
+
+/**
+ * Function to create a `TransactionInstruction` generated from an IDL.
+ * Additionally it provides an `accounts` utility method, returning a list
+ * of ordered accounts for the instruction.
+ */
+export type InstructionFn = IxProps & ((...args: any[]) => any);
+type IxProps = {
+  accounts: (ctx: Accounts) => any;
+};
+
+export type InstructionEncodeFn = (ixName: string, ix: any) => Buffer;
+
 // Throws error if any argument required for the `ix` is not given.
 function validateInstruction(ix: IdlInstruction, ...args: any[]) {
   // todo

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

@@ -3,25 +3,12 @@ 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 RpcNamespace {
-  [key: string]: RpcFn;
-}
-
-/**
- * RpcFn is a single rpc method generated from an IDL.
- */
-export type RpcFn = (...args: any[]) => Promise<TransactionSignature>;
+import { TransactionFn } from "./transaction";
 
 export default class RpcFactory {
-  // Builds the rpc namespace.
   public static build(
     idlIx: IdlInstruction,
-    txFn: TxFn,
+    txFn: TransactionFn,
     idlErrors: Map<number, string>,
     provider: Provider
   ): RpcFn {
@@ -44,3 +31,47 @@ export default class RpcFactory {
     return rpc;
   }
 }
+
+/**
+ * The namespace provides async methods to send signed transactions for each
+ * *non*-state method on Anchor program.
+ *
+ * Keys are method names, values are RPC functions returning a
+ * [[TransactionInstruction]].
+ *
+ * ## Usage
+ *
+ * ```javascript
+ * rpc.<method>(...args, ctx);
+ * ```
+ *
+ * ## Parameters
+ *
+ * 1. `args` - The positional arguments for the program. The type and number
+ *    of these arguments depend on the program being used.
+ * 2. `ctx`  - [[Context]] non-argument parameters to pass to the method.
+ *    Always the last parameter in the method call.
+ * ```
+ *
+ * ## Example
+ *
+ * To send a transaction invoking the `increment` method above,
+ *
+ * ```javascript
+ * const txSignature = await program.rpc.increment({
+ *   accounts: {
+ *     counter,
+ *     authority,
+ *   },
+ * });
+ * ```
+ */
+export interface RpcNamespace {
+  [key: string]: RpcFn;
+}
+
+/**
+ * RpcFn is a single RPC method generated from an IDL, sending a transaction
+ * paid for and signed by the configured provider.
+ */
+export type RpcFn = (...args: any[]) => Promise<TransactionSignature>;

+ 53 - 20
ts/src/program/namespace/simulate.ts

@@ -3,33 +3,15 @@ import Provider from "../../provider";
 import { IdlInstruction } from "../../idl";
 import { translateError } from "../common";
 import { splitArgsAndCtx } from "../context";
-import { TxFn } from "./transaction";
+import { TransactionFn } from "./transaction";
 import { EventParser } from "../event";
 import Coder from "../../coder";
 import { Idl } from "../../idl";
 
-/**
- * Dynamically generated simualte namespace.
- */
-export interface SimulateNamespace {
-  [key: string]: SimulateFn;
-}
-
-/**
- * RpcFn is a single rpc method generated from an IDL.
- */
-export type SimulateFn = (...args: any[]) => Promise<SimulateResponse>;
-
-type SimulateResponse = {
-  events: Event[];
-  raw: string[];
-};
-
 export default class SimulateFactory {
-  // Builds the rpc namespace.
   public static build(
     idlIx: IdlInstruction,
-    txFn: TxFn,
+    txFn: TransactionFn,
     idlErrors: Map<number, string>,
     provider: Provider,
     coder: Coder,
@@ -74,3 +56,54 @@ export default class SimulateFactory {
     return simulate;
   }
 }
+
+/**
+ * The namespace provides functions to simulate transactions for each method
+ * of a program, returning a list of deserialized events *and* raw program
+ * logs.
+ *
+ * One can use this to read data calculated from a program on chain, by
+ * emitting an event in the program and reading the emitted event client side
+ * via the `simulate` namespace.
+ *
+ * ## Usage
+ *
+ * ```javascript
+ * program.simulate.<method>(...args, ctx);
+ * ```
+ *
+ * ## Parameters
+ *
+ * 1. `args` - The positional arguments for the program. The type and number
+ *    of these arguments depend on the program being used.
+ * 2. `ctx`  - [[Context]] non-argument parameters to pass to the method.
+ *    Always the last parameter in the method call.
+ *
+ * ## Example
+ *
+ * To simulate the `increment` method above,
+ *
+ * ```javascript
+ * const events = await program.simulate.increment({
+ *   accounts: {
+ *     counter,
+ *   },
+ * });
+ * ```
+ */
+export interface SimulateNamespace {
+  [key: string]: SimulateFn;
+}
+
+/**
+ * RpcFn is a single method generated from an IDL. It simulates a method
+ * against a cluster configured by the provider, returning a list of all the
+ * events and raw logs that were emitted during the execution of the
+ * method.
+ */
+export type SimulateFn = (...args: any[]) => Promise<SimulateResponse>;
+
+type SimulateResponse = {
+  events: Event[];
+  raw: string[];
+};

+ 183 - 135
ts/src/program/namespace/state.ts

@@ -1,176 +1,224 @@
 import EventEmitter from "eventemitter3";
+import camelCase from "camelcase";
 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 { RpcNamespace, InstructionNamespace } from "./";
-import {
-  Subscription,
-  translateError,
-  toInstruction,
-  validateAccounts,
-} from "../common";
-import { Accounts, splitArgsAndCtx } from "../context";
+import { RpcNamespace, InstructionNamespace, TransactionNamespace } from "./";
+import { getProvider } from "../../";
+import { Subscription, validateAccounts, parseIdlErrors } from "../common";
+import { findProgramAddressSync, createWithSeedSync } from "../../utils/pubkey";
+import { Accounts } from "../context";
 import InstructionNamespaceFactory from "./instruction";
-
-export type StateNamespace = () =>
-  | Promise<any>
-  | {
-      address: () => Promise<PublicKey>;
-      rpc: RpcNamespace;
-      instruction: InstructionNamespace;
-      subscribe: (commitment?: Commitment) => EventEmitter;
-      unsubscribe: () => void;
-    };
+import RpcNamespaceFactory from "./rpc";
+import TransactionNamespaceFactory from "./transaction";
 
 export default class StateFactory {
-  // Builds the state namespace.
   public static build(
     idl: Idl,
     coder: Coder,
     programId: PublicKey,
-    idlErrors: Map<number, string>,
     provider: Provider
-  ): StateNamespace | undefined {
+  ): StateClient | undefined {
     if (idl.state === undefined) {
       return undefined;
     }
+    return new StateClient(idl, programId, provider, coder);
+  }
+}
 
-    // 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);
-    };
+/**
+ * A client for the program state. Similar to the base [[Program]] client,
+ * one can use this to send transactions and read accounts for the state
+ * abstraction.
+ */
+export class StateClient {
+  /**
+   * [[RpcNamespace]] for all state methods.
+   */
+  readonly rpc: RpcNamespace;
+
+  /**
+   * [[InstructionNamespace]] for all state methods.
+   */
+  readonly instruction: InstructionNamespace;
 
-    // Namespace with all rpc functions.
-    const rpc: RpcNamespace = {};
-    const ix: InstructionNamespace = {};
+  /**
+   * [[TransactionNamespace]] for all state methods.
+   */
+  readonly transaction: TransactionNamespace;
 
-    idl.state.methods.forEach((m: IdlStateMethod) => {
-      const accounts = async (accounts: Accounts): Promise<any> => {
-        const keys = await stateInstructionKeys(
-          programId,
-          provider,
+  /**
+   * Returns the program ID owning the state.
+   */
+  get programId(): PublicKey {
+    return this._programId;
+  }
+  private _programId: PublicKey;
+
+  /**
+   * Returns the client's wallet and network provider.
+   */
+  get provider(): Provider {
+    return this._provider;
+  }
+  private _provider: Provider;
+
+  /**
+   * Returns the coder.
+   */
+  get coder(): Coder {
+    return this._coder;
+  }
+
+  private _address: PublicKey;
+  private _coder: Coder;
+  private _idl: Idl;
+  private _sub: Subscription | null;
+
+  constructor(
+    idl: Idl,
+    programId: PublicKey,
+    provider?: Provider,
+    coder?: Coder
+  ) {
+    this._idl = idl;
+    this._programId = programId;
+    this._address = programStateAddress(programId);
+    this._provider = provider ?? getProvider();
+    this._coder = coder ?? new Coder(idl);
+    this._sub = null;
+
+    // Build namespaces.
+    const [instruction, transaction, rpc] = ((): [
+      InstructionNamespace,
+      TransactionNamespace,
+      RpcNamespace
+    ] => {
+      let instruction: InstructionNamespace = {};
+      let transaction: TransactionNamespace = {};
+      let rpc: RpcNamespace = {};
+
+      idl.state.methods.forEach((m: IdlStateMethod) => {
+        // Build instruction method.
+        const ixItem = InstructionNamespaceFactory.build(
           m,
-          accounts
-        );
-        return keys.concat(
-          InstructionNamespaceFactory.accountsArray(accounts, m.accounts)
+          (ixName: string, ix: any) =>
+            coder.instruction.encodeState(ixName, ix),
+          programId
         );
-      };
-      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
+        ixItem["accounts"] = (accounts: Accounts) => {
+          const keys = stateInstructionKeys(programId, provider, m, accounts);
+          return keys.concat(
+            InstructionNamespaceFactory.accountsArray(accounts, m.accounts)
+          );
+        };
+        // Build transaction method.
+        const txItem = TransactionNamespaceFactory.build(m, ixItem);
+        // Build RPC method.
+        const rpcItem = RpcNamespaceFactory.build(
+          m,
+          txItem,
+          parseIdlErrors(idl),
+          provider
         );
 
-        sub = {
-          ee,
-          listener,
-        };
+        // Attach them all to their respective namespaces.
+        const name = camelCase(m.name);
+        instruction[name] = ixItem;
+        transaction[name] = txItem;
+        rpc[name] = rpcItem;
       });
 
-      return ee;
-    };
+      return [instruction, transaction, rpc];
+    })();
+    this.instruction = instruction;
+    this.transaction = transaction;
+    this.rpc = rpc;
+  }
+
+  /**
+   * Returns the deserialized state account.
+   */
+  async fetch(): Promise<Object> {
+    const addr = this.address();
+    const accountInfo = await this.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(
+      this._idl.state.struct.name
+    );
+    if (expectedDiscriminator.compare(accountInfo.data.slice(0, 8))) {
+      throw new Error("Invalid account discriminator");
+    }
+    return this.coder.state.decode(accountInfo.data);
+  }
 
-    // Unsubscribe from account changes.
-    state["unsubscribe"] = () => {
-      if (sub !== null) {
-        provider.connection
-          .removeAccountChangeListener(sub.listener)
-          .then(async () => {
-            sub = null;
-          })
-          .catch(console.error);
-      }
+  /**
+   * Returns the state address.
+   */
+  address(): PublicKey {
+    return this._address;
+  }
+
+  /**
+   * Returns an `EventEmitter` with a `"change"` event that's fired whenever
+   * the state account cahnges.
+   */
+  subscribe(commitment?: Commitment): EventEmitter {
+    if (this._sub !== null) {
+      return this._sub.ee;
+    }
+    const ee = new EventEmitter();
+
+    const listener = this.provider.connection.onAccountChange(
+      this.address(),
+      (acc) => {
+        const account = this.coder.state.decode(acc.data);
+        ee.emit("change", account);
+      },
+      commitment
+    );
+
+    this._sub = {
+      ee,
+      listener,
     };
 
-    return state;
+    return ee;
+  }
+
+  /**
+   * Unsubscribes to state changes.
+   */
+  unsubscribe() {
+    if (this._sub !== null) {
+      this.provider.connection
+        .removeAccountChangeListener(this._sub.listener)
+        .then(async () => {
+          this._sub = null;
+        })
+        .catch(console.error);
+    }
   }
 }
 
 // Calculates the deterministic address of the program's "state" account.
-async function programStateAddress(programId: PublicKey): Promise<PublicKey> {
-  let [registrySigner] = await PublicKey.findProgramAddress([], programId);
-  return PublicKey.createWithSeed(registrySigner, "unversioned", programId);
+function programStateAddress(programId: PublicKey): PublicKey {
+  let [registrySigner] = findProgramAddressSync([], programId);
+  return createWithSeedSync(registrySigner, "unversioned", programId);
 }
 
 // Returns the common keys that are prepended to all instructions targeting
 // the "state" of a program.
-async function stateInstructionKeys(
+function stateInstructionKeys(
   programId: PublicKey,
   provider: Provider,
   m: IdlStateMethod,
@@ -178,7 +226,7 @@ async function stateInstructionKeys(
 ) {
   if (m.name === "new") {
     // Ctor `new` method.
-    const [programSigner] = await PublicKey.findProgramAddress([], programId);
+    const [programSigner] = findProgramAddressSync([], programId);
     return [
       {
         pubkey: provider.wallet.publicKey,
@@ -186,7 +234,7 @@ async function stateInstructionKeys(
         isSigner: true,
       },
       {
-        pubkey: await programStateAddress(programId),
+        pubkey: programStateAddress(programId),
         isWritable: true,
         isSigner: false,
       },
@@ -208,7 +256,7 @@ async function stateInstructionKeys(
     validateAccounts(m.accounts, accounts);
     return [
       {
-        pubkey: await programStateAddress(programId),
+        pubkey: programStateAddress(programId),
         isWritable: true,
         isSigner: false,
       },

+ 43 - 15
ts/src/program/namespace/transaction.ts

@@ -1,23 +1,13 @@
 import { Transaction } from "@solana/web3.js";
 import { IdlInstruction } from "../../idl";
 import { splitArgsAndCtx } from "../context";
-import { IxFn } from "./instruction";
-
-/**
- * Dynamically generated transaction namespace.
- */
-export interface TransactionNamespace {
-  [key: string]: TxFn;
-}
-
-/**
- * Tx is a function to create a `Transaction` generate from an IDL.
- */
-export type TxFn = (...args: any[]) => Transaction;
+import { InstructionFn } from "./instruction";
 
 export default class TransactionFactory {
-  // Builds the transaction namespace.
-  public static build(idlIx: IdlInstruction, ixFn: IxFn): TxFn {
+  public static build(
+    idlIx: IdlInstruction,
+    ixFn: InstructionFn
+  ): TransactionFn {
     const txFn = (...args: any[]): Transaction => {
       const [, ctx] = splitArgsAndCtx(idlIx, [...args]);
       const tx = new Transaction();
@@ -31,3 +21,41 @@ export default class TransactionFactory {
     return txFn;
   }
 }
+
+/**
+ * The namespace provides functions to build [[Transaction]] objects for each
+ * method of a program.
+ *
+ * ## Usage
+ *
+ * ```javascript
+ * program.transaction.<method>(...args, ctx);
+ * ```
+ *
+ * ## Parameters
+ *
+ * 1. `args` - The positional arguments for the program. The type and number
+ *    of these arguments depend on the program being used.
+ * 2. `ctx`  - [[Context]] non-argument parameters to pass to the method.
+ *    Always the last parameter in the method call.
+ *
+ * ## Example
+ *
+ * To create an instruction for the `increment` method above,
+ *
+ * ```javascript
+ * const tx = await program.transaction.increment({
+ *   accounts: {
+ *     counter,
+ *   },
+ * });
+ * ```
+ */
+export interface TransactionNamespace {
+  [key: string]: TransactionFn;
+}
+
+/**
+ * Tx is a function to create a `Transaction` for a given program instruction.
+ */
+export type TransactionFn = (...args: any[]) => Transaction;

+ 14 - 0
ts/src/utils/index.ts

@@ -0,0 +1,14 @@
+import { sha256 } from "crypto-hash";
+import * as bs58 from "bs58";
+import * as rpc from "./rpc";
+import * as publicKey from "./pubkey";
+
+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);
+}
+
+export default { sha256, bs58, rpc, publicKey };

+ 78 - 0
ts/src/utils/pubkey.ts

@@ -0,0 +1,78 @@
+import BN from "bn.js";
+import { sha256 as sha256Sync } from "js-sha256";
+import { PublicKey } from "@solana/web3.js";
+
+// Sync version of web3.PublicKey.createWithSeed.
+export function createWithSeedSync(
+  fromPublicKey: PublicKey,
+  seed: string,
+  programId: PublicKey
+): PublicKey {
+  const buffer = Buffer.concat([
+    fromPublicKey.toBuffer(),
+    Buffer.from(seed),
+    programId.toBuffer(),
+  ]);
+  const hash = sha256Sync.digest(buffer);
+  return new PublicKey(Buffer.from(hash));
+}
+
+// Sync version of web3.PublicKey.createProgramAddress.
+export function createProgramAddressSync(
+  seeds: Array<Buffer | Uint8Array>,
+  programId: PublicKey
+): PublicKey {
+  const MAX_SEED_LENGTH = 32;
+
+  let buffer = Buffer.alloc(0);
+  seeds.forEach(function (seed) {
+    if (seed.length > MAX_SEED_LENGTH) {
+      throw new TypeError(`Max seed length exceeded`);
+    }
+    buffer = Buffer.concat([buffer, toBuffer(seed)]);
+  });
+  buffer = Buffer.concat([
+    buffer,
+    programId.toBuffer(),
+    Buffer.from("ProgramDerivedAddress"),
+  ]);
+  let hash = sha256Sync(new Uint8Array(buffer));
+  let publicKeyBytes = new BN(hash, 16).toArray(undefined, 32);
+  if (PublicKey.isOnCurve(new Uint8Array(publicKeyBytes))) {
+    throw new Error(`Invalid seeds, address must fall off the curve`);
+  }
+  return new PublicKey(publicKeyBytes);
+}
+
+// Sync version of web3.PublicKey.findProgramAddress.
+export function findProgramAddressSync(
+  seeds: Array<Buffer | Uint8Array>,
+  programId: PublicKey
+): [PublicKey, number] {
+  let nonce = 255;
+  let address: PublicKey | undefined;
+  while (nonce != 0) {
+    try {
+      const seedsWithNonce = seeds.concat(Buffer.from([nonce]));
+      address = createProgramAddressSync(seedsWithNonce, programId);
+    } catch (err) {
+      if (err instanceof TypeError) {
+        throw err;
+      }
+      nonce--;
+      continue;
+    }
+    return [address, nonce];
+  }
+  throw new Error(`Unable to find a viable program address nonce`);
+}
+
+const toBuffer = (arr: Buffer | Uint8Array | Array<number>): Buffer => {
+  if (arr instanceof Buffer) {
+    return arr;
+  } else if (arr instanceof Uint8Array) {
+    return Buffer.from(arr.buffer, arr.byteOffset, arr.byteLength);
+  } else {
+    return Buffer.from(arr);
+  }
+};

+ 1 - 25
ts/src/utils.ts → ts/src/utils/rpc.ts

@@ -1,14 +1,7 @@
-import * as bs58 from "bs58";
-import { sha256 } from "crypto-hash";
 import assert from "assert";
 import { PublicKey, AccountInfo, Connection } from "@solana/web3.js";
-import { idlAddress } from "./idl";
 
-export const TOKEN_PROGRAM_ID = new PublicKey(
-  "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA"
-);
-
-async function getMultipleAccounts(
+export async function getMultipleAccounts(
   connection: Connection,
   publicKeys: PublicKey[]
 ): Promise<
@@ -68,20 +61,3 @@ 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,
-  getMultipleAccounts,
-  idlAddress,
-};
-
-export default utils;