This repository has been archived by the owner on Apr 26, 2024. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add state from event tuple handler (#121)
* 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
1 parent
2354d3b
commit 51214a2
Showing
6 changed files
with
343 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
200 changes: 200 additions & 0 deletions
200
packages/discovery/src/discovery/handlers/user/StateFromEventTupleHandler.test.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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, | ||
}) | ||
}) | ||
}) | ||
}) |
128 changes: 128 additions & 0 deletions
128
packages/discovery/src/discovery/handlers/user/StateFromEventTupleHandler.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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, | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters