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, 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)).
|
||||
|
||||
### Fixes
|
||||
|
|
|
@ -1,9 +1,19 @@
|
|||
import camelCase from "camelcase";
|
||||
import { Layout } from "buffer-layout";
|
||||
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 { sighash } from "./common";
|
||||
import { AccountMeta, PublicKey } from "@solana/web3.js";
|
||||
|
||||
/**
|
||||
* Namespace for state method function signatures.
|
||||
|
@ -19,13 +29,35 @@ export const SIGHASH_GLOBAL_NAMESPACE = "global";
|
|||
* Encodes and decodes program instructions.
|
||||
*/
|
||||
export class InstructionCoder {
|
||||
/**
|
||||
* Instruction args layout. Maps namespaced method
|
||||
*/
|
||||
// Instruction args layout. Maps namespaced method
|
||||
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);
|
||||
|
||||
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
|
||||
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,
|
||||
TypesCoder,
|
||||
} from "./coder";
|
||||
import { Instruction } from "./coder/instruction";
|
||||
import { Idl } from "./idl";
|
||||
import workspace from "./workspace";
|
||||
import * as utils from "./utils";
|
||||
|
@ -56,6 +57,7 @@ export {
|
|||
StateCoder,
|
||||
TypesCoder,
|
||||
Event,
|
||||
Instruction,
|
||||
setProvider,
|
||||
getProvider,
|
||||
Provider,
|
||||
|
|
Loading…
Reference in New Issue