Răsfoiți Sursa

ts: Improve TypeScript types (#739)

Ian Macalinao 4 ani în urmă
părinte
comite
90df0b1976

+ 2 - 2
ts/package.json

@@ -5,7 +5,7 @@
   "main": "dist/cjs/index.js",
   "module": "dist/esm/index.js",
   "license": "(MIT OR Apache-2.0)",
-  "types": "dist/index.d.ts",
+  "types": "dist/cjs/index.d.ts",
   "publishConfig": {
     "access": "public"
   },
@@ -58,6 +58,6 @@
     "ts-jest": "^26.4.3",
     "ts-node": "^9.0.0",
     "typedoc": "^0.20.36",
-    "typescript": "^4.0.5"
+    "typescript": "^4.4.3"
   }
 }

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

@@ -35,6 +35,9 @@ export class AccountsCoder {
   ): Promise<Buffer> {
     const buffer = Buffer.alloc(1000); // TODO: use a tighter buffer.
     const layout = this.accountLayouts.get(accountName);
+    if (!layout) {
+      throw new Error(`Unknown account: ${accountName}`);
+    }
     const len = layout.encode(account, buffer);
     let accountData = buffer.slice(0, len);
     let discriminator = await accountDiscriminator(accountName);
@@ -45,6 +48,9 @@ export class AccountsCoder {
     // Chop off the discriminator before decoding.
     const data = ix.slice(8);
     const layout = this.accountLayouts.get(accountName);
+    if (!layout) {
+      throw new Error(`Unknown account: ${accountName}`);
+    }
     return layout.decode(data);
   }
 }

+ 14 - 31
ts/src/coder/common.ts

@@ -3,29 +3,21 @@ 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 {
+export function accountSize(idl: Idl, idlAccount: IdlTypeDef): number {
   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 variant.fields
+          .map((f: IdlField | IdlType) => {
+            if (!(typeof f === "object" && "name" in f)) {
+              throw new Error("Tuple enum variants not yet implemented.");
+            }
+            return typeSize(idl, f.type);
+          })
+          .reduce((a: number, b: number) => a + b);
       }
     );
     return Math.max(...variantSizes) + 1;
