ts: Add program simulate namespace (#266)
This commit is contained in:
parent
425997a12d
commit
9b446dbae1
|
@ -13,6 +13,7 @@ incremented for features.
|
|||
|
||||
## Features
|
||||
|
||||
* ts: Add `program.simulate` namespace ([#266](https://github.com/project-serum/anchor/pull/266)).
|
||||
* cli: Add yarn flag to test command ([#267](https://github.com/project-serum/anchor/pull/267)).
|
||||
|
||||
## [0.5.0] - 2021-05-07
|
||||
|
|
|
@ -57,6 +57,13 @@ pub mod misc {
|
|||
ctx.accounts.my_account.data = data;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn test_simulate(_ctx: Context<TestSimulate>, data: u32) -> ProgramResult {
|
||||
emit!(E1 { data });
|
||||
emit!(E2 { data: 1234 });
|
||||
emit!(E3 { data: 9 });
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Accounts)]
|
||||
|
@ -120,6 +127,9 @@ pub struct TestU16<'info> {
|
|||
rent: Sysvar<'info, Rent>,
|
||||
}
|
||||
|
||||
#[derive(Accounts)]
|
||||
pub struct TestSimulate {}
|
||||
|
||||
#[associated]
|
||||
pub struct TestData {
|
||||
data: u64,
|
||||
|
@ -135,3 +145,18 @@ pub struct Data {
|
|||
pub struct DataU16 {
|
||||
data: u16,
|
||||
}
|
||||
|
||||
#[event]
|
||||
pub struct E1 {
|
||||
data: u32,
|
||||
}
|
||||
|
||||
#[event]
|
||||
pub struct E2 {
|
||||
data: u32,
|
||||
}
|
||||
|
||||
#[event]
|
||||
pub struct E3 {
|
||||
data: u32,
|
||||
}
|
||||
|
|
|
@ -173,4 +173,23 @@ describe("misc", () => {
|
|||
);
|
||||
assert.ok(account.data.toNumber() === 1234);
|
||||
});
|
||||
|
||||
it("Can retrieve events when simulating a transaction", async () => {
|
||||
const resp = await program.simulate.testSimulate(44);
|
||||
const expectedRaw = [
|
||||
"Program Z2Ddx1Lcd8CHTV9tkWtNnFQrSz6kxz2H38wrr18zZRZ invoke [1]",
|
||||
"Program log: NgyCA9omwbMsAAAA",
|
||||
"Program log: fPhuIELK/k7SBAAA",
|
||||
"Program log: jvbowsvlmkcJAAAA",
|
||||
"Program Z2Ddx1Lcd8CHTV9tkWtNnFQrSz6kxz2H38wrr18zZRZ consumed 4819 of 200000 compute units",
|
||||
"Program Z2Ddx1Lcd8CHTV9tkWtNnFQrSz6kxz2H38wrr18zZRZ success",
|
||||
];
|
||||
assert.ok(JSON.stringify(expectedRaw), resp.raw);
|
||||
assert.ok(resp.events[0].name === "E1");
|
||||
assert.ok(resp.events[0].data.data === 44);
|
||||
assert.ok(resp.events[1].name === "E2");
|
||||
assert.ok(resp.events[1].data.data === 1234);
|
||||
assert.ok(resp.events[2].name === "E3");
|
||||
assert.ok(resp.events[2].data.data === 9);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -2,20 +2,33 @@ import { PublicKey } from "@solana/web3.js";
|
|||
import * as base64 from "base64-js";
|
||||
import * as assert from "assert";
|
||||
import Coder, { eventDiscriminator } from "../coder";
|
||||
import { Idl } from "../idl";
|
||||
|
||||
const LOG_START_INDEX = "Program log: ".length;
|
||||
|
||||
export class EventParser<T> {
|
||||
// Deserialized event.
|
||||
export type Event = {
|
||||
name: string;
|
||||
data: Object;
|
||||
};
|
||||
|
||||
export class EventParser {
|
||||
private coder: Coder;
|
||||
private programId: PublicKey;
|
||||
private eventName: string;
|
||||
private discriminator: Buffer;
|
||||
// Maps base64 encoded event discriminator to event name.
|
||||
private discriminators: Map<string, string>;
|
||||
|
||||
constructor(coder: Coder, programId: PublicKey, eventName: string) {
|
||||
constructor(coder: Coder, programId: PublicKey, idl: Idl) {
|
||||
this.coder = coder;
|
||||
this.programId = programId;
|
||||
this.eventName = eventName;
|
||||
this.discriminator = eventDiscriminator(eventName);
|
||||
this.discriminators = new Map<string, string>(
|
||||
idl.events === undefined
|
||||
? []
|
||||
: idl.events.map((e) => [
|
||||
base64.fromByteArray(eventDiscriminator(e.name)),
|
||||
e.name,
|
||||
])
|
||||
);
|
||||
}
|
||||
|
||||
// Each log given, represents an array of messages emitted by
|
||||
|
@ -29,10 +42,9 @@ export class EventParser<T> {
|
|||
// its emission, thereby allowing us to know if a given log event was
|
||||
// emitted by *this* program. If it was, then we parse the raw string and
|
||||
// emit the event if the string matches the event being subscribed to.
|
||||
public parseLogs(logs: string[], callback: (log: T) => void) {
|
||||
public parseLogs(logs: string[], callback: (log: Event) => void) {
|
||||
const logScanner = new LogScanner(logs);
|
||||
const execution = new ExecutionContext(logScanner.next() as string);
|
||||
|
||||
let log = logScanner.next();
|
||||
while (log !== null) {
|
||||
let [event, newProgram, didPop] = this.handleLog(execution, log);
|
||||
|
@ -44,11 +56,57 @@ export class EventParser<T> {
|
|||
}
|
||||
if (didPop) {
|
||||
execution.pop();
|
||||
// Skip the "success" log, which always follows the consumed log.
|
||||
logScanner.next();
|
||||
}
|
||||
log = logScanner.next();
|
||||
}
|
||||
}
|
||||
|
||||
// Main log handler. Returns a three element array of the event, the
|
||||
// next program that was invoked for CPI, and a boolean indicating if
|
||||
// a program has completed execution (and thus should be popped off the
|
||||
// execution stack).
|
||||
private handleLog(
|
||||
execution: ExecutionContext,
|
||||
log: string
|
||||
): [Event | null, string | null, boolean] {
|
||||
// Executing program is this program.
|
||||
if (execution.program() === this.programId.toString()) {
|
||||
return this.handleProgramLog(log);
|
||||
}
|
||||
// Executing program is not this program.
|
||||
else {
|
||||
return [null, ...this.handleSystemLog(log)];
|
||||
}
|
||||
}
|
||||
|
||||
// Handles logs from *this* program.
|
||||
private handleProgramLog(
|
||||
log: string
|
||||
): [Event | null, string | null, boolean] {
|
||||
// This is a `msg!` log.
|
||||
if (log.startsWith("Program log:")) {
|
||||
const logStr = log.slice(LOG_START_INDEX);
|
||||
const logArr = Buffer.from(base64.toByteArray(logStr));
|
||||
const disc = base64.fromByteArray(logArr.slice(0, 8));
|
||||
// Only deserialize if the discriminator implies a proper event.
|
||||
let event = null;
|
||||
let eventName = this.discriminators.get(disc);
|
||||
if (eventName !== undefined) {
|
||||
event = {
|
||||
name: eventName,
|
||||
data: this.coder.events.decode(eventName, logArr.slice(8)),
|
||||
};
|
||||
}
|
||||
return [event, null, false];
|
||||
}
|
||||
// System log.
|
||||
else {
|
||||
return [null, ...this.handleSystemLog(log)];
|
||||
}
|
||||
}
|
||||
|
||||
// Handles logs when the current program being executing is *not* this.
|
||||
private handleSystemLog(log: string): [string | null, boolean] {
|
||||
// System component.
|
||||
|
@ -68,44 +126,6 @@ export class EventParser<T> {
|
|||
return [null, false];
|
||||
}
|
||||
}
|
||||
|
||||
// Handles logs from *this* program.
|
||||
private handleProgramLog(log: string): [T | null, string | null, boolean] {
|
||||
// This is a `msg!` log.
|
||||
if (log.startsWith("Program log:")) {
|
||||
const logStr = log.slice(LOG_START_INDEX);
|
||||
const logArr = Buffer.from(base64.toByteArray(logStr));
|
||||
const disc = logArr.slice(0, 8);
|
||||
// Only deserialize if the discriminator implies a proper event.
|
||||
let event = null;
|
||||
if (disc.equals(this.discriminator)) {
|
||||
event = this.coder.events.decode(this.eventName, logArr.slice(8));
|
||||
}
|
||||
return [event, null, false];
|
||||
}
|
||||
// System log.
|
||||
else {
|
||||
return [null, ...this.handleSystemLog(log)];
|
||||
}
|
||||
}
|
||||
|
||||
// Main log handler. Returns a three element array of the event, the
|
||||
// next program that was invoked for CPI, and a boolean indicating if
|
||||
// a program has completed execution (and thus should be popped off the
|
||||
// execution stack).
|
||||
private handleLog(
|
||||
execution: ExecutionContext,
|
||||
log: string
|
||||
): [T | null, string | null, boolean] {
|
||||
// Executing program is this program.
|
||||
if (execution.program() === this.programId.toString()) {
|
||||
return this.handleProgramLog(log);
|
||||
}
|
||||
// Executing program is not this program.
|
||||
else {
|
||||
return [null, ...this.handleSystemLog(log)];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Stack frame execution context, allowing one to track what program is
|
||||
|
|
|
@ -3,7 +3,14 @@ import { PublicKey } from "@solana/web3.js";
|
|||
import Provider from "../provider";
|
||||
import { Idl, idlAddress, decodeIdlAccount } from "../idl";
|
||||
import Coder from "../coder";
|
||||
import NamespaceFactory, { Rpcs, Ixs, Txs, Accounts, State } from "./namespace";
|
||||
import NamespaceFactory, {
|
||||
Rpcs,
|
||||
Ixs,
|
||||
Txs,
|
||||
Accounts,
|
||||
State,
|
||||
Simulate,
|
||||
} from "./namespace";
|
||||
import { getProvider } from "../";
|
||||
import { decodeUtf8 } from "../utils";
|
||||
import { EventParser } from "./event";
|
||||
|
@ -23,8 +30,7 @@ export class Program {
|
|||
readonly idl: Idl;
|
||||
|
||||
/**
|
||||
* Async functions to invoke instructions against a Solana priogram running
|
||||
* on a cluster.
|
||||
* Async functions to invoke instructions against an Anchor program.
|
||||
*/
|
||||
readonly rpc: Rpcs;
|
||||
|
||||
|
@ -43,6 +49,11 @@ export class Program {
|
|||
*/
|
||||
readonly transaction: Txs;
|
||||
|
||||
/**
|
||||
* Async functions to simulate instructions against an Anchor program.
|
||||
*/
|
||||
readonly simulate: Simulate;
|
||||
|
||||
/**
|
||||
* Coder for serializing rpc requests.
|
||||
*/
|
||||
|
@ -67,7 +78,7 @@ export class Program {
|
|||
const coder = new Coder(idl);
|
||||
|
||||
// Build the dynamic namespaces.
|
||||
const [rpcs, ixs, txs, accounts, state] = NamespaceFactory.build(
|
||||
const [rpcs, ixs, txs, accounts, state, simulate] = NamespaceFactory.build(
|
||||
idl,
|
||||
coder,
|
||||
programId,
|
||||
|
@ -79,6 +90,7 @@ export class Program {
|
|||
this.account = accounts;
|
||||
this.coder = coder;
|
||||
this.state = state;
|
||||
this.simulate = simulate;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -105,22 +117,20 @@ export class Program {
|
|||
/**
|
||||
* Invokes the given callback everytime the given event is emitted.
|
||||
*/
|
||||
public addEventListener<T>(
|
||||
public addEventListener(
|
||||
eventName: string,
|
||||
callback: (event: T, slot: number) => void
|
||||
callback: (event: any, slot: number) => void
|
||||
): number {
|
||||
const eventParser = new EventParser<T>(
|
||||
this.coder,
|
||||
this.programId,
|
||||
eventName
|
||||
);
|
||||
const eventParser = new EventParser(this.coder, this.programId, this.idl);
|
||||
return this.provider.connection.onLogs(this.programId, (logs, ctx) => {
|
||||
if (logs.err) {
|
||||
console.error(logs);
|
||||
return;
|
||||
}
|
||||
eventParser.parseLogs(logs.logs, (event) => {
|
||||
callback(event, ctx.slot);
|
||||
if (event.name === eventName) {
|
||||
callback(event.data, ctx.slot);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
|
@ -9,6 +9,7 @@ import InstructionNamespace, { Ixs } from "./instruction";
|
|||
import TransactionNamespace, { Txs } from "./transaction";
|
||||
import RpcNamespace, { Rpcs } from "./rpc";
|
||||
import AccountNamespace, { Accounts } from "./account";
|
||||
import SimulateNamespace, { Simulate } from "./simulate";
|
||||
|
||||
// Re-exports.
|
||||
export { State } from "./state";
|
||||
|
@ -16,6 +17,7 @@ export { Ixs } from "./instruction";
|
|||
export { Txs, TxFn } from "./transaction";
|
||||
export { Rpcs, RpcFn } from "./rpc";
|
||||
export { Accounts, AccountFn, ProgramAccount } from "./account";
|
||||
export { Simulate } from "./simulate";
|
||||
|
||||
export default class NamespaceFactory {
|
||||
/**
|
||||
|
@ -28,12 +30,14 @@ export default class NamespaceFactory {
|
|||
coder: Coder,
|
||||
programId: PublicKey,
|
||||
provider: Provider
|
||||
): [Rpcs, Ixs, Txs, Accounts, State] {
|
||||
): [Rpcs, Ixs, Txs, Accounts, State, Simulate] {
|
||||
const idlErrors = parseIdlErrors(idl);
|
||||
|
||||
const rpcs: Rpcs = {};
|
||||
const ixFns: Ixs = {};
|
||||
const txFns: Txs = {};
|
||||
const simulateFns: Simulate = {};
|
||||
|
||||
const state = StateNamespace.build(
|
||||
idl,
|
||||
coder,
|
||||
|
@ -46,18 +50,28 @@ export default class NamespaceFactory {
|
|||
const ix = InstructionNamespace.build(idlIx, coder, programId);
|
||||
const tx = TransactionNamespace.build(idlIx, ix);
|
||||
const rpc = RpcNamespace.build(idlIx, tx, idlErrors, provider);
|
||||
const simulate = SimulateNamespace.build(
|
||||
idlIx,
|
||||
tx,
|
||||
idlErrors,
|
||||
provider,
|
||||
coder,
|
||||
programId,
|
||||
idl
|
||||
);
|
||||
|
||||
const name = camelCase(idlIx.name);
|
||||
|
||||
ixFns[name] = ix;
|
||||
txFns[name] = tx;
|
||||
rpcs[name] = rpc;
|
||||
simulateFns[name] = simulate;
|
||||
});
|
||||
|
||||
const accountFns = idl.accounts
|
||||
? AccountNamespace.build(idl, coder, programId, provider)
|
||||
: {};
|
||||
|
||||
return [rpcs, ixFns, txFns, accountFns, state];
|
||||
return [rpcs, ixFns, txFns, accountFns, state, simulateFns];
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,76 @@
|
|||
import { PublicKey } from "@solana/web3.js";
|
||||
import Provider from "../../provider";
|
||||
import { IdlInstruction } from "../../idl";
|
||||
import { translateError } from "../common";
|
||||
import { splitArgsAndCtx } from "../context";
|
||||
import { TxFn } from "./transaction";
|
||||
import { EventParser } from "../event";
|
||||
import Coder from "../../coder";
|
||||
import { Idl } from "../../idl";
|
||||
|
||||
/**
|
||||
* Dynamically generated simualte namespace.
|
||||
*/
|
||||
export interface Simulate {
|
||||
[key: string]: SimulateFn;
|
||||
}
|
||||
|
||||
/**
|
||||
* RpcFn is a single rpc method generated from an IDL.
|
||||
*/
|
||||
export type SimulateFn = (...args: any[]) => Promise<SimulateResponse>;
|
||||
|
||||
type SimulateResponse = {
|
||||
events: Event[];
|
||||
raw: string[];
|
||||
};
|
||||
|
||||
export default class SimulateNamespace {
|
||||
// Builds the rpc namespace.
|
||||
public static build(
|
||||
idlIx: IdlInstruction,
|
||||
txFn: TxFn,
|
||||
idlErrors: Map<number, string>,
|
||||
provider: Provider,
|
||||
coder: Coder,
|
||||
programId: PublicKey,
|
||||
idl: Idl
|
||||
): SimulateFn {
|
||||
const simulate = async (...args: any[]): Promise<SimulateResponse> => {
|
||||
const tx = txFn(...args);
|
||||
const [_, ctx] = splitArgsAndCtx(idlIx, [...args]);
|
||||
let resp = undefined;
|
||||
try {
|
||||
resp = await provider.simulate(tx, ctx.signers, ctx.options);
|
||||
} catch (err) {
|
||||
console.log("Translating error", err);
|
||||
let translatedErr = translateError(idlErrors, err);
|
||||
if (translatedErr === null) {
|
||||
throw err;
|
||||
}
|
||||
throw translatedErr;
|
||||
}
|
||||
if (resp === undefined) {
|
||||
throw new Error("Unable to simulate transaction");
|
||||
}
|
||||
if (resp.value.err) {
|
||||
throw new Error(`Simulate error: ${resp.value.err.toString()}`);
|
||||
}
|
||||
const logs = resp.value.logs;
|
||||
if (!logs) {
|
||||
throw new Error("Simulated logs not found");
|
||||
}
|
||||
|
||||
const events = [];
|
||||
if (idl.events) {
|
||||
let parser = new EventParser(coder, programId, idl);
|
||||
parser.parseLogs(logs, (event) => {
|
||||
events.push(event);
|
||||
});
|
||||
}
|
||||
return { events, raw: logs };
|
||||
};
|
||||
|
||||
return simulate;
|
||||
}
|
||||
}
|
|
@ -6,6 +6,9 @@ import {
|
|||
TransactionSignature,
|
||||
ConfirmOptions,
|
||||
sendAndConfirmRawTransaction,
|
||||
RpcResponseAndContext,
|
||||
SimulatedTransactionResponse,
|
||||
Commitment,
|
||||
} from "@solana/web3.js";
|
||||
|
||||
export default class Provider {
|
||||
|
@ -134,6 +137,35 @@ export default class Provider {
|
|||
|
||||
return sigs;
|
||||
}
|
||||
|
||||
async simulate(
|
||||
tx: Transaction,
|
||||
signers?: Array<Account | undefined>,
|
||||
opts?: ConfirmOptions
|
||||
): Promise<RpcResponseAndContext<SimulatedTransactionResponse>> {
|
||||
if (signers === undefined) {
|
||||
signers = [];
|
||||
}
|
||||
if (opts === undefined) {
|
||||
opts = this.opts;
|
||||
}
|
||||
|
||||
const signerKps = signers.filter((s) => s !== undefined) as Array<Account>;
|
||||
const signerPubkeys = [this.wallet.publicKey].concat(
|
||||
signerKps.map((s) => s.publicKey)
|
||||
);
|
||||
|
||||
tx.setSigners(...signerPubkeys);
|
||||
tx.recentBlockhash = (
|
||||
await this.connection.getRecentBlockhash(opts.preflightCommitment)
|
||||
).blockhash;
|
||||
|
||||
await this.wallet.signTransaction(tx);
|
||||
signerKps.forEach((kp) => {
|
||||
tx.partialSign(kp);
|
||||
});
|
||||
return await simulateTransaction(this.connection, tx, opts.commitment);
|
||||
}
|
||||
}
|
||||
|
||||
export type SendTxRequest = {
|
||||
|
@ -182,3 +214,30 @@ export class NodeWallet implements Wallet {
|
|||
return this.payer.publicKey;
|
||||
}
|
||||
}
|
||||
|
||||
// Copy of Connection.simulateTransaction that takes a commitment parameter.
|
||||
async function simulateTransaction(
|
||||
connection: Connection,
|
||||
transaction: Transaction,
|
||||
commitment: Commitment
|
||||
): Promise<RpcResponseAndContext<SimulatedTransactionResponse>> {
|
||||
// @ts-ignore
|
||||
transaction.recentBlockhash = await connection._recentBlockhash(
|
||||
// @ts-ignore
|
||||
connection._disableBlockhashCaching
|
||||
);
|
||||
|
||||
const signData = transaction.serializeMessage();
|
||||
// @ts-ignore
|
||||
const wireTransaction = transaction._serialize(signData);
|
||||
const encodedTransaction = wireTransaction.toString("base64");
|
||||
const config: any = { encoding: "base64", commitment };
|
||||
const args = [encodedTransaction, config];
|
||||
|
||||
// @ts-ignore
|
||||
const res = await connection._rpcRequest("simulateTransaction", args);
|
||||
if (res.error) {
|
||||
throw new Error("failed to simulate transaction: " + res.error.message);
|
||||
}
|
||||
return res.result;
|
||||
}
|
||||
|
|
|
@ -29,9 +29,7 @@ export default new Proxy({} as any, {
|
|||
}
|
||||
|
||||
if (projectRoot === undefined) {
|
||||
throw new Error(
|
||||
"Could not find workspace root. Perhaps set the `OASIS_WORKSPACE` env var?"
|
||||
);
|
||||
throw new Error("Could not find workspace root.");
|
||||
}
|
||||
|
||||
find
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"include": ["src"],
|
||||
"include": ["./src/**/*"],
|
||||
"compilerOptions": {
|
||||
"moduleResolution": "node",
|
||||
"module": "es6",
|
||||
|
@ -18,6 +18,7 @@
|
|||
"noImplicitAny": false,
|
||||
|
||||
"esModuleInterop": true,
|
||||
"resolveJsonModule": true,
|
||||
"composite": true,
|
||||
"baseUrl": ".",
|
||||
"typeRoots": ["types/", "node_modules/@types"],
|
||||
|
|
Loading…
Reference in New Issue