From e121e4e09dbc77f7b3fa089ebfaf94638409ec0f Mon Sep 17 00:00:00 2001 From: Armani Ferrante Date: Sat, 15 Jan 2022 17:09:53 -0500 Subject: [PATCH] ts: builder api (#1324) --- CHANGELOG.md | 1 + cli/src/lib.rs | 2 +- tests/cfo/tests/cfo.js | 113 ++++++++++----------- ts/src/program/index.ts | 9 ++ ts/src/program/namespace/index.ts | 12 +++ ts/src/program/namespace/methods.ts | 143 +++++++++++++++++++++++++++ ts/src/program/namespace/simulate.ts | 2 +- 7 files changed, 225 insertions(+), 57 deletions(-) create mode 100644 ts/src/program/namespace/methods.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 8d66ef870..070c67392 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ incremented for features. * lang: Add `seeds::program` constraint for specifying which program_id to use when deriving PDAs.([#1197](https://github.com/project-serum/anchor/pull/1197)) * ts: Remove error logging in the event parser when log websocket encounters a program error. ([#1313](https://github.com/project-serum/anchor/pull/1313)) +* ts: Add new `methods` namespace to the program client, introducing a more ergonomic builder API ([#1324](https://github.com/project-serum/anchor/pull/1324)). ### Breaking diff --git a/cli/src/lib.rs b/cli/src/lib.rs index 05d3e6fc0..d0c048ee1 100644 --- a/cli/src/lib.rs +++ b/cli/src/lib.rs @@ -1021,7 +1021,7 @@ fn docker_build_bpf( println!( "Building {} manifest: {:?}", binary_name, - manifest_path.display().to_string() + manifest_path.display() ); // Execute the build. diff --git a/tests/cfo/tests/cfo.js b/tests/cfo/tests/cfo.js index 8bf06ce80..c118d81d5 100644 --- a/tests/cfo/tests/cfo.js +++ b/tests/cfo/tests/cfo.js @@ -225,29 +225,24 @@ describe("cfo", () => { stake: stakeBump, treasury: treasuryBump, }; - await program.rpc.createOfficer( - bumps, - distribution, - registrar, - msrmRegistrar, - { - accounts: { - officer, - srmVault, - usdcVault, - stake, - treasury, - srmMint: ORDERBOOK_ENV.mintA, - usdcMint: ORDERBOOK_ENV.usdc, - authority: program.provider.wallet.publicKey, - dexProgram: DEX_PID, - swapProgram: SWAP_PID, - tokenProgram: TOKEN_PID, - systemProgram: SystemProgram.programId, - rent: SYSVAR_RENT_PUBKEY, - }, - } - ); + await program.methods + .createOfficer(bumps, distribution, registrar, msrmRegistrar) + .accounts({ + officer, + srmVault, + usdcVault, + stake, + treasury, + srmMint: ORDERBOOK_ENV.mintA, + usdcMint: ORDERBOOK_ENV.usdc, + authority: program.provider.wallet.publicKey, + dexProgram: DEX_PID, + swapProgram: SWAP_PID, + tokenProgram: TOKEN_PID, + systemProgram: SystemProgram.programId, + rent: SYSVAR_RENT_PUBKEY, + }) + .rpc(); officerAccount = await program.account.officer.fetch(officer); assert.ok( @@ -260,8 +255,9 @@ describe("cfo", () => { }); it("Creates a token account for the officer associated with the market", async () => { - await program.rpc.createOfficerToken(bBump, { - accounts: { + await program.methods + .createOfficerToken(bBump) + .accounts({ officer, token: bVault, mint: ORDERBOOK_ENV.mintB, @@ -269,16 +265,17 @@ describe("cfo", () => { systemProgram: SystemProgram.programId, tokenProgram: TOKEN_PID, rent: SYSVAR_RENT_PUBKEY, - }, - }); + }) + .rpc(); const tokenAccount = await B_TOKEN_CLIENT.getAccountInfo(bVault); assert.ok(tokenAccount.state === 1); assert.ok(tokenAccount.isInitialized); }); it("Creates an open orders account for the officer", async () => { - await program.rpc.createOfficerOpenOrders(openOrdersBump, { - accounts: { + await program.methods + .createOfficerOpenOrders(openOrdersBump) + .accounts({ officer, openOrders, payer: program.provider.wallet.publicKey, @@ -286,8 +283,8 @@ describe("cfo", () => { systemProgram: SystemProgram.programId, rent: SYSVAR_RENT_PUBKEY, market: ORDERBOOK_ENV.marketA.address, - }, - }); + }) + .rpc(); await program.rpc.createOfficerOpenOrders(openOrdersBumpB, { accounts: { officer, @@ -310,8 +307,9 @@ describe("cfo", () => { program.provider, sweepVault ); - await program.rpc.sweepFees({ - accounts: { + await program.methods + .sweepFees() + .accounts({ officer, sweepVault, mint: ORDERBOOK_ENV.usdc, @@ -323,8 +321,8 @@ describe("cfo", () => { dexProgram: DEX_PID, tokenProgram: TOKEN_PID, }, - }, - }); + }) + .rpc(); const afterTokenAccount = await serumCmn.getTokenAccount( program.provider, sweepVault @@ -336,26 +334,28 @@ describe("cfo", () => { }); it("Creates a market auth token", async () => { - await program.rpc.authorizeMarket(marketAuthBump, { - accounts: { + await program.methods + .authorizeMarket(marketAuthBump) + .accounts({ officer, authority: program.provider.wallet.publicKey, marketAuth, payer: program.provider.wallet.publicKey, market: ORDERBOOK_ENV.marketA.address, systemProgram: SystemProgram.programId, - }, - }); - await program.rpc.authorizeMarket(marketAuthBumpB, { - accounts: { + }) + .rpc(); + await program.methods + .authorizeMarket(marketAuthBumpB) + .accounts({ officer, authority: program.provider.wallet.publicKey, marketAuth: marketAuthB, payer: program.provider.wallet.publicKey, market: ORDERBOOK_ENV.marketB.address, systemProgram: SystemProgram.programId, - }, - }); + }) + .rpc(); }); it("Transfers into the mintB vault", async () => { @@ -378,8 +378,9 @@ describe("cfo", () => { quoteDecimals: 6, strict: false, }; - await program.rpc.swapToUsdc(minExchangeRate, { - accounts: { + await program.methods + .swapToUsdc(minExchangeRate) + .accounts({ officer, market: { market: marketBClient.address, @@ -402,8 +403,8 @@ describe("cfo", () => { tokenProgram: TOKEN_PID, instructions: SYSVAR_INSTRUCTIONS_PUBKEY, rent: SYSVAR_RENT_PUBKEY, - }, - }); + }) + .rpc(); const bVaultAfter = await B_TOKEN_CLIENT.getAccountInfo(bVault); const usdcVaultAfter = await USDC_TOKEN_CLIENT.getAccountInfo(usdcVault); @@ -424,8 +425,9 @@ describe("cfo", () => { quoteDecimals: 6, strict: false, }; - await program.rpc.swapToSrm(minExchangeRate, { - accounts: { + await program.methods + .swapToSrm(minExchangeRate) + .accounts({ officer, market: { market: marketAClient.address, @@ -449,8 +451,8 @@ describe("cfo", () => { tokenProgram: TOKEN_PID, instructions: SYSVAR_INSTRUCTIONS_PUBKEY, rent: SYSVAR_RENT_PUBKEY, - }, - }); + }) + .rpc(); const srmVaultAfter = await SRM_TOKEN_CLIENT.getAccountInfo(srmVault); const usdcVaultAfter = await USDC_TOKEN_CLIENT.getAccountInfo(usdcVault); @@ -467,8 +469,9 @@ describe("cfo", () => { const stakeBefore = await SRM_TOKEN_CLIENT.getAccountInfo(stake); const mintInfoBefore = await SRM_TOKEN_CLIENT.getMintInfo(); - await program.rpc.distribute({ - accounts: { + await program.methods + .distribute() + .accounts({ officer, treasury, stake, @@ -476,8 +479,8 @@ describe("cfo", () => { srmMint: ORDERBOOK_ENV.mintA, tokenProgram: TOKEN_PID, dexProgram: DEX_PID, - }, - }); + }) + .rpc(); const srmVaultAfter = await SRM_TOKEN_CLIENT.getAccountInfo(srmVault); const treasuryAfter = await SRM_TOKEN_CLIENT.getAccountInfo(treasury); diff --git a/ts/src/program/index.ts b/ts/src/program/index.ts index 6e6f06bc6..7971fa396 100644 --- a/ts/src/program/index.ts +++ b/ts/src/program/index.ts @@ -10,6 +10,7 @@ import NamespaceFactory, { AccountNamespace, StateClient, SimulateNamespace, + MethodsNamespace, } from "./namespace/index.js"; import { utf8 } from "../utils/bytes/index.js"; import { EventManager } from "./event.js"; @@ -206,6 +207,12 @@ export class Program { */ readonly state?: StateClient; + /** + * The namespace provides a builder API for all APIs on the program. + * This is an alternative to using namespace the other namespaces.. + */ + readonly methods: MethodsNamespace; + /** * Address of the program. */ @@ -275,6 +282,7 @@ export class Program { transaction, account, simulate, + methods, state, ] = NamespaceFactory.build(idl, this._coder, programId, provider); this.rpc = rpc; @@ -282,6 +290,7 @@ export class Program { this.transaction = transaction; this.account = account; this.simulate = simulate; + this.methods = methods; this.state = state; } diff --git a/ts/src/program/namespace/index.ts b/ts/src/program/namespace/index.ts index 53b34100d..365b3cf15 100644 --- a/ts/src/program/namespace/index.ts +++ b/ts/src/program/namespace/index.ts @@ -11,6 +11,7 @@ import AccountFactory, { AccountNamespace } from "./account.js"; import SimulateFactory, { SimulateNamespace } from "./simulate.js"; import { parseIdlErrors } from "../common.js"; import { AllInstructions } from "./types.js"; +import { MethodsBuilderFactory, MethodsNamespace } from "./methods"; // Re-exports. export { StateClient } from "./state.js"; @@ -20,6 +21,7 @@ export { RpcNamespace, RpcFn } from "./rpc.js"; export { AccountNamespace, AccountClient, ProgramAccount } from "./account.js"; export { SimulateNamespace, SimulateFn } from "./simulate.js"; export { IdlAccounts, IdlTypes } from "./types.js"; +export { MethodsBuilderFactory, MethodsNamespace } from "./methods"; export default class NamespaceFactory { /** @@ -36,12 +38,14 @@ export default class NamespaceFactory { TransactionNamespace, AccountNamespace, SimulateNamespace, + MethodsNamespace, StateClient | undefined ] { const rpc: RpcNamespace = {}; const instruction: InstructionNamespace = {}; const transaction: TransactionNamespace = {}; const simulate: SimulateNamespace = {}; + const methods: MethodsNamespace = {}; const idlErrors = parseIdlErrors(idl); @@ -64,6 +68,12 @@ export default class NamespaceFactory { programId, idl ); + const methodItem = MethodsBuilderFactory.build( + ixItem, + txItem, + rpcItem, + simulateItem + ); const name = camelCase(idlIx.name); @@ -71,6 +81,7 @@ export default class NamespaceFactory { transaction[name] = txItem; rpc[name] = rpcItem; simulate[name] = simulateItem; + methods[name] = methodItem; }); const account: AccountNamespace = idl.accounts @@ -83,6 +94,7 @@ export default class NamespaceFactory { transaction as TransactionNamespace, account, simulate as SimulateNamespace, + methods as MethodsNamespace, state, ]; } diff --git a/ts/src/program/namespace/methods.ts b/ts/src/program/namespace/methods.ts new file mode 100644 index 000000000..7085ede39 --- /dev/null +++ b/ts/src/program/namespace/methods.ts @@ -0,0 +1,143 @@ +import { + ConfirmOptions, + AccountMeta, + Signer, + Transaction, + TransactionInstruction, + TransactionSignature, + PublicKey, +} from "@solana/web3.js"; +import { SimulateResponse } from "./simulate"; +import { TransactionFn } from "./transaction.js"; +import { Idl } from "../../idl.js"; +import { + AllInstructions, + InstructionContextFn, + MakeInstructionsNamespace, +} from "./types"; +import { InstructionFn } from "./instruction"; +import { RpcFn } from "./rpc"; +import { SimulateFn } from "./simulate"; + +export class MethodsBuilderFactory { + public static build>( + ixFn: InstructionFn, + txFn: TransactionFn, + rpcFn: RpcFn, + simulateFn: SimulateFn + ): MethodFn { + const request: MethodFn = (...args) => { + return new MethodsBuilder(args, ixFn, txFn, rpcFn, simulateFn); + }; + return request; + } +} + +export class MethodsBuilder> { + private _accounts: { [name: string]: PublicKey } = {}; + private _remainingAccounts: Array = []; + private _signers: Array = []; + private _preInstructions: Array = []; + private _postInstructions: Array = []; + + constructor( + private _args: Array, + private _ixFn: InstructionFn, + private _txFn: TransactionFn, + private _rpcFn: RpcFn, + private _simulateFn: SimulateFn + ) {} + + // TODO: don't use any. + public accounts(accounts: any): MethodsBuilder { + Object.assign(this._accounts, accounts); + return this; + } + + public remainingAccounts( + accounts: Array + ): MethodsBuilder { + this._remainingAccounts = this._remainingAccounts.concat(accounts); + return this; + } + + public preInstructions( + ixs: Array + ): MethodsBuilder { + this._preInstructions = this._preInstructions.concat(ixs); + return this; + } + + public postInstructions( + ixs: Array + ): MethodsBuilder { + this._postInstructions = this._postInstructions.concat(ixs); + return this; + } + + public async rpc(options: ConfirmOptions): Promise { + await this.resolvePdas(); + // @ts-ignore + return this._rpcFn(...this._args, { + accounts: this._accounts, + signers: this._signers, + remainingAccounts: this._remainingAccounts, + preInstructions: this._preInstructions, + postInstructions: this._postInstructions, + options: options, + }); + } + + public async simulate( + options: ConfirmOptions + ): Promise> { + await this.resolvePdas(); + // @ts-ignore + return this._simulateFn(...this._args, { + accounts: this._accounts, + signers: this._signers, + remainingAccounts: this._remainingAccounts, + preInstructions: this._preInstructions, + postInstructions: this._postInstructions, + options: options, + }); + } + + public async instruction(): Promise { + await this.resolvePdas(); + // @ts-ignore + return this._ixFn(...this._args, { + accounts: this._accounts, + signers: this._signers, + remainingAccounts: this._remainingAccounts, + preInstructions: this._preInstructions, + postInstructions: this._postInstructions, + }); + } + + public async transaction(): Promise { + await this.resolvePdas(); + // @ts-ignore + return this._txFn(...this._args, { + accounts: this._accounts, + signers: this._signers, + remainingAccounts: this._remainingAccounts, + preInstructions: this._preInstructions, + postInstructions: this._postInstructions, + }); + } + + private async resolvePdas() { + // TODO: resolve all PDAs and accounts not provided. + } +} + +export type MethodsNamespace< + IDL extends Idl = Idl, + I extends AllInstructions = AllInstructions +> = MakeInstructionsNamespace; // TODO: don't use any. + +export type MethodFn< + IDL extends Idl = Idl, + I extends AllInstructions = AllInstructions +> = InstructionContextFn>; diff --git a/ts/src/program/namespace/simulate.ts b/ts/src/program/namespace/simulate.ts index e15dfd985..96408b959 100644 --- a/ts/src/program/namespace/simulate.ts +++ b/ts/src/program/namespace/simulate.ts @@ -134,7 +134,7 @@ export type SimulateFn< Promise, IdlTypes>> >; -type SimulateResponse = { +export type SimulateResponse = { events: readonly Event[]; raw: readonly string[]; };