o1-labs / o1js

TypeScript framework for zk-SNARKs and zkApps

Home Page:https://docs.minaprotocol.com/en/zkapps/how-to-write-a-zkapp

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

typing of `SmartContract.emitEvent`

harrysolovay opened this issue · comments

Some questions about the signature of SmartContract.emitEvent...

emitEvent<K extends keyof this['events']>(type: K, event: any): void;

Although SmartContract.emitEvent is generic over event name, it is not generic over the event data. Is there a particular reason for this?

Let's dig deeper with an example: a simple Counter contract, with two event variants: Updated and Incremented.

import { Empty, method, SmartContract, State, state, Struct, UInt64 } from "o1js"

export class Counter extends SmartContract {
  readonly events = CounterEvents

  @state(UInt64)
  count = State<UInt64>()

  init() {
    super.init()
    this.count.set(new UInt64(0))
  }

  @method
  increment() {
    const initial = this.count.getAndRequireEquals()
    const final = initial.add(1)
    this.emitEvent("Updated", { initial, final })
    this.emitEvent("Incremented", Empty.empty())
    this.count.set(final)
  }
}

export const CounterEvents = {
  Updated: Struct({
    initial: UInt64,
    final: UInt64,
  }),
  Incremented: Empty,
} as const

Within the increment method, we emit two events: Updated and Incremented. If we supply invalid event data, there is no type error.

- this.emitEvent("Updated", { initial, final })
+ this.emitEvent("Updated", {
+   initials: Empty.empty(),
+   finals: Empty.empty(),
+ })

A secondary consequence of the lack of generic typing of event data is we miss out on completions and key-bound tsdocs offered by the language service.

Another question: in cases where there is no event data, can the second emitEvent argument be omitted?

- this.emitEvent("Incremented", Empty.empty())
+ this.emitEvent("Incremented")

Currently, this causes a type error.

this.emitEvent("Incremented")
//   ~~~~~~~~~~~~~~~~~~~~~~~~
//   ^
//   Expected 2 arguments, but got 1.

A related idea/question (perhaps for another issue). I'm curious if the following API was considered?

- this.emitEvent("Updated", { initial, final })
+ this.emit.Updated({ initial, final })
  • gets rid of the quotes
  • the event emission method becomes unary

I couldn't make type-safe this.emitEvent() work. Curious if you can, but it seems TS is just not able to use the this['events'] type.

As the solution I thought we could create events and methods for them at the same time, i.e.

class extends SmartContract {
  events = Events({ Updated: { initial: UInt64, final: UInt64 });
}

// in a method:
this.events.emit("Updated", ...) // properly typed

But that must be done very soon if we don't want to wait until the next major version

I prefer the approach you suggested:

class extends SmartContract {
  events = Events({ Updated: { initial: UInt64, final: UInt64 });
}

This relieves developers of needing to remember readonly/const modifiers on the event definition record. One could create a member named events that has nothing to do with contract events.

Another question: how do we get typed retrieval of events? Continuing off of the example from above...

const counter = new Counter(deployedPk)

const events = await counter.fetchEvents()
events.forEach((event) => {
  event.type // `string`
  event.event.data // `any`
})

The data contained within the event is of type any. Perhaps we could deprecate fetchEvents and instead reference fetch on the Events-factory-initialized member.

class extends Counter extends SmartContract {
  events = Events({ Updated: { initial: UInt64, final: UInt64 } });
}

const counter = new Counter(deployedPk)

const events = await counter.events.fetch()
events.forEach((event) => {
  event.type // `"Updated"`
  event.event.data // `{ initial: UInt64; final: UInt64 }`
})

This API has the added benefit of looking similar to that of state retrieval.

Perhaps fetch can accept an event name(s) as well, to save bandwidth in the (common) case that the developer only wants a particular event.

const updateEvents = await counter.events.fetch("Update")
const updateAndIncrementEvents = await counter.events.fetch("Update", "Increment")
const updateAndDecrementEvents = await counter.events.fetch("Update", "Decrement")

Or if we really care about keeping event retrieval similar to that of state, events could be a callable object.

const updateEvents = await counter.events("Update").fetch()