Browse Source

ts: builder api (#1324)

Armani Ferrante 3 years ago
parent
commit
e121e4e09d

+ 1 - 0
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))
 * 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: 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
 ### Breaking
 
 

+ 1 - 1
cli/src/lib.rs

@@ -1021,7 +1021,7 @@ fn docker_build_bpf(
     println!(
     println!(
         "Building {} manifest: {:?}",
         "Building {} manifest: {:?}",
         binary_name,
         binary_name,
-        manifest_path.display().to_string()
+        manifest_path.display()
     );
     );
 
 
     // Execute the build.
     // Execute the build.

+ 58 - 55
tests/cfo/tests/cfo.js

@@ -225,29 +225,24 @@ describe("cfo", () => {
       stake: stakeBump,
       stake: stakeBump,
       treasury: treasuryBump,
       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);
     officerAccount = await program.account.officer.fetch(officer);
     assert.ok(
     assert.ok(
@@ -260,8 +255,9 @@ describe("cfo", () => {
   });
   });
 
 
   it("Creates a token account for the officer associated with the market", async () => {
   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,
         officer,
         token: bVault,
         token: bVault,
         mint: ORDERBOOK_ENV.mintB,
         mint: ORDERBOOK_ENV.mintB,
@@ -269,16 +265,17 @@ describe("cfo", () => {
         systemProgram: SystemProgram.programId,
         systemProgram: SystemProgram.programId,
         tokenProgram: TOKEN_PID,
         tokenProgram: TOKEN_PID,
         rent: SYSVAR_RENT_PUBKEY,
         rent: SYSVAR_RENT_PUBKEY,
-      },
-    });
+      })
+      .rpc();
     const tokenAccount = await B_TOKEN_CLIENT.getAccountInfo(bVault);
     const tokenAccount = await B_TOKEN_CLIENT.getAccountInfo(bVault);
     assert.ok(tokenAccount.state === 1);
     assert.ok(tokenAccount.state === 1);
     assert.ok(tokenAccount.isInitialized);
     assert.ok(tokenAccount.isInitialized);
   });
   });
 
 
   it("Creates an open orders account for the officer", async () => {
   it("Creates an open orders account for the officer", async () => {
-    await program.rpc.createOfficerOpenOrders(openOrdersBump, {
-      accounts: {
+    await program.methods
+      .createOfficerOpenOrders(openOrdersBump)
+      .accounts({
         officer,
         officer,
         openOrders,
         openOrders,
         payer: program.provider.wallet.publicKey,
         payer: program.provider.wallet.publicKey,
@@ -286,8 +283,8 @@ describe("cfo", () => {
         systemProgram: SystemProgram.programId,
         systemProgram: SystemProgram.programId,
         rent: SYSVAR_RENT_PUBKEY,
         rent: SYSVAR_RENT_PUBKEY,
         market: ORDERBOOK_ENV.marketA.address,
         market: ORDERBOOK_ENV.marketA.address,
-      },
-    });
+      })
+      .rpc();
     await program.rpc.createOfficerOpenOrders(openOrdersBumpB, {
     await program.rpc.createOfficerOpenOrders(openOrdersBumpB, {
       accounts: {
       accounts: {
         officer,
         officer,
@@ -310,8 +307,9 @@ describe("cfo", () => {
       program.provider,
       program.provider,
       sweepVault
       sweepVault
     );
     );
-    await program.rpc.sweepFees({
-      accounts: {
+    await program.methods
+      .sweepFees()
+      .accounts({
         officer,
         officer,
         sweepVault,
         sweepVault,
         mint: ORDERBOOK_ENV.usdc,
         mint: ORDERBOOK_ENV.usdc,
@@ -323,8 +321,8 @@ describe("cfo", () => {
           dexProgram: DEX_PID,
           dexProgram: DEX_PID,
           tokenProgram: TOKEN_PID,
           tokenProgram: TOKEN_PID,
         },
         },
-      },
-    });
+      })
+      .rpc();
     const afterTokenAccount = await serumCmn.getTokenAccount(
     const afterTokenAccount = await serumCmn.getTokenAccount(
       program.provider,
       program.provider,
       sweepVault
       sweepVault
@@ -336,26 +334,28 @@ describe("cfo", () => {
   });
   });
 
 
   it("Creates a market auth token", async () => {
   it("Creates a market auth token", async () => {
-    await program.rpc.authorizeMarket(marketAuthBump, {
-      accounts: {
+    await program.methods
+      .authorizeMarket(marketAuthBump)
+      .accounts({
         officer,
         officer,
         authority: program.provider.wallet.publicKey,
         authority: program.provider.wallet.publicKey,
         marketAuth,
         marketAuth,
         payer: program.provider.wallet.publicKey,
         payer: program.provider.wallet.publicKey,
         market: ORDERBOOK_ENV.marketA.address,
         market: ORDERBOOK_ENV.marketA.address,
         systemProgram: SystemProgram.programId,
         systemProgram: SystemProgram.programId,
-      },
-    });
-    await program.rpc.authorizeMarket(marketAuthBumpB, {
-      accounts: {
+      })
+      .rpc();
+    await program.methods
+      .authorizeMarket(marketAuthBumpB)
+      .accounts({
         officer,
         officer,
         authority: program.provider.wallet.publicKey,
         authority: program.provider.wallet.publicKey,
         marketAuth: marketAuthB,
         marketAuth: marketAuthB,
         payer: program.provider.wallet.publicKey,
         payer: program.provider.wallet.publicKey,
         market: ORDERBOOK_ENV.marketB.address,
         market: ORDERBOOK_ENV.marketB.address,
         systemProgram: SystemProgram.programId,
         systemProgram: SystemProgram.programId,
-      },
-    });
+      })
+      .rpc();
   });
   });
 
 
   it("Transfers into the mintB vault", async () => {
   it("Transfers into the mintB vault", async () => {
@@ -378,8 +378,9 @@ describe("cfo", () => {
       quoteDecimals: 6,
       quoteDecimals: 6,
       strict: false,
       strict: false,
     };
     };
-    await program.rpc.swapToUsdc(minExchangeRate, {
-      accounts: {
+    await program.methods
+      .swapToUsdc(minExchangeRate)
+      .accounts({
         officer,
         officer,
         market: {
         market: {
           market: marketBClient.address,
           market: marketBClient.address,
@@ -402,8 +403,8 @@ describe("cfo", () => {
         tokenProgram: TOKEN_PID,
         tokenProgram: TOKEN_PID,
         instructions: SYSVAR_INSTRUCTIONS_PUBKEY,
         instructions: SYSVAR_INSTRUCTIONS_PUBKEY,
         rent: SYSVAR_RENT_PUBKEY,
         rent: SYSVAR_RENT_PUBKEY,
-      },
-    });
+      })
+      .rpc();
 
 
     const bVaultAfter = await B_TOKEN_CLIENT.getAccountInfo(bVault);
     const bVaultAfter = await B_TOKEN_CLIENT.getAccountInfo(bVault);
     const usdcVaultAfter = await USDC_TOKEN_CLIENT.getAccountInfo(usdcVault);
     const usdcVaultAfter = await USDC_TOKEN_CLIENT.getAccountInfo(usdcVault);
@@ -424,8 +425,9 @@ describe("cfo", () => {
       quoteDecimals: 6,
       quoteDecimals: 6,
       strict: false,
       strict: false,
     };
     };
-    await program.rpc.swapToSrm(minExchangeRate, {
-      accounts: {
+    await program.methods
+      .swapToSrm(minExchangeRate)
+      .accounts({
         officer,
         officer,
         market: {
         market: {
           market: marketAClient.address,
           market: marketAClient.address,
@@ -449,8 +451,8 @@ describe("cfo", () => {
         tokenProgram: TOKEN_PID,
         tokenProgram: TOKEN_PID,
         instructions: SYSVAR_INSTRUCTIONS_PUBKEY,
         instructions: SYSVAR_INSTRUCTIONS_PUBKEY,
         rent: SYSVAR_RENT_PUBKEY,
         rent: SYSVAR_RENT_PUBKEY,
-      },
-    });
+      })
+      .rpc();
 
 
     const srmVaultAfter = await SRM_TOKEN_CLIENT.getAccountInfo(srmVault);
     const srmVaultAfter = await SRM_TOKEN_CLIENT.getAccountInfo(srmVault);
     const usdcVaultAfter = await USDC_TOKEN_CLIENT.getAccountInfo(usdcVault);
     const usdcVaultAfter = await USDC_TOKEN_CLIENT.getAccountInfo(usdcVault);
@@ -467,8 +469,9 @@ describe("cfo", () => {
     const stakeBefore = await SRM_TOKEN_CLIENT.getAccountInfo(stake);
     const stakeBefore = await SRM_TOKEN_CLIENT.getAccountInfo(stake);
     const mintInfoBefore = await SRM_TOKEN_CLIENT.getMintInfo();
     const mintInfoBefore = await SRM_TOKEN_CLIENT.getMintInfo();
 
 
-    await program.rpc.distribute({
-      accounts: {
+    await program.methods
+      .distribute()
+      .accounts({
         officer,
         officer,
         treasury,
         treasury,
         stake,
         stake,
@@ -476,8 +479,8 @@ describe("cfo", () => {
         srmMint: ORDERBOOK_ENV.mintA,
         srmMint: ORDERBOOK_ENV.mintA,
         tokenProgram: TOKEN_PID,
         tokenProgram: TOKEN_PID,
         dexProgram: DEX_PID,
         dexProgram: DEX_PID,
-      },
-    });
+      })
+      .rpc();
 
 
     const srmVaultAfter = await SRM_TOKEN_CLIENT.getAccountInfo(srmVault);
     const srmVaultAfter = await SRM_TOKEN_CLIENT.getAccountInfo(srmVault);
     const treasuryAfter = await SRM_TOKEN_CLIENT.getAccountInfo(treasury);
     const treasuryAfter = await SRM_TOKEN_CLIENT.getAccountInfo(treasury);

+ 9 - 0
ts/src/program/index.ts

@@ -10,6 +10,7 @@ import NamespaceFactory, {
   AccountNamespace,
   AccountNamespace,
   StateClient,
   StateClient,
   SimulateNamespace,
   SimulateNamespace,
+  MethodsNamespace,
 } from "./namespace/index.js";
 } from "./namespace/index.js";
 import { utf8 } from "../utils/bytes/index.js";
 import { utf8 } from "../utils/bytes/index.js";
 import { EventManager } from "./event.js";
 import { EventManager } from "./event.js";
@@ -206,6 +207,12 @@ export class Program<IDL extends Idl = Idl> {
    */
    */
   readonly state?: StateClient<IDL>;
   readonly state?: StateClient<IDL>;
 
 
+  /**
+   * 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<IDL>;
+
   /**
   /**
    * Address of the program.
    * Address of the program.
    */
    */
@@ -275,6 +282,7 @@ export class Program<IDL extends Idl = Idl> {
       transaction,
       transaction,
       account,
       account,
       simulate,
       simulate,
+      methods,
       state,
       state,
     ] = NamespaceFactory.build(idl, this._coder, programId, provider);
     ] = NamespaceFactory.build(idl, this._coder, programId, provider);
     this.rpc = rpc;
     this.rpc = rpc;
@@ -282,6 +290,7 @@ export class Program<IDL extends Idl = Idl> {
     this.transaction = transaction;
     this.transaction = transaction;
     this.account = account;
     this.account = account;
     this.simulate = simulate;
     this.simulate = simulate;
+    this.methods = methods;
     this.state = state;
     this.state = state;
   }
   }
 
 

+ 12 - 0
ts/src/program/namespace/index.ts

@@ -11,6 +11,7 @@ import AccountFactory, { AccountNamespace } from "./account.js";
 import SimulateFactory, { SimulateNamespace } from "./simulate.js";
 import SimulateFactory, { SimulateNamespace } from "./simulate.js";
 import { parseIdlErrors } from "../common.js";
 import { parseIdlErrors } from "../common.js";
 import { AllInstructions } from "./types.js";
 import { AllInstructions } from "./types.js";
+import { MethodsBuilderFactory, MethodsNamespace } from "./methods";
 
 
 // Re-exports.
 // Re-exports.
 export { StateClient } from "./state.js";
 export { StateClient } from "./state.js";
@@ -20,6 +21,7 @@ export { RpcNamespace, RpcFn } from "./rpc.js";
 export { AccountNamespace, AccountClient, ProgramAccount } from "./account.js";
 export { AccountNamespace, AccountClient, ProgramAccount } from "./account.js";
 export { SimulateNamespace, SimulateFn } from "./simulate.js";
 export { SimulateNamespace, SimulateFn } from "./simulate.js";
 export { IdlAccounts, IdlTypes } from "./types.js";
 export { IdlAccounts, IdlTypes } from "./types.js";
+export { MethodsBuilderFactory, MethodsNamespace } from "./methods";
 
 
 export default class NamespaceFactory {
 export default class NamespaceFactory {
   /**
   /**
@@ -36,12 +38,14 @@ export default class NamespaceFactory {
     TransactionNamespace<IDL>,
     TransactionNamespace<IDL>,
     AccountNamespace<IDL>,
     AccountNamespace<IDL>,
     SimulateNamespace<IDL>,
     SimulateNamespace<IDL>,
+    MethodsNamespace<IDL>,
     StateClient<IDL> | undefined
     StateClient<IDL> | undefined
   ] {
   ] {
     const rpc: RpcNamespace = {};
     const rpc: RpcNamespace = {};
     const instruction: InstructionNamespace = {};
     const instruction: InstructionNamespace = {};
     const transaction: TransactionNamespace = {};
     const transaction: TransactionNamespace = {};
     const simulate: SimulateNamespace = {};
     const simulate: SimulateNamespace = {};
+    const methods: MethodsNamespace = {};
 
 
     const idlErrors = parseIdlErrors(idl);
     const idlErrors = parseIdlErrors(idl);
 
 
@@ -64,6 +68,12 @@ export default class NamespaceFactory {
         programId,
         programId,
         idl
         idl
       );
       );
+      const methodItem = MethodsBuilderFactory.build(
+        ixItem,
+        txItem,
+        rpcItem,
+        simulateItem
+      );
 
 
       const name = camelCase(idlIx.name);
       const name = camelCase(idlIx.name);
 
 
@@ -71,6 +81,7 @@ export default class NamespaceFactory {
       transaction[name] = txItem;
       transaction[name] = txItem;
       rpc[name] = rpcItem;
       rpc[name] = rpcItem;
       simulate[name] = simulateItem;
       simulate[name] = simulateItem;
+      methods[name] = methodItem;
     });
     });
 
 
     const account: AccountNamespace<IDL> = idl.accounts
     const account: AccountNamespace<IDL> = idl.accounts
@@ -83,6 +94,7 @@ export default class NamespaceFactory {
       transaction as TransactionNamespace<IDL>,
       transaction as TransactionNamespace<IDL>,
       account,
       account,
       simulate as SimulateNamespace<IDL>,
       simulate as SimulateNamespace<IDL>,
+      methods as MethodsNamespace<IDL>,
       state,
       state,
     ];
     ];
   }
   }

+ 143 - 0
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<IDL extends Idl, I extends AllInstructions<IDL>>(
+    ixFn: InstructionFn<IDL>,
+    txFn: TransactionFn<IDL>,
+    rpcFn: RpcFn<IDL>,
+    simulateFn: SimulateFn<IDL>
+  ): MethodFn {
+    const request: MethodFn<IDL, I> = (...args) => {
+      return new MethodsBuilder(args, ixFn, txFn, rpcFn, simulateFn);
+    };
+    return request;
+  }
+}
+
+export class MethodsBuilder<IDL extends Idl, I extends AllInstructions<IDL>> {
+  private _accounts: { [name: string]: PublicKey } = {};
+  private _remainingAccounts: Array<AccountMeta> = [];
+  private _signers: Array<Signer> = [];
+  private _preInstructions: Array<TransactionInstruction> = [];
+  private _postInstructions: Array<TransactionInstruction> = [];
+
+  constructor(
+    private _args: Array<any>,
+    private _ixFn: InstructionFn<IDL>,
+    private _txFn: TransactionFn<IDL>,
+    private _rpcFn: RpcFn<IDL>,
+    private _simulateFn: SimulateFn<IDL>
+  ) {}
+
+  // TODO: don't use any.
+  public accounts(accounts: any): MethodsBuilder<IDL, I> {
+    Object.assign(this._accounts, accounts);
+    return this;
+  }
+
+  public remainingAccounts(
+    accounts: Array<AccountMeta>
+  ): MethodsBuilder<IDL, I> {
+    this._remainingAccounts = this._remainingAccounts.concat(accounts);
+    return this;
+  }
+
+  public preInstructions(
+    ixs: Array<TransactionInstruction>
+  ): MethodsBuilder<IDL, I> {
+    this._preInstructions = this._preInstructions.concat(ixs);
+    return this;
+  }
+
+  public postInstructions(
+    ixs: Array<TransactionInstruction>
+  ): MethodsBuilder<IDL, I> {
+    this._postInstructions = this._postInstructions.concat(ixs);
+    return this;
+  }
+
+  public async rpc(options: ConfirmOptions): Promise<TransactionSignature> {
+    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<SimulateResponse<any, any>> {
+    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<TransactionInstruction> {
+    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<Transaction> {
+    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<IDL> = AllInstructions<IDL>
+> = MakeInstructionsNamespace<IDL, I, any>; // TODO: don't use any.
+
+export type MethodFn<
+  IDL extends Idl = Idl,
+  I extends AllInstructions<IDL> = AllInstructions<IDL>
+> = InstructionContextFn<IDL, I, MethodsBuilder<IDL, I>>;

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

@@ -134,7 +134,7 @@ export type SimulateFn<
   Promise<SimulateResponse<NullableEvents<IDL>, IdlTypes<IDL>>>
   Promise<SimulateResponse<NullableEvents<IDL>, IdlTypes<IDL>>>
 >;
 >;
 
 
-type SimulateResponse<E extends IdlEvent, Defined> = {
+export type SimulateResponse<E extends IdlEvent, Defined> = {
   events: readonly Event<E, Defined>[];
   events: readonly Event<E, Defined>[];
   raw: readonly string[];
   raw: readonly string[];
 };
 };