anchor/ts/packages/anchor/src/coder/borsh/instruction.ts

357 lines
9.5 KiB
TypeScript

import bs58 from "bs58";
import { Buffer } from "buffer";
import { Layout } from "buffer-layout";
import camelCase from "camelcase";
import { snakeCase } from "snake-case";
import { sha256 } from "js-sha256";
import * as borsh from "@coral-xyz/borsh";
import { AccountMeta, PublicKey } from "@solana/web3.js";
import {
Idl,
IdlField,
IdlType,
IdlTypeDef,
IdlAccount,
IdlAccountItem,
IdlTypeDefTyStruct,
IdlTypeVec,
IdlTypeOption,
IdlTypeDefined,
IdlAccounts,
} from "../../idl.js";
import { IdlCoder } from "./idl.js";
import { InstructionCoder } from "../index.js";
/**
* Namespace for global instruction function signatures (i.e. functions
* that aren't namespaced by the state or any of its trait implementations).
*/
export const SIGHASH_GLOBAL_NAMESPACE = "global";
/**
* Encodes and decodes program instructions.
*/
export class BorshInstructionCoder implements InstructionCoder {
// Instruction args layout. Maps namespaced method
private ixLayout: Map<string, Layout>;
// Base58 encoded sighash to instruction layout.
private sighashLayouts: Map<string, { layout: Layout; name: string }>;
public constructor(private idl: Idl) {
this.ixLayout = BorshInstructionCoder.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,
});
});
this.sighashLayouts = sighashLayouts;
}
/**
* Encodes a program instruction.
*/
public encode(ixName: string, ix: any): Buffer {
return this._encode(SIGHASH_GLOBAL_NAMESPACE, ixName, ix);
}
private _encode(nameSpace: string, ixName: string, ix: any): Buffer {
const buffer = Buffer.alloc(1000); // TODO: use a tighter buffer.
const methodName = camelCase(ixName);
const layout = this.ixLayout.get(methodName);
if (!layout) {
throw new Error(`Unknown method: ${methodName}`);
}
const len = layout.encode(ix, buffer);
const data = buffer.slice(0, len);
return Buffer.concat([sighash(nameSpace, ixName), data]);
}
private static parseIxLayout(idl: Idl): Map<string, Layout> {
const ixLayouts = idl.instructions.map((ix): [string, Layout<unknown>] => {
let fieldLayouts = ix.args.map((arg: IdlField) =>
IdlCoder.fieldLayout(
arg,
Array.from([...(idl.accounts ?? []), ...(idl.types ?? [])])
)
);
const name = camelCase(ix.name);
return [name, borsh.struct(fieldLayouts, name)];
});
return new Map(ixLayouts);
}
/**
* Decodes a program instruction.
*/
public decode(
ix: Buffer | string,
encoding: "hex" | "base58" = "hex"
): Instruction | null {
if (typeof ix === "string") {
ix = encoding === "hex" ? Buffer.from(ix, "hex") : 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;
}
if ("vec" in idlType) {
return `Vec<${this.formatIdlType(idlType.vec)}>`;
}
if ("option" in idlType) {
return `Option<${this.formatIdlType(idlType.option)}>`;
}
if ("defined" in idlType) {
return idlType.defined;
}
if ("array" in idlType) {
return `Array<${idlType.array[0]}; ${idlType.array[1]}>`;
}
throw new Error(`Unknown IDL type: ${idlType}`);
}
private static formatIdlData(
idlField: IdlField,
data: Object,
types?: IdlTypeDef[]
): string {
if (typeof idlField.type === "string") {
return data.toString();
}
if (idlField.type.hasOwnProperty("vec")) {
return (
"[" +
(<Array<IdlField>>data)
.map((d: IdlField) =>
this.formatIdlData(
{ name: "", type: (<IdlTypeVec>idlField.type).vec },
d
)
)
.join(", ") +
"]"
);
}
if (idlField.type.hasOwnProperty("option")) {
return data === null
? "null"
: this.formatIdlData(
{ name: "", type: (<IdlTypeOption>idlField.type).option },
data,
types
);
}
if (idlField.type.hasOwnProperty("defined")) {
if (types === undefined) {
throw new Error("User defined types not provided");
}
const filtered = types.filter(
(t) => t.name === (<IdlTypeDefined>idlField.type).defined
);
if (filtered.length !== 1) {
throw new Error(
`Type not found: ${(<IdlTypeDefined>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 struct: IdlTypeDefTyStruct = typeDef.type;
const fields = Object.keys(data)
.map((k) => {
const f = struct.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 variants = typeDef.type.variants;
const variant = Object.keys(data)[0];
const enumType = data[variant];
const namedFields = Object.keys(enumType)
.map((f) => {
const fieldData = enumType[f];
const idlField = 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[],
prefix?: string
): IdlAccount[] {
return accounts
.map((account) => {
const accName = sentenceCase(account.name);
if (account.hasOwnProperty("accounts")) {
const newPrefix = prefix ? `${prefix} > ${accName}` : accName;
return InstructionFormatter.flattenIdlAccounts(
(<IdlAccounts>account).accounts,
newPrefix
);
} else {
return {
...(<IdlAccount>account),
name: prefix ? `${prefix} > ${accName}` : accName,
};
}
})
.flat();
}
}
function sentenceCase(field: string): string {
const result = field.replace(/([A-Z])/g, " $1");
return result.charAt(0).toUpperCase() + result.slice(1);
}
// Not technically sighash, since we don't include the arguments, as Rust
// doesn't allow function overloading.
function sighash(nameSpace: string, ixName: string): Buffer {
let name = snakeCase(ixName);
let preimage = `${nameSpace}:${name}`;
return Buffer.from(sha256.digest(preimage)).slice(0, 8);
}