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))
* 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

View File

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

View File

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

View File

@ -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<IDL extends Idl = 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.
*/
@ -275,6 +282,7 @@ export class Program<IDL extends Idl = Idl> {
transaction,
account,
simulate,
methods,
state,
] = NamespaceFactory.build(idl, this._coder, programId, provider);
this.rpc = rpc;
@ -282,6 +290,7 @@ export class Program<IDL extends Idl = Idl> {
this.transaction = transaction;
this.account = account;
this.simulate = simulate;
this.methods = methods;
this.state = state;
}

View File

@ -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<IDL>,
AccountNamespace<IDL>,
SimulateNamespace<IDL>,
MethodsNamespace<IDL>,
StateClient<IDL> | 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> = idl.accounts
@ -83,6 +94,7 @@ export default class NamespaceFactory {
transaction as TransactionNamespace<IDL>,
account,
simulate as SimulateNamespace<IDL>,
methods as MethodsNamespace<IDL>,
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>>>
>;
type SimulateResponse<E extends IdlEvent, Defined> = {
export type SimulateResponse<E extends IdlEvent, Defined> = {
events: readonly Event<E, Defined>[];
raw: readonly string[];
};