@apibara/starknet 2.0.0-beta.9 → 2.1.0-beta.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/event.ts ADDED
@@ -0,0 +1,204 @@
1
+ import type { Abi } from "abi-wan-kanabi";
2
+ import type {
3
+ AbiEventMember,
4
+ EventToPrimitiveType,
5
+ ExtractAbiEventNames,
6
+ } from "abi-wan-kanabi/kanabi";
7
+ import {
8
+ PrimitiveTypeParsers,
9
+ getArrayElementType,
10
+ getEventSelector,
11
+ getOptionType,
12
+ getSpanType,
13
+ isArrayType,
14
+ isEmptyType,
15
+ isOptionType,
16
+ isPrimitiveType,
17
+ isSpanType,
18
+ } from "./abi";
19
+ import type { Event } from "./block";
20
+ import {
21
+ ParseError,
22
+ type Parser,
23
+ parseArray,
24
+ parseEmpty,
25
+ parseOption,
26
+ parseSpan,
27
+ parseStruct,
28
+ } from "./parser";
29
+
30
+ export class DecodeEventError extends Error {
31
+ constructor(message: string) {
32
+ super(message);
33
+ this.name = "DecodeEventError";
34
+ }
35
+ }
36
+
37
+ export type DecodeEventArgs<
38
+ TAbi extends Abi = Abi,
39
+ TEventName extends ExtractAbiEventNames<TAbi> = ExtractAbiEventNames<TAbi>,
40
+ TStrict extends boolean = true,
41
+ > = {
42
+ abi: TAbi;
43
+ eventName: TEventName;
44
+ event: Event;
45
+ strict?: TStrict;
46
+ };
47
+
48
+ export type DecodedEvent<
49
+ TAbi extends Abi = Abi,
50
+ TEventName extends ExtractAbiEventNames<TAbi> = ExtractAbiEventNames<TAbi>,
51
+ > = Event & {
52
+ eventName: TEventName;
53
+ args: EventToPrimitiveType<TAbi, TEventName>;
54
+ };
55
+
56
+ export type DecodeEventReturn<
57
+ TAbi extends Abi = Abi,
58
+ TEventName extends ExtractAbiEventNames<TAbi> = ExtractAbiEventNames<TAbi>,
59
+ TStrict extends boolean = true,
60
+ > = TStrict extends true
61
+ ? DecodedEvent<TAbi, TEventName>
62
+ : DecodedEvent<TAbi, TEventName> | null;
63
+
64
+ /** Decodes a single event.
65
+ *
66
+ * If `strict: true`, this function throws on failure. Otherwise, returns null.
67
+ */
68
+ export function decodeEvent<
69
+ TAbi extends Abi = Abi,
70
+ TEventName extends ExtractAbiEventNames<TAbi> = ExtractAbiEventNames<TAbi>,
71
+ TStrict extends boolean = true,
72
+ >(
73
+ args: DecodeEventArgs<TAbi, TEventName, TStrict>,
74
+ ): DecodeEventReturn<TAbi, TEventName, TStrict> {
75
+ const { abi, event, eventName, strict = true } = args;
76
+
77
+ const eventAbi = abi.find(
78
+ (item) => item.name === eventName && item.type === "event",
79
+ );
80
+
81
+ if (!eventAbi || eventAbi.type !== "event") {
82
+ if (strict) {
83
+ throw new DecodeEventError(`Event ${eventName} not found in ABI`);
84
+ }
85
+
86
+ return null as DecodeEventReturn<TAbi, TEventName, TStrict>;
87
+ }
88
+
89
+ if (eventAbi.kind === "enum") {
90
+ throw new DecodeEventError("enum: not implemented");
91
+ }
92
+
93
+ const selector = BigInt(getEventSelector(eventName));
94
+ if ((event.keys && selector !== BigInt(event.keys[0])) || !event.keys) {
95
+ if (strict) {
96
+ throw new DecodeEventError(
97
+ `Selector mismatch. Expected ${selector}, got ${event.keys?.[0]}`,
98
+ );
99
+ }
100
+
101
+ return null as DecodeEventReturn<TAbi, TEventName, TStrict>;
102
+ }
103
+
104
+ const keysAbi = eventAbi.members.filter((m) => m.kind === "key");
105
+ const dataAbi = eventAbi.members.filter((m) => m.kind === "data");
106
+
107
+ try {
108
+ const keysParser = compileEventMembers(abi, keysAbi);
109
+ const dataParser = compileEventMembers(abi, dataAbi);
110
+
111
+ const keysWithoutSelector = event.keys?.slice(1) ?? [];
112
+ const { out: decodedKeys } = keysParser(keysWithoutSelector, 0);
113
+ const { out: decodedData } = dataParser(event.data ?? [], 0);
114
+
115
+ const decoded = {
116
+ ...decodedKeys,
117
+ ...decodedData,
118
+ } as EventToPrimitiveType<TAbi, TEventName>;
119
+
120
+ return {
121
+ ...event,
122
+ eventName,
123
+ args: decoded,
124
+ } as DecodedEvent<TAbi, TEventName>;
125
+ } catch (error) {
126
+ if (error instanceof DecodeEventError && !strict) {
127
+ return null as DecodeEventReturn<TAbi, TEventName, TStrict>;
128
+ }
129
+
130
+ if (error instanceof ParseError && !strict) {
131
+ return null as DecodeEventReturn<TAbi, TEventName, TStrict>;
132
+ }
133
+
134
+ throw error;
135
+ }
136
+ }
137
+
138
+ function compileEventMembers<T extends Record<string, unknown>>(
139
+ abi: Abi,
140
+ members: AbiEventMember[],
141
+ ): Parser<T> {
142
+ return compileStructParser(abi, members) as Parser<T>;
143
+ }
144
+
145
+ function compileTypeParser(abi: Abi, type: string): Parser<unknown> {
146
+ if (isPrimitiveType(type)) {
147
+ return PrimitiveTypeParsers[type as keyof typeof PrimitiveTypeParsers];
148
+ }
149
+
150
+ if (isArrayType(type)) {
151
+ const elementType = getArrayElementType(type);
152
+ return parseArray(compileTypeParser(abi, elementType));
153
+ }
154
+
155
+ if (isSpanType(type)) {
156
+ const elementType = getSpanType(type);
157
+ return parseSpan(compileTypeParser(abi, elementType));
158
+ }
159
+
160
+ if (isOptionType(type)) {
161
+ const elementType = getOptionType(type);
162
+ return parseOption(compileTypeParser(abi, elementType));
163
+ }
164
+
165
+ if (isEmptyType(type)) {
166
+ return parseEmpty;
167
+ }
168
+
169
+ // Not a well-known type. Look it up in the ABI.
170
+ const typeAbi = abi.find((item) => item.name === type);
171
+ if (!typeAbi) {
172
+ throw new DecodeEventError(`Type ${type} not found in ABI`);
173
+ }
174
+
175
+ switch (typeAbi.type) {
176
+ case "struct": {
177
+ return compileStructParser(abi, typeAbi.members);
178
+ }
179
+ case "enum":
180
+ throw new DecodeEventError("enum: not implemented");
181
+ default:
182
+ throw new DecodeEventError(`Invalid type ${typeAbi.type}`);
183
+ }
184
+ }
185
+
186
+ type AbiMember = {
187
+ name: string;
188
+ type: string;
189
+ };
190
+
191
+ function compileStructParser(
192
+ abi: Abi,
193
+ members: readonly AbiMember[],
194
+ ): Parser<unknown> {
195
+ const parsers: Record<string, { index: number; parser: Parser<unknown> }> =
196
+ {};
197
+ for (const [index, member] of members.entries()) {
198
+ parsers[member.name] = {
199
+ index,
200
+ parser: compileTypeParser(abi, member.type),
201
+ };
202
+ }
203
+ return parseStruct(parsers);
204
+ }
package/src/index.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import { StreamConfig } from "@apibara/protocol";
2
+ export type { Abi } from "abi-wan-kanabi";
2
3
  import { BlockFromBytes } from "./block";
