Skip to content

Commit 3ceb063

Browse files
devDesuhenrye
authored andcommitted
Add support for idl enum tuples, enchance types (solana-foundation#2185)
1 parent 5cad63e commit 3ceb063

File tree

6 files changed

+199
-31
lines changed

6 files changed

+199
-31
lines changed

tests/misc/programs/misc/src/event.rs

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,3 +32,24 @@ pub struct E5 {
3232
pub struct E6 {
3333
pub data: [u8; MAX_EVENT_SIZE_U8 as usize],
3434
}
35+
36+
#[derive(Debug, Clone, Copy, AnchorSerialize, AnchorDeserialize, PartialEq, Eq)]
37+
pub struct TestStruct {
38+
pub data1: u8,
39+
pub data2: u16,
40+
pub data3: u32,
41+
pub data4: u64,
42+
}
43+
44+
#[derive(Debug, Clone, Copy, AnchorSerialize, AnchorDeserialize, PartialEq, Eq)]
45+
pub enum TestEnum {
46+
First,
47+
Second {x: u64, y: u64},
48+
TupleTest (u8, u8, u16, u16),
49+
TupleStructTest (TestStruct),
50+
}
51+
52+
#[event]
53+
pub struct E7 {
54+
pub data: TestEnum,
55+
}

tests/misc/programs/misc/src/lib.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,11 @@ pub mod misc {
9292
Ok(())
9393
}
9494

95+
pub fn test_input_enum(ctx: Context<TestSimulate>, data: TestEnum) -> Result<()> {
96+
emit!(E7 { data: data });
97+
Ok(())
98+
}
99+
95100
pub fn test_i8(ctx: Context<TestI8>, data: i8) -> Result<()> {
96101
ctx.accounts.data.data = data;
97102
Ok(())

tests/misc/tests/misc/misc.ts

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ import {
55
IdlAccounts,
66
AnchorError,
77
Wallet,
8+
IdlTypes,
9+
IdlEvents,
810
} from "@project-serum/anchor";
911
import {
1012
PublicKey,
@@ -190,6 +192,41 @@ describe("misc", () => {
190192
);
191193
});
192194

195+
it("Can use enum in idl", async () => {
196+
const resp1 = await program.methods.testInputEnum({ first: {} }).simulate();
197+
const event1 = resp1.events[0].data as IdlEvents<Misc>["E7"];
198+
assert.deepEqual(event1.data.first, {});
199+
200+
const resp2 = await program.methods
201+
.testInputEnum({ second: { x: new BN(1), y: new BN(2) } })
202+
.simulate();
203+
const event2 = resp2.events[0].data as IdlEvents<Misc>["E7"];
204+
assert.isTrue(new BN(1).eq(event2.data.second.x));
205+
assert.isTrue(new BN(2).eq(event2.data.second.y));
206+
207+
const resp3 = await program.methods
208+
.testInputEnum({
209+
tupleStructTest: [
210+
{ data1: 1, data2: 11, data3: 111, data4: new BN(1111) },
211+
],
212+
})
213+
.simulate();
214+
const event3 = resp3.events[0].data as IdlEvents<Misc>["E7"];
215+
assert.strictEqual(event3.data.tupleStructTest[0].data1, 1);
216+
assert.strictEqual(event3.data.tupleStructTest[0].data2, 11);
217+
assert.strictEqual(event3.data.tupleStructTest[0].data3, 111);
218+
assert.isTrue(event3.data.tupleStructTest[0].data4.eq(new BN(1111)));
219+
220+
const resp4 = await program.methods
221+
.testInputEnum({ tupleTest: [1, 2, 3, 4] })
222+
.simulate();
223+
const event4 = resp4.events[0].data as IdlEvents<Misc>["E7"];
224+
assert.strictEqual(event4.data.tupleTest[0], 1);
225+
assert.strictEqual(event4.data.tupleTest[1], 2);
226+
assert.strictEqual(event4.data.tupleTest[2], 3);
227+
assert.strictEqual(event4.data.tupleTest[3], 4);
228+
});
229+
193230
let dataI8;
194231

195232
it("Can use i8 in the idl", async () => {

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

Lines changed: 14 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -129,16 +129,21 @@ export class IdlCoder {
129129
if (variant.fields === undefined) {
130130
return borsh.struct([], name);
131131
}
132-
const fieldLayouts = variant.fields.map((f: IdlField | IdlType) => {
133-
if (!f.hasOwnProperty("name")) {
134-
throw new Error("Tuple enum variants not yet implemented.");
132+
const fieldLayouts = variant.fields.map(
133+
(f: IdlField | IdlType, i: number) => {
134+
if (!f.hasOwnProperty("name")) {
135+
return IdlCoder.fieldLayout(
136+
{ type: f as IdlType, name: i.toString() },
137+
types
138+
);
139+
}
140+
// this typescript conversion is ok
141+
// because if f were of type IdlType
142+
// (that does not have a name property)
143+
// the check before would've errored
144+
return IdlCoder.fieldLayout(f as IdlField, types);
135145
}
136-
// this typescript conversion is ok
137-
// because if f were of type IdlType
138-
// (that does not have a name property)
139-
// the check before would've errored
140-
return IdlCoder.fieldLayout(f as IdlField, types);
141-
});
146+
);
142147
return borsh.struct(fieldLayouts, name);
143148
});
144149

ts/packages/anchor/src/program/namespace/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ export { TransactionNamespace, TransactionFn } from "./transaction.js";
2121
export { RpcNamespace, RpcFn } from "./rpc.js";
2222
export { AccountNamespace, AccountClient, ProgramAccount } from "./account.js";
2323
export { SimulateNamespace, SimulateFn } from "./simulate.js";
24-
export { IdlAccounts, IdlTypes, DecodeType } from "./types.js";
24+
export { IdlAccounts, IdlTypes, DecodeType, IdlEvents } from "./types.js";
2525
export { MethodsBuilderFactory, MethodsNamespace } from "./methods";
2626
export { ViewNamespace, ViewFn } from "./views";
2727

ts/packages/anchor/src/program/namespace/types.ts

Lines changed: 121 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@ import { PublicKey } from "@solana/web3.js";
22
import BN from "bn.js";
33
import { Idl } from "../../";
44
import {
5+
IdlEnumFields,
6+
IdlEnumFieldsNamed,
7+
IdlEnumFieldsTuple,
58
IdlField,
69
IdlInstruction,
710
IdlType,
@@ -140,37 +143,134 @@ type ArgsTuple<A extends IdlField[], Defined> = {
140143
? DecodeType<A[K]["type"], Defined>
141144
: unknown;
142145
} & unknown[];
146+
/**
147+
* flat {a: number, b: {c: string}} into number | string
148+
*/
149+
type UnboxToUnion<T> = T extends (infer U)[]
150+
? UnboxToUnion<U>
151+
: T extends Record<string, never> // empty object, eg: named enum variant without fields
152+
? "__empty_object__"
153+
: T extends Record<string, infer V> // object with props, eg: struct
154+
? UnboxToUnion<V>
155+
: T;
156+
157+
/**
158+
* decode single enum.field
159+
*/
160+
declare type DecodeEnumField<F, Defined> = F extends IdlType
161+
? DecodeType<F, Defined>
162+
: never;
163+
164+
/**
165+
* decode enum variant: named or tuple
166+
*/
167+
declare type DecodeEnumFields<
168+
F extends IdlEnumFields,
169+
Defined
170+
> = F extends IdlEnumFieldsNamed
171+
? {
172+
[F2 in F[number] as F2["name"]]: DecodeEnumField<F2["type"], Defined>;
173+
}
174+
: F extends IdlEnumFieldsTuple
175+
? {
176+
[F3 in keyof F as Exclude<F3, keyof unknown[]>]: DecodeEnumField<
177+
F[F3],
178+
Defined
179+
>;
180+
}
181+
: Record<string, never>;
143182

144-
type FieldsOfType<I extends IdlTypeDef> = NonNullable<
145-
I["type"] extends IdlTypeDefTyStruct
146-
? I["type"]["fields"]
147-
: I["type"] extends IdlTypeDefTyEnum
148-
? I["type"]["variants"][number]["fields"]
149-
: any[]
150-
>[number];
183+
/**
184+
* Since TypeScript do not provide OneOf helper we can
185+
* simply mark enum variants with +?
186+
*/
187+
declare type DecodeEnum<K extends IdlTypeDefTyEnum, Defined> = {
188+
// X = IdlEnumVariant
189+
[X in K["variants"][number] as Uncapitalize<X["name"]>]+?: DecodeEnumFields<
190+
NonNullable<X["fields"]>,
191+
Defined
192+
>;
193+
};
151194

152-
export type TypeDef<I extends IdlTypeDef, Defined> = {
153-
[F in FieldsOfType<I> as F["name"]]: DecodeType<F["type"], Defined>;
195+
type DecodeStruct<I extends IdlTypeDefTyStruct, Defined> = {
196+
[F in I["fields"][number] as F["name"]]: DecodeType<F["type"], Defined>;
154197
};
155198

199+
export type TypeDef<
200+
I extends IdlTypeDef,
201+
Defined
202+
> = I["type"] extends IdlTypeDefTyEnum
203+
? DecodeEnum<I["type"], Defined>
204+
: I["type"] extends IdlTypeDefTyStruct
205+
? DecodeStruct<I["type"], Defined>
206+
: never;
207+
156208
type TypeDefDictionary<T extends IdlTypeDef[], Defined> = {
157209
[K in T[number] as K["name"]]: TypeDef<K, Defined>;
158210
};
159211

160-
type NestedTypeDefDictionary<T extends IdlTypeDef[]> = {
161-
[Outer in T[number] as Outer["name"]]: TypeDef<
162-
Outer,
163-
{
164-
[Inner in T[number] as Inner["name"]]: Inner extends Outer
165-
? never
166-
: TypeDef<Inner, Record<string, never>>;
167-
}
168-
>;
212+
type DecodedHelper<T extends IdlTypeDef[], Defined> = {
213+
[D in T[number] as D["name"]]: TypeDef<D, Defined>;
169214
};
170215

171-
export type IdlTypes<T extends Idl> = NestedTypeDefDictionary<
172-
NonNullable<T["types"]>
173-
>;
216+
type UnknownType = "__unknown_defined_type__";
217+
/**
218+
* empty "defined" object to produce UnknownType instead of never/unknown during idl types decoding
219+
* */
220+
type EmptyDefined = Record<UnknownType, never>;
221+
222+
type RecursiveDepth2<
223+
T extends IdlTypeDef[],
224+
Defined = EmptyDefined,
225+
Decoded = DecodedHelper<T, Defined>
226+
> = UnknownType extends UnboxToUnion<Decoded>
227+
? RecursiveDepth3<T, DecodedHelper<T, Defined>>
228+
: Decoded;
229+
230+
type RecursiveDepth3<
231+
T extends IdlTypeDef[],
232+
Defined = EmptyDefined,
233+
Decoded = DecodedHelper<T, Defined>
234+
> = UnknownType extends UnboxToUnion<Decoded>
235+
? RecursiveDepth4<T, DecodedHelper<T, Defined>>
236+
: Decoded;
237+
238+
type RecursiveDepth4<
239+
T extends IdlTypeDef[],
240+
Defined = EmptyDefined
241+
> = DecodedHelper<T, Defined>;
242+
243+
/**
244+
* typescript can't handle truly recursive type (RecursiveTypes instead of RecursiveDepth2).
245+
* Hence we're doing "recursion" of depth=4 manually
246+
* */
247+
type RecursiveTypes<
248+
T extends IdlTypeDef[],
249+
Defined = EmptyDefined,
250+
Decoded = DecodedHelper<T, Defined>
251+
> =
252+
// check if some of decoded types is Unknown (not decoded properly)
253+
UnknownType extends UnboxToUnion<Decoded>
254+
? RecursiveDepth2<T, DecodedHelper<T, Defined>>
255+
: Decoded;
256+
257+
export type IdlTypes<T extends Idl> = RecursiveTypes<NonNullable<T["types"]>>;
258+
259+
type IdlEventType<
260+
I extends Idl,
261+
Event extends NonNullable<I["events"]>[number],
262+
Defined
263+
> = {
264+
[F in Event["fields"][number] as F["name"]]: DecodeType<F["type"], Defined>;
265+
};
266+
267+
export type IdlEvents<I extends Idl, Defined = IdlTypes<I>> = {
268+
[E in NonNullable<I["events"]>[number] as E["name"]]: IdlEventType<
269+
I,
270+
E,
271+
Defined
272+
>;
273+
};
174274

175275
export type IdlAccounts<T extends Idl> = TypeDefDictionary<
176276
NonNullable<T["accounts"]>,

0 commit comments

Comments
 (0)