ts: Add instruction decode api (#372)
This commit is contained in:
parent
381a3dfde6
commit
278d87e402
|
@ -15,6 +15,8 @@ incremented for features.
|
||||||
|
|
||||||
* cli: Add `--program-name` option for build command to build a single program at a time ([#362](https://github.com/project-serum/anchor/pull/362)).
|
* cli: Add `--program-name` option for build command to build a single program at a time ([#362](https://github.com/project-serum/anchor/pull/362)).
|
||||||
* cli, client: Parse custom cluster urls from str ([#369](https://github.com/project-serum/anchor/pull/369)).
|
* cli, client: Parse custom cluster urls from str ([#369](https://github.com/project-serum/anchor/pull/369)).
|
||||||
|
* cli, client, lang: Update solana toolchain to v1.7.1 ([#368](https://github.com/project-serum/anchor/pull/369)).
|
||||||
|
* ts: Instruction decoding and formatting ([#372](https://github.com/project-serum/anchor/pull/372)).
|
||||||
* lang: Add `#[account(close = <destination>)]` constraint for closing accounts and sending the rent exemption lamports to a specified destination account ([#371](https://github.com/project-serum/anchor/pull/371)).
|
* lang: Add `#[account(close = <destination>)]` constraint for closing accounts and sending the rent exemption lamports to a specified destination account ([#371](https://github.com/project-serum/anchor/pull/371)).
|
||||||
|
|
||||||
### Fixes
|
### Fixes
|
||||||
|
|
|
@ -1,9 +1,19 @@
|
||||||
import camelCase from "camelcase";
|
import camelCase from "camelcase";
|
||||||
import { Layout } from "buffer-layout";
|
import { Layout } from "buffer-layout";
|
||||||
import * as borsh from "@project-serum/borsh";
|
import * as borsh from "@project-serum/borsh";
|
||||||
import { Idl, IdlField, IdlStateMethod } from "../idl";
|
import * as bs58 from "bs58";
|
||||||
|
import {
|
||||||
|
Idl,
|
||||||
|
IdlField,
|
||||||
|
IdlStateMethod,
|
||||||
|
IdlType,
|
||||||
|
IdlTypeDef,
|
||||||
|
IdlAccount,
|
||||||
|
IdlAccountItem,
|
||||||
|
} from "../idl";
|
||||||
import { IdlCoder } from "./idl";
|
import { IdlCoder } from "./idl";
|
||||||
import { sighash } from "./common";
|
import { sighash } from "./common";
|
||||||
|
import { AccountMeta, PublicKey } from "@solana/web3.js";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Namespace for state method function signatures.
|
* Namespace for state method function signatures.
|
||||||
|
@ -19,13 +29,35 @@ export const SIGHASH_GLOBAL_NAMESPACE = "global";
|
||||||
* Encodes and decodes program instructions.
|
* Encodes and decodes program instructions.
|
||||||
*/
|
*/
|
||||||
export class InstructionCoder {
|
export class InstructionCoder {
|
||||||
/**
|
// Instruction args layout. Maps namespaced method
|
||||||
* Instruction args layout. Maps namespaced method
|
|
||||||
*/
|
|
||||||
private ixLayout: Map<string, Layout>;
|
private ixLayout: Map<string, Layout>;
|
||||||
|
|
||||||
public constructor(idl: Idl) {
|
// Base58 encoded sighash to instruction layout.
|
||||||
|
private sighashLayouts: Map<string, { layout: Layout; name: string }>;
|
||||||
|
|
||||||
|
public constructor(private idl: Idl) {
|
||||||
this.ixLayout = InstructionCoder.parseIxLayout(idl);
|
this.ixLayout = InstructionCoder.parseIxLayout(idl);
|
||||||
|
|
||||||
|
const sighashLayouts = new Map();
|
||||||
|
idl.instructions.forEach((ix) => {
|
||||||
|
const sh = sighash(SIGHASH_GLOBAL_NAMESPACE, ix.name);
|
||||||
|
sighashLayouts.set(bs58.encode(sh), {
|
||||||
|
layout: this.ixLayout.get(ix.name),
|
||||||
|
name: ix.name,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
if (idl.state) {
|
||||||
|
idl.state.methods.map((ix) => {
|
||||||
|
const sh = sighash(SIGHASH_STATE_NAMESPACE, ix.name);
|
||||||
|
sighashLayouts.set(bs58.encode(sh), {
|
||||||
|
layout: this.ixLayout.get(ix.name) as Layout,
|
||||||
|
name: ix.name,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
this.sighashLayouts = sighashLayouts;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -73,4 +105,251 @@ export class InstructionCoder {
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
return new Map(ixLayouts);
|
return new Map(ixLayouts);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Dewcodes a program instruction.
|
||||||
|
*/
|
||||||
|
public decode(ix: Buffer | string): Instruction | null {
|
||||||
|
if (typeof ix === "string") {
|
||||||
|
ix = bs58.decode(ix);
|
||||||
|
}
|
||||||
|
let sighash = bs58.encode(ix.slice(0, 8));
|
||||||
|
let data = ix.slice(8);
|
||||||
|
const decoder = this.sighashLayouts.get(sighash);
|
||||||
|
if (!decoder) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
data: decoder.layout.decode(data),
|
||||||
|
name: decoder.name,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a formatted table of all the fields in the given instruction data.
|
||||||
|
*/
|
||||||
|
public format(
|
||||||
|
ix: Instruction,
|
||||||
|
accountMetas: AccountMeta[]
|
||||||
|
): InstructionDisplay | null {
|
||||||
|
return InstructionFormatter.format(ix, accountMetas, this.idl);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Instruction = {
|
||||||
|
name: string;
|
||||||
|
data: Object;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type InstructionDisplay = {
|
||||||
|
args: { name: string; type: string; data: string }[];
|
||||||
|
accounts: {
|
||||||
|
name?: string;
|
||||||
|
pubkey: PublicKey;
|
||||||
|
isSigner: boolean;
|
||||||
|
isWritable: boolean;
|
||||||
|
}[];
|
||||||
|
};
|
||||||
|
|
||||||
|
class InstructionFormatter {
|
||||||
|
public static format(
|
||||||
|
ix: Instruction,
|
||||||
|
accountMetas: AccountMeta[],
|
||||||
|
idl: Idl
|
||||||
|
): InstructionDisplay | null {
|
||||||
|
const idlIx = idl.instructions.filter((i) => ix.name === i.name)[0];
|
||||||
|
if (idlIx === undefined) {
|
||||||
|
console.error("Invalid instruction given");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const args = idlIx.args.map((idlField) => {
|
||||||
|
return {
|
||||||
|
name: idlField.name,
|
||||||
|
type: InstructionFormatter.formatIdlType(idlField.type),
|
||||||
|
data: InstructionFormatter.formatIdlData(
|
||||||
|
idlField,
|
||||||
|
ix.data[idlField.name],
|
||||||
|
idl.types
|
||||||
|
),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const flatIdlAccounts = InstructionFormatter.flattenIdlAccounts(
|
||||||
|
idlIx.accounts
|
||||||
|
);
|
||||||
|
|
||||||
|
const accounts = accountMetas.map((meta, idx) => {
|
||||||
|
if (idx < flatIdlAccounts.length) {
|
||||||
|
return {
|
||||||
|
name: flatIdlAccounts[idx].name,
|
||||||
|
...meta,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
// "Remaining accounts" are unnamed in Anchor.
|
||||||
|
else {
|
||||||
|
return {
|
||||||
|
name: undefined,
|
||||||
|
...meta,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
args,
|
||||||
|
accounts,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static formatIdlType(idlType: IdlType): string {
|
||||||
|
if (typeof idlType === "string") {
|
||||||
|
return idlType as string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// @ts-ignore
|
||||||
|
if (idlType.vec) {
|
||||||
|
// @ts-ignore
|
||||||
|
return `Vec<${this.formatIdlType(idlType.vec)}>`;
|
||||||
|
}
|
||||||
|
// @ts-ignore
|
||||||
|
if (idlType.option) {
|
||||||
|
// @ts-ignore
|
||||||
|
return `Option<${this.formatIdlType(idlType.option)}>`;
|
||||||
|
}
|
||||||
|
// @ts-ignore
|
||||||
|
if (idlType.defined) {
|
||||||
|
// @ts-ignore
|
||||||
|
return idlType.defined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static formatIdlData(
|
||||||
|
idlField: IdlField,
|
||||||
|
data: Object,
|
||||||
|
types?: IdlTypeDef[]
|
||||||
|
): string {
|
||||||
|
if (typeof idlField.type === "string") {
|
||||||
|
return data.toString();
|
||||||
|
}
|
||||||
|
// @ts-ignore
|
||||||
|
if (idlField.type.vec) {
|
||||||
|
// @ts-ignore
|
||||||
|
return (
|
||||||
|
"[" +
|
||||||
|
data
|
||||||
|
// @ts-ignore
|
||||||
|
.map((d: IdlField) =>
|
||||||
|
this.formatIdlData(
|
||||||
|
// @ts-ignore
|
||||||
|
{ name: "", type: idlField.type.vec },
|
||||||
|
d
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.join(", ") +
|
||||||
|
"]"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
// @ts-ignore
|
||||||
|
if (idlField.type.option) {
|
||||||
|
// @ts-ignore
|
||||||
|
return data === null
|
||||||
|
? "null"
|
||||||
|
: this.formatIdlData(
|
||||||
|
// @ts-ignore
|
||||||
|
{ name: "", type: idlField.type.option },
|
||||||
|
data
|
||||||
|
);
|
||||||
|
}
|
||||||
|
// @ts-ignore
|
||||||
|
if (idlField.type.defined) {
|
||||||
|
if (types === undefined) {
|
||||||
|
throw new Error("User defined types not provided");
|
||||||
|
}
|
||||||
|
// @ts-ignore
|
||||||
|
const filtered = types.filter((t) => t.name === idlField.type.defined);
|
||||||
|
if (filtered.length !== 1) {
|
||||||
|
// @ts-ignore
|
||||||
|
throw new Error(`Type not found: ${idlField.type.defined}`);
|
||||||
|
}
|
||||||
|
return InstructionFormatter.formatIdlDataDefined(
|
||||||
|
filtered[0],
|
||||||
|
data,
|
||||||
|
types
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return "unknown";
|
||||||
|
}
|
||||||
|
|
||||||
|
private static formatIdlDataDefined(
|
||||||
|
typeDef: IdlTypeDef,
|
||||||
|
data: Object,
|
||||||
|
types: IdlTypeDef[]
|
||||||
|
): string {
|
||||||
|
if (typeDef.type.kind === "struct") {
|
||||||
|
const fields = Object.keys(data)
|
||||||
|
.map((k) => {
|
||||||
|
const f = typeDef.type.fields.filter((f) => f.name === k)[0];
|
||||||
|
if (f === undefined) {
|
||||||
|
throw new Error("Unable to find type");
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
k + ": " + InstructionFormatter.formatIdlData(f, data[k], types)
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.join(", ");
|
||||||
|
return "{ " + fields + " }";
|
||||||
|
} else {
|
||||||
|
if (typeDef.type.variants.length === 0) {
|
||||||
|
return "{}";
|
||||||
|
}
|
||||||
|
// Struct enum.
|
||||||
|
if (typeDef.type.variants[0].name) {
|
||||||
|
const variant = Object.keys(data)[0];
|
||||||
|
const enumType = data[variant];
|
||||||
|
const namedFields = Object.keys(enumType)
|
||||||
|
.map((f) => {
|
||||||
|
const fieldData = enumType[f];
|
||||||
|
const idlField = typeDef.type.variants[variant]?.filter(
|
||||||
|
(v: IdlField) => v.name === f
|
||||||
|
)[0];
|
||||||
|
if (idlField === undefined) {
|
||||||
|
throw new Error("Unable to find variant");
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
f +
|
||||||
|
": " +
|
||||||
|
InstructionFormatter.formatIdlData(idlField, fieldData, types)
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.join(", ");
|
||||||
|
|
||||||
|
const variantName = camelCase(variant, { pascalCase: true });
|
||||||
|
if (namedFields.length === 0) {
|
||||||
|
return variantName;
|
||||||
|
}
|
||||||
|
return `${variantName} { ${namedFields} }`;
|
||||||
|
}
|
||||||
|
// Tuple enum.
|
||||||
|
else {
|
||||||
|
// TODO.
|
||||||
|
return "Tuple formatting not yet implemented";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static flattenIdlAccounts(accounts: IdlAccountItem[]): IdlAccount[] {
|
||||||
|
// @ts-ignore
|
||||||
|
return accounts
|
||||||
|
.map((account) => {
|
||||||
|
// @ts-ignore
|
||||||
|
if (account.accounts) {
|
||||||
|
// @ts-ignore
|
||||||
|
return InstructionFormatter.flattenIdlAccounts(account.accounts);
|
||||||
|
} else {
|
||||||
|
return account;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.flat();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,6 +11,7 @@ import Coder, {
|
||||||
StateCoder,
|
StateCoder,
|
||||||
TypesCoder,
|
TypesCoder,
|
||||||
} from "./coder";
|
} from "./coder";
|
||||||
|
import { Instruction } from "./coder/instruction";
|
||||||
import { Idl } from "./idl";
|
import { Idl } from "./idl";
|
||||||
import workspace from "./workspace";
|
import workspace from "./workspace";
|
||||||
import * as utils from "./utils";
|
import * as utils from "./utils";
|
||||||
|
@ -56,6 +57,7 @@ export {
|
||||||
StateCoder,
|
StateCoder,
|
||||||
TypesCoder,
|
TypesCoder,
|
||||||
Event,
|
Event,
|
||||||
|
Instruction,
|
||||||
setProvider,
|
setProvider,
|
||||||
getProvider,
|
getProvider,
|
||||||
Provider,
|
Provider,
|
||||||
|
|
Loading…
Reference in New Issue