diff --git a/CHANGELOG.md b/CHANGELOG.md index 65fd5515b0..01147e3520 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 = )]` 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 diff --git a/ts/src/coder/instruction.ts b/ts/src/coder/instruction.ts index d371458842..708a331649 100644 --- a/ts/src/coder/instruction.ts +++ b/ts/src/coder/instruction.ts @@ -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; - public constructor(idl: Idl) { + // Base58 encoded sighash to instruction layout. + private sighashLayouts: Map; + + 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(); + } } diff --git a/ts/src/index.ts b/ts/src/index.ts index 0f596c1e93..789fc18025 100644 --- a/ts/src/index.ts +++ b/ts/src/index.ts @@ -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,