From e65ecfa03a19092198dcb375cf6fbda87cea837f Mon Sep 17 00:00:00 2001 From: armaniferrante Date: Tue, 25 May 2021 01:37:24 -0700 Subject: [PATCH 01/16] ts: Refactor --- ts/src/idl.ts | 4 +- ts/src/index.ts | 6 +- ts/src/program/common.ts | 2 - ts/src/program/namespace/account.ts | 393 ++++++++++++++-------------- ts/src/program/namespace/index.ts | 2 +- ts/src/program/namespace/rpc.ts | 3 +- 6 files changed, 212 insertions(+), 198 deletions(-) diff --git a/ts/src/idl.ts b/ts/src/idl.ts index 8ff0254405..821fd1b76f 100644 --- a/ts/src/idl.ts +++ b/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" diff --git a/ts/src/index.ts b/ts/src/index.ts index 964aa73755..8d845aab07 100644 --- a/ts/src/index.ts +++ b/ts/src/index.ts @@ -7,7 +7,7 @@ import workspace from "./workspace"; import utils from "./utils"; import { Program } from "./program"; import { Address } from "./program/common"; -import { ProgramAccount } from "./program/namespace"; +import { ProgramAccount, AccountNamespace, AccountClient, RpcNamespace, RpcFn } from "./program/namespace"; import { Context, Accounts } from "./program/context"; let _provider: Provider | null = null; @@ -26,6 +26,10 @@ function getProvider(): Provider { export { workspace, Program, + AccountNamespace, + AccountClient, + RpcNamespace, + RpcFn, ProgramAccount, Context, Accounts, diff --git a/ts/src/program/common.ts b/ts/src/program/common.ts index 8e902f51af..74640c72cb 100644 --- a/ts/src/program/common.ts +++ b/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; diff --git a/ts/src/program/namespace/account.ts b/ts/src/program/namespace/account.ts index cc3aa2b66e..58caeade5d 100644 --- a/ts/src/program/namespace/account.ts +++ b/ts/src/program/namespace/account.ts @@ -9,7 +9,7 @@ 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, @@ -17,31 +17,215 @@ import Coder, { } from "../../coder"; import { Subscription, Address, translateAddress } from "../common"; +export default class AccountFactory { + // Returns the generated accounts namespace. + public static build( + idl: Idl, + coder: Coder, + programId: PublicKey, + provider: Provider + ): AccountNamespace { + const accountFns: AccountNamespace = {}; + + idl.accounts.forEach((idlAccount) => { + const name = camelCase(idlAccount.name); + accountFns[name] = new AccountClient(idl, idlAccount, coder, programId, provider); + }); + + return accountFns; + } +} + /** * Accounts is a dynamically generated object to fetch any given account * of a program. */ export interface AccountNamespace { - [key: string]: AccountFn; + [key: string]: AccountClient; } -/** - * Account is a function returning a deserialized account, given an address. - */ -export type AccountFn = AccountProps & ((address: PublicKey) => T); +export class AccountClient { + /** + * Returns the number of bytes in this account. + */ + get size(): number { + return this._size; + } + private _size: number; + + get programId(): PublicKey { + return this._programId; + } + private _programId: PublicKey; + + get provider(): Provider { + return this._provider; + } + private _provider: Provider; + + get coder(): Coder { + return this._coder; + } + private _coder: Coder; + + private _idlAccount: IdlTypeDef; + + constructor( + idl: Idl, + idlAccount: IdlTypeDef, + coder: Coder, + programId: PublicKey, + provider: Provider + ) { + this._idlAccount = idlAccount; + this._coder = coder; + this._programId = programId; + this._provider = provider; + 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 { + 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[]> { + let bytes = await accountDiscriminator(this._idlAccount.name); + if (filter !== undefined) { + bytes = Buffer.concat([bytes, filter]); + } + + let resp = await this._provider.connection.getProgramAccounts( + this._programId, + { + commitment: this._provider.connection.commitment, + filters: [ + { + memcmp: { + offset: 0, + bytes: bs58.encode(bytes), + }, + }, + ], + }, + ); + return ( + resp + .map(({ pubkey, account }) => { + return { + publicKey: pubkey, + account: this._coder.accounts.decode(this._idlAccount.name, account.data), + }; + }) + ); + } + + /** + * 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; + } + + 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 + ); + + subscriptions.set(address.toString(), { + ee, + listener, + }); -/** - * Non function properties on the acccount namespace. - */ -type AccountProps = { - size: number; - all: (filter?: Buffer) => Promise[]>; - subscribe: (address: Address, commitment?: Commitment) => EventEmitter; - unsubscribe: (address: Address) => void; - createInstruction: (signer: Signer) => Promise; - associated: (...args: PublicKey[]) => Promise; - associatedAddress: (...args: PublicKey[]) => Promise; -}; + 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 { + 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 { + 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 { + 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 @@ -55,176 +239,3 @@ export type ProgramAccount = { // Tracks all subscriptions. const subscriptions: Map = new Map(); - -export default class AccountFactory { - // Returns the generated accounts namespace. - public static build( - idl: Idl, - coder: Coder, - programId: PublicKey, - provider: Provider - ): AccountNamespace { - const accountFns: AccountNamespace = {}; - - idl.accounts.forEach((idlAccount) => { - const name = camelCase(idlAccount.name); - - // Fetches the decoded account from the network. - const accountsNamespace = async (address: Address): Promise => { - const accountInfo = await 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(idlAccount.name); - if (discriminator.compare(accountInfo.data.slice(0, 8))) { - throw new Error("Invalid account discriminator"); - } - - return coder.accounts.decode(idlAccount.name, accountInfo.data); - }; - - // Returns the size of the account. - // @ts-ignore - accountsNamespace["size"] = - ACCOUNT_DISCRIMINATOR_SIZE + accountSize(idl, idlAccount); - - // Returns an instruction for creating this account. - // @ts-ignore - accountsNamespace["createInstruction"] = async ( - signer: Signer, - sizeOverride?: number - ): Promise => { - // @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, - }); - }; - - // 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 - ); - - subscriptions.set(address.toString(), { - ee, - listener, - }); - - return ee; - }; - - // 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); - } - }; - - // Returns all instances of this account type for the program. - // @ts-ignore - accountsNamespace["all"] = async ( - filter?: Buffer - ): Promise[]> => { - let bytes = await accountDiscriminator(idlAccount.name); - if (filter !== undefined) { - bytes = Buffer.concat([bytes, filter]); - } - // @ts-ignore - let resp = await provider.connection._rpcRequest("getProgramAccounts", [ - programId.toBase58(), - { - commitment: provider.connection.commitment, - filters: [ - { - memcmp: { - offset: 0, - bytes: bs58.encode(bytes), - }, - }, - ], - }, - ]); - if (resp.error) { - console.error(resp); - throw new Error("Failed to get accounts"); - } - return ( - resp.result - // @ts-ignore - .map(({ pubkey, account: { data } }) => { - data = bs58.decode(data); - return { - publicKey: new PublicKey(pubkey), - account: coder.accounts.decode(idlAccount.name, data), - }; - }) - ); - }; - - // Function returning the associated address. Args are keys to associate. - // Order matters. - accountsNamespace["associatedAddress"] = async ( - ...args: Address[] - ): Promise => { - 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; - }; - - // Function returning the associated account. Args are keys to associate. - // Order matters. - accountsNamespace["associated"] = async ( - ...args: Address[] - ): Promise => { - const addr = await accountsNamespace["associatedAddress"](...args); - return await accountsNamespace(addr); - }; - - accountFns[name] = accountsNamespace; - }); - - return accountFns; - } -} diff --git a/ts/src/program/namespace/index.ts b/ts/src/program/namespace/index.ts index 3f74740971..0a0a578ef3 100644 --- a/ts/src/program/namespace/index.ts +++ b/ts/src/program/namespace/index.ts @@ -16,7 +16,7 @@ export { StateNamespace } from "./state"; export { InstructionNamespace } from "./instruction"; export { TransactionNamespace, TxFn } from "./transaction"; export { RpcNamespace, RpcFn } from "./rpc"; -export { AccountNamespace, AccountFn, ProgramAccount } from "./account"; +export { AccountNamespace, AccountClient, ProgramAccount } from "./account"; export { SimulateNamespace } from "./simulate"; export default class NamespaceFactory { diff --git a/ts/src/program/namespace/rpc.ts b/ts/src/program/namespace/rpc.ts index 10ff718367..b72f6d392c 100644 --- a/ts/src/program/namespace/rpc.ts +++ b/ts/src/program/namespace/rpc.ts @@ -13,7 +13,8 @@ export interface RpcNamespace { } /** - * RpcFn is a single rpc method generated from an IDL. + * 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; From 4d3e3c76236c5ef46086a9ff50e43881081798d8 Mon Sep 17 00:00:00 2001 From: armaniferrante Date: Tue, 25 May 2021 01:37:57 -0700 Subject: [PATCH 02/16] Prettier --- ts/src/idl.ts | 4 +- ts/src/index.ts | 16 ++- ts/src/program/namespace/account.ts | 156 +++++++++++++++------------- 3 files changed, 97 insertions(+), 79 deletions(-) diff --git a/ts/src/idl.ts b/ts/src/idl.ts index 821fd1b76f..95aab38c84 100644 --- a/ts/src/idl.ts +++ b/ts/src/idl.ts @@ -78,8 +78,8 @@ export type IdlType = | "i32" | "u64" | "i64" - | "u128" - | "i128" + | "u128" + | "i128" | "bytes" | "string" | "publicKey" diff --git a/ts/src/index.ts b/ts/src/index.ts index 8d845aab07..d7ab609bdd 100644 --- a/ts/src/index.ts +++ b/ts/src/index.ts @@ -7,7 +7,13 @@ import workspace from "./workspace"; import utils from "./utils"; import { Program } from "./program"; import { Address } from "./program/common"; -import { ProgramAccount, AccountNamespace, AccountClient, RpcNamespace, RpcFn } from "./program/namespace"; +import { + ProgramAccount, + AccountNamespace, + AccountClient, + RpcNamespace, + RpcFn, +} from "./program/namespace"; import { Context, Accounts } from "./program/context"; let _provider: Provider | null = null; @@ -26,10 +32,10 @@ function getProvider(): Provider { export { workspace, Program, - AccountNamespace, - AccountClient, - RpcNamespace, - RpcFn, + AccountNamespace, + AccountClient, + RpcNamespace, + RpcFn, ProgramAccount, Context, Accounts, diff --git a/ts/src/program/namespace/account.ts b/ts/src/program/namespace/account.ts index 58caeade5d..c5dea6f819 100644 --- a/ts/src/program/namespace/account.ts +++ b/ts/src/program/namespace/account.ts @@ -29,7 +29,13 @@ export default class AccountFactory { idl.accounts.forEach((idlAccount) => { const name = camelCase(idlAccount.name); - accountFns[name] = new AccountClient(idl, idlAccount, coder, programId, provider); + accountFns[name] = new AccountClient( + idl, + idlAccount, + coder, + programId, + provider + ); }); return accountFns; @@ -45,51 +51,51 @@ export interface AccountNamespace { } export class AccountClient { - /** - * Returns the number of bytes in this account. - */ + /** + * Returns the number of bytes in this account. + */ get size(): number { - return this._size; - } - private _size: number; + return this._size; + } + private _size: number; - get programId(): PublicKey { - return this._programId; - } - private _programId: PublicKey; + get programId(): PublicKey { + return this._programId; + } + private _programId: PublicKey; - get provider(): Provider { - return this._provider; - } - private _provider: Provider; + get provider(): Provider { + return this._provider; + } + private _provider: Provider; - get coder(): Coder { - return this._coder; - } - private _coder: Coder; + get coder(): Coder { + return this._coder; + } + private _coder: Coder; - private _idlAccount: IdlTypeDef; + private _idlAccount: IdlTypeDef; - constructor( + constructor( idl: Idl, - idlAccount: IdlTypeDef, + idlAccount: IdlTypeDef, coder: Coder, programId: PublicKey, provider: Provider - ) { - this._idlAccount = idlAccount; - this._coder = coder; - this._programId = programId; - this._provider = provider; - 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 { + ) { + this._idlAccount = idlAccount; + this._coder = coder; + this._programId = programId; + this._provider = provider; + 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 { const accountInfo = await this._provider.connection.getAccountInfo( translateAddress(address) ); @@ -104,11 +110,11 @@ export class AccountClient { } return this._coder.accounts.decode(this._idlAccount.name, accountInfo.data); - } + } - /** - * Returns all instances of this account type for the program. - */ + /** + * Returns all instances of this account type for the program. + */ async all(filter?: Buffer): Promise[]> { let bytes = await accountDiscriminator(this._idlAccount.name); if (filter !== undefined) { @@ -127,24 +133,24 @@ export class AccountClient { }, }, ], - }, + } ); - return ( - resp - .map(({ pubkey, account }) => { - return { - publicKey: pubkey, - account: this._coder.accounts.decode(this._idlAccount.name, account.data), - }; - }) - ); - } + return resp.map(({ pubkey, account }) => { + return { + publicKey: pubkey, + account: this._coder.accounts.decode( + this._idlAccount.name, + account.data + ), + }; + }); + } - /** - * Returns an `EventEmitter` emitting a "change" event whenever the account - * changes. - */ - subscribe (address: Address, commitment?: Commitment): EventEmitter { + /** + * 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; } @@ -154,7 +160,10 @@ export class AccountClient { const listener = this._provider.connection.onAccountChange( address, (acc) => { - const account = this._coder.accounts.decode(this._idlAccount.name, acc.data); + const account = this._coder.accounts.decode( + this._idlAccount.name, + acc.data + ); ee.emit("change", account); }, commitment @@ -166,11 +175,11 @@ export class AccountClient { }); return ee; - } + } - /** - * Unsubscribes from the account at the given address. - */ + /** + * Unsubscribes from the account at the given address. + */ unsubscribe(address: Address) { let sub = subscriptions.get(address.toString()); if (!sub) { @@ -185,12 +194,15 @@ export class AccountClient { }) .catch(console.error); } - } + } /** - * Returns an instruction for creating this account. - */ - async createInstruction(signer: Signer, sizeOverride?: number): Promise { + * Returns an instruction for creating this account. + */ + async createInstruction( + signer: Signer, + sizeOverride?: number + ): Promise { const size = this.size; return SystemProgram.createAccount({ @@ -202,21 +214,21 @@ export class AccountClient { ), programId: this._programId, }); - } + } /** - * Function returning the associated account. Args are keys to associate. + * Function returning the associated account. Args are keys to associate. * Order matters. - */ + */ async associated(...args: PublicKey[]): Promise { const addr = await this.associatedAddress(...args); return await this.fetch(addr); - } + } /** - * Function returning the associated address. Args are keys to associate. + * Function returning the associated address. Args are keys to associate. * Order matters. - */ + */ async associatedAddress(...args: PublicKey[]): Promise { let seeds = [Buffer.from([97, 110, 99, 104, 111, 114])]; // b"anchor". args.forEach((arg) => { @@ -224,7 +236,7 @@ export class AccountClient { }); const [assoc] = await PublicKey.findProgramAddress(seeds, this._programId); return assoc; - } + } } /** From e15271eab42e0cbc8bd0afeaf0b01b5eebefb5c5 Mon Sep 17 00:00:00 2001 From: armaniferrante Date: Tue, 25 May 2021 02:02:49 -0700 Subject: [PATCH 03/16] Stash --- ts/src/index.ts | 16 +++ ts/src/program/namespace/account.ts | 3 +- ts/src/program/namespace/index.ts | 8 +- ts/src/program/namespace/instruction.ts | 6 +- ts/src/program/namespace/rpc.ts | 4 +- ts/src/program/namespace/simulate.ts | 9 +- ts/src/program/namespace/state.ts | 129 ++++++++++++++++-------- ts/src/program/namespace/transaction.ts | 13 ++- 8 files changed, 125 insertions(+), 63 deletions(-) diff --git a/ts/src/index.ts b/ts/src/index.ts index d7ab609bdd..e5e0465a5d 100644 --- a/ts/src/index.ts +++ b/ts/src/index.ts @@ -11,8 +11,16 @@ import { ProgramAccount, AccountNamespace, AccountClient, + StateNamespace, + StateClient, RpcNamespace, RpcFn, + SimulateNamespace, + SimulateFn, + TransactionNamespace, + TransactionFn, + InstructionNamespace, + InstructionFn, } from "./program/namespace"; import { Context, Accounts } from "./program/context"; @@ -34,8 +42,16 @@ export { Program, AccountNamespace, AccountClient, + StateNamespace, + StateClient, RpcNamespace, RpcFn, + SimulateNamespace, + SimulateFn, + TransactionNamespace, + TransactionFn, + InstructionNamespace, + InstructionFn, ProgramAccount, Context, Accounts, diff --git a/ts/src/program/namespace/account.ts b/ts/src/program/namespace/account.ts index c5dea6f819..eef574d4f5 100644 --- a/ts/src/program/namespace/account.ts +++ b/ts/src/program/namespace/account.ts @@ -43,8 +43,7 @@ export default class AccountFactory { } /** - * Accounts is a dynamically generated object to fetch any given account - * of a program. + * Dynamically generated account namespace. */ export interface AccountNamespace { [key: string]: AccountClient; diff --git a/ts/src/program/namespace/index.ts b/ts/src/program/namespace/index.ts index 0a0a578ef3..981a9dc86f 100644 --- a/ts/src/program/namespace/index.ts +++ b/ts/src/program/namespace/index.ts @@ -12,12 +12,12 @@ import AccountFactory, { AccountNamespace } from "./account"; import SimulateFactory, { SimulateNamespace } from "./simulate"; // Re-exports. -export { StateNamespace } from "./state"; -export { InstructionNamespace } from "./instruction"; -export { TransactionNamespace, TxFn } from "./transaction"; +export { StateNamespace, StateClient } from "./state"; +export { InstructionNamespace, InstructionFn } from "./instruction"; +export { TransactionNamespace, TransactionFn } from "./transaction"; export { RpcNamespace, RpcFn } from "./rpc"; export { AccountNamespace, AccountClient, ProgramAccount } from "./account"; -export { SimulateNamespace } from "./simulate"; +export { SimulateNamespace, SimulateFn } from "./simulate"; export default class NamespaceFactory { /** diff --git a/ts/src/program/namespace/instruction.ts b/ts/src/program/namespace/instruction.ts index a796fac71f..3f99d883de 100644 --- a/ts/src/program/namespace/instruction.ts +++ b/ts/src/program/namespace/instruction.ts @@ -14,13 +14,13 @@ import { Accounts, splitArgsAndCtx } from "../context"; * Dynamically generated instruction namespace. */ export interface InstructionNamespace { - [key: string]: IxFn; + [key: string]: InstructionFn; } /** * Ix is a function to create a `TransactionInstruction` generated from an IDL. */ -export type IxFn = IxProps & ((...args: any[]) => any); +export type InstructionFn = IxProps & ((...args: any[]) => any); type IxProps = { accounts: (ctx: Accounts) => any; }; @@ -31,7 +31,7 @@ export default class InstructionNamespaceFactory { idlIx: IdlInstruction, coder: Coder, programId: PublicKey - ): IxFn { + ): InstructionFn { if (idlIx.name === "_inner") { throw new IdlError("the _inner name is reserved"); } diff --git a/ts/src/program/namespace/rpc.ts b/ts/src/program/namespace/rpc.ts index b72f6d392c..17d8beb10f 100644 --- a/ts/src/program/namespace/rpc.ts +++ b/ts/src/program/namespace/rpc.ts @@ -3,7 +3,7 @@ import Provider from "../../provider"; import { IdlInstruction } from "../../idl"; import { translateError } from "../common"; import { splitArgsAndCtx } from "../context"; -import { TxFn } from "./transaction"; +import { TransactionFn } from "./transaction"; /** * Dynamically generated rpc namespace. @@ -22,7 +22,7 @@ export default class RpcFactory { // Builds the rpc namespace. public static build( idlIx: IdlInstruction, - txFn: TxFn, + txFn: TransactionFn, idlErrors: Map, provider: Provider ): RpcFn { diff --git a/ts/src/program/namespace/simulate.ts b/ts/src/program/namespace/simulate.ts index 48e1618a0e..4a020d76e4 100644 --- a/ts/src/program/namespace/simulate.ts +++ b/ts/src/program/namespace/simulate.ts @@ -3,7 +3,7 @@ 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"; @@ -16,7 +16,10 @@ export interface SimulateNamespace { } /** - * RpcFn is a single rpc method generated from an IDL. + * 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; @@ -29,7 +32,7 @@ export default class SimulateFactory { // Builds the rpc namespace. public static build( idlIx: IdlInstruction, - txFn: TxFn, + txFn: TransactionFn, idlErrors: Map, provider: Provider, coder: Coder, diff --git a/ts/src/program/namespace/state.ts b/ts/src/program/namespace/state.ts index dacc6db280..46ee00f40c 100644 --- a/ts/src/program/namespace/state.ts +++ b/ts/src/program/namespace/state.ts @@ -21,6 +21,91 @@ import { import { Accounts, splitArgsAndCtx } from "../context"; import InstructionNamespaceFactory from "./instruction"; +export class StateClient { + get rpc(): RpcNamespace { + return this._rpc; + } + private _rpc: RpcNamespace; + + get instruction(): InstructionNamespace { + return this._instruction; + } + private _instruction: InstructionNamespace; + + get programId(): PublicKey { + return this._programId; + } + private _programId: PublicKey; + + get provider(): Provider { + return this._provider; + } + private _provider: Provider; + + get coder(): Coder { + return this._coder; + } + private _coder: Coder; + + private _idl: Idl; + + private _sub: Subscription | null; + + constructor( + idl: Idl, + coder: Coder, + programId: PublicKey, + idlErrors: Map, + provider: Provider + ) { + this._idl = idl; + this._coder = coder; + this._programId = programId; + this._provider = provider; + this._sub = null; + } + + async address(): Promise { + return await programStateAddress(this.programId); + } + + subscribe(commitment?: Commitment): EventEmitter { + if (this._sub !== null) { + return this._sub.ee; + } + const ee = new EventEmitter(); + + this.address().then((address) => { + const listener = this.provider.connection.onAccountChange( + address, + (acc) => { + const account = this.coder.state.decode(acc.data); + ee.emit("change", account); + }, + commitment + ); + + this._sub = { + ee, + listener, + }; + }); + + return ee; + } + + unsubscribe() { + if (this._sub !== null) { + this.provider.connection + .removeAccountChangeListener(this._sub.listener) + .then(async () => { + this._sub = null; + }) + .catch(console.error); + } + } +} + export type StateNamespace = () => | Promise | { @@ -113,50 +198,6 @@ export default class StateFactory { state["rpc"] = rpc; state["instruction"] = ix; - // Calculates the address of the program's global state object account. - state["address"] = async (): Promise => - programStateAddress(programId); - - // Subscription singleton. - let sub: null | Subscription = null; - - // Subscribe to account changes. - state["subscribe"] = (commitment?: Commitment): EventEmitter => { - if (sub !== null) { - return sub.ee; - } - const ee = new EventEmitter(); - - state["address"]().then((address) => { - const listener = provider.connection.onAccountChange( - address, - (acc) => { - const account = coder.state.decode(acc.data); - ee.emit("change", account); - }, - commitment - ); - - sub = { - ee, - listener, - }; - }); - - return ee; - }; - - // Unsubscribe from account changes. - state["unsubscribe"] = () => { - if (sub !== null) { - provider.connection - .removeAccountChangeListener(sub.listener) - .then(async () => { - sub = null; - }) - .catch(console.error); - } - }; return state; } diff --git a/ts/src/program/namespace/transaction.ts b/ts/src/program/namespace/transaction.ts index 06ee67aabe..e08c6cc46d 100644 --- a/ts/src/program/namespace/transaction.ts +++ b/ts/src/program/namespace/transaction.ts @@ -1,23 +1,26 @@ import { Transaction } from "@solana/web3.js"; import { IdlInstruction } from "../../idl"; import { splitArgsAndCtx } from "../context"; -import { IxFn } from "./instruction"; +import { InstructionFn } from "./instruction"; /** * Dynamically generated transaction namespace. */ export interface TransactionNamespace { - [key: string]: TxFn; + [key: string]: TransactionFn; } /** - * Tx is a function to create a `Transaction` generate from an IDL. + * Tx is a function to create a `Transaction` for a given program instruction. */ -export type TxFn = (...args: any[]) => Transaction; +export type TransactionFn = (...args: any[]) => Transaction; 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(); From 5514c877e722fa08ba0d45d186a6799afc86b21f Mon Sep 17 00:00:00 2001 From: armaniferrante Date: Tue, 25 May 2021 14:10:02 -0700 Subject: [PATCH 04/16] Update --- ts/src/index.ts | 2 - ts/src/program/index.ts | 4 +- ts/src/program/namespace/index.ts | 12 +- ts/src/program/namespace/instruction.ts | 15 +- ts/src/program/namespace/state.ts | 254 +++++++++++------------- ts/src/utils.ts | 82 ++++++++ 6 files changed, 212 insertions(+), 157 deletions(-) diff --git a/ts/src/index.ts b/ts/src/index.ts index e5e0465a5d..1d5b8f014a 100644 --- a/ts/src/index.ts +++ b/ts/src/index.ts @@ -11,7 +11,6 @@ import { ProgramAccount, AccountNamespace, AccountClient, - StateNamespace, StateClient, RpcNamespace, RpcFn, @@ -42,7 +41,6 @@ export { Program, AccountNamespace, AccountClient, - StateNamespace, StateClient, RpcNamespace, RpcFn, diff --git a/ts/src/program/index.ts b/ts/src/program/index.ts index 99f15f6277..b643053c6d 100644 --- a/ts/src/program/index.ts +++ b/ts/src/program/index.ts @@ -8,7 +8,7 @@ import NamespaceFactory, { InstructionNamespace, TransactionNamespace, AccountNamespace, - StateNamespace, + StateClient, SimulateNamespace, } from "./namespace"; import { getProvider } from "../"; @@ -194,7 +194,7 @@ export class Program { /** * Object with state account accessors and rpcs. */ - readonly state: StateNamespace; + readonly state: StateClient; /** * Address of the program. diff --git a/ts/src/program/namespace/index.ts b/ts/src/program/namespace/index.ts index 981a9dc86f..2821a03d15 100644 --- a/ts/src/program/namespace/index.ts +++ b/ts/src/program/namespace/index.ts @@ -4,7 +4,7 @@ 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"; @@ -12,7 +12,7 @@ import AccountFactory, { AccountNamespace } from "./account"; import SimulateFactory, { SimulateNamespace } from "./simulate"; // Re-exports. -export { StateNamespace, StateClient } from "./state"; +export { StateClient } from "./state"; export { InstructionNamespace, InstructionFn } from "./instruction"; export { TransactionNamespace, TransactionFn } from "./transaction"; export { RpcNamespace, RpcFn } from "./rpc"; @@ -33,7 +33,7 @@ export default class NamespaceFactory { InstructionNamespace, TransactionNamespace, AccountNamespace, - StateNamespace, + StateClient, SimulateNamespace ] { const idlErrors = parseIdlErrors(idl); @@ -52,7 +52,11 @@ export default class NamespaceFactory { ); 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( diff --git a/ts/src/program/namespace/instruction.ts b/ts/src/program/namespace/instruction.ts index 3f99d883de..90a0190ead 100644 --- a/ts/src/program/namespace/instruction.ts +++ b/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, @@ -25,11 +24,13 @@ type IxProps = { accounts: (ctx: Accounts) => any; }; +export type InstructionEncodeFn = (ixName: string, ix: any) => Buffer; + export default class InstructionNamespaceFactory { // Builds the instuction namespace. public static build( idlIx: IdlInstruction, - coder: Coder, + encodeFn: InstructionEncodeFn, programId: PublicKey ): InstructionFn { if (idlIx.name === "_inner") { @@ -41,10 +42,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 +54,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)), }); }; diff --git a/ts/src/program/namespace/state.ts b/ts/src/program/namespace/state.ts index 46ee00f40c..f8d07c4fb2 100644 --- a/ts/src/program/namespace/state.ts +++ b/ts/src/program/namespace/state.ts @@ -1,36 +1,44 @@ 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 { Subscription, validateAccounts } from "../common"; +import { findProgramAddressSync, createWithSeedSync } from "../../utils"; +import { Accounts } from "../context"; import InstructionNamespaceFactory from "./instruction"; +import RpcNamespaceFactory from "./rpc"; +import TransactionNamespaceFactory from "./transaction"; -export class StateClient { - get rpc(): RpcNamespace { - return this._rpc; +export default class StateFactory { + // Builds the state namespace. + public static build( + idl: Idl, + coder: Coder, + programId: PublicKey, + idlErrors: Map, + provider: Provider + ): StateClient | undefined { + if (idl.state === undefined) { + return undefined; + } + return new StateClient(idl, coder, programId, idlErrors, provider); } - private _rpc: RpcNamespace; +} - get instruction(): InstructionNamespace { - return this._instruction; - } - private _instruction: InstructionNamespace; +export class StateClient { + readonly rpc: RpcNamespace; + + readonly instruction: InstructionNamespace; + + readonly transaction: TransactionNamespace; get programId(): PublicKey { return this._programId; @@ -45,6 +53,9 @@ export class StateClient { get coder(): Coder { return this._coder; } + + private _address: PublicKey; + private _coder: Coder; private _idl: Idl; @@ -63,10 +74,74 @@ export class StateClient { this._programId = programId; this._provider = provider; this._sub = null; + this._address = programStateAddress(programId); + + // 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, + (ixName: string, ix: any) => + coder.instruction.encodeState(ixName, ix), + programId + ); + 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, + idlErrors, + provider + ); + + // Attach them all to their respective namespaces. + const name = camelCase(m.name); + instruction[name] = ixItem; + transaction[name] = txItem; + rpc[name] = rpcItem; + }); + + return [instruction, transaction, rpc]; + })(); + this.instruction = instruction; + this.transaction = transaction; + this.rpc = rpc; + } + + async fetch(): Promise { + 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); } - async address(): Promise { - return await programStateAddress(this.programId); + address(): PublicKey { + return this._address; } subscribe(commitment?: Commitment): EventEmitter { @@ -75,21 +150,19 @@ export class StateClient { } const ee = new EventEmitter(); - this.address().then((address) => { - const listener = this.provider.connection.onAccountChange( - address, - (acc) => { - const account = this.coder.state.decode(acc.data); - ee.emit("change", account); - }, - commitment - ); + 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, - }; - }); + this._sub = { + ee, + listener, + }; return ee; } @@ -106,112 +179,15 @@ export class StateClient { } } -export type StateNamespace = () => - | Promise - | { - address: () => Promise; - rpc: RpcNamespace; - instruction: InstructionNamespace; - subscribe: (commitment?: Commitment) => EventEmitter; - unsubscribe: () => void; - }; - -export default class StateFactory { - // Builds the state namespace. - public static build( - idl: Idl, - coder: Coder, - programId: PublicKey, - idlErrors: Map, - provider: Provider - ): StateNamespace | undefined { - if (idl.state === undefined) { - return undefined; - } - - // Fetches the state object from the blockchain. - const state = async (): Promise => { - const addr = await programStateAddress(programId); - const accountInfo = await provider.connection.getAccountInfo(addr); - if (accountInfo === null) { - throw new Error(`Account does not exist ${addr.toString()}`); - } - // Assert the account discriminator is correct. - const expectedDiscriminator = await stateDiscriminator( - idl.state.struct.name - ); - if (expectedDiscriminator.compare(accountInfo.data.slice(0, 8))) { - throw new Error("Invalid account discriminator"); - } - return coder.state.decode(accountInfo.data); - }; - - // Namespace with all rpc functions. - const rpc: RpcNamespace = {}; - const ix: InstructionNamespace = {}; - - idl.state.methods.forEach((m: IdlStateMethod) => { - const accounts = async (accounts: Accounts): Promise => { - const keys = await stateInstructionKeys( - programId, - provider, - m, - accounts - ); - return keys.concat( - InstructionNamespaceFactory.accountsArray(accounts, m.accounts) - ); - }; - const ixFn = async (...args: any[]): Promise => { - 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 => { - 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; - - return state; - } -} - // Calculates the deterministic address of the program's "state" account. -async function programStateAddress(programId: PublicKey): Promise { - 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, @@ -219,7 +195,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, @@ -227,7 +203,7 @@ async function stateInstructionKeys( isSigner: true, }, { - pubkey: await programStateAddress(programId), + pubkey: programStateAddress(programId), isWritable: true, isSigner: false, }, @@ -249,7 +225,7 @@ async function stateInstructionKeys( validateAccounts(m.accounts, accounts); return [ { - pubkey: await programStateAddress(programId), + pubkey: programStateAddress(programId), isWritable: true, isSigner: false, }, diff --git a/ts/src/utils.ts b/ts/src/utils.ts index 8410b8fde4..d9e4268858 100644 --- a/ts/src/utils.ts +++ b/ts/src/utils.ts @@ -1,5 +1,7 @@ +import BN from "bn.js"; import * as bs58 from "bs58"; import { sha256 } from "crypto-hash"; +import { sha256 as sha256Sync } from "js-sha256"; import assert from "assert"; import { PublicKey, AccountInfo, Connection } from "@solana/web3.js"; import { idlAddress } from "./idl"; @@ -77,11 +79,91 @@ export function decodeUtf8(array: Uint8Array): string { return decoder.decode(array); } +// 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, + 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, + 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): 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); + } +}; + const utils = { bs58, sha256, getMultipleAccounts, idlAddress, + publicKey: { + createProgramAddressSync, + findProgramAddressSync, + createWithSeedSync, + }, }; export default utils; From b58a382eab21ae97f97709382ce3fb5489e335a3 Mon Sep 17 00:00:00 2001 From: armaniferrante Date: Tue, 25 May 2021 14:25:30 -0700 Subject: [PATCH 05/16] Update --- ts/src/coder.ts | 10 +++++----- ts/src/index.ts | 13 ++++++++++++- 2 files changed, 17 insertions(+), 6 deletions(-) diff --git a/ts/src/coder.ts b/ts/src/coder.ts index bbf57f2b74..00b53c97f8 100644 --- a/ts/src/coder.ts +++ b/ts/src/coder.ts @@ -76,7 +76,7 @@ export default class Coder { /** * Encodes and decodes program instructions. */ -class InstructionCoder { +export class InstructionCoder { /** * Instruction args layout. Maps namespaced method */ @@ -136,7 +136,7 @@ class InstructionCoder { /** * Encodes and decodes account objects. */ -class AccountsCoder { +export class AccountsCoder { /** * Maps account type identifier to a layout. */ @@ -177,7 +177,7 @@ class AccountsCoder { /** * Encodes and decodes user defined types. */ -class TypesCoder { +export class TypesCoder { /** * Maps account type identifier to a layout. */ @@ -209,7 +209,7 @@ class TypesCoder { } } -class EventCoder { +export class EventCoder { /** * Maps account type identifier to a layout. */ @@ -266,7 +266,7 @@ class EventCoder { } } -class StateCoder { +export class StateCoder { private layout: Layout; public constructor(idl: Idl) { diff --git a/ts/src/index.ts b/ts/src/index.ts index 1d5b8f014a..ae05042449 100644 --- a/ts/src/index.ts +++ b/ts/src/index.ts @@ -1,12 +1,18 @@ 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 { Event } from "./program/event"; import { ProgramAccount, AccountNamespace, @@ -54,6 +60,11 @@ export { Context, Accounts, Coder, + InstructionCoder, + EventCoder, + StateCoder, + TypesCoder, + Event, setProvider, getProvider, Provider, From e3e83a6bb54abb9780672b8cf67726af6d724f41 Mon Sep 17 00:00:00 2001 From: armaniferrante Date: Tue, 25 May 2021 14:49:09 -0700 Subject: [PATCH 06/16] Update examples --- .../cashiers-check/tests/cashiers-check.js | 4 +-- examples/chat/tests/chat.js | 4 +-- examples/composite/tests/composite.js | 4 +-- examples/interface/tests/interface.js | 2 +- examples/lockup/tests/lockup.js | 34 +++++++++---------- examples/misc/tests/misc.js | 18 +++++----- examples/multisig/tests/multisig.js | 6 ++-- examples/tutorial/basic-1/tests/basic-1.js | 8 ++--- examples/tutorial/basic-2/tests/basic-2.js | 4 +-- examples/tutorial/basic-3/tests/basic-3.js | 2 +- examples/tutorial/basic-4/tests/basic-4.js | 2 +- examples/zero-copy/tests/zero-copy.js | 18 +++++----- 12 files changed, 53 insertions(+), 53 deletions(-) diff --git a/examples/cashiers-check/tests/cashiers-check.js b/examples/cashiers-check/tests/cashiers-check.js index bd1270601a..60b3726683 100644 --- a/examples/cashiers-check/tests/cashiers-check.js +++ b/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( diff --git a/examples/chat/tests/chat.js b/examples/chat/tests/chat.js index 2e5be183d1..19b444744f 100644 --- a/examples/chat/tests/chat.js +++ b/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); diff --git a/examples/composite/tests/composite.js b/examples/composite/tests/composite.js index 1913806a9f..67b7197b46 100644 --- a/examples/composite/tests/composite.js +++ b/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))); diff --git a/examples/interface/tests/interface.js b/examples/interface/tests/interface.js index 7696062cd3..bcf675bdce 100644 --- a/examples/interface/tests/interface.js +++ b/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)); }); diff --git a/examples/lockup/tests/lockup.js b/examples/lockup/tests/lockup.js index 91e91300a3..3a3831a85d 100644 --- a/examples/lockup/tests/lockup.js +++ b/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 ); diff --git a/examples/misc/tests/misc.js b/examples/misc/tests/misc.js index a806fd59b0..1c55ea0d34 100644 --- a/examples/misc/tests/misc.js +++ b/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); }); }); diff --git a/examples/multisig/tests/multisig.js b/examples/multisig/tests/multisig.js index 1662ed98ef..90a7cb96e3 100644 --- a/examples/multisig/tests/multisig.js +++ b/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))); diff --git a/examples/tutorial/basic-1/tests/basic-1.js b/examples/tutorial/basic-1/tests/basic-1.js index d4694da493..f4fd460e2f 100644 --- a/examples/tutorial/basic-1/tests/basic-1.js +++ b/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))); diff --git a/examples/tutorial/basic-2/tests/basic-2.js b/examples/tutorial/basic-2/tests/basic-2.js index 6c20a2429c..67093ec9c5 100644 --- a/examples/tutorial/basic-2/tests/basic-2.js +++ b/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) diff --git a/examples/tutorial/basic-3/tests/basic-3.js b/examples/tutorial/basic-3/tests/basic-3.js index 9832f83eb1..ac6db1e224 100644 --- a/examples/tutorial/basic-3/tests/basic-3.js +++ b/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))); }); }); diff --git a/examples/tutorial/basic-4/tests/basic-4.js b/examples/tutorial/basic-4/tests/basic-4.js index 5a8f684520..40228bd20d 100644 --- a/examples/tutorial/basic-4/tests/basic-4.js +++ b/examples/tutorial/basic-4/tests/basic-4.js @@ -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))); }); }); diff --git a/examples/zero-copy/tests/zero-copy.js b/examples/zero-copy/tests/zero-copy.js index b27b4eb907..3411bb2aeb 100644 --- a/examples/zero-copy/tests/zero-copy.js +++ b/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) { From d2c32bc307312309c9fce70866bf5889303d55cd Mon Sep 17 00:00:00 2001 From: armaniferrante Date: Tue, 25 May 2021 15:05:54 -0700 Subject: [PATCH 07/16] update --- CHANGELOG.md | 1 + ts/src/program/index.ts | 4 +-- ts/src/program/namespace/account.ts | 1 - ts/src/program/namespace/index.ts | 6 ++-- ts/src/program/namespace/instruction.ts | 35 ++++++++++----------- ts/src/program/namespace/rpc.ts | 27 ++++++++-------- ts/src/program/namespace/simulate.ts | 41 ++++++++++++------------- ts/src/program/namespace/state.ts | 1 - ts/src/program/namespace/transaction.ts | 25 ++++++++------- 9 files changed, 68 insertions(+), 73 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9e0aada096..556a9772dc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,7 @@ incremented for features. ## Breaking Changes +* ts: Retrieving deserialized accounts from the `.account.` and `.state` namespaces now require explicitly invoking the `fetch` API. For example, `program.account.myAccount()` and `program.state()` is now `program.account.myAccount.fetch(
)` 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. diff --git a/ts/src/program/index.ts b/ts/src/program/index.ts index b643053c6d..029c9a5138 100644 --- a/ts/src/program/index.ts +++ b/ts/src/program/index.ts @@ -249,15 +249,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; } /** diff --git a/ts/src/program/namespace/account.ts b/ts/src/program/namespace/account.ts index eef574d4f5..74872c38d6 100644 --- a/ts/src/program/namespace/account.ts +++ b/ts/src/program/namespace/account.ts @@ -18,7 +18,6 @@ import Coder, { import { Subscription, Address, translateAddress } from "../common"; export default class AccountFactory { - // Returns the generated accounts namespace. public static build( idl: Idl, coder: Coder, diff --git a/ts/src/program/namespace/index.ts b/ts/src/program/namespace/index.ts index 2821a03d15..fae9d2d155 100644 --- a/ts/src/program/namespace/index.ts +++ b/ts/src/program/namespace/index.ts @@ -33,8 +33,8 @@ export default class NamespaceFactory { InstructionNamespace, TransactionNamespace, AccountNamespace, - StateClient, - SimulateNamespace + SimulateNamespace, + StateClient ] { const idlErrors = parseIdlErrors(idl); @@ -81,6 +81,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]; } } diff --git a/ts/src/program/namespace/instruction.ts b/ts/src/program/namespace/instruction.ts index 90a0190ead..07480e1be6 100644 --- a/ts/src/program/namespace/instruction.ts +++ b/ts/src/program/namespace/instruction.ts @@ -9,25 +9,7 @@ import { } from "../common"; import { Accounts, splitArgsAndCtx } from "../context"; -/** - * Dynamically generated instruction namespace. - */ -export interface InstructionNamespace { - [key: string]: InstructionFn; -} - -/** - * Ix is a function to create a `TransactionInstruction` generated from an IDL. - */ -export type InstructionFn = IxProps & ((...args: any[]) => any); -type IxProps = { - accounts: (ctx: Accounts) => any; -}; - -export type InstructionEncodeFn = (ixName: string, ix: any) => Buffer; - export default class InstructionNamespaceFactory { - // Builds the instuction namespace. public static build( idlIx: IdlInstruction, encodeFn: InstructionEncodeFn, @@ -91,6 +73,23 @@ export default class InstructionNamespaceFactory { } } +/** + * Dynamically generated instruction namespace. + */ +export interface InstructionNamespace { + [key: string]: InstructionFn; +} + +/** + * Ix is a function to create a `TransactionInstruction` generated from an IDL. + */ +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 diff --git a/ts/src/program/namespace/rpc.ts b/ts/src/program/namespace/rpc.ts index 17d8beb10f..b600500582 100644 --- a/ts/src/program/namespace/rpc.ts +++ b/ts/src/program/namespace/rpc.ts @@ -5,21 +5,7 @@ import { translateError } from "../common"; import { splitArgsAndCtx } from "../context"; import { TransactionFn } from "./transaction"; -/** - * Dynamically generated rpc namespace. - */ -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; - export default class RpcFactory { - // Builds the rpc namespace. public static build( idlIx: IdlInstruction, txFn: TransactionFn, @@ -45,3 +31,16 @@ export default class RpcFactory { return rpc; } } + +/** + * Dynamically generated rpc namespace. + */ +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; diff --git a/ts/src/program/namespace/simulate.ts b/ts/src/program/namespace/simulate.ts index 4a020d76e4..c57c5ae13b 100644 --- a/ts/src/program/namespace/simulate.ts +++ b/ts/src/program/namespace/simulate.ts @@ -8,28 +8,7 @@ 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 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; - -type SimulateResponse = { - events: Event[]; - raw: string[]; -}; - export default class SimulateFactory { - // Builds the rpc namespace. public static build( idlIx: IdlInstruction, txFn: TransactionFn, @@ -77,3 +56,23 @@ export default class SimulateFactory { return simulate; } } + +/** + * Dynamically generated simualte namespace. + */ +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; + +type SimulateResponse = { + events: Event[]; + raw: string[]; +}; diff --git a/ts/src/program/namespace/state.ts b/ts/src/program/namespace/state.ts index f8d07c4fb2..51e282bc4b 100644 --- a/ts/src/program/namespace/state.ts +++ b/ts/src/program/namespace/state.ts @@ -18,7 +18,6 @@ import RpcNamespaceFactory from "./rpc"; import TransactionNamespaceFactory from "./transaction"; export default class StateFactory { - // Builds the state namespace. public static build( idl: Idl, coder: Coder, diff --git a/ts/src/program/namespace/transaction.ts b/ts/src/program/namespace/transaction.ts index e08c6cc46d..9bdf9ce81e 100644 --- a/ts/src/program/namespace/transaction.ts +++ b/ts/src/program/namespace/transaction.ts @@ -3,20 +3,7 @@ import { IdlInstruction } from "../../idl"; import { splitArgsAndCtx } from "../context"; import { InstructionFn } from "./instruction"; -/** - * Dynamically generated transaction namespace. - */ -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; - export default class TransactionFactory { - // Builds the transaction namespace. public static build( idlIx: IdlInstruction, ixFn: InstructionFn @@ -34,3 +21,15 @@ export default class TransactionFactory { return txFn; } } + +/** + * Dynamically generated transaction namespace. + */ +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; From b2467aa5c3b5775e5793a2234fa2e746790b4d4b Mon Sep 17 00:00:00 2001 From: armaniferrante Date: Tue, 25 May 2021 15:43:59 -0700 Subject: [PATCH 08/16] stash --- ts/src/program/index.ts | 42 +++++++------------------ ts/src/program/namespace/account.ts | 9 ++++++ ts/src/program/namespace/instruction.ts | 4 ++- ts/src/program/namespace/rpc.ts | 31 +++++++++++++++++- ts/src/program/namespace/state.ts | 27 +++++++++++++--- 5 files changed, 76 insertions(+), 37 deletions(-) diff --git a/ts/src/program/index.ts b/ts/src/program/index.ts index 029c9a5138..7c454096fe 100644 --- a/ts/src/program/index.ts +++ b/ts/src/program/index.ts @@ -41,36 +41,18 @@ import { Address, translateAddress } from "./common"; * [here](https://project-serum.github.io/anchor/ts/#examples). */ export class Program { - /** - * Async methods to send signed transactions invoking *non*-state methods - * on an Anchor program. - * - * ## rpc - * - * ```javascript - * program.rpc.(...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, - * }, - * }); - * ``` - */ + + /** + * The RPC namespace provides async methods for each instruction of an Anchor + * program. Invoking an RPC sends a signed transaction paid for and signed by + * the configured provider. + * + * ## rpc + * + * ```javascript + * program.rpc.(...args, ctx); + * ``` + */ readonly rpc: RpcNamespace; /** diff --git a/ts/src/program/namespace/account.ts b/ts/src/program/namespace/account.ts index 74872c38d6..c8980cc080 100644 --- a/ts/src/program/namespace/account.ts +++ b/ts/src/program/namespace/account.ts @@ -57,16 +57,25 @@ export class AccountClient { } private _size: number; + /** + * Returns the program ID owning all accounts. + */ get programId(): PublicKey { return this._programId; } private _programId: PublicKey; + /** + * Returns the cleint's wallet and network provider. + */ get provider(): Provider { return this._provider; } private _provider: Provider; + /** + * Returns the coder. + */ get coder(): Coder { return this._coder; } diff --git a/ts/src/program/namespace/instruction.ts b/ts/src/program/namespace/instruction.ts index 07480e1be6..05ba4add58 100644 --- a/ts/src/program/namespace/instruction.ts +++ b/ts/src/program/namespace/instruction.ts @@ -81,7 +81,9 @@ export interface InstructionNamespace { } /** - * Ix is a function to create a `TransactionInstruction` generated from an IDL. + * 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 = { diff --git a/ts/src/program/namespace/rpc.ts b/ts/src/program/namespace/rpc.ts index b600500582..18d4a1cbe1 100644 --- a/ts/src/program/namespace/rpc.ts +++ b/ts/src/program/namespace/rpc.ts @@ -33,7 +33,36 @@ export default class RpcFactory { } /** - * Dynamically generated rpc namespace. + * Dynamically generated rpc namespace, providing async methods to send signed + * transactions invoking *non*-state methods on an Anchor program. + * + * Keys are method names, values are RPC functions. + * + * ## rpc + * + * ```javascript + * rpc.(...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; diff --git a/ts/src/program/namespace/state.ts b/ts/src/program/namespace/state.ts index 51e282bc4b..78af66ca82 100644 --- a/ts/src/program/namespace/state.ts +++ b/ts/src/program/namespace/state.ts @@ -34,31 +34,35 @@ export default class StateFactory { export class StateClient { readonly rpc: RpcNamespace; - readonly instruction: InstructionNamespace; - readonly transaction: TransactionNamespace; + /** + * 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( @@ -123,6 +127,9 @@ export class StateClient { this.rpc = rpc; } + /** + * Returns the deserialized state account. + */ async fetch(): Promise { const addr = this.address(); const accountInfo = await this.provider.connection.getAccountInfo(addr); @@ -139,10 +146,17 @@ export class StateClient { return this.coder.state.decode(accountInfo.data); } + /** + * 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; @@ -166,6 +180,9 @@ export class StateClient { return ee; } + /** + * Unsubscribes to state changes. + */ unsubscribe() { if (this._sub !== null) { this.provider.connection From 14559d3a6827b101caa677c6a537528ad2e760dc Mon Sep 17 00:00:00 2001 From: armaniferrante Date: Tue, 25 May 2021 16:54:57 -0700 Subject: [PATCH 09/16] update --- examples/interface/tests/interface.js | 2 +- examples/tutorial/basic-4/tests/basic-4.js | 2 +- ts/package.json | 2 +- ts/src/program/index.ts | 67 +++++++++++++++------- ts/src/program/namespace/account.ts | 19 +++++- ts/src/program/namespace/instruction.ts | 30 +++++++++- ts/src/program/namespace/rpc.ts | 10 ++-- ts/src/program/namespace/simulate.ts | 33 ++++++++++- ts/src/program/namespace/transaction.ts | 28 ++++++++- 9 files changed, 159 insertions(+), 34 deletions(-) diff --git a/examples/interface/tests/interface.js b/examples/interface/tests/interface.js index bcf675bdce..a0e33413bb 100644 --- a/examples/interface/tests/interface.js +++ b/examples/interface/tests/interface.js @@ -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))); }); }); diff --git a/examples/tutorial/basic-4/tests/basic-4.js b/examples/tutorial/basic-4/tests/basic-4.js index 40228bd20d..509519a1b1 100644 --- a/examples/tutorial/basic-4/tests/basic-4.js +++ b/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))); diff --git a/ts/package.json b/ts/package.json index 346aadfbbf..05110902da 100644 --- a/ts/package.json +++ b/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", diff --git a/ts/src/program/index.ts b/ts/src/program/index.ts index 7c454096fe..f0be471b37 100644 --- a/ts/src/program/index.ts +++ b/ts/src/program/index.ts @@ -43,45 +43,64 @@ import { Address, translateAddress } from "./common"; export class Program { /** - * The RPC namespace provides async methods for each instruction of an Anchor - * program. Invoking an RPC sends a signed transaction paid for and signed by - * the configured provider. + * Async methods to send signed transactions to *non*-state methods on the + * program, returning a [[TransactionSignature]]. * - * ## rpc + * ## Usage * * ```javascript - * program.rpc.(...args, ctx); + * rpc.(...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, + * }, + * }); * ``` */ 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.(address); + * program.account. * ``` * - * ## 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.(...args, ctx); @@ -109,9 +128,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.(...args, ctx); @@ -139,8 +159,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 @@ -164,7 +185,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, * }, @@ -174,7 +195,9 @@ 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: StateClient; diff --git a/ts/src/program/namespace/account.ts b/ts/src/program/namespace/account.ts index c8980cc080..0cde1f91bb 100644 --- a/ts/src/program/namespace/account.ts +++ b/ts/src/program/namespace/account.ts @@ -42,7 +42,24 @@ export default class AccountFactory { } /** - * Dynamically generated account namespace. + * The namespace provides handles to an [[AccountClient]] object for each + * account in a program. + * + * ## Usage + * + * ```javascript + * account. + * ``` + * + * ## 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; diff --git a/ts/src/program/namespace/instruction.ts b/ts/src/program/namespace/instruction.ts index 05ba4add58..b14a194026 100644 --- a/ts/src/program/namespace/instruction.ts +++ b/ts/src/program/namespace/instruction.ts @@ -73,8 +73,34 @@ export default class InstructionNamespaceFactory { } } -/** - * Dynamically generated instruction namespace. +/* + * The namespace provides functions to build [[TransactionInstruction]] + * objects for each method of a program. + * + * ## Usage + * + * ```javascript + * instruction.(...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; diff --git a/ts/src/program/namespace/rpc.ts b/ts/src/program/namespace/rpc.ts index 18d4a1cbe1..8dfe805c68 100644 --- a/ts/src/program/namespace/rpc.ts +++ b/ts/src/program/namespace/rpc.ts @@ -33,12 +33,13 @@ export default class RpcFactory { } /** - * Dynamically generated rpc namespace, providing async methods to send signed - * transactions invoking *non*-state methods on an Anchor program. + * 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. + * Keys are method names, values are RPC functions returning a + * [[TransactionInstruction]]. * - * ## rpc + * ## Usage * * ```javascript * rpc.(...args, ctx); @@ -50,6 +51,7 @@ export default class RpcFactory { * 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 * diff --git a/ts/src/program/namespace/simulate.ts b/ts/src/program/namespace/simulate.ts index c57c5ae13b..310ea97599 100644 --- a/ts/src/program/namespace/simulate.ts +++ b/ts/src/program/namespace/simulate.ts @@ -58,7 +58,38 @@ export default class SimulateFactory { } /** - * Dynamically generated simualte namespace. + * 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.(...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; diff --git a/ts/src/program/namespace/transaction.ts b/ts/src/program/namespace/transaction.ts index 9bdf9ce81e..484f7e0853 100644 --- a/ts/src/program/namespace/transaction.ts +++ b/ts/src/program/namespace/transaction.ts @@ -23,7 +23,33 @@ export default class TransactionFactory { } /** - * Dynamically generated transaction namespace. + * The namespace provides functions to build [[Transaction]] objects for each + * method of a program. + * + * ## Usage + * + * ```javascript + * program.transaction.(...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; From 5011eb445b6faad0709744d4eaebee10de3d1a97 Mon Sep 17 00:00:00 2001 From: armaniferrante Date: Tue, 25 May 2021 17:08:32 -0700 Subject: [PATCH 10/16] Update --- ts/src/index.ts | 2 +- ts/src/program/index.ts | 91 ++++++------- ts/src/program/namespace/account.ts | 18 +-- ts/src/program/namespace/instruction.ts | 2 +- ts/src/program/namespace/state.ts | 18 ++- ts/src/utils.ts | 169 ------------------------ ts/src/utils/index.ts | 14 ++ ts/src/utils/pubkey.ts | 78 +++++++++++ ts/src/utils/rpc.ts | 63 +++++++++ 9 files changed, 229 insertions(+), 226 deletions(-) delete mode 100644 ts/src/utils.ts create mode 100644 ts/src/utils/index.ts create mode 100644 ts/src/utils/pubkey.ts create mode 100644 ts/src/utils/rpc.ts diff --git a/ts/src/index.ts b/ts/src/index.ts index ae05042449..e6fc509e57 100644 --- a/ts/src/index.ts +++ b/ts/src/index.ts @@ -9,7 +9,7 @@ import Coder, { } from "./coder"; import { Idl } from "./idl"; import workspace from "./workspace"; -import utils from "./utils"; +import * as utils from "./utils"; import { Program } from "./program"; import { Address } from "./program/common"; import { Event } from "./program/event"; diff --git a/ts/src/program/index.ts b/ts/src/program/index.ts index f0be471b37..69c0cf4037 100644 --- a/ts/src/program/index.ts +++ b/ts/src/program/index.ts @@ -28,55 +28,56 @@ 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.. + * program.. * ``` * * 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 to *non*-state methods on the - * program, returning a [[TransactionSignature]]. - * - * ## Usage - * - * ```javascript - * rpc.(...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, - * }, - * }); - * ``` - */ + /** + * Async methods to send signed transactions to *non*-state methods on the + * program, returning a [[TransactionSignature]]. + * + * ## Usage + * + * ```javascript + * rpc.(...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, + * }, + * }); + * ``` + */ readonly rpc: RpcNamespace; /** * The namespace provides handles to an [[AccountClient]] object for each - * account in the program. + * account in the program. * * ## Usage * @@ -91,14 +92,14 @@ export class Program { * ```javascript * const counter = await program.account.counter.fetch(address); * ``` - * - * For the full API, see the [[AccountClient]] reference. + * + * For the full API, see the [[AccountClient]] reference. */ readonly account: AccountNamespace; /** * The namespace provides functions to build [[TransactionInstruction]] - * objects for each method of a program. + * objects for each method of a program. * * ## Usage * @@ -129,7 +130,7 @@ export class Program { /** * The namespace provides functions to build [[Transaction]] objects for each - * method of a program. + * method of a program. * * ## Usage * @@ -160,8 +161,8 @@ export class Program { /** * The namespace provides functions to simulate transactions for each method - * of a program, returning a list of deserialized events *and* raw program - * logs. + * 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 @@ -196,8 +197,8 @@ export class Program { /** * 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. + * one can use this to send transactions and read accounts for the state + * abstraction. */ readonly state: StateClient; diff --git a/ts/src/program/namespace/account.ts b/ts/src/program/namespace/account.ts index 0cde1f91bb..61483557e7 100644 --- a/ts/src/program/namespace/account.ts +++ b/ts/src/program/namespace/account.ts @@ -74,25 +74,25 @@ export class AccountClient { } private _size: number; - /** - * Returns the program ID owning all accounts. - */ + /** + * Returns the program ID owning all accounts. + */ get programId(): PublicKey { return this._programId; } private _programId: PublicKey; - /** - * Returns the cleint's wallet and network provider. - */ + /** + * Returns the cleint's wallet and network provider. + */ get provider(): Provider { return this._provider; } private _provider: Provider; - /** - * Returns the coder. - */ + /** + * Returns the coder. + */ get coder(): Coder { return this._coder; } diff --git a/ts/src/program/namespace/instruction.ts b/ts/src/program/namespace/instruction.ts index b14a194026..92fc501825 100644 --- a/ts/src/program/namespace/instruction.ts +++ b/ts/src/program/namespace/instruction.ts @@ -73,7 +73,7 @@ export default class InstructionNamespaceFactory { } } -/* +/** * The namespace provides functions to build [[TransactionInstruction]] * objects for each method of a program. * diff --git a/ts/src/program/namespace/state.ts b/ts/src/program/namespace/state.ts index 78af66ca82..253d317ebd 100644 --- a/ts/src/program/namespace/state.ts +++ b/ts/src/program/namespace/state.ts @@ -11,7 +11,7 @@ import { Idl, IdlStateMethod } from "../../idl"; import Coder, { stateDiscriminator } from "../../coder"; import { RpcNamespace, InstructionNamespace, TransactionNamespace } from "./"; import { Subscription, validateAccounts } from "../common"; -import { findProgramAddressSync, createWithSeedSync } from "../../utils"; +import { findProgramAddressSync, createWithSeedSync } from "../../utils/pubkey"; import { Accounts } from "../context"; import InstructionNamespaceFactory from "./instruction"; import RpcNamespaceFactory from "./rpc"; @@ -32,9 +32,25 @@ export default class StateFactory { } } +/** + * 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; + + /** + * [[TransactionNamespace]] for all state methods. + */ readonly transaction: TransactionNamespace; /** diff --git a/ts/src/utils.ts b/ts/src/utils.ts deleted file mode 100644 index d9e4268858..0000000000 --- a/ts/src/utils.ts +++ /dev/null @@ -1,169 +0,0 @@ -import BN from "bn.js"; -import * as bs58 from "bs58"; -import { sha256 } from "crypto-hash"; -import { sha256 as sha256Sync } from "js-sha256"; -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( - connection: Connection, - publicKeys: PublicKey[] -): Promise< - Array }> -> { - const args = [publicKeys.map((k) => k.toBase58()), { commitment: "recent" }]; - // @ts-ignore - const res = await connection._rpcRequest("getMultipleAccounts", args); - if (res.error) { - throw new Error( - "failed to get info about accounts " + - publicKeys.map((k) => k.toBase58()).join(", ") + - ": " + - res.error.message - ); - } - assert(typeof res.result !== "undefined"); - const accounts: Array = []; - for (const account of res.result.value) { - let value: { - executable: any; - owner: PublicKey; - lamports: any; - data: Buffer; - } | null = null; - if (account === null) { - accounts.push(null); - continue; - } - if (res.result.value) { - const { executable, owner, lamports, data } = account; - assert(data[1] === "base64"); - value = { - executable, - owner: new PublicKey(owner), - lamports, - data: Buffer.from(data[0], "base64"), - }; - } - if (value === null) { - throw new Error("Invalid response"); - } - accounts.push(value); - } - return accounts.map((account, idx) => { - if (account === null) { - return null; - } - return { - publicKey: publicKeys[idx], - account, - }; - }); -} - -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); -} - -// 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, - 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, - 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): 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); - } -}; - -const utils = { - bs58, - sha256, - getMultipleAccounts, - idlAddress, - publicKey: { - createProgramAddressSync, - findProgramAddressSync, - createWithSeedSync, - }, -}; - -export default utils; diff --git a/ts/src/utils/index.ts b/ts/src/utils/index.ts new file mode 100644 index 0000000000..fab820847b --- /dev/null +++ b/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 { sha256, bs58, rpc, publicKey }; diff --git a/ts/src/utils/pubkey.ts b/ts/src/utils/pubkey.ts new file mode 100644 index 0000000000..efa0527c7d --- /dev/null +++ b/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, + 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, + 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): 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); + } +}; diff --git a/ts/src/utils/rpc.ts b/ts/src/utils/rpc.ts new file mode 100644 index 0000000000..a6595e53a6 --- /dev/null +++ b/ts/src/utils/rpc.ts @@ -0,0 +1,63 @@ +import assert from "assert"; +import { PublicKey, AccountInfo, Connection } from "@solana/web3.js"; + +export async function getMultipleAccounts( + connection: Connection, + publicKeys: PublicKey[] +): Promise< + Array }> +> { + const args = [publicKeys.map((k) => k.toBase58()), { commitment: "recent" }]; + // @ts-ignore + const res = await connection._rpcRequest("getMultipleAccounts", args); + if (res.error) { + throw new Error( + "failed to get info about accounts " + + publicKeys.map((k) => k.toBase58()).join(", ") + + ": " + + res.error.message + ); + } + assert(typeof res.result !== "undefined"); + const accounts: Array = []; + for (const account of res.result.value) { + let value: { + executable: any; + owner: PublicKey; + lamports: any; + data: Buffer; + } | null = null; + if (account === null) { + accounts.push(null); + continue; + } + if (res.result.value) { + const { executable, owner, lamports, data } = account; + assert(data[1] === "base64"); + value = { + executable, + owner: new PublicKey(owner), + lamports, + data: Buffer.from(data[0], "base64"), + }; + } + if (value === null) { + throw new Error("Invalid response"); + } + accounts.push(value); + } + return accounts.map((account, idx) => { + if (account === null) { + return null; + } + return { + publicKey: publicKeys[idx], + account, + }; + }); +} From 7961771c83867f9b64f00ac65a39c1ffd1001b26 Mon Sep 17 00:00:00 2001 From: armaniferrante Date: Tue, 25 May 2021 17:56:11 -0700 Subject: [PATCH 11/16] Move coder into its own dir --- ts/src/coder.ts | 571 ------------------------------------ ts/src/coder/accounts.ts | 50 ++++ ts/src/coder/common.ts | 113 +++++++ ts/src/coder/event.ts | 67 +++++ ts/src/coder/idl.ts | 154 ++++++++++ ts/src/coder/index.ts | 56 ++++ ts/src/coder/instruction.ts | 74 +++++ ts/src/coder/state.ts | 36 +++ ts/src/coder/types.ts | 38 +++ 9 files changed, 588 insertions(+), 571 deletions(-) delete mode 100644 ts/src/coder.ts create mode 100644 ts/src/coder/accounts.ts create mode 100644 ts/src/coder/common.ts create mode 100644 ts/src/coder/event.ts create mode 100644 ts/src/coder/idl.ts create mode 100644 ts/src/coder/index.ts create mode 100644 ts/src/coder/instruction.ts create mode 100644 ts/src/coder/state.ts create mode 100644 ts/src/coder/types.ts diff --git a/ts/src/coder.ts b/ts/src/coder.ts deleted file mode 100644 index 00b53c97f8..0000000000 --- a/ts/src/coder.ts +++ /dev/null @@ -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. - */ -export class InstructionCoder { - /** - * Instruction args layout. Maps namespaced method - */ - private ixLayout: Map; - - 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 { - 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. - */ -export class AccountsCoder { - /** - * Maps account type identifier to a layout. - */ - private accountLayouts: Map; - - 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( - accountName: string, - account: T - ): Promise { - 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(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. - */ -export class TypesCoder { - /** - * Maps account type identifier to a layout. - */ - private layouts: Map; - - 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(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(accountName: string, ix: Buffer): T { - const layout = this.layouts.get(accountName); - return layout.decode(ix); - } -} - -export class EventCoder { - /** - * Maps account type identifier to a layout. - */ - private layouts: Map; - - /** - * Maps base64 encoded event discriminator to event name. - */ - private discriminators: Map; - - 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( - 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 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(name: string, account: T): Promise { - 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(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 { - // @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 { - // @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); -} diff --git a/ts/src/coder/accounts.ts b/ts/src/coder/accounts.ts new file mode 100644 index 0000000000..57438d71f2 --- /dev/null +++ b/ts/src/coder/accounts.ts @@ -0,0 +1,50 @@ +import { Layout } from "buffer-layout"; +import { Idl } from "../idl"; +import { IdlCoder } from "./idl"; +import { sha256 } from "js-sha256"; + +/** + * Encodes and decodes account objects. + */ +export class AccountsCoder { + /** + * Maps account type identifier to a layout. + */ + private accountLayouts: Map; + + 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( + accountName: string, + account: T + ): Promise { + 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(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 { + return Buffer.from(sha256.digest(`account:${name}`)).slice(0, 8); +} diff --git a/ts/src/coder/common.ts b/ts/src/coder/common.ts new file mode 100644 index 0000000000..cc93f80302 --- /dev/null +++ b/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); +} diff --git a/ts/src/coder/event.ts b/ts/src/coder/event.ts new file mode 100644 index 0000000000..3b73a10068 --- /dev/null +++ b/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; + + /** + * Maps base64 encoded event discriminator to event name. + */ + private discriminators: Map; + + 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( + 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); +} diff --git a/ts/src/coder/idl.ts b/ts/src/coder/idl.ts new file mode 100644 index 0000000000..30be64a99e --- /dev/null +++ b/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}`); + } + } +} diff --git a/ts/src/coder/index.ts b/ts/src/coder/index.ts new file mode 100644 index 0000000000..582c51ff3a --- /dev/null +++ b/ts/src/coder/index.ts @@ -0,0 +1,56 @@ +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"; + +/** + * Number of bytes of the account discriminator. + */ +export const ACCOUNT_DISCRIMINATOR_SIZE = 8; + +/** + * 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); + } +} diff --git a/ts/src/coder/instruction.ts b/ts/src/coder/instruction.ts new file mode 100644 index 0000000000..283f9a614e --- /dev/null +++ b/ts/src/coder/instruction.ts @@ -0,0 +1,74 @@ +import camelCase from "camelcase"; +import { Layout } from "buffer-layout"; +import * as borsh from "@project-serum/borsh"; +import { Idl, IdlField, IdlStateMethod } from "../idl"; + +/** + * 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; + + 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 { + 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); + } +} diff --git a/ts/src/coder/state.ts b/ts/src/coder/state.ts new file mode 100644 index 0000000000..293e7e6eff --- /dev/null +++ b/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(name: string, account: T): Promise { + 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(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 { + return Buffer.from(sha256.digest(`state:${name}`)).slice(0, 8); +} diff --git a/ts/src/coder/types.ts b/ts/src/coder/types.ts new file mode 100644 index 0000000000..da3807b6da --- /dev/null +++ b/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; + + 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(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(accountName: string, ix: Buffer): T { + const layout = this.layouts.get(accountName); + return layout.decode(ix); + } +} From e4c96fc4e9f33da054de329fab71fe2d5d242242 Mon Sep 17 00:00:00 2001 From: armaniferrante Date: Tue, 25 May 2021 18:01:41 -0700 Subject: [PATCH 12/16] Re-export coder vars --- ts/src/coder/index.ts | 7 +++++++ ts/src/coder/instruction.ts | 2 ++ 2 files changed, 9 insertions(+) diff --git a/ts/src/coder/index.ts b/ts/src/coder/index.ts index 582c51ff3a..07ccdd35d4 100644 --- a/ts/src/coder/index.ts +++ b/ts/src/coder/index.ts @@ -6,6 +6,13 @@ 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 } from "./accounts"; +export { EventCoder, eventDiscriminator } from "./event"; +export { StateCoder, stateDiscriminator } from "./state"; + /** * Number of bytes of the account discriminator. */ diff --git a/ts/src/coder/instruction.ts b/ts/src/coder/instruction.ts index 283f9a614e..d371458842 100644 --- a/ts/src/coder/instruction.ts +++ b/ts/src/coder/instruction.ts @@ -2,6 +2,8 @@ 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. From 29f40b362421aae1f515c4d78baf20c6a38d777d Mon Sep 17 00:00:00 2001 From: armaniferrante Date: Tue, 25 May 2021 18:09:10 -0700 Subject: [PATCH 13/16] move constant --- ts/src/coder/accounts.ts | 5 +++++ ts/src/coder/index.ts | 11 +++++------ 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/ts/src/coder/accounts.ts b/ts/src/coder/accounts.ts index 57438d71f2..0eb9f6ac17 100644 --- a/ts/src/coder/accounts.ts +++ b/ts/src/coder/accounts.ts @@ -3,6 +3,11 @@ 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. */ diff --git a/ts/src/coder/index.ts b/ts/src/coder/index.ts index 07ccdd35d4..e0ececbe43 100644 --- a/ts/src/coder/index.ts +++ b/ts/src/coder/index.ts @@ -9,15 +9,14 @@ import { sighash } from "./common"; export { accountSize } from "./common"; export { TypesCoder } from "./types"; export { InstructionCoder } from "./instruction"; -export { AccountsCoder, accountDiscriminator } from "./accounts"; +export { + AccountsCoder, + accountDiscriminator, + ACCOUNT_DISCRIMINATOR_SIZE, +} from "./accounts"; export { EventCoder, eventDiscriminator } from "./event"; export { StateCoder, stateDiscriminator } from "./state"; -/** - * Number of bytes of the account discriminator. - */ -export const ACCOUNT_DISCRIMINATOR_SIZE = 8; - /** * Coder provides a facade for encoding and decoding all IDL related objects. */ From d2c339204adca56457acc8fa1afa8141b2d713e8 Mon Sep 17 00:00:00 2001 From: armaniferrante Date: Tue, 25 May 2021 18:12:22 -0700 Subject: [PATCH 14/16] changelog --- CHANGELOG.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 556a9772dc..1a4c5c6a25 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,8 +21,8 @@ incremented for features. * ts: Retrieving deserialized accounts from the `.account.` and `.state` namespaces now require explicitly invoking the `fetch` API. For example, `program.account.myAccount()` and `program.state()` is now `program.account.myAccount.fetch(
)` 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 From 1631fd9e69338e8688a90c5e957c638985ff694e Mon Sep 17 00:00:00 2001 From: armaniferrante Date: Tue, 25 May 2021 18:33:19 -0700 Subject: [PATCH 15/16] update utils --- ts/src/index.ts | 8 +++++++- ts/src/utils/index.ts | 2 +- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/ts/src/index.ts b/ts/src/index.ts index e6fc509e57..3661365591 100644 --- a/ts/src/index.ts +++ b/ts/src/index.ts @@ -9,7 +9,7 @@ import Coder, { } from "./coder"; import { Idl } from "./idl"; import workspace from "./workspace"; -import * as utils from "./utils"; +import utils from "./utils"; import { Program } from "./program"; import { Address } from "./program/common"; import { Event } from "./program/event"; @@ -31,10 +31,16 @@ 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(); diff --git a/ts/src/utils/index.ts b/ts/src/utils/index.ts index fab820847b..9d8ee83191 100644 --- a/ts/src/utils/index.ts +++ b/ts/src/utils/index.ts @@ -11,4 +11,4 @@ export function decodeUtf8(array: Uint8Array): string { return decoder.decode(array); } -export { sha256, bs58, rpc, publicKey }; +export default { sha256, bs58, rpc, publicKey }; From 136980e9fb2cea77f1c327a3bbf9b3f3c7b6dbb6 Mon Sep 17 00:00:00 2001 From: armaniferrante Date: Tue, 25 May 2021 20:02:15 -0700 Subject: [PATCH 16/16] update --- ts/src/program/namespace/account.ts | 13 +++++++------ ts/src/program/namespace/index.ts | 14 ++++---------- ts/src/program/namespace/state.ts | 19 +++++++++---------- 3 files changed, 20 insertions(+), 26 deletions(-) diff --git a/ts/src/program/namespace/account.ts b/ts/src/program/namespace/account.ts index 61483557e7..c54001cddd 100644 --- a/ts/src/program/namespace/account.ts +++ b/ts/src/program/namespace/account.ts @@ -16,6 +16,7 @@ import Coder, { accountSize, } from "../../coder"; import { Subscription, Address, translateAddress } from "../common"; +import { getProvider } from "../../"; export default class AccountFactory { public static build( @@ -31,9 +32,9 @@ export default class AccountFactory { accountFns[name] = new AccountClient( idl, idlAccount, - coder, programId, - provider + provider, + coder ); }); @@ -103,14 +104,14 @@ export class AccountClient { constructor( idl: Idl, idlAccount: IdlTypeDef, - coder: Coder, programId: PublicKey, - provider: Provider + provider?: Provider, + coder?: Coder ) { this._idlAccount = idlAccount; - this._coder = coder; this._programId = programId; - this._provider = provider; + this._provider = provider ?? getProvider(); + this._coder = coder ?? new Coder(idl); this._size = ACCOUNT_DISCRIMINATOR_SIZE + accountSize(idl, idlAccount); } diff --git a/ts/src/program/namespace/index.ts b/ts/src/program/namespace/index.ts index fae9d2d155..d3443a9129 100644 --- a/ts/src/program/namespace/index.ts +++ b/ts/src/program/namespace/index.ts @@ -3,13 +3,13 @@ import { PublicKey } from "@solana/web3.js"; import Coder from "../../coder"; import Provider from "../../provider"; import { Idl } from "../../idl"; -import { parseIdlErrors } from "../common"; 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 { StateClient } from "./state"; @@ -36,20 +36,14 @@ export default class NamespaceFactory { 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( diff --git a/ts/src/program/namespace/state.ts b/ts/src/program/namespace/state.ts index 253d317ebd..84039d7064 100644 --- a/ts/src/program/namespace/state.ts +++ b/ts/src/program/namespace/state.ts @@ -10,7 +10,8 @@ import Provider from "../../provider"; import { Idl, IdlStateMethod } from "../../idl"; import Coder, { stateDiscriminator } from "../../coder"; import { RpcNamespace, InstructionNamespace, TransactionNamespace } from "./"; -import { Subscription, validateAccounts } from "../common"; +import { getProvider } from "../../"; +import { Subscription, validateAccounts, parseIdlErrors } from "../common"; import { findProgramAddressSync, createWithSeedSync } from "../../utils/pubkey"; import { Accounts } from "../context"; import InstructionNamespaceFactory from "./instruction"; @@ -22,13 +23,12 @@ export default class StateFactory { idl: Idl, coder: Coder, programId: PublicKey, - idlErrors: Map, provider: Provider ): StateClient | undefined { if (idl.state === undefined) { return undefined; } - return new StateClient(idl, coder, programId, idlErrors, provider); + return new StateClient(idl, programId, provider, coder); } } @@ -83,17 +83,16 @@ export class StateClient { constructor( idl: Idl, - coder: Coder, programId: PublicKey, - idlErrors: Map, - provider: Provider + provider?: Provider, + coder?: Coder ) { this._idl = idl; - this._coder = coder; this._programId = programId; - this._provider = provider; - this._sub = null; this._address = programStateAddress(programId); + this._provider = provider ?? getProvider(); + this._coder = coder ?? new Coder(idl); + this._sub = null; // Build namespaces. const [instruction, transaction, rpc] = ((): [ @@ -125,7 +124,7 @@ export class StateClient { const rpcItem = RpcNamespaceFactory.build( m, txItem, - idlErrors, + parseIdlErrors(idl), provider );