From f8cbc5db3e1ae7e480fa3ebe46194396d79163d7 Mon Sep 17 00:00:00 2001 From: armaniferrante Date: Wed, 9 Jun 2021 16:48:20 -0700 Subject: [PATCH 1/5] ts: Add instruction decode api --- CHANGELOG.md | 2 ++ ts/src/coder/instruction.ts | 28 ++++++++++++++++++++++++++++ 2 files changed, 30 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5f3488989d..bd541c3699 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 ([#]()). ### Fixes diff --git a/ts/src/coder/instruction.ts b/ts/src/coder/instruction.ts index d371458842..5803445b30 100644 --- a/ts/src/coder/instruction.ts +++ b/ts/src/coder/instruction.ts @@ -1,6 +1,7 @@ import camelCase from "camelcase"; import { Layout } from "buffer-layout"; import * as borsh from "@project-serum/borsh"; +import * as bs58 from "bs58"; import { Idl, IdlField, IdlStateMethod } from "../idl"; import { IdlCoder } from "./idl"; import { sighash } from "./common"; @@ -24,8 +25,35 @@ export class InstructionCoder { */ private ixLayout: Map; + // Base58 encoded sighash to instruction layout. + private sighashLayouts: Map; + public constructor(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), this.ixLayout.get(ix.name)); + }); + + if (idl.state) { + idl.state.methods.map((ix) => { + const sh = sighash(SIGHASH_STATE_NAMESPACE, ix.name); + sighashLayouts.set(bs58.encode(sh), this.ixLayout.get(ix.name)); + }); + } + + this.sighashLayouts = sighashLayouts; + } + + public decode(ix: Buffer | string): Object | undefined { + if (typeof ix === "string") { + ix = bs58.decode(ix); + } + let sighash = bs58.encode(ix.slice(0, 8)); + let data = ix.slice(8); + return this.sighashLayouts.get(sighash)?.decode(data); } /** From ea719bba058f4a8528f7e69c719c51e57da40b97 Mon Sep 17 00:00:00 2001 From: armaniferrante Date: Thu, 10 Jun 2021 00:18:22 -0700 Subject: [PATCH 2/5] Update --- ts/src/coder/instruction.ts | 156 +++++++++++++++++++++++++++++++++--- ts/src/index.ts | 2 + 2 files changed, 147 insertions(+), 11 deletions(-) diff --git a/ts/src/coder/instruction.ts b/ts/src/coder/instruction.ts index 5803445b30..cae741c3ed 100644 --- a/ts/src/coder/instruction.ts +++ b/ts/src/coder/instruction.ts @@ -2,7 +2,7 @@ import camelCase from "camelcase"; import { Layout } from "buffer-layout"; import * as borsh from "@project-serum/borsh"; import * as bs58 from "bs58"; -import { Idl, IdlField, IdlStateMethod } from "../idl"; +import { Idl, IdlField, IdlStateMethod, IdlType, IdlTypeDef } from "../idl"; import { IdlCoder } from "./idl"; import { sighash } from "./common"; @@ -20,40 +20,51 @@ 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; // Base58 encoded sighash to instruction layout. - private sighashLayouts: Map; + private sighashLayouts: Map; - public constructor(idl: Idl) { + public constructor(private idl: Idl) { this.ixLayout = InstructionCoder.parseIxLayout(idl); - const sighashLayouts = new Map(); + const sighashLayouts = new Map(); idl.instructions.forEach((ix) => { const sh = sighash(SIGHASH_GLOBAL_NAMESPACE, ix.name); - sighashLayouts.set(bs58.encode(sh), this.ixLayout.get(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), this.ixLayout.get(ix.name)); + sighashLayouts.set(bs58.encode(sh), { + layout: this.ixLayout.get(ix.name) as Layout, + name: ix.name, + }); }); } this.sighashLayouts = sighashLayouts; } - public decode(ix: Buffer | string): Object | undefined { + 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); - return this.sighashLayouts.get(sighash)?.decode(data); + const decoder = this.sighashLayouts.get(sighash); + if (!decoder) { + return null; + } + return { + data: decoder.layout.decode(data), + name: decoder.name, + }; } /** @@ -101,4 +112,127 @@ export class InstructionCoder { // @ts-ignore return new Map(ixLayouts); } + + public formatFields( + ix: Instruction + ): { name: string; type: string; data: string }[] { + const idlIx = this.idl.instructions.filter((i) => ix.name === i.name)[0]; + if (idlIx === undefined) { + throw new Error("Invalid instruction given"); + } + return idlIx.args.map((idlField) => { + return { + name: idlField.name, + type: this.formatIdlType(idlField.type), + data: this.formatIdlData( + idlField, + ix.data[idlField.name], + this.idl.types + ), + }; + }); + } + + private 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 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 + .map((d) => + 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 this.formatIdlDataDefined(filtered[0], data, types); + } + + return "unknown"; + } + + private formatIdlDataDefined( + typeDef: IdlTypeDef, + data: Object, + types: IdlTypeDef[] + ): string { + console.log(data); + 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 + ": " + this.formatIdlData(f, data[k], types); + }) + .join(", "); + return "{ " + fields + " }"; + } else { + // todo + return "{}"; + } + } } + +export type Instruction = { + name: string; + data: Object; +}; 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, From b5c7d27d5597754590078fce83060075fb9ce385 Mon Sep 17 00:00:00 2001 From: armaniferrante Date: Thu, 10 Jun 2021 16:15:48 -0700 Subject: [PATCH 3/5] Update --- ts/src/coder/instruction.ts | 160 +++++++++++++++++++++++++++--------- 1 file changed, 122 insertions(+), 38 deletions(-) diff --git a/ts/src/coder/instruction.ts b/ts/src/coder/instruction.ts index cae741c3ed..6e82bd7816 100644 --- a/ts/src/coder/instruction.ts +++ b/ts/src/coder/instruction.ts @@ -2,9 +2,18 @@ import camelCase from "camelcase"; import { Layout } from "buffer-layout"; import * as borsh from "@project-serum/borsh"; import * as bs58 from "bs58"; -import { Idl, IdlField, IdlStateMethod, IdlType, IdlTypeDef } from "../idl"; +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. @@ -51,22 +60,6 @@ export class InstructionCoder { this.sighashLayouts = sighashLayouts; } - 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, - }; - } - /** * Encodes a program instruction. */ @@ -113,27 +106,102 @@ export class InstructionCoder { return new Map(ixLayouts); } - public formatFields( - ix: Instruction - ): { name: string; type: string; data: string }[] { - const idlIx = this.idl.instructions.filter((i) => ix.name === i.name)[0]; + /** + * 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) { - throw new Error("Invalid instruction given"); + console.error("Invalid instruction given"); + return null; } - return idlIx.args.map((idlField) => { + + const args = idlIx.args.map((idlField) => { return { name: idlField.name, - type: this.formatIdlType(idlField.type), - data: this.formatIdlData( + type: InstructionFormatter.formatIdlType(idlField.type), + data: InstructionFormatter.formatIdlData( idlField, ix.data[idlField.name], - this.idl.types + 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 formatIdlType(idlType: IdlType): string { + private static formatIdlType(idlType: IdlType): string { if (typeof idlType === "string") { return idlType as string; } @@ -155,7 +223,7 @@ export class InstructionCoder { } } - private formatIdlData( + private static formatIdlData( idlField: IdlField, data: Object, types?: IdlTypeDef[] @@ -169,7 +237,8 @@ export class InstructionCoder { return ( "[" + data - .map((d) => + // @ts-ignore + .map((d: IdlField) => this.formatIdlData( // @ts-ignore { name: "", type: idlField.type.vec }, @@ -202,18 +271,21 @@ export class InstructionCoder { // @ts-ignore throw new Error(`Type not found: ${idlField.type.defined}`); } - return this.formatIdlDataDefined(filtered[0], data, types); + return InstructionFormatter.formatIdlDataDefined( + filtered[0], + data, + types + ); } return "unknown"; } - private formatIdlDataDefined( + private static formatIdlDataDefined( typeDef: IdlTypeDef, data: Object, types: IdlTypeDef[] ): string { - console.log(data); if (typeDef.type.kind === "struct") { const fields = Object.keys(data) .map((k) => { @@ -221,7 +293,9 @@ export class InstructionCoder { if (f === undefined) { throw new Error("Unable to find type"); } - return k + ": " + this.formatIdlData(f, data[k], types); + return ( + k + ": " + InstructionFormatter.formatIdlData(f, data[k], types) + ); }) .join(", "); return "{ " + fields + " }"; @@ -230,9 +304,19 @@ export class InstructionCoder { return "{}"; } } -} -export type Instruction = { - name: string; - data: Object; -}; + 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(); + } +} From ca254993807eadd4da6eedb25c6bb3609db21f60 Mon Sep 17 00:00:00 2001 From: armaniferrante Date: Thu, 10 Jun 2021 16:59:50 -0700 Subject: [PATCH 4/5] Update --- ts/src/coder/instruction.ts | 37 +++++++++++++++++++++++++++++++++++-- 1 file changed, 35 insertions(+), 2 deletions(-) diff --git a/ts/src/coder/instruction.ts b/ts/src/coder/instruction.ts index 6e82bd7816..23d25f559e 100644 --- a/ts/src/coder/instruction.ts +++ b/ts/src/coder/instruction.ts @@ -300,8 +300,41 @@ class InstructionFormatter { .join(", "); return "{ " + fields + " }"; } else { - // todo - return "{}"; + 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"; + } } } From 2e31ee935fd1cd9687bd6714a48a807d4df0a69a Mon Sep 17 00:00:00 2001 From: armaniferrante Date: Thu, 10 Jun 2021 17:01:29 -0700 Subject: [PATCH 5/5] Changelog --- CHANGELOG.md | 2 +- ts/src/coder/instruction.ts | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bd541c3699..5836bcda7d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,7 +16,7 @@ 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 ([#]()). +* ts: Instruction decoding and formatting ([#372](https://github.com/project-serum/anchor/pull/372)). ### Fixes diff --git a/ts/src/coder/instruction.ts b/ts/src/coder/instruction.ts index 23d25f559e..708a331649 100644 --- a/ts/src/coder/instruction.ts +++ b/ts/src/coder/instruction.ts @@ -209,12 +209,12 @@ class InstructionFormatter { // @ts-ignore if (idlType.vec) { // @ts-ignore - return `vec<${this.formatIdlType(idlType.vec)}>`; + return `Vec<${this.formatIdlType(idlType.vec)}>`; } // @ts-ignore if (idlType.option) { // @ts-ignore - return `option<${this.formatIdlType(idlType.option)}>`; + return `Option<${this.formatIdlType(idlType.option)}>`; } // @ts-ignore if (idlType.defined) {