Browse Source

Add support for idl enum tuples, enchance types (#2185)

Totoro 3 years ago
parent
commit
8e66d5bb5f

+ 21 - 0
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,
+}

+ 5 - 0
tests/misc/programs/misc/src/lib.rs

@@ -92,6 +92,11 @@ pub mod misc {
         Ok(())
     }
 
+    pub fn test_input_enum(ctx: Context<TestSimulate>, data: TestEnum) -> Result<()> {
+        emit!(E7 { data: data });
+        Ok(())
+    }
+
     pub fn test_i8(ctx: Context<TestI8>, data: i8) -> Result<()> {
         ctx.accounts.data.data = data;
         Ok(())

+ 37 - 0
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<Misc>["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<Misc>["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<Misc>["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<Misc>["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 () => {

+ 14 - 9
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);
       });
 

+ 1 - 1
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";
 

+ 121 - 21
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<A extends IdlField[], Defined> = {
     ? DecodeType<A[K]["type"], Defined>
     : unknown;
 } & unknown[];
+/**
+ * flat {a: number, b: {c: string}} into number | string
+ */
+type UnboxToUnion<T> = T extends (infer U)[]
+  ? UnboxToUnion<U>
+  : T extends Record<string, never> // empty object, eg: named enum variant without fields
+  ? "__empty_object__"
+  : T extends Record<string, infer V> // object with props, eg: struct
+  ? UnboxToUnion<V>
+  : T;
+
+/**
+ * decode single enum.field
+ */
+declare type DecodeEnumField<F, Defined> = F extends IdlType
+  ? DecodeType<F, Defined>
+  : never;
+
+/**
+ * decode enum variant: named or tuple
+ */
+declare type DecodeEnumFields<
+  F extends IdlEnumFields,
+  Defined
+> = F extends IdlEnumFieldsNamed
+  ? {
+      [F2 in F[number] as F2["name"]]: DecodeEnumField<F2["type"], Defined>;
+    }
+  : F extends IdlEnumFieldsTuple
+  ? {
+      [F3 in keyof F as Exclude<F3, keyof unknown[]>]: DecodeEnumField<
+        F[F3],
+        Defined
+      >;
+    }
+  : Record<string, never>;
 
-type FieldsOfType<I extends IdlTypeDef> = NonNullable<
-  I["type"] extends IdlTypeDefTyStruct
-    ? I["type"]["fields"]
-    : I["type"] extends IdlTypeDefTyEnum
-    ? I["type"]["variants"][number]["fields"]
-    : any[]
->[number];
+/**
+ * Since TypeScript do not provide OneOf helper we can
+ * simply mark enum variants with +?
+ */
+declare type DecodeEnum<K extends IdlTypeDefTyEnum, Defined> = {
+  // X = IdlEnumVariant
+  [X in K["variants"][number] as Uncapitalize<X["name"]>]+?: DecodeEnumFields<
+    NonNullable<X["fields"]>,
+    Defined
+  >;
+};
 
-export type TypeDef<I extends IdlTypeDef, Defined> = {
-  [F in FieldsOfType<I> as F["name"]]: DecodeType<F["type"], Defined>;
+type DecodeStruct<I extends IdlTypeDefTyStruct, Defined> = {
+  [F in I["fields"][number] as F["name"]]: DecodeType<F["type"], Defined>;
 };
 
+export type TypeDef<
+  I extends IdlTypeDef,
+  Defined
+> = I["type"] extends IdlTypeDefTyEnum
+  ? DecodeEnum<I["type"], Defined>
+  : I["type"] extends IdlTypeDefTyStruct
+  ? DecodeStruct<I["type"], Defined>
+  : never;
+
 type TypeDefDictionary<T extends IdlTypeDef[], Defined> = {
   [K in T[number] as K["name"]]: TypeDef<K, Defined>;
 };
 
-type NestedTypeDefDictionary<T extends IdlTypeDef[]> = {
-  [Outer in T[number] as Outer["name"]]: TypeDef<
-    Outer,
-    {
-      [Inner in T[number] as Inner["name"]]: Inner extends Outer
-        ? never
-        : TypeDef<Inner, Record<string, never>>;
-    }
-  >;
+type DecodedHelper<T extends IdlTypeDef[], Defined> = {
+  [D in T[number] as D["name"]]: TypeDef<D, Defined>;
 };
 
-export type IdlTypes<T extends Idl> = NestedTypeDefDictionary<
-  NonNullable<T["types"]>
->;
+type UnknownType = "__unknown_defined_type__";
+/**
+ * empty "defined" object to produce UnknownType instead of never/unknown during idl types decoding
+ *  */
+type EmptyDefined = Record<UnknownType, never>;
+
+type RecursiveDepth2<
+  T extends IdlTypeDef[],
+  Defined = EmptyDefined,
+  Decoded = DecodedHelper<T, Defined>
+> = UnknownType extends UnboxToUnion<Decoded>
+  ? RecursiveDepth3<T, DecodedHelper<T, Defined>>
+  : Decoded;
+
+type RecursiveDepth3<
+  T extends IdlTypeDef[],
+  Defined = EmptyDefined,
+  Decoded = DecodedHelper<T, Defined>
+> = UnknownType extends UnboxToUnion<Decoded>
+  ? RecursiveDepth4<T, DecodedHelper<T, Defined>>
+  : Decoded;
+
+type RecursiveDepth4<
+  T extends IdlTypeDef[],
+  Defined = EmptyDefined
+> = DecodedHelper<T, Defined>;
+
+/**
+ * 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<T, Defined>
+> =
+  // check if some of decoded types is Unknown (not decoded properly)
+  UnknownType extends UnboxToUnion<Decoded>
+    ? RecursiveDepth2<T, DecodedHelper<T, Defined>>
+    : Decoded;
+
+export type IdlTypes<T extends Idl> = RecursiveTypes<NonNullable<T["types"]>>;
+
+type IdlEventType<
+  I extends Idl,
+  Event extends NonNullable<I["events"]>[number],
+  Defined
+> = {
+  [F in Event["fields"][number] as F["name"]]: DecodeType<F["type"], Defined>;
+};
+
+export type IdlEvents<I extends Idl, Defined = IdlTypes<I>> = {
+  [E in NonNullable<I["events"]>[number] as E["name"]]: IdlEventType<
+    I,
+    E,
+    Defined
+  >;
+};
 
 export type IdlAccounts<T extends Idl> = TypeDefDictionary<
   NonNullable<T["accounts"]>,