diff --git a/evm/evm-utils/src/abi-components/event.ts b/evm/evm-utils/src/abi-components/event.ts new file mode 100644 index 000000000..0a17b81bf --- /dev/null +++ b/evm/evm-utils/src/abi-components/event.ts @@ -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 extends readonly [ + { indexed: true; isDynamic: true; name?: string }, + ...infer R +] + ? [ + typeof bytes32 & { indexed: true; name: T[0]["name"] }, + ...IndexedCodecs + ] + : T extends readonly [any, ...infer R] + ? [T[0], ...IndexedCodecs] + : T extends readonly [] + ? [] + : never; + +export class AbiEvent< + const T extends ReadonlyArray> +> { + 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; + } + + is(rec: EventRecord): boolean { + return rec.topics[0] === this.topic; + } + + decode(rec: EventRecord): ParsedNamedCodecList> { + 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; + } +} diff --git a/evm/evm-utils/src/abi-components/function.ts b/evm/evm-utils/src/abi-components/function.ts index 5b55be23c..9d74d1dc9 100644 --- a/evm/evm-utils/src/abi-components/function.ts +++ b/evm/evm-utils/src/abi-components/function.ts @@ -1,7 +1,7 @@ import { ParsedNamedCodecList, NamedCodec, - NamedCodecListArgs, + CodecListArgs, Codec, } from "../codec"; import { Sink } from "../sink"; @@ -19,7 +19,7 @@ export class AbiFunction< constructor( public selector: string, public readonly args: T, - public readonly returnType: Codec + public readonly returnType?: Codec ) { assert(selector.startsWith("0x"), "selector must start with 0x"); assert(selector.length === 10, "selector must be 4 bytes long"); @@ -32,7 +32,7 @@ export class AbiFunction< return calldata.startsWith(this.selector); } - encode(...args: NamedCodecListArgs) { + encode(...args: CodecListArgs) { const sink = new Sink(this.slotsCount); for (let i = 0; i < this.args.length; i++) { this.args[i].encode(sink, args[i]); @@ -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); } } diff --git a/evm/evm-utils/src/codec.ts b/evm/evm-utils/src/codec.ts index 9b2d38685..3954b156c 100644 --- a/evm/evm-utils/src/codec.ts +++ b/evm/evm-utils/src/codec.ts @@ -10,7 +10,7 @@ export interface Codec { slotsCount?: number; } -export type NamedCodec = { name?: S } & Codec; +export type NamedCodec = { name?: S } & Codec; type Identity = T extends object ? { @@ -38,10 +38,12 @@ type DeepReadonly = Readonly<{ : DeepReadonly; }>; -export type NamedCodecListArgs = T extends readonly [ - NamedCodec -] +export type CodecListArgs = T extends readonly [Codec] ? readonly [DeepReadonly] - : T extends readonly [NamedCodec, ...infer R] - ? readonly [DeepReadonly, ...NamedCodecListArgs] + : T extends readonly [Codec, ...infer R] + ? readonly [DeepReadonly, ...CodecListArgs] : never; + +export type IndexedCodec = Identity< + NamedCodec & { indexed?: true } +>; diff --git a/evm/evm-utils/src/codecs/primitives.ts b/evm/evm-utils/src/codecs/primitives.ts index af7d1c5b3..808481fae 100644 --- a/evm/evm-utils/src/codecs/primitives.ts +++ b/evm/evm-utils/src/codecs/primitives.ts @@ -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 = { encode: function (sink: Sink, val: boolean) { @@ -135,7 +136,7 @@ export const int256: Codec = { isDynamic: false, }; -export const string: Codec = { +export const string = { encode(sink: Sink, val: string) { sink.offset(); sink.string(val); @@ -147,7 +148,7 @@ export const string: Codec = { isDynamic: true, }; -export const bytes: Codec = { +export const bytes = { encode(sink: Sink, val: Uint8Array) { sink.offset(); sink.bytes(val); @@ -221,9 +222,20 @@ export const array = (item: Codec): Codec => new ArrayCodec(item); export const struct = []>(...components: T) => new StructCodec(...components); -export const fun = []>( +export const tuple = struct; + +export const fun = < + const T extends NamedCodec[], + R extends Codec +>( signature: string, - args: T -) => new AbiFunction(signature, args); + args: T, + returnType?: R +) => new AbiFunction(signature, args, returnType); + +export const event = >>( + topic: string, + ...args: T +) => new AbiEvent(topic, ...args); -export { arg } from "../utils"; +export { arg, indexed } from "../utils"; diff --git a/evm/evm-utils/src/utils.ts b/evm/evm-utils/src/utils.ts index 33d0530fb..82208a269 100644 --- a/evm/evm-utils/src/utils.ts +++ b/evm/evm-utils/src/utils.ts @@ -1,4 +1,4 @@ -import type { Codec, NamedCodec } from "./codec"; +import type { Codec } from "./codec"; export function slotsCount(codecs: readonly Codec[]) { let count = 0; @@ -8,10 +8,10 @@ export function slotsCount(codecs: readonly Codec[]) { return count; } -export function arg( +export function arg, S extends string>( name: S, - codec: Codec -): NamedCodec { + codec: T +): T & { name: S } { return new Proxy(codec, { get(target: any, prop, receiver) { if (prop === "name") { @@ -28,3 +28,21 @@ export function arg( }, }); } + +export function indexed>(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; + }, + }); +} diff --git a/evm/evm-utils/test/event.test.ts b/evm/evm-utils/test/event.test.ts new file mode 100644 index 000000000..9121acd14 --- /dev/null +++ b/evm/evm-utils/test/event.test.ts @@ -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, + }); + }); +}); diff --git a/evm/evm-utils/test/function.test.ts b/evm/evm-utils/test/function.test.ts index c1cd8bb52..5fe1cb291 100644 --- a/evm/evm-utils/test/function.test.ts +++ b/evm/evm-utils/test/function.test.ts @@ -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, @@ -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(