3
4
  import { FilterFromBytes, mergeFilter } from "./filter";
4
5
 
@@ -9,6 +10,16 @@ export * from "./filter";
9
10
  export * from "./block";
10
11
 
11
12
  export * from "./access";
13
+ export * from "./event";
14
+ export { getBigIntSelector, getEventSelector, getSelector } from "./abi";
15
+
16
+ declare module "abi-wan-kanabi" {
17
+ interface Config {
18
+ FeltType: bigint;
19
+ BigIntType: bigint;
20
+ U256Type: bigint;
21
+ }
22
+ }
12
23
 
13
24
  export const StarknetStream = new StreamConfig(
14
25
  FilterFromBytes,
@@ -0,0 +1,169 @@
1
+ import { describe, expect, it } from "vitest";
2
+
3
+ import {
4
+ ParseError,
5
+ parseArray,
6
+ parseAsHex,
7
+ parseBool,
8
+ parseFelt252,
9
+ parseOption,
10
+ parseStruct,
11
+ parseTuple,
12
+ parseU8,
13
+ parseU16,
14
+ parseU32,
15
+ parseU64,
16
+ parseU128,
17
+ parseU256,
18
+ } from "./parser";
19
+
20
+ describe("Primitive types parser", () => {
21
+ it("can parse a bool", () => {
22
+ const data = ["0x1", "0x0"] as const;
23
+ const { out, offset } = parseBool(data, 0);
24
+ expect(out).toBe(true);
25
+ expect(offset).toBe(1);
26
+ });
27
+
28
+ it("can parse a u8", () => {
29
+ const data = ["0xf", "0x0"] as const;
30
+ const { out, offset } = parseU8(data, 0);
31
+ expect(out).toBe(15n);
32
+ expect(offset).toBe(1);
33
+ });
34
+
35
+ it("can parse a u16", () => {
36
+ const data = ["0x1234", "0x0"] as const;
37
+ const { out, offset } = parseU16(data, 0);
38
+ expect(out).toBe(4660n);
39
+ expect(offset).toBe(1);
40
+ });
41
+
42
+ it("can parse a u32", () => {
43
+ const data = ["0x12345678", "0x0"] as const;
44
+ const { out, offset } = parseU32(data, 0);
45
+ expect(out).toBe(305419896n);
46
+ expect(offset).toBe(1);
47
+ });
48
+
49
+ it("can parse a u64", () => {
50
+ const data = ["0x1234567890abcdef", "0x0"] as const;
51
+ const { out, offset } = parseU64(data, 0);
52
+ expect(out).toBe(1311768467294899695n);
53
+ expect(offset).toBe(1);
54
+ });
55
+
56
+ it("can parse a u128", () => {
57
+ const data = ["0x1234567890abcdef1234567890abcdef", "0x0"] as const;
58
+ const { out, offset } = parseU128(data, 0);
59
+ expect(out).toBe(24197857200151252728969465429440056815n);
60
+ expect(offset).toBe(1);
61
+ });
62
+
63
+ it("can parse a u256", () => {
64
+ const data = ["0x1234567890abcdef1234567890abcdef", "0x0"] as const;
65
+ const { out, offset } = parseU256(data, 0);
66
+ expect(out).toBe(24197857200151252728969465429440056815n);
67
+ expect(offset).toBe(2);
68
+ });
69
+
70
+ it("can parse an address", () => {
71
+ const data = ["0x1234567890abcdef1234567890abcdef", "0x0"] as const;
72
+ const { out, offset } = parseAsHex(data, 0);
73
+ expect(out).toBe("0x1234567890abcdef1234567890abcdef");
74
+ expect(offset).toBe(1);
75
+ });
76
+
77
+ it("can parse a felt252", () => {
78
+ const data = ["0x1234567890abcdef1234567890abcdef", "0x0"] as const;
79
+ const { out, offset } = parseFelt252(data, 0);
80
+ expect(out).toBe(24197857200151252728969465429440056815n);
81
+ expect(offset).toBe(1);
82
+ });
83
+ });
84
+
85
+ describe("Array parser", () => {
86
+ it("can parse an array", () => {
87
+ const data = ["0x3", "0x1", "0x2", "0x3"] as const;
88
+ const { out, offset } = parseArray(parseU8)(data, 0);
89
+ expect(out).toEqual([1n, 2n, 3n]);
90
+ expect(offset).toBe(4);
91
+ });
92
+
93
+ it("throws an error if there is not enough data", () => {
94
+ const data = ["0x3", "0x1", "0x2"] as const;
95
+ expect(() => parseArray(parseU8)(data, 0)).toThrow(ParseError);
96
+ });
97
+ });
98
+
99
+ describe("Option parser", () => {
100
+ it("can parse an option", () => {
101
+ const data = ["0x1", "0x2"] as const;
102
+ const { out, offset } = parseOption(parseU8)(data, 0);
103
+ expect(out).toBe(2n);
104
+ expect(offset).toBe(2);
105
+ });
106
+
107
+ it("returns null if the option is not present", () => {
108
+ const data = ["0x0"] as const;
109
+ const { out, offset } = parseOption(parseU8)(data, 0);
110
+ expect(out).toBeNull();
111
+ expect(offset).toBe(1);
112
+ });
113
+ });
114
+
115
+ describe("Struct parser", () => {
116
+ it("can parse a struct with primitive types", () => {
117
+ const data = ["0x1", "0x2", "0x3"] as const;
118
+ const { out, offset } = parseStruct({
119
+ a: { index: 0, parser: parseU8 },
120
+ b: { index: 1, parser: parseU8 },
121
+ c: { index: 2, parser: parseU8 },
122
+ })(data, 0);
123
+ expect(out).toEqual({ a: 1n, b: 2n, c: 3n });
124
+ expect(offset).toBe(3);
125
+ });
126
+
127
+ it("can parse a struct with an option", () => {
128
+ const data = ["0x1", "0x1", "0x3"] as const;
129
+ const { out, offset } = parseStruct({
130
+ a: { index: 0, parser: parseU8 },
131
+ b: { index: 1, parser: parseOption(parseU8) },
132
+ })(data, 0);
133
+ expect(out).toEqual({ a: 1n, b: 3n });
134
+ expect(offset).toBe(3);
135
+ });
136
+
137
+ it("can parse a nested struct", () => {
138
+ const data = ["0x1", "0x2"] as const;
139
+ const { out, offset } = parseStruct({
140
+ a: { index: 0, parser: parseU8 },
141
+ b: {
142
+ index: 1,
143
+ parser: parseStruct({ c: { index: 0, parser: parseU8 } }),
144
+ },
145
+ })(data, 0);
146
+ expect(out).toEqual({ a: 1n, b: { c: 2n } });
147
+ expect(offset).toBe(2);
148
+ });
149
+ });
150
+
151
+ describe("Tuple parser", () => {
152
+ it("can parse a tuple", () => {
153
+ const data = ["0x1", "0x2", "0x0"] as const;
154
+ const { out, offset } = parseTuple(parseU8, parseAsHex, parseBool)(data, 0);
155
+ expect(out).toEqual([1n, "0x2", false]);
156
+ expect(offset).toBe(3);
157
+ });
158
+
159
+ it("can parse a nested tuple", () => {
160
+ const data = ["0x1", "0x2", "0x3", "0x0"] as const;
161
+ const { out, offset } = parseTuple(
162
+ parseU8,
163
+ parseTuple(parseU8, parseU8),
164
+ parseBool,
165
+ )(data, 0);
166
+ expect(out).toEqual([1n, [2n, 3n], false]);
167
+ expect(offset).toBe(4);
168
+ });
169
+ });
package/src/parser.ts ADDED
@@ -0,0 +1,170 @@
1
+ /*
2
+ * Calldata combinatorial parsers.
3
+ *
4
+ * Based on the Ekubo's event parser.
5
+ *
6
+ * MIT License
7
+ *
8
+ * Copyright (c) 2023 Ekubo, Inc.
9
+ *
10
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
11
+ * of this software and associated documentation files (the "Software"), to deal
12
+ * in the Software without restriction, including without limitation the rights
13
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
14
+ * copies of the Software, and to permit persons to whom the Software is
15
+ * furnished to do so, subject to the following conditions:
16
+ *
17
+ * The above copyright notice and this permission notice shall be included in all
18
+ * copies or substantial portions of the Software.
19
+ *
20
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
21
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
22
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
23
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
24
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
25
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
26
+ * SOFTWARE.
27
+ */
28
+ import type { FieldElement } from "./common";
29
+
30
+ export type Parser<TOut> = (
31
+ data: readonly FieldElement[],
32
+ offset: number,
33
+ ) => { out: TOut; offset: number };
34
+
35
+ export class ParseError extends Error {
36
+ constructor(message: string) {
37
+ super(message);
38
+ this.name = "ParseError";
39
+ }
40
+ }
41
+
42
+ // Primitive types.
43
+
44
+ function assertInBounds(data: readonly FieldElement[], offset: number) {
45
+ if (offset >= data.length) {
46
+ throw new ParseError(
47
+ `Offset out of bounds. Data length ${data.length}, offset ${offset}`,
48
+ );
49
+ }
50
+ }
51
+
52
+ export function parseBool(data: readonly FieldElement[], offset: number) {
53
+ assertInBounds(data, offset);
54
+ return { out: BigInt(data[offset]) > 0n, offset: offset + 1 };
55
+ }
56
+
57
+ export function parseAsBigInt(data: readonly FieldElement[], offset: number) {
58
+ assertInBounds(data, offset);
59
+ return { out: BigInt(data[offset]), offset: offset + 1 };
60
+ }
61
+
62
+ export const parseU8 = parseAsBigInt;
63
+ export const parseU16 = parseAsBigInt;
64
+ export const parseU32 = parseAsBigInt;
65
+ export const parseU64 = parseAsBigInt;
66
+ export const parseU128 = parseAsBigInt;
67
+ export const parseUsize = parseAsBigInt;
68
+
69
+ export function parseU256(data: readonly FieldElement[], offset: number) {
70
+ assertInBounds(data, offset + 1);
71
+ return {
72
+ out: BigInt(data[offset]) + (BigInt(data[offset + 1]) << 128n),
73
+ offset: offset + 2,
74
+ };
75
+ }
76
+
77
+ export function parseAsHex(data: readonly FieldElement[], offset: number) {
78
+ assertInBounds(data, offset);
79
+ return {
80
+ out: String(data[offset]),
81
+ offset: offset + 1,
82
+ };
83
+ }
84
+
85
+ export const parseContractAddress = parseAsHex;
86
+ export const parseEthAddress = parseAsHex;
87
+ export const parseStorageAddress = parseAsHex;
88
+ export const parseClassHash = parseAsHex;
89
+ export const parseBytes31 = parseAsHex;
90
+
91
+ export function parseFelt252(data: readonly FieldElement[], offset: number) {
92
+ assertInBounds(data, offset);
93
+ return {
94
+ out: BigInt(data[offset]),
95
+ offset: offset + 1,
96
+ };
97
+ }
98
+
99
+ export function parseEmpty(data: readonly FieldElement[], offset: number) {
100
+ return { out: null, offset };
101
+ }
102
+
103
+ // Higher-level types.
104
+
105
+ export function parseArray<T>(type: Parser<T>): Parser<T[]> {
106
+ return (data: readonly FieldElement[], startingOffset: number) => {
107
+ let offset = startingOffset;
108
+ const length = BigInt(data[offset]);
109
+
110
+ offset++;
111
+
112
+ const out: T[] = [];
113
+ for (let i = 0; i < length; i++) {
114
+ const { out: item, offset: newOffset } = type(data, offset);
115
+ out.push(item);
116
+ offset = newOffset;
117
+ }
118
+
119
+ return { out, offset };
120
+ };
121
+ }
122
+
123
+ export const parseSpan = parseArray;
124
+
125
+ export function parseOption<T>(type: Parser<T>) {
126
+ return (data: readonly FieldElement[], offset: number) => {
127
+ const hasValue = BigInt(data[offset]) === 1n;
128
+ if (hasValue) {
129
+ return type(data, offset + 1);
130
+ }
131
+ return { out: null, offset: offset + 1 };
132
+ };
133
+ }
134
+
135
+ export function parseStruct<T extends { [key: string]: unknown }>(
136
+ parsers: { [K in keyof T]: { index: number; parser: Parser<T[K]> } },
137
+ ) {
138
+ const sortedParsers = Object.entries(parsers).sort(
139
+ (a, b) => a[1].index - b[1].index,
140
+ );
141
+ return (data: readonly FieldElement[], startingOffset: number) => {
142
+ let offset = startingOffset;
143
+ const out: Record<string, unknown> = {};
144
+ for (const [key, { parser }] of sortedParsers) {
145
+ const { out: value, offset: newOffset } = parser(data, offset);
146
+ out[key] = value;
147
+ offset = newOffset;
148
+ }
149
+ return { out, offset };
150
+ };
151
+ }
152
+
153
+ export function parseTuple<T extends Parser<unknown>[]>(
154
+ ...parsers: T
155
+ ): Parser<UnwrapParsers<T>> {
156
+ return (data: readonly FieldElement[], startingOffset: number) => {
157
+ let offset = startingOffset;
158
+ const out = [];
159
+ for (const parser of parsers) {
160
+ const { out: value, offset: newOffset } = parser(data, offset);
161
+ out.push(value);
162
+ offset = newOffset;
163
+ }
164
+ return { out, offset } as { out: UnwrapParsers<T>; offset: number };
165
+ };
166
+ }
167
+
168
+ type UnwrapParsers<TP> = {
169
+ [Index in keyof TP]: TP[Index] extends Parser<infer U> ? U : never;
170
+ };