Skip to content

Commit

Permalink
add event codec
Browse files Browse the repository at this point in the history
  • Loading branch information
vanruch committed Mar 24, 2024
1 parent 0be6aae commit 1b7f049
Show file tree
Hide file tree
Showing 7 changed files with 191 additions and 26 deletions.
60 changes: 60 additions & 0 deletions evm/evm-utils/src/abi-components/event.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { bytes, bytes32 } from "../codecs/primitives";
import { Src } from "../src";
import { IndexedCodec, ParsedNamedCodecList } from "../codec";

export interface EventRecord {
topics: string[];
data: string;
}

export type IndexedCodecs<T> = T extends readonly [
{ indexed: true; isDynamic: true; name?: string },
...infer R
]
? [
typeof bytes32 & { indexed: true; name: T[0]["name"] },
...IndexedCodecs<R>
]
: T extends readonly [any, ...infer R]
? [T[0], ...IndexedCodecs<R>]
: T extends readonly []
? []
: never;

export class AbiEvent<
const T extends ReadonlyArray<IndexedCodec<any, string>>
> {
public readonly params: any;
constructor(public readonly topic: string, ...args: T) {
this.params = args.map((arg) =>
arg.indexed && arg.isDynamic
? {
...bytes32,
name: arg.name,
isDynamic: true,
indexed: true,
}
: arg
) as IndexedCodecs<T>;
}

is(rec: EventRecord): boolean {
return rec.topics[0] === this.topic;
}

decode(rec: EventRecord): ParsedNamedCodecList<IndexedCodecs<T>> {
const src = new Src(Buffer.from(rec.data.slice(2), "hex"));
const result = {} as any;
let topicCounter = 1;
for (let i = 0; i < this.params.length; i++) {
if (this.params[i].indexed) {
const topic = rec.topics[topicCounter++];
const topicSrc = new Src(Buffer.from(topic.slice(2), "hex"));
result[this.params[i].name ?? i] = this.params[i].decode(topicSrc);
} else {
result[this.params[i].name ?? i] = this.params[i].decode(src);
}
}
return result;
}
}
10 changes: 5 additions & 5 deletions evm/evm-utils/src/abi-components/function.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import {
ParsedNamedCodecList,
NamedCodec,
NamedCodecListArgs,
CodecListArgs,
Codec,
} from "../codec";
import { Sink } from "../sink";
Expand All @@ -19,7 +19,7 @@ export class AbiFunction<
constructor(
public selector: string,
public readonly args: T,
public readonly returnType: Codec<R>
public readonly returnType?: Codec<R>
) {
assert(selector.startsWith("0x"), "selector must start with 0x");
assert(selector.length === 10, "selector must be 4 bytes long");
Expand All @@ -32,7 +32,7 @@ export class AbiFunction<
return calldata.startsWith(this.selector);
}

