Skip to content

Commit f288075

Browse files
authored
starknet: fix enum handling and types in event decoder (#185)
fixes an issue where event decoder `decodeEvent()` would throw error in case of nested enums parsing. also overrides a `abi-wan-kanabi` type to improve type safety for enum types and match with our own runtime values.
2 parents 411f59d + c869ed3 commit f288075

6 files changed

Lines changed: 571 additions & 4 deletions

File tree

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"type": "prerelease",
3+
"comment": "starknet: fix enum handling and types in eventDecoder",
4+
"packageName": "@apibara/starknet",
5+
"email": "jadejajaipal5@gmail.com",
6+
"dependentChangeType": "patch"
7+
}

packages/starknet/src/abi-wan-helpers.ts

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,10 @@ For more information, please refer to https://unlicense.org
1515
import type { Abi } from "abi-wan-kanabi";
1616
import type {
1717
AbiEventMember,
18+
ExtractAbiEnum,
1819
ExtractAbiEvent,
1920
ExtractAbiEventNames,
20-
StringToPrimitiveType,
21+
StringToPrimitiveType as OriginalStringToPrimitiveType,
2122
} from "abi-wan-kanabi/kanabi";
2223

2324
export type AbiEventStruct = {
@@ -39,10 +40,50 @@ export type AbiEventEnum = {
3940
variants: AbiEventMember[];
4041
};
4142

43+
export type AbiParameter = {
44+
name: string;
45+
type: string;
46+
};
47+
48+
export type AbiEnum = {
49+
type: "enum";
50+
name: string;
51+
variants: readonly AbiParameter[];
52+
};
53+
4254
export type AbiEvent = AbiEventStruct | AbiEventEnum;
4355

4456
export type AbiItem = Abi[number];
4557

58+
// Custom StringToPrimitiveType that overrides abi-wan-kanabi's enum handling.
59+
// The original StringToPrimitiveType from abi-wan-kanabi produces ObjectToUnion types
60+
// for enums, which don't include the `_tag` property that our runtime parser generates.
61+
// This custom version ensures TypeScript types match the actual runtime values.
62+
export type StringToPrimitiveType<
63+
TAbi extends Abi,
64+
T extends string,
65+
> = ExtractAbiEnum<TAbi, T> extends never
66+
? // Not an enum type, forward to original abi-wan-kanabi type
67+
OriginalStringToPrimitiveType<TAbi, T>
68+
: ExtractAbiEnum<TAbi, T> extends {
69+
type: "enum";
70+
variants: infer TVariants extends readonly AbiParameter[];
71+
}
72+
? // It's an enum type, create tagged union with _tag property
73+
{
74+
[Variant in TVariants[number] as Variant["name"]]: Variant["type"] extends "()"
75+
? // Unit variant (no data): { _tag: "VariantName"; VariantName: null }
76+
{ _tag: Variant["name"] } & { [K in Variant["name"]]: null }
77+
: // Variant with data: { _tag: "VariantName"; VariantName: StringToPrimitiveType }
78+
{ _tag: Variant["name"] } & {
79+
[K in Variant["name"]]: StringToPrimitiveType<
80+
TAbi,
81+
Variant["type"]
82+
>;
83+
};
84+
}[TVariants[number]["name"]]
85+
: never;
86+
4687
export type DecodeEventArgs<
4788
TAbi extends Abi = Abi,
4889
TEventName extends ExtractAbiEventNames<TAbi> = ExtractAbiEventNames<TAbi>,

packages/starknet/src/event.ts

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import {
1717
isSpanType,
1818
} from "./abi";
1919
import {
20+
type AbiEnum,
2021
type AbiEvent,
2122
type AbiEventEnum,
2223
type AbiEventStruct,
@@ -33,6 +34,7 @@ import {
3334
parseArray,
3435
parseByteArray,
3536
parseEmpty,
37+
parseEnum,
3638
parseOption,
3739
parseSpan,
3840
parseStruct,
@@ -315,9 +317,7 @@ function compileTypeParser(abi: Abi, type: string): Parser<unknown> {
315317
return compileStructParser(abi, typeAbi.members);
316318
}
317319
case "enum": {
318-
// This should never happen anyways as compileTypeParser is only called
319-
// primitive types or to compile structs parsers.
320-
throw new DecodeEventError(`Enum types are not supported: ${type}`);
320+
return compileEnumParser(abi, typeAbi);
321321
}
322322
default:
323323
throw new DecodeEventError(`Invalid type ${typeAbi.type}`);
@@ -338,3 +338,15 @@ function compileStructParser(
338338
}
339339
return parseStruct(parsers);
340340
}
341+
342+
function compileEnumParser(abi: Abi, enumAbi: AbiEnum): Parser<unknown> {
343+
const parsers: Record<string, { index: number; parser: Parser<unknown> }> =
344+
{};
345+
for (const [index, variant] of enumAbi.variants.entries()) {
346+
parsers[variant.name] = {
347+
index,
348+
parser: compileTypeParser(abi, variant.type),
349+
};
350+
}
351+
return parseEnum(parsers);
352+
}

packages/starknet/src/parser.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,34 @@ export function parseStruct<T extends Record<string, unknown>>(
153153
return parser as Parser<{ [K in keyof T]: T[K] }>;
154154
}
155155

156+
export function parseEnum<T extends Record<string, unknown>>(
157+
parsers: {
158+
[K in keyof T]: { index: number; parser: Parser<T[K]> };
159+
},
160+
): Parser<T[keyof T]> {
161+
return (data: readonly FieldElement[], startingOffset: number) => {
162+
const selectorFelt = data[startingOffset];
163+
const selector = Number(BigInt(selectorFelt));
164+
165+
// Find the parser by index
166+
const parserEntry = Object.entries(parsers).find(
167+
([, { index }]) => index === selector,
168+
);
169+
170+
if (!parserEntry) {
171+
throw new ParseError(`Unknown enum variant selector: ${selector}`);
172+
}
173+
174+
const [variantName, { parser }] = parserEntry;
175+
const { out, offset: newOffset } = parser(data, startingOffset + 1);
176+
177+
return {
178+
out: { _tag: variantName, [variantName]: out } as T[keyof T],
179+
offset: newOffset,
180+
};
181+
};
182+
}
183+
156184
export function parseTuple<T extends Parser<unknown>[]>(
157185
...parsers: T
158186
): Parser<UnwrapParsers<T>> {

packages/starknet/tests/event.test.ts

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { chainlinkAbi } from "./fixtures/chainlink-abi";
88
import { ekuboAbi } from "./fixtures/ekubo-abi";
99
import { golifeAbi } from "./fixtures/golife-abi";
1010
import { paymentAbi } from "./fixtures/payment-abi";
11+
import { registryAbi } from "./fixtures/registry-abi";
1112

1213
describe("decodeEvent", () => {
1314
describe("non strict mode", () => {
@@ -668,5 +669,67 @@ describe("decodeEvent", () => {
668669

669670
expect(decoded).toBeNull();
670671
});
672+
673+
it('can decode event with nested enum with null ["()" type] data', () => {
674+
const abi = registryAbi;
675+
const userRegisteredEventSelector = getEventSelector("UserRegistered");
676+
677+
const event = {
678+
transactionHash:
679+
"0x07c2ea2b2211421e1a74e25f9fe9ad36862c83d6b7189b91185496c41cd83fc9",
680+
address:
681+
"0x04319b2cdf8f484e965a7df709e75c4d5c89780d3601f5105e1bc26243dd45b6",
682+
keys: [
683+
userRegisteredEventSelector,
684+
"0x348ed741a6cfdfa876b4bc233e8f769468a624ae66543d610235112943cc993",
685+
"0x796f64613133",
686+
],
687+
data: ["0x6877a1fd", "0x1"],
688+
filterIds: [],
689+
eventIndex: 0,
690+
eventIndexInTransaction: 0,
691+
transactionIndex: 0,
692+
transactionStatus: "succeeded",
693+
} as const satisfies Event;
694+
695+
const decoded = decodeEvent({
696+
abi,
697+
event,
698+
eventName:
699+
"opinion_competitions::utils::common::events::UserRegistered",
700+
strict: true,
701+
});
702+
703+
expect(decoded).toMatchInlineSnapshot(`
704+
{
705+
"address": "0x04319b2cdf8f484e965a7df709e75c4d5c89780d3601f5105e1bc26243dd45b6",
706+
"args": {
707+
"timestamp": 1752670717n,
708+
"user": "0x348ed741a6cfdfa876b4bc233e8f769468a624ae66543d610235112943cc993",
709+
"username": 133519332421939n,
710+
"verification_level": {
711+
"ORB": null,
712+
"_tag": "ORB",
713+
},
714+
},
715+
"data": [
716+
"0x6877a1fd",
717+
"0x1",
718+
],
719+
"eventIndex": 0,
720+
"eventIndexInTransaction": 0,
721+
"eventName": "opinion_competitions::utils::common::events::UserRegistered",
722+
"filterIds": [],
723+
"keys": [
724+
"0x015854d06e396351cf7ea2143ce9ba79f24588d0f1b1617f54e9acca7a6ac325",
725+
"0x348ed741a6cfdfa876b4bc233e8f769468a624ae66543d610235112943cc993",
726+
"0x796f64613133",
727+
],
728+
"transactionHash": "0x07c2ea2b2211421e1a74e25f9fe9ad36862c83d6b7189b91185496c41cd83fc9",
729+
"transactionIndex": 0,
730+
"transactionStatus": "succeeded",
731+
}
732+
`);
733+
});
671734
});
672735
});

0 commit comments

Comments
 (0)