diff --git a/tests/misc/programs/misc/src/event.rs b/tests/misc/programs/misc/src/event.rs index 80b7dd8f8..31fcbd31d 100644 --- a/tests/misc/programs/misc/src/event.rs +++ b/tests/misc/programs/misc/src/event.rs @@ -32,3 +32,24 @@ pub struct E5 { pub struct E6 { pub data: [u8; MAX_EVENT_SIZE_U8 as usize], } + +#[derive(Debug, Clone, Copy, AnchorSerialize, AnchorDeserialize, PartialEq, Eq)] +pub struct TestStruct { + pub data1: u8, + pub data2: u16, + pub data3: u32, + pub data4: u64, +} + +#[derive(Debug, Clone, Copy, AnchorSerialize, AnchorDeserialize, PartialEq, Eq)] +pub enum TestEnum { + First, + Second {x: u64, y: u64}, + TupleTest (u8, u8, u16, u16), + TupleStructTest (TestStruct), +} + +#[event] +pub struct E7 { + pub data: TestEnum, +} \ No newline at end of file diff --git a/tests/misc/programs/misc/src/lib.rs b/tests/misc/programs/misc/src/lib.rs index 44893563a..32f41c647 100644 --- a/tests/misc/programs/misc/src/lib.rs +++ b/tests/misc/programs/misc/src/lib.rs @@ -92,6 +92,11 @@ pub mod misc { Ok(()) } + pub fn test_input_enum(ctx: Context, data: TestEnum) -> Result<()> { + emit!(E7 { data: data }); + Ok(()) + } + pub fn test_i8(ctx: Context, data: i8) -> Result<()> { ctx.accounts.data.data = data; Ok(()) diff --git a/tests/misc/tests/misc/misc.ts b/tests/misc/tests/misc/misc.ts index b2a528a80..4bb8fcea0 100644 --- a/tests/misc/tests/misc/misc.ts +++ b/tests/misc/tests/misc/misc.ts @@ -5,6 +5,8 @@ import { IdlAccounts, AnchorError, Wallet, + IdlTypes, + IdlEvents, } from "@project-serum/anchor"; import { PublicKey, @@ -190,6 +192,41 @@ describe("misc", () => { ); }); + it("Can use enum in idl", async () => { + const resp1 = await program.methods.testInputEnum({ first: {} }).simulate(); + const event1 = resp1.events[0].data as IdlEvents["E7"]; + assert.deepEqual(event1.data.first, {}); + + const resp2 = await program.methods + .testInputEnum({ second: { x: new BN(1), y: new BN(2) } }) + .simulate(); + const event2 = resp2.events[0].data as IdlEvents["E7"]; + assert.isTrue(new BN(1).eq(event2.data.second.x)); + assert.isTrue(new BN(2).eq(event2.data.second.y)); + + const resp3 = await program.methods + .testInputEnum({ + tupleStructTest: [ + { data1: 1, data2: 11, data3: 111, data4: new BN(1111) }, + ], + }) + .simulate(); + const event3 = resp3.events[0].data as IdlEvents["E7"]; + assert.strictEqual(event3.data.tupleStructTest[0].data1, 1); + assert.strictEqual(event3.data.tupleStructTest[0].data2, 11); + assert.strictEqual(event3.data.tupleStructTest[0].data3, 111); + assert.isTrue(event3.data.tupleStructTest[0].data4.eq(new BN(1111))); + + const resp4 = await program.methods + .testInputEnum({ tupleTest: [1, 2, 3, 4] }) + .simulate(); + const event4 = resp4.events[0].data as IdlEvents["E7"]; + assert.strictEqual(event4.data.tupleTest[0], 1); + assert.strictEqual(event4.data.tupleTest[1], 2); + assert.strictEqual(event4.data.tupleTest[2], 3); + assert.strictEqual(event4.data.tupleTest[3], 4); + }); + let dataI8; it("Can use i8 in the idl", async () => { diff --git a/ts/packages/anchor/src/coder/borsh/idl.ts b/ts/packages/anchor/src/coder/borsh/idl.ts index b7ec0e8a3..93eb77bbe 100644 --- a/ts/packages/anchor/src/coder/borsh/idl.ts +++ b/ts/packages/anchor/src/coder/borsh/idl.ts @@ -129,16 +129,21 @@ export class IdlCoder { if (variant.fields === undefined) { return borsh.struct([], name); } - const fieldLayouts = variant.fields.map((f: IdlField | IdlType) => { - if (!f.hasOwnProperty("name")) { - throw new Error("Tuple enum variants not yet implemented."); + const fieldLayouts = variant.fields.map( + (f: IdlField | IdlType, i: number) => { + if (!f.hasOwnProperty("name")) { + return IdlCoder.fieldLayout( + { type: f as IdlType, name: i.toString() }, + types + ); + } + // this typescript conversion is ok + // because if f were of type IdlType + // (that does not have a name property) + // the check before would've errored + return IdlCoder.fieldLayout(f as IdlField, types); } - // this typescript conversion is ok - // because if f were of type IdlType - // (that does not have a name property) - // the check before would've errored - return IdlCoder.fieldLayout(f as IdlField, types); - }); + ); return borsh.struct(fieldLayouts, name); }); diff --git a/ts/packages/anchor/src/program/namespace/index.ts b/ts/packages/anchor/src/program/namespace/index.ts index 25c13b351..8a10d50d0 100644 --- a/ts/packages/anchor/src/program/namespace/index.ts +++ b/ts/packages/anchor/src/program/namespace/index.ts @@ -21,7 +21,7 @@ export { TransactionNamespace, TransactionFn } from "./transaction.js"; export { RpcNamespace, RpcFn } from "./rpc.js"; export { AccountNamespace, AccountClient, ProgramAccount } from "./account.js"; export { SimulateNamespace, SimulateFn } from "./simulate.js"; -export { IdlAccounts, IdlTypes, DecodeType } from "./types.js"; +export { IdlAccounts, IdlTypes, DecodeType, IdlEvents } from "./types.js"; export { MethodsBuilderFactory, MethodsNamespace } from "./methods"; export { ViewNamespace, ViewFn } from "./views"; diff --git a/ts/packages/anchor/src/program/namespace/types.ts b/ts/packages/anchor/src/program/namespace/types.ts index f8b92801f..5d1e908f1 100644 --- a/ts/packages/anchor/src/program/namespace/types.ts +++ b/ts/packages/anchor/src/program/namespace/types.ts @@ -2,6 +2,9 @@ import { PublicKey } from "@solana/web3.js"; import BN from "bn.js"; import { Idl } from "../../"; import { + IdlEnumFields, + IdlEnumFieldsNamed, + IdlEnumFieldsTuple, IdlField, IdlInstruction, IdlType, @@ -140,37 +143,134 @@ type ArgsTuple = { ? DecodeType : unknown; } & unknown[]; +/** + * flat {a: number, b: {c: string}} into number | string + */ +type UnboxToUnion = T extends (infer U)[] + ? UnboxToUnion + : T extends Record // empty object, eg: named enum variant without fields + ? "__empty_object__" + : T extends Record // object with props, eg: struct + ? UnboxToUnion + : T; -type FieldsOfType = NonNullable< - I["type"] extends IdlTypeDefTyStruct - ? I["type"]["fields"] - : I["type"] extends IdlTypeDefTyEnum - ? I["type"]["variants"][number]["fields"] - : any[] ->[number]; +/** + * decode single enum.field + */ +declare type DecodeEnumField = F extends IdlType + ? DecodeType + : never; -export type TypeDef = { - [F in FieldsOfType as F["name"]]: DecodeType; +/** + * decode enum variant: named or tuple + */ +declare type DecodeEnumFields< + F extends IdlEnumFields, + Defined +> = F extends IdlEnumFieldsNamed + ? { + [F2 in F[number] as F2["name"]]: DecodeEnumField; + } + : F extends IdlEnumFieldsTuple + ? { + [F3 in keyof F as Exclude]: DecodeEnumField< + F[F3], + Defined + >; + } + : Record; + +/** + * Since TypeScript do not provide OneOf helper we can + * simply mark enum variants with +? + */ +declare type DecodeEnum = { + // X = IdlEnumVariant + [X in K["variants"][number] as Uncapitalize]+?: DecodeEnumFields< + NonNullable, + Defined + >; }; +type DecodeStruct = { + [F in I["fields"][number] as F["name"]]: DecodeType; +}; + +export type TypeDef< + I extends IdlTypeDef, + Defined +> = I["type"] extends IdlTypeDefTyEnum + ? DecodeEnum + : I["type"] extends IdlTypeDefTyStruct + ? DecodeStruct + : never; + type TypeDefDictionary = { [K in T[number] as K["name"]]: TypeDef; }; -type NestedTypeDefDictionary = { - [Outer in T[number] as Outer["name"]]: TypeDef< - Outer, - { - [Inner in T[number] as Inner["name"]]: Inner extends Outer - ? never - : TypeDef>; - } - >; +type DecodedHelper = { + [D in T[number] as D["name"]]: TypeDef; }; -export type IdlTypes = NestedTypeDefDictionary< - NonNullable ->; +type UnknownType = "__unknown_defined_type__"; +/** + * empty "defined" object to produce UnknownType instead of never/unknown during idl types decoding + * */ +type EmptyDefined = Record; + +type RecursiveDepth2< + T extends IdlTypeDef[], + Defined = EmptyDefined, + Decoded = DecodedHelper +> = UnknownType extends UnboxToUnion + ? RecursiveDepth3> + : Decoded; + +type RecursiveDepth3< + T extends IdlTypeDef[], + Defined = EmptyDefined, + Decoded = DecodedHelper +> = UnknownType extends UnboxToUnion + ? RecursiveDepth4> + : Decoded; + +type RecursiveDepth4< + T extends IdlTypeDef[], + Defined = EmptyDefined +> = DecodedHelper; + +/** + * typescript can't handle truly recursive type (RecursiveTypes instead of RecursiveDepth2). + * Hence we're doing "recursion" of depth=4 manually + * */ +type RecursiveTypes< + T extends IdlTypeDef[], + Defined = EmptyDefined, + Decoded = DecodedHelper +> = + // check if some of decoded types is Unknown (not decoded properly) + UnknownType extends UnboxToUnion + ? RecursiveDepth2> + : Decoded; + +export type IdlTypes = RecursiveTypes>; + +type IdlEventType< + I extends Idl, + Event extends NonNullable[number], + Defined +> = { + [F in Event["fields"][number] as F["name"]]: DecodeType; +}; + +export type IdlEvents> = { + [E in NonNullable[number] as E["name"]]: IdlEventType< + I, + E, + Defined + >; +}; export type IdlAccounts = TypeDefDictionary< NonNullable,