encode(...args: NamedCodecListArgs<T>) {
encode(...args: CodecListArgs<T>) {
const sink = new Sink(this.slotsCount);
for (let i = 0; i < this.args.length; i++) {
this.args[i].encode(sink, args[i]);
Expand All @@ -55,8 +55,8 @@ export class AbiFunction<
return result;
}

decodeResult(output: string): R {
decodeResult(output: string): R | undefined {
const src = new Src(Buffer.from(output.slice(2), "hex"));
return this.returnType.decode(src);
return this.returnType?.decode(src);
}
}
14 changes: 8 additions & 6 deletions evm/evm-utils/src/codec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ export interface Codec<T> {
slotsCount?: number;
}

export type NamedCodec<T, S extends string> = { name?: S } & Codec<T>;
export type NamedCodec<T, S> = { name?: S } & Codec<T>;

type Identity<T> = T extends object
? {
Expand Down Expand Up @@ -38,10 +38,12 @@ type DeepReadonly<T> = Readonly<{
: DeepReadonly<T[K]>;
}>;

export type NamedCodecListArgs<T> = T extends readonly [
NamedCodec<infer U, any>
]
export type CodecListArgs<T> = T extends readonly [Codec<infer U>]
? readonly [DeepReadonly<U>]
: T extends readonly [NamedCodec<infer U, any>, ...infer R]
? readonly [DeepReadonly<U>, ...NamedCodecListArgs<R>]
: T extends readonly [Codec<infer U>, ...infer R]
? readonly [DeepReadonly<U>, ...CodecListArgs<R>]
: never;

export type IndexedCodec<T, U extends string> = Identity<
NamedCodec<T, U> & { indexed?: true }
>;
26 changes: 19 additions & 7 deletions evm/evm-utils/src/codecs/primitives.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import type { Codec, NamedCodec } from "../codec";
import { Codec, IndexedCodec, NamedCodec } from "../codec";
import { Sink } from "../sink";
import { Src } from "../src";
import { ArrayCodec, FixedArrayCodec } from "./array";
import { StructCodec } from "./struct";
import { AbiFunction } from "../abi-components/function";
import { AbiEvent } from "../abi-components/event";

export const bool: Codec<boolean> = {
encode: function (sink: Sink, val: boolean) {
Expand Down Expand Up @@ -135,7 +136,7 @@ export const int256: Codec<bigint> = {
isDynamic: false,
};

export const string: Codec<string> = {
export const string = <const>{
encode(sink: Sink, val: string) {
sink.offset();
sink.string(val);
Expand All @@ -147,7 +148,7 @@ export const string: Codec<string> = {
isDynamic: true,
};

export const bytes: Codec<Uint8Array> = {
export const bytes = <const>{
encode(sink: Sink, val: Uint8Array) {
sink.offset();
sink.bytes(val);
Expand Down Expand Up @@ -221,9 +222,20 @@ export const array = <T>(item: Codec<T>): Codec<T[]> => new ArrayCodec(item);
export const struct = <T extends NamedCodec<any, string>[]>(...components: T) =>
new StructCodec<T>(...components);

export const fun = <T extends NamedCodec<any, string>[]>(
export const tuple = struct;

export const fun = <
const T extends NamedCodec<any, string>[],
R extends Codec<any>
>(
signature: string,
args: T
) => new AbiFunction(signature, args);
args: T,
returnType?: R
) => new AbiFunction<T, R>(signature, args, returnType);

export const event = <const T extends ReadonlyArray<IndexedCodec<any, string>>>(
topic: string,
...args: T
) => new AbiEvent<T>(topic, ...args);

export { arg } from "../utils";
export { arg, indexed } from "../utils";
26 changes: 22 additions & 4 deletions evm/evm-utils/src/utils.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { Codec, NamedCodec } from "./codec";
import type { Codec } from "./codec";

export function slotsCount(codecs: readonly Codec<any>[]) {
let count = 0;
Expand All @@ -8,10 +8,10 @@ export function slotsCount(codecs: readonly Codec<any>[]) {
return count;
}

export function arg<T, S extends string>(
export function arg<T extends Codec<any>, S extends string>(
name: S,
codec: Codec<T>
): NamedCodec<T, S> {
codec: T
): T & { name: S } {
return new Proxy(codec, {
get(target: any, prop, receiver) {
if (prop === "name") {
Expand All @@ -28,3 +28,21 @@ export function arg<T, S extends string>(
},
});
}

export function indexed<T extends Codec<any>>(codec: T): T & { indexed: true } {
return new Proxy(codec, {
get(target: any, prop, receiver) {
if (prop === "indexed") {
return true;
}
const value = target[prop];
if (value instanceof Function) {
return function (...args: any[]) {
// @ts-ignore
return value.apply(this === receiver ? target : this, args);
};
}
return value;
},
});
}
73 changes: 73 additions & 0 deletions evm/evm-utils/test/event.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import { describe, expect, it } from "vitest";
import { encodeAbiParameters, encodeEventTopics, parseAbiItem } from "viem";
import {
arg,
bool,
bytes,
indexed,
string,
struct,
uint256,
event as _event,
} from "../src";

describe("Event", () => {
it("decodes simple args", () => {
const topics = encodeEventTopics({
abi: [parseAbiItem("event Test(uint256 indexed a, uint256 b)")],
eventName: "Test",
args: { a: 123n },
});
const event = _event(
topics[0],
indexed(arg("a", uint256)),
arg("b", uint256)
);
const decoded = event.decode({
topics,
data: encodeAbiParameters([{ type: "uint256" }], [100n]),
});
expect(decoded).toEqual({ a: 123n, b: 100n });
});

it("decodes complex args", () => {
const topics = encodeEventTopics({
abi: [
parseAbiItem(
"event Test(string indexed a, string b, bytes c, (uint256, string) d, bool indexed e)"
),
],
eventName: "Test",
args: { a: "xdxdxd", e: true },
});
const event = _event(
topics[0],
indexed(arg("a", string)),
arg("b", string),
arg("c", bytes),
arg("d", struct(uint256, string)),
indexed(arg("e", bool))
);
const decoded = event.decode({
topics,
data: encodeAbiParameters(
[
{ type: "string" },
{ type: "bytes" },
{
type: "tuple",
components: [{ type: "uint256" }, { type: "string" }],
},
],
["hello", "0x1234", [100n, "world"]]
),
});
expect(decoded).toEqual({
a: Buffer.from(topics[1].slice(2), "hex"),
b: "hello",
c: Buffer.from([0x12, 0x34]),
d: { 0: 100n, 1: "world" },
e: true,
});
});
});
8 changes: 4 additions & 4 deletions evm/evm-utils/test/function.test.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,20 @@
import { describe, expect, it } from "vitest";
import { AbiFunction } from "../src/abi-components/function";
import { encodeFunctionData } from "viem";
import {
arg,
array,
bool,
bytes4,
fixedArray,
fun,
int32,
struct,
uint256,
} from "../src";
import { encodeFunctionData } from "viem";

describe("Function", () => {
it("encodes/decodes simple args", () => {
const simpleFunction = new AbiFunction("0x12345678", [
const simpleFunction = fun("0x12345678", [
arg("foo", uint256),
int32,
bool,
Expand All @@ -34,7 +34,7 @@ describe("Function", () => {

it("encodes/decodes dynamic args", () => {
const staticStruct = struct(arg("foo", uint256), arg("bar", bytes4));
const dynamicFunction = new AbiFunction("0x423917ce", [
const dynamicFunction = fun("0x423917ce", [
arg("arg1", array(uint256)),
arg("arg2", fixedArray(array(uint256), 10)),
arg(
Expand Down

0 comments on commit 1b7f049

Please sign in to comment.