Skip to content
This repository has been archived by the owner on Apr 26, 2024. It is now read-only.

Commit

Permalink
Add state from event tuple handler (#121)
Browse files Browse the repository at this point in the history
* wip: state from tuple handler

* feat: add expandParam to stateFromEventTuple

* test: cover StateFromEventTuple

* deps: changeset

---------

Co-authored-by: Michał Podsiadły <[email protected]>
  • Loading branch information
michalsidzej and sdlyy authored Feb 8, 2024
1 parent 2354d3b commit 51214a2
Show file tree
Hide file tree
Showing 6 changed files with 343 additions and 2 deletions.
6 changes: 6 additions & 0 deletions packages/discovery/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
# @l2beat/discovery

## 0.41.0

### Minor Changes

- Added StateFromEventTuple handler

## 0.40.1

### Patch Changes
Expand Down
2 changes: 1 addition & 1 deletion packages/discovery/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "@l2beat/discovery",
"description": "L2Beat discovery - engine & tooling utilized for keeping an eye on L2s",
"version": "0.40.1",
"version": "0.41.0",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"bin": {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,200 @@
import { expect, mockObject } from 'earl'
import { providers, utils } from 'ethers'

import { EthereumAddress } from '../../../utils/EthereumAddress'
import { DiscoveryLogger } from '../../DiscoveryLogger'
import { DiscoveryProvider } from '../../provider/DiscoveryProvider'
import { StateFromEventTupleHandler } from './StateFromEventTupleHandler'

const EVENT =
'event TupleEvent(tuple(uint32 eid, tuple(uint64 foo, uint8 bar, uint8 baz, uint8 qux) config)[] params)'

describe(StateFromEventTupleHandler.name, () => {
const BLOCK_NUMBER = 1234

describe('constructor', () => {
it('finds the specified event by name', () => {
const handler = new StateFromEventTupleHandler(
'someName',
{
type: 'stateFromEventTuple',
event: 'TupleEvent',
returnParam: 'params',
},
[EVENT],
DiscoveryLogger.SILENT,
)
expect(handler.getEvent()).toEqual(EVENT)
})
})

describe('execute', () => {
const abi = new utils.Interface([EVENT])

function TupleEvent(
eid: number,
config: {
foo: number
bar: number
baz: number
qux: number
},
): providers.Log {
const data = [[eid, [config.foo, config.bar, config.baz, config.qux]]]

return abi.encodeEventLog(abi.getEvent('TupleEvent'), [
data,
]) as providers.Log
}

it('no logs', async () => {
const address = EthereumAddress.random()
const provider = mockObject<DiscoveryProvider>({
async getLogs(providedAddress, topics, fromBlock, toBlock) {
expect(providedAddress).toEqual(address)
expect(topics).toEqual([abi.getEventTopic('TupleEvent')])
expect(fromBlock).toEqual(0)
expect(toBlock).toEqual(BLOCK_NUMBER)
return []
},
})

const handler = new StateFromEventTupleHandler(
'someName',
{
type: 'stateFromEventTuple',
event: EVENT,
returnParam: 'params',
},
[],
DiscoveryLogger.SILENT,
)
const value = await handler.execute(provider, address, BLOCK_NUMBER)

expect(value).toEqual({
field: 'someName',
value: {},
ignoreRelative: undefined,
})
})

it('many logs as array', async () => {
const address = EthereumAddress.random()
const provider = mockObject<DiscoveryProvider>({
async getLogs() {
return [
TupleEvent(1, {
foo: 1,
bar: 2,
baz: 3,
qux: 4,
}),
TupleEvent(2, {
foo: 5,
bar: 6,
baz: 7,
qux: 8,
}),
TupleEvent(3, {
foo: 9,
bar: 10,
baz: 11,
qux: 12,
}),
// Persist last
TupleEvent(3, {
foo: 20,
bar: 20,
baz: 20,
qux: 20,
}),
]
},
})

const handler = new StateFromEventTupleHandler(
'someName',
{
type: 'stateFromEventTuple',
event: EVENT,
returnParam: 'params',
},
[],
DiscoveryLogger.SILENT,
)
const value = await handler.execute(provider, address, BLOCK_NUMBER)

expect(value).toEqual({
field: 'someName',
value: { '1': [1, 2, 3, 4], '2': [5, 6, 7, 8], '3': [20, 20, 20, 20] },
ignoreRelative: undefined,
})
})

it('many logs as array with param expansion', async () => {
const address = EthereumAddress.random()
const provider = mockObject<DiscoveryProvider>({
async getLogs() {
return [
TupleEvent(1, {
foo: 1,
bar: 2,
baz: 3,
qux: 4,
}),
TupleEvent(2, {
foo: 5,
bar: 6,
baz: 7,
qux: 8,
}),
TupleEvent(3, {
foo: 9,
bar: 10,
baz: 11,
qux: 12,
}),
]
},
})

const handler = new StateFromEventTupleHandler(
'someName',
{
type: 'stateFromEventTuple',
event: EVENT,
returnParam: 'params',
expandParam: 'config',
},
[],
DiscoveryLogger.SILENT,
)
const value = await handler.execute(provider, address, BLOCK_NUMBER)

expect(value).toEqual({
field: 'someName',
value: {
'1': {
foo: 1,
bar: 2,
baz: 3,
qux: 4,
},
'2': {
foo: 5,
bar: 6,
baz: 7,
qux: 8,
},
'3': {
foo: 9,
bar: 10,
baz: 11,
qux: 12,
},
},
ignoreRelative: undefined,
})
})
})
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
import { assert } from '@l2beat/backend-tools'
import { ContractValue } from '@l2beat/discovery-types'
import { utils } from 'ethers'
import { reduce } from 'lodash'
import * as z from 'zod'

import { EthereumAddress } from '../../../utils/EthereumAddress'
import { DiscoveryLogger } from '../../DiscoveryLogger'
import { DiscoveryProvider } from '../../provider/DiscoveryProvider'
import { ClassicHandler, HandlerResult } from '../Handler'
import { getEventFragment } from '../utils/getEventFragment'
import { toContractValue } from '../utils/toContractValue'
import { toTopics } from '../utils/toTopics'

/**
* This handler was created specifically for the LayerZero v2 contracts.
* example event:
* event DefaultConfigsSet(tuple(uint32 eid, tuple(...) config)[] params)",
*
* As there is a lot of similar events, the logic is quite coupled to the event structure
* for the sake of simplicity. This can be improved in the future if more generic approach is needed.
*
* Logic:
* 1. Get all logs for the event
* 2. Group logs by returnParam[0] (it is always eid with current approach)
* 3. Expand tuple of values into named values dictionary if expandParam is provided
* 4. Keep only the latest log for each group
*/

export type StateFromEventTupleDefinition = z.infer<
typeof StateFromEventTupleDefinition
>
export const StateFromEventTupleDefinition = z.strictObject({
type: z.literal('stateFromEventTuple'),
event: z.string(),
returnParam: z.string(),
expandParam: z.string().optional(),
ignoreRelative: z.boolean().optional(),
})

export class StateFromEventTupleHandler implements ClassicHandler {
readonly dependencies: string[] = []
private readonly fragment: utils.EventFragment
private readonly abi: utils.Interface

constructor(
readonly field: string,
readonly definition: StateFromEventTupleDefinition,
abi: string[],
readonly logger: DiscoveryLogger,
) {
this.fragment = getEventFragment(definition.event, abi, () => true)
assert(this.fragment.inputs.length === 1, 'Event should have 1 input')
assert(
this.fragment.inputs[0]?.name === definition.returnParam,
`Invalid returnParam, ${this.fragment.inputs[0]?.name ?? ''} expected, ${
definition.returnParam
} given`,
)
this.abi = new utils.Interface([this.fragment])
}

getEvent(): string {
return this.fragment.format(utils.FormatTypes.full)
}

async execute(
provider: DiscoveryProvider,
address: EthereumAddress,
blockNumber: number,
): Promise<HandlerResult> {
this.logger.logExecution(this.field, ['Querying ', this.fragment.name])
const topics = toTopics(this.abi, this.fragment)
const logs = await provider.getLogs(address, topics, 0, blockNumber)

const values = new Map<number, ContractValue>()
for (const log of logs) {
const parsed = this.abi.parseLog(log)
const params = reduce(
parsed.args,
(acc, value, key) => {
acc[key] = toContractValue(value) as [number, ContractValue][]
return acc
},
{} as Record<string, [number, ContractValue][]>,
)
for (const array of Object.values(params)) {
assert(Array.isArray(array), 'Invalid param type')
for (const tuple of array) {
if (this.definition.expandParam) {
const returnParam = this.fragment.inputs[0]
assert(returnParam, 'Return param not returned from event')
const inputToExpand = returnParam.components.find(
(component) => component.name === this.definition.expandParam,
)

assert(
inputToExpand,
'Input to expand not found in the return param',
)

const inputLabels = inputToExpand.components.map(
(component) => component.name,
)

assert(Array.isArray(tuple[1]), 'Cannot expand non-tuple value')

const expanded = Object.fromEntries(
tuple[1].map((value, index) => [inputLabels[index], value]),
) as ContractValue

values.set(tuple[0], expanded)
} else {
values.set(tuple[0], tuple[1])
}
}
}
}

const value = Object.fromEntries(values.entries())

return {
field: this.field,
value,
ignoreRelative: this.definition.ignoreRelative,
}
}
}
7 changes: 7 additions & 0 deletions packages/discovery/src/discovery/handlers/user/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,10 @@ import {
StateFromEventDefinition,
StateFromEventHandler,
} from './StateFromEventHandler'
import {
StateFromEventTupleDefinition,
StateFromEventTupleHandler,
} from './StateFromEventTupleHandler'
import { StorageHandler, StorageHandlerDefinition } from './StorageHandler'

export type UserHandlerDefinition = z.infer<typeof UserHandlerDefinition>
Expand All @@ -93,6 +97,7 @@ export const UserHandlerDefinition = z.union([
ConstructorArgsDefinition,
EventCountHandlerDefinition,
StateFromEventDefinition,
StateFromEventTupleDefinition,
HardCodedDefinition,
StarkWareGovernanceHandlerDefinition,
LayerZeroMultisigHandlerDefinition,
Expand Down Expand Up @@ -142,6 +147,8 @@ export function getUserHandler(
return new StarkWareGovernanceHandler(field, definition, abi, logger)
case 'stateFromEvent':
return new StateFromEventHandler(field, definition, abi, logger)
case 'stateFromEventTuple':
return new StateFromEventTupleHandler(field, definition, abi, logger)
case 'layerZeroMultisig':
return new LayerZeroMultisigHandler(field, abi, logger)
case 'arbitrumActors':
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { utils } from 'ethers'
export function toTopics(
abi: utils.Interface,
fragment: utils.EventFragment,
definitionTopics: (string | null)[] | undefined,
definitionTopics?: (string | null)[],
): (string | null)[] {
const topic0 = abi.getEventTopic(fragment)
const topics = definitionTopics
Expand Down

0 comments on commit 51214a2

Please sign in to comment.