ts: builder api (#1324)

This commit is contained in:
Armani Ferrante 2022-01-15 17:09:53 -05:00 committed by GitHub
parent 1f95929f1d
commit e121e4e09d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 225 additions and 57 deletions

View File

@ -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

View File

@ -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.

View File

@ -225,29 +225,24 @@ describe("cfo", () => {
stake: stakeBump, stake: stakeBump,
treasury: treasuryBump, treasury: treasuryBump,
}; };
await program.rpc.createOfficer( await program.methods
bumps, .createOfficer(bumps, distribution, registrar, msrmRegistrar)
distribution, .accounts({
registrar, officer,
msrmRegistrar, srmVault,
{ usdcVault,
accounts: { stake,
officer, treasury,
srmVault, srmMint: ORDERBOOK_ENV.mintA,
usdcVault, usdcMint: ORDERBOOK_ENV.usdc,
stake, authority: program.provider.wallet.publicKey,
treasury, dexProgram: DEX_PID,
srmMint: ORDERBOOK_ENV.mintA, swapProgram: SWAP_PID,
usdcMint: ORDERBOOK_ENV.usdc, tokenProgram: TOKEN_PID,
authority: program.provider.wallet.publicKey, systemProgram: SystemProgram.programId,
dexProgram: DEX_PID, rent: SYSVAR_RENT_PUBKEY,
swapProgram: SWAP_PID, })
tokenProgram: TOKEN_PID, .rpc();
systemProgram: SystemProgram.programId,
rent: SYSVAR_RENT_PUBKEY,
},
}
);
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, { await program.methods
accounts: { .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, { await program.methods
accounts: { .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({ await program.methods
accounts: { .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, { await program.methods
accounts: { .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,
}, })
}); .rpc();
await program.rpc.authorizeMarket(marketAuthBumpB, { await program.methods
accounts: { .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, { await program.methods
accounts: { .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, { await program.methods
accounts: { .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({ await program.methods
accounts: { .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);

View File

@ -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;
} }

View File

@ -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,
]; ];
} }

View File

@ -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>>;

View File

@ -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[];
}; };