@@ -71,19 +63,14 @@ function typeSize(idl: Idl, ty: IdlType): number {
     case "publicKey":
       return 32;
     default:
-      // @ts-ignore
-      if (ty.vec !== undefined) {
+      if ("vec" in ty) {
         return 1;
       }
-      // @ts-ignore
-      if (ty.option !== undefined) {
-        // @ts-ignore
+      if ("option" in ty) {
         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 ("defined" in ty) {
+        const filtered = idl.types?.filter((t) => t.name === ty.defined) ?? [];
         if (filtered.length !== 1) {
           throw new IdlError(`Type not found: ${JSON.stringify(ty)}`);
         }
@@ -91,13 +78,9 @@ function typeSize(idl: Idl, ty: IdlType): number {
 
         return accountSize(idl, typeDef);
       }
-      // @ts-ignore
-      if (ty.array !== undefined) {
-        // @ts-ignore
+      if ("array" in ty) {
         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)}`);

+ 5 - 2
ts/src/coder/event.ts

@@ -46,7 +46,7 @@ export class EventCoder {
     );
   }
 
-  public decode(log: string): Event | null {
+  public decode<T = Record<string, unknown>>(log: string): Event<T> | null {
     let logArr: Buffer;
     // This will throw if log length is not a multiple of 4.
     try {
@@ -63,7 +63,10 @@ export class EventCoder {
     }
 
     const layout = this.layouts.get(eventName);
-    const data = layout.decode(logArr.slice(8));
+    if (!layout) {
+      throw new Error(`Unknown event: ${eventName}`);
+    }
+    const data = layout.decode(logArr.slice(8)) as T;
     return { data, name: eventName };
   }
 }

+ 11 - 15
ts/src/coder/idl.ts

@@ -5,7 +5,10 @@ import { IdlField, IdlTypeDef, IdlEnumVariant, IdlType } from "../idl";
 import { IdlError } from "../error";
 
 export class IdlCoder {
-  public static fieldLayout(field: IdlField, types?: IdlTypeDef[]): Layout {
+  public static fieldLayout(
+    field: { name?: string } & Pick<IdlField, "type">,
+    types?: IdlTypeDef[]
+  ): Layout {
     const fieldName =
       field.name !== undefined ? camelCase(field.name) : undefined;
     switch (field.type) {
@@ -52,8 +55,7 @@ export class IdlCoder {
         return borsh.publicKey(fieldName);
       }
       default: {
-        // @ts-ignore
-        if (field.type.vec) {
+        if ("vec" in field.type) {
           return borsh.vec(
             IdlCoder.fieldLayout(
               {
@@ -65,36 +67,30 @@ export class IdlCoder {
             ),
             fieldName
           );
-          // @ts-ignore
-        } else if (field.type.option) {
+        } else if ("option" in field.type) {
           return borsh.option(
             IdlCoder.fieldLayout(
               {
                 name: undefined,
-                // @ts-ignore
                 type: field.type.option,
               },
               types
             ),
             fieldName
           );
-          // @ts-ignore
-        } else if (field.type.defined) {
+        } else if ("defined" in field.type) {
+          const defined = 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);
+          const filtered = types.filter((t) => t.name === 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
+        } else if ("array" in field.type) {
           let arrayTy = field.type.array[0];
-          // @ts-ignore
           let arrayLen = field.type.array[1];
           let innerLayout = IdlCoder.fieldLayout(
             {
@@ -113,7 +109,7 @@ export class IdlCoder {
 
   public static typeDefLayout(
     typeDef: IdlTypeDef,
-    types: IdlTypeDef[],
+    types: IdlTypeDef[] = [],
     name?: string
   ): Layout {
     if (typeDef.type.kind === "struct") {

+ 18 - 12
ts/src/coder/instruction.ts

@@ -10,6 +10,7 @@ import {
   IdlTypeDef,
   IdlAccount,
   IdlAccountItem,
+  IdlTypeDefTyStruct,
 } from "../idl";
 import { IdlCoder } from "./idl";
 import { sighash } from "./common";
@@ -77,7 +78,11 @@ export class InstructionCoder {
   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 layout = this.ixLayout.get(methodName);
+    if (!layout) {
+      throw new Error(`Unknown method: ${methodName}`);
+    }
+    const len = layout.encode(ix, buffer);
     const data = buffer.slice(0, len);
     return Buffer.concat([sighash(nameSpace, ixName), data]);
   }
@@ -215,21 +220,20 @@ class InstructionFormatter {
       return idlType as string;
     }
 
-    // @ts-ignore
-    if (idlType.vec) {
-      // @ts-ignore
+    if ("vec" in idlType) {
       return `Vec<${this.formatIdlType(idlType.vec)}>`;
     }
-    // @ts-ignore
-    if (idlType.option) {
-      // @ts-ignore
+    if ("option" in idlType) {
       return `Option<${this.formatIdlType(idlType.option)}>`;
     }
-    // @ts-ignore
-    if (idlType.defined) {
-      // @ts-ignore
+    if ("defined" in idlType) {
       return idlType.defined;
     }
+    if ("array" in idlType) {
+      return `Array<${idlType.array[0]}; ${idlType.array[1]}>`;
+    }
+
+    throw new Error(`Unknown IDL type: ${idlType}`);
   }
 
   private static formatIdlData(
@@ -296,9 +300,10 @@ class InstructionFormatter {
     types: IdlTypeDef[]
   ): string {
     if (typeDef.type.kind === "struct") {
+      const struct: IdlTypeDefTyStruct = typeDef.type;
       const fields = Object.keys(data)
         .map((k) => {
-          const f = typeDef.type.fields.filter((f) => f.name === k)[0];
+          const f = struct.fields.filter((f) => f.name === k)[0];
           if (f === undefined) {
             throw new Error("Unable to find type");
           }
@@ -314,12 +319,13 @@ class InstructionFormatter {
       }
       // Struct enum.
       if (typeDef.type.variants[0].name) {
+        const variants = typeDef.type.variants;
         const variant = Object.keys(data)[0];
         const enumType = data[variant];
         const namedFields = Object.keys(enumType)
           .map((f) => {
             const fieldData = enumType[f];
-            const idlField = typeDef.type.variants[variant]?.filter(
+            const idlField = variants[variant]?.filter(
               (v: IdlField) => v.name === f
             )[0];
             if (idlField === undefined) {

+ 9 - 2
ts/src/coder/types.ts

@@ -16,8 +16,9 @@ export class TypesCoder {
       this.layouts = new Map();
       return;
     }
-    const layouts = idl.types.map((acc) => {
-      return [acc.name, IdlCoder.typeDefLayout(acc, idl.types)];
+    const types = idl.types;
+    const layouts = types.map((acc) => {
+      return [acc.name, IdlCoder.typeDefLayout(acc, types)];
     });
 
     // @ts-ignore
@@ -27,12 +28,18 @@ export class TypesCoder {
   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);
+    if (!layout) {
+      throw new Error(`Unknown account type: ${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);
+    if (!layout) {
+      throw new Error(`Unknown account type: ${accountName}`);
+    }
     return layout.decode(ix);
   }
 }

+ 6 - 1
ts/src/error.ts

@@ -1,4 +1,9 @@
-export class IdlError extends Error {}
+export class IdlError extends Error {
+  constructor(message: string) {
+    super(message);
+    this.name = "IdlError";
+  }
+}
 
 // An error from a user defined program.
 export class ProgramError extends Error {

+ 10 - 4
ts/src/idl.ts

@@ -60,12 +60,18 @@ export type IdlTypeDef = {
   type: IdlTypeDefTy;
 };
 
-type IdlTypeDefTy = {
-  kind: "struct" | "enum";
-  fields?: IdlTypeDefStruct;
-  variants?: IdlEnumVariant[];
+export type IdlTypeDefTyStruct = {
+  kind: "struct";
+  fields: IdlTypeDefStruct;
 };
 
+export type IdlTypeDefTyEnum = {
+  kind: "enum";
+  variants: IdlEnumVariant[];
+};
+
+type IdlTypeDefTy = IdlTypeDefTyEnum | IdlTypeDefTyStruct;
+
 type IdlTypeDefStruct = Array<IdlField>;
 
 export type IdlType =

+ 17 - 53
ts/src/index.ts

@@ -1,11 +1,13 @@
-import BN from "bn.js";
-import * as web3 from "@solana/web3.js";
-import Provider, {
+export { default as BN } from "bn.js";
+export * as web3 from "@solana/web3.js";
+export {
+  default as Provider,
   getProvider,
   setProvider,
   NodeWallet as Wallet,
 } from "./provider";
-import Coder, {
+export {
+  default as Coder,
   InstructionCoder,
   EventCoder,
   StateCoder,
@@ -13,15 +15,15 @@ import Coder, {
   AccountsCoder,
 } from "./coder";
 
-import { ProgramError } from "./error";
-import { Instruction } from "./coder/instruction";
-import { Idl } from "./idl";
-import workspace from "./workspace";
-import * as utils from "./utils";
-import { Program } from "./program";
-import { Address } from "./program/common";
-import { Event } from "./program/event";
-import {
+export * from "./error";
+export { Instruction } from "./coder/instruction";
+export { Idl } from "./idl";
+export { default as workspace } from "./workspace";
+export * as utils from "./utils";
+export { Program } from "./program";
+export { Address } from "./program/common";
+export { Event } from "./program/event";
+export {
   ProgramAccount,
   AccountNamespace,
   AccountClient,
@@ -35,43 +37,5 @@ import {
   InstructionNamespace,
   InstructionFn,
 } from "./program/namespace";
-import { Context, Accounts } from "./program/context";
-import { EventParser } from "./program/event";
-
-export {
-  workspace,
-  Program,
-  AccountNamespace,
-  AccountClient,
-  StateClient,
-  RpcNamespace,
-  RpcFn,
-  SimulateNamespace,
-  SimulateFn,
-  TransactionNamespace,
-  TransactionFn,
-  InstructionNamespace,
-  InstructionFn,
-  ProgramAccount,
-  Context,
-  Accounts,
-  Coder,
-  InstructionCoder,
-  EventCoder,
-  StateCoder,
-  TypesCoder,
-  AccountsCoder,
-  Event,
-  Instruction,
-  setProvider,
-  getProvider,
-  Provider,
-  BN,
-  web3,
-  Idl,
-  utils,
-  Wallet,
-  Address,
-  EventParser,
-  ProgramError,
-};
+export { Context, Accounts } from "./program/context";
+export { EventParser } from "./program/event";

+ 3 - 5
ts/src/program/common.ts

@@ -40,13 +40,11 @@ export function toInstruction(
 // Throws error if any account required for the `ix` is not given.
 export function validateAccounts(
   ixAccounts: IdlAccountItem[],
-  accounts: Accounts
+  accounts: Accounts = {}
 ) {
   ixAccounts.forEach((acc) => {
-    // @ts-ignore
-    if (acc.accounts !== undefined) {
-      // @ts-ignore
-      validateAccounts(acc.accounts, accounts[acc.name]);
+    if ("accounts" in acc) {
+      validateAccounts(acc.accounts, accounts[acc.name] as Accounts);
     } else {
       if (accounts[acc.name] === undefined) {
         throw new Error(`Invalid arguments: ${acc.name} not provided.`);

+ 18 - 10
ts/src/program/event.ts

@@ -6,9 +6,9 @@ import Provider from "../provider";
 const LOG_START_INDEX = "Program log: ".length;
 
 // Deserialized event.
-export type Event = {
+export type Event<T = Record<string, unknown>> = {
   name: string;
-  data: Object;
+  data: T;
 };
 
 type EventCallback = (event: any, slot: number) => void;
@@ -71,7 +71,7 @@ export class EventManager {
     }
     this._eventListeners.set(
       eventName,
-      this._eventListeners.get(eventName).concat(listener)
+      (this._eventListeners.get(eventName) ?? []).concat(listener)
     );
 
     // Store the callback into the listener map.
@@ -93,8 +93,11 @@ export class EventManager {
           const allListeners = this._eventListeners.get(event.name);
           if (allListeners) {
             allListeners.forEach((listener) => {
-              const [, callback] = this._eventCallbacks.get(listener);
-              callback(event.data, ctx.slot);
+              const listenerCb = this._eventCallbacks.get(listener);
+              if (listenerCb) {
+                const [, callback] = listenerCb;
+                callback(event.data, ctx.slot);
+              }
             });
           }
         });
@@ -128,10 +131,12 @@ export class EventManager {
     // Kill the websocket connection if all listeners have been removed.
     if (this._eventCallbacks.size == 0) {
       assert.ok(this._eventListeners.size === 0);
-      await this._provider.connection.removeOnLogsListener(
-        this._onLogsSubscriptionId
-      );
-      this._onLogsSubscriptionId = undefined;
+      if (this._onLogsSubscriptionId !== undefined) {
+        await this._provider.connection.removeOnLogsListener(
+          this._onLogsSubscriptionId
+        );
+        this._onLogsSubscriptionId = undefined;
+      }
     }
   }
 }
@@ -243,7 +248,10 @@ class ExecutionContext {
   constructor(log: string) {
     // Assumes the first log in every transaction is an `invoke` log from the
     // runtime.
-    const program = /^Program (.*) invoke.*$/g.exec(log)[1];
+    const program = /^Program (.*) invoke.*$/g.exec(log)?.[1];
+    if (!program) {
+      throw new Error(`Could not find program invocation log line`);
+    }
     this.stack = [program];
   }
 

+ 28 - 28
ts/src/program/index.ts

@@ -200,7 +200,7 @@ export class Program {
    * one can use this to send transactions and read accounts for the state
    * abstraction.
    */
-  readonly state: StateClient;
+  readonly state?: StateClient;
 
   /**
    * Address of the program.
@@ -210,14 +210,6 @@ export class Program {
   }
   private _programId: PublicKey;
 
-  /**
-   * IDL defining the program's interface.
-   */
-  public get idl(): Idl {
-    return this._idl;
-  }
-  private _idl: Idl;
-
   /**
    * Coder for serializing requests.
    */
@@ -226,14 +218,6 @@ export class Program {
   }
   private _coder: Coder;
 
-  /**
-   * Wallet and network provider.
-   */
-  public get provider(): Provider {
-    return this._provider;
-  }
-  private _provider: Provider;
-
   /**
    * Handles event subscriptions.
    */
@@ -245,19 +229,23 @@ export class Program {
    * @param provider  The network and wallet context to use. If not provided
    *                  then uses [[getProvider]].
    */
-  public constructor(idl: Idl, programId: Address, provider?: Provider) {
+  public constructor(
+    /**
+     * IDL defining the program's interface.
+     */
+    public readonly idl: Idl,
+    programId: Address,
+    /**
+     * Wallet and network provider.
+     */
+    public readonly provider: Provider = getProvider()
+  ) {
     programId = translateAddress(programId);
 
     // Fields.
-    this._idl = idl;
     this._programId = programId;
-    this._provider = provider ?? getProvider();
     this._coder = new Coder(idl);
-    this._events = new EventManager(
-      this._programId,
-      this._provider,
-      this._coder
-    );
+    this._events = new EventManager(this._programId, provider, this._coder);
 
     // Dynamic namespaces.
     const [
@@ -267,7 +255,7 @@ export class Program {
       account,
       simulate,
       state,
-    ] = NamespaceFactory.build(idl, this._coder, programId, this._provider);
+    ] = NamespaceFactory.build(idl, this._coder, programId, provider);
     this.rpc = rpc;
     this.instruction = instruction;
     this.transaction = transaction;
@@ -285,10 +273,16 @@ export class Program {
    * @param programId The on-chain address of the program.
    * @param provider  The network and wallet context.
    */
-  public static async at(address: Address, provider?: Provider) {
+  public static async at(
+    address: Address,
+    provider?: Provider
+  ): Promise<Program> {
     const programId = translateAddress(address);
 
     const idl = await Program.fetchIdl(programId, provider);
+    if (!idl) {
+      throw new Error(`IDL not found for program: ${address.toString()}`);
+    }
     return new Program(idl, programId, provider);
   }
 
@@ -301,12 +295,18 @@ export class Program {
    * @param programId The on-chain address of the program.
    * @param provider  The network and wallet context.
    */
-  public static async fetchIdl(address: Address, provider?: Provider) {
+  public static async fetchIdl(
+    address: Address,
+    provider?: Provider
+  ): Promise<Idl | null> {
     provider = provider ?? getProvider();
     const programId = translateAddress(address);
 
     const idlAddr = await idlAddress(programId);
     const accountInfo = await provider.connection.getAccountInfo(idlAddr);
+    if (!accountInfo) {
+      return null;
+    }
     // Chop off account discriminator.
     let idlAccount = decodeIdlAccount(accountInfo.data.slice(8));
     const inflatedIdl = inflate(idlAccount.data);

+ 6 - 4
ts/src/program/namespace/account.ts

@@ -28,7 +28,7 @@ export default class AccountFactory {
   ): AccountNamespace {
     const accountFns: AccountNamespace = {};
 
-    idl.accounts.forEach((idlAccount) => {
+    idl.accounts?.forEach((idlAccount) => {
       const name = camelCase(idlAccount.name);
       accountFns[name] = new AccountClient(
         idl,
@@ -113,7 +113,8 @@ export class AccountClient {
     this._programId = programId;
     this._provider = provider ?? getProvider();
     this._coder = coder ?? new Coder(idl);
-    this._size = ACCOUNT_DISCRIMINATOR_SIZE + accountSize(idl, idlAccount);
+    this._size =
+      ACCOUNT_DISCRIMINATOR_SIZE + (accountSize(idl, idlAccount) ?? 0);
   }
 
   /**
@@ -177,8 +178,9 @@ export class AccountClient {
    * changes.
    */
   subscribe(address: Address, commitment?: Commitment): EventEmitter {
-    if (subscriptions.get(address.toString())) {
-      return subscriptions.get(address.toString()).ee;
+    const sub = subscriptions.get(address.toString());
+    if (sub) {
+      return sub.ee;
     }
 
     const ee = new EventEmitter();

+ 1 - 1
ts/src/program/namespace/index.ts

@@ -34,7 +34,7 @@ export default class NamespaceFactory {
     TransactionNamespace,
     AccountNamespace,
     SimulateNamespace,
-    StateClient
+    StateClient | undefined
   ] {
     const rpc: RpcNamespace = {};
     const instruction: InstructionNamespace = {};

+ 13 - 6
ts/src/program/namespace/instruction.ts

@@ -1,4 +1,8 @@
-import { PublicKey, TransactionInstruction } from "@solana/web3.js";
+import {
+  AccountMeta,
+  PublicKey,
+  TransactionInstruction,
+} from "@solana/web3.js";
 import { IdlAccount, IdlInstruction, IdlAccountItem } from "../../idl";
 import { IdlError } from "../../error";
 import {
@@ -41,19 +45,22 @@ export default class InstructionNamespaceFactory {
     };
 
     // Utility fn for ordering the accounts for this instruction.
-    ix["accounts"] = (accs: Accounts) => {
+    ix["accounts"] = (accs: Accounts = {}) => {
       return InstructionNamespaceFactory.accountsArray(accs, idlIx.accounts);
     };
 
     return ix;
   }
 
-  public static accountsArray(ctx: Accounts, accounts: IdlAccountItem[]): any {
+  public static accountsArray(
+    ctx: Accounts,
+    accounts: IdlAccountItem[]
+  ): AccountMeta[] {
     return accounts
       .map((acc: IdlAccountItem) => {
         // Nested accounts.
-        // @ts-ignore
-        const nestedAccounts: IdlAccountItem[] | undefined = acc.accounts;
+        const nestedAccounts: IdlAccountItem[] | undefined =
+          "accounts" in acc ? acc.accounts : undefined;
         if (nestedAccounts !== undefined) {
           const rpcAccs = ctx[acc.name] as Accounts;
           return InstructionNamespaceFactory.accountsArray(
@@ -113,7 +120,7 @@ export interface InstructionNamespace {
  */
 export type InstructionFn = IxProps & ((...args: any[]) => any);
 type IxProps = {
-  accounts: (ctx: Accounts) => any;
+  accounts: (ctx: Accounts) => AccountMeta[];
 };
 
 export type InstructionEncodeFn = (ixName: string, ix: any) => Buffer;

+ 1 - 1
ts/src/program/namespace/rpc.ts

@@ -1,7 +1,7 @@
 import { TransactionSignature } from "@solana/web3.js";
 import Provider from "../../provider";
 import { IdlInstruction } from "../../idl";
-import { splitArgsAndCtx } from "../context";
+import { Context, splitArgsAndCtx } from "../context";
 import { TransactionFn } from "./transaction";
 import { ProgramError } from "../../error";
 

+ 9 - 3
ts/src/program/namespace/simulate.ts

@@ -1,4 +1,8 @@
-import { PublicKey } from "@solana/web3.js";
+import {
+  PublicKey,
+  RpcResponseAndContext,
+  SimulatedTransactionResponse,
+} from "@solana/web3.js";
 import Provider from "../../provider";
 import { IdlInstruction } from "../../idl";
 import { splitArgsAndCtx } from "../context";
@@ -21,7 +25,9 @@ export default class SimulateFactory {
     const simulate = async (...args: any[]): Promise<SimulateResponse> => {
       const tx = txFn(...args);
       const [, ctx] = splitArgsAndCtx(idlIx, [...args]);
-      let resp = undefined;
+      let resp:
+        | RpcResponseAndContext<SimulatedTransactionResponse>
+        | undefined = undefined;
       try {
         resp = await provider.simulate(tx, ctx.signers, ctx.options);
       } catch (err) {
@@ -43,7 +49,7 @@ export default class SimulateFactory {
         throw new Error("Simulated logs not found");
       }
 
-      const events = [];
+      const events: Event[] = [];
       if (idl.events) {
         let parser = new EventParser(programId, coder);
         parser.parseLogs(logs, (event) => {

+ 14 - 24
ts/src/program/namespace/state.ts

@@ -56,37 +56,25 @@ export class StateClient {
   }
   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
+    /**
+     * Returns the client's wallet and network provider.
+     */
+    public readonly provider: Provider = getProvider(),
+    /**
+     * Returns the coder.
+     */
+    public readonly coder: Coder = new Coder(idl)
   ) {
     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.
@@ -99,7 +87,7 @@ export class StateClient {
       let transaction: TransactionNamespace = {};
       let rpc: RpcNamespace = {};
 
-      idl.state.methods.forEach((m: IdlStateMethod) => {
+      idl.state?.methods.forEach((m: IdlStateMethod) => {
         // Build instruction method.
         const ixItem = InstructionNamespaceFactory.build(
           m,
@@ -147,9 +135,11 @@ export class StateClient {
       throw new Error(`Account does not exist ${addr.toString()}`);
     }
     // Assert the account discriminator is correct.
-    const expectedDiscriminator = await stateDiscriminator(
-      this._idl.state.struct.name
-    );
+    const state = this._idl.state;
+    if (!state) {
+      throw new Error("State is not specified in IDL.");
+    }
+    const expectedDiscriminator = await stateDiscriminator(state.struct.name);
     if (expectedDiscriminator.compare(accountInfo.data.slice(0, 8))) {
       throw new Error("Invalid account discriminator");
     }

+ 9 - 10
ts/src/provider.ts

@@ -61,7 +61,9 @@ export default class Provider {
    * (This api is for Node only.)
    */
   static env(): Provider {
-    if (isBrowser) return;
+    if (isBrowser) {
+      throw new Error(`Provider env is not available on browser.`);
+    }
 
     const process = require("process");
     const url = process.env.ANCHOR_PROVIDER_URL;
@@ -102,7 +104,7 @@ export default class Provider {
 
     await this.wallet.signTransaction(tx);
     signers
-      .filter((s) => s !== undefined)
+      .filter((s): s is Signer => s !== undefined)
       .forEach((kp) => {
         tx.partialSign(kp);
       });
@@ -144,7 +146,7 @@ export default class Provider {
       tx.recentBlockhash = blockhash.blockhash;
 
       signers
-        .filter((s) => s !== undefined)
+        .filter((s): s is Signer => s !== undefined)
         .forEach((kp) => {
           tx.partialSign(kp);
         });
@@ -154,7 +156,7 @@ export default class Provider {
 
     const signedTxs = await this.wallet.signAllTransactions(txs);
 
-    const sigs = [];
+    const sigs: TransactionSignature[] = [];
 
     for (let k = 0; k < txs.length; k += 1) {
       const tx = signedTxs[k];
@@ -178,14 +180,11 @@ export default class Provider {
   async simulate(
     tx: Transaction,
     signers?: Array<Signer | undefined>,
-    opts?: ConfirmOptions
+    opts: ConfirmOptions = this.opts
   ): Promise<RpcResponseAndContext<SimulatedTransactionResponse>> {
     if (signers === undefined) {
       signers = [];
     }
-    if (opts === undefined) {
-      opts = this.opts;
-    }
 
     tx.feePayer = this.wallet.publicKey;
     tx.recentBlockhash = (
@@ -196,7 +195,7 @@ export default class Provider {
 
     await this.wallet.signTransaction(tx);
     signers
-      .filter((s) => s !== undefined)
+      .filter((s): s is Signer => s !== undefined)
       .forEach((kp) => {
         tx.partialSign(kp);
       });
@@ -204,7 +203,7 @@ export default class Provider {
     return await simulateTransaction(
       this.connection,
       tx,
-      opts.commitment ?? this.opts.commitment
+      opts.commitment ?? this.opts.commitment ?? "recent"
     );
   }
 }

+ 3 - 0
ts/src/workspace.ts

@@ -97,6 +97,9 @@ function attachWorkspaceOverride(
     if (typeof entry !== "string" && entry.idl) {
       idl = JSON.parse(require("fs").readFileSync(entry.idl, "utf-8"));
     }
+    if (!idl) {
+      throw new Error(`Error loading workspace IDL for ${programName}`);
+    }
     workspaceCache[wsProgramName] = new Program(idl, overrideAddress);
   });
 }

+ 2 - 2
ts/tsconfig.json

@@ -7,7 +7,6 @@
 
     "outDir": "dist/esm/",
     "rootDir": "./src",
-    "declarationDir": "dist",
 
     "sourceMap": true,
     "declaration": true,
@@ -16,6 +15,7 @@
     "experimentalDecorators": true,
     "emitDecoratorMetadata": true,
     "noImplicitAny": false,
+    "strictNullChecks": true,
 
     "esModuleInterop": true,
     "resolveJsonModule": true,
@@ -23,7 +23,7 @@
     "baseUrl": ".",
     "typeRoots": ["types/", "node_modules/@types"],
     "paths": {
-      "@solana/web3.js":  ["./node_modules/@solana/web3.js/lib"]
+      "@solana/web3.js": ["./node_modules/@solana/web3.js/lib"]
     }
   }
 }

+ 4 - 4
ts/yarn.lock

@@ -5303,10 +5303,10 @@ typedoc@^0.20.36:
     shiki "^0.9.3"
     typedoc-default-themes "^0.12.10"
 
-typescript@^4.0.5:
-  version "4.1.3"
-  resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.1.3.tgz#519d582bd94cba0cf8934c7d8e8467e473f53bb7"
-  integrity sha512-B3ZIOf1IKeH2ixgHhj6la6xdwR9QrLC5d1VKeCSY4tvkqhF2eqd9O7txNlS0PO3GrBAFIdr3L1ndNwteUbZLYg==
+typescript@^4.4.3:
+  version "4.4.3"
+  resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.4.3.tgz#bdc5407caa2b109efd4f82fe130656f977a29324"
+  integrity sha512-4xfscpisVgqqDfPaJo5vkd+Qd/ItkoagnHpufr+i2QCHBsNYp+G7UAoyFl8aPtx879u38wPV65rZ8qbGZijalA==
 
 uglify-js@^3.1.4:
   version "3.13.5"