From 90df0b19766ef2a0937312a3864f44e49bb16cad Mon Sep 17 00:00:00 2001 From: Ian Macalinao Date: Thu, 16 Sep 2021 13:42:11 -0700 Subject: [PATCH] ts: Improve TypeScript types (#739) --- ts/package.json | 4 +- ts/src/coder/accounts.ts | 6 +++ ts/src/coder/common.ts | 45 +++++----------- ts/src/coder/event.ts | 7 ++- ts/src/coder/idl.ts | 26 ++++----- ts/src/coder/instruction.ts | 30 ++++++----- ts/src/coder/types.ts | 11 +++- ts/src/error.ts | 7 ++- ts/src/idl.ts | 14 +++-- ts/src/index.ts | 70 ++++++------------------- ts/src/program/common.ts | 8 ++- ts/src/program/event.ts | 28 ++++++---- ts/src/program/index.ts | 56 ++++++++++---------- ts/src/program/namespace/account.ts | 10 ++-- ts/src/program/namespace/index.ts | 2 +- ts/src/program/namespace/instruction.ts | 19 ++++--- ts/src/program/namespace/rpc.ts | 2 +- ts/src/program/namespace/simulate.ts | 12 +++-- ts/src/program/namespace/state.ts | 38 +++++--------- ts/src/provider.ts | 19 ++++--- ts/src/workspace.ts | 3 ++ ts/tsconfig.json | 4 +- ts/yarn.lock | 8 +-- 23 files changed, 209 insertions(+), 220 deletions(-) diff --git a/ts/package.json b/ts/package.json index 0180c83c9..5e16fe810 100644 --- a/ts/package.json +++ b/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" } } diff --git a/ts/src/coder/accounts.ts b/ts/src/coder/accounts.ts index 0eb9f6ac1..5b86ea6b4 100644 --- a/ts/src/coder/accounts.ts +++ b/ts/src/coder/accounts.ts @@ -35,6 +35,9 @@ export class AccountsCoder { ): Promise { 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); } } diff --git a/ts/src/coder/common.ts b/ts/src/coder/common.ts index ad2678ff4..9adec07e5 100644 --- a/ts/src/coder/common.ts +++ b/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)}`); diff --git a/ts/src/coder/event.ts b/ts/src/coder/event.ts index 82bee6aaf..47938eb12 100644 --- a/ts/src/coder/event.ts +++ b/ts/src/coder/event.ts @@ -46,7 +46,7 @@ export class EventCoder { ); } - public decode(log: string): Event | null { + public decode>(log: string): Event | 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 }; } } diff --git a/ts/src/coder/idl.ts b/ts/src/coder/idl.ts index 30be64a99..a2344897c 100644 --- a/ts/src/coder/idl.ts +++ b/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, + 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") { diff --git a/ts/src/coder/instruction.ts b/ts/src/coder/instruction.ts index a282c706d..dce8886c7 100644 --- a/ts/src/coder/instruction.ts +++ b/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) { diff --git a/ts/src/coder/types.ts b/ts/src/coder/types.ts index da3807b6d..b79d8a31e 100644 --- a/ts/src/coder/types.ts +++ b/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(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(accountName: string, ix: Buffer): T { const layout = this.layouts.get(accountName); + if (!layout) { + throw new Error(`Unknown account type: ${accountName}`); + } return layout.decode(ix); } } diff --git a/ts/src/error.ts b/ts/src/error.ts index 28242c910..cb32adb14 100644 --- a/ts/src/error.ts +++ b/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 { diff --git a/ts/src/idl.ts b/ts/src/idl.ts index 1c92e3c70..3d8548923 100644 --- a/ts/src/idl.ts +++ b/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; export type IdlType = diff --git a/ts/src/index.ts b/ts/src/index.ts index 76be00bfe..b7c78a80d 100644 --- a/ts/src/index.ts +++ b/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"; diff --git a/ts/src/program/common.ts b/ts/src/program/common.ts index 0587a8b9e..242f4627c 100644 --- a/ts/src/program/common.ts +++ b/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.`); diff --git a/ts/src/program/event.ts b/ts/src/program/event.ts index e4f686cae..4b3b28671 100644 --- a/ts/src/program/event.ts +++ b/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> = { 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]; } diff --git a/ts/src/program/index.ts b/ts/src/program/index.ts index 8831cf212..9d04d2009 100644 --- a/ts/src/program/index.ts +++ b/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 { 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 { 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); diff --git a/ts/src/program/namespace/account.ts b/ts/src/program/namespace/account.ts index a2979b0e8..f020f7710 100644 --- a/ts/src/program/namespace/account.ts +++ b/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(); diff --git a/ts/src/program/namespace/index.ts b/ts/src/program/namespace/index.ts index d3443a912..bea630ca3 100644 --- a/ts/src/program/namespace/index.ts +++ b/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 = {}; diff --git a/ts/src/program/namespace/instruction.ts b/ts/src/program/namespace/instruction.ts index 92fc50182..b2eb87d5c 100644 --- a/ts/src/program/namespace/instruction.ts +++ b/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; diff --git a/ts/src/program/namespace/rpc.ts b/ts/src/program/namespace/rpc.ts index c45b07b76..35fdd8cca 100644 --- a/ts/src/program/namespace/rpc.ts +++ b/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"; diff --git a/ts/src/program/namespace/simulate.ts b/ts/src/program/namespace/simulate.ts index 1581a597c..6c4e62413 100644 --- a/ts/src/program/namespace/simulate.ts +++ b/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 => { const tx = txFn(...args); const [, ctx] = splitArgsAndCtx(idlIx, [...args]); - let resp = undefined; + let resp: + | RpcResponseAndContext + | 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) => { diff --git a/ts/src/program/namespace/state.ts b/ts/src/program/namespace/state.ts index f341f3e51..f4eb5881c 100644 --- a/ts/src/program/namespace/state.ts +++ b/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"); } diff --git a/ts/src/provider.ts b/ts/src/provider.ts index ace0740e3..da9369c75 100644 --- a/ts/src/provider.ts +++ b/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, - opts?: ConfirmOptions + opts: ConfirmOptions = this.opts ): Promise> { 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" ); } } diff --git a/ts/src/workspace.ts b/ts/src/workspace.ts index 867ac1df5..a1e01bfef 100644 --- a/ts/src/workspace.ts +++ b/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); }); } diff --git a/ts/tsconfig.json b/ts/tsconfig.json index 38d53980a..019919169 100644 --- a/ts/tsconfig.json +++ b/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"] } } } diff --git a/ts/yarn.lock b/ts/yarn.lock index 90dfdae89..ed2f35e8c 100644 --- a/ts/yarn.lock +++ b/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"