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

Add the ability to pick out struct fields from call response #169

Merged
merged 5 commits into from
Apr 5, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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.46.7

### Patch Changes

- Allow to pick fields from struct returns in CallHandler

## 0.46.6

### 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.46.6",
"version": "0.46.7",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"bin": {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ export const ArrayHandlerDefinition = z.strictObject({
length: z.optional(z.union([z.number().int().nonnegative(), Reference])),
maxLength: z.optional(z.number().int().nonnegative()),
startIndex: z.optional(z.number().int().nonnegative()),
filter: z.optional(z.array(z.union([z.string(), z.number()]))),
pickFields: z.optional(z.array(z.union([z.string(), z.number()]))),
ignoreRelative: z.optional(z.boolean()),
})

Expand Down Expand Up @@ -74,7 +74,7 @@ export class ArrayHandler implements ClassicHandler {
address,
this.fragment,
blockNumber,
this.definition.filter,
this.definition.pickFields,
)
if (resolved.indices) {
const results = await Promise.all(
Expand Down Expand Up @@ -136,7 +136,7 @@ function createCallIndex(
address: EthereumAddress,
fragment: utils.FunctionFragment,
blockNumber: number,
filter?: (string | number)[],
pickFields?: (string | number)[],
) {
return async (index: number) => {
return await callMethod(
Expand All @@ -145,7 +145,7 @@ function createCallIndex(
fragment,
[index],
blockNumber,
filter,
pickFields,
)
}
}
Expand Down
2 changes: 2 additions & 0 deletions packages/discovery/src/discovery/handlers/user/CallHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ export const CallHandlerDefinition = z.strictObject({
method: z.optional(z.string()),
args: z.array(z.union([z.string(), z.number()])),
ignoreRelative: z.optional(z.boolean()),
pickFields: z.optional(z.array(z.union([z.string(), z.number()]))),
expectRevert: z.optional(z.boolean()),
})

Expand Down Expand Up @@ -67,6 +68,7 @@ export class CallHandler implements ClassicHandler {
this.fragment,
resolved.args,
blockNumber,
this.definition.pickFields,
)

if (this.definition.expectRevert && callResult.error === EXEC_REVERT_MSG) {
Expand Down
196 changes: 196 additions & 0 deletions packages/discovery/src/discovery/handlers/utils/callMethod.test.ts
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

image

Original file line number Diff line number Diff line change
@@ -0,0 +1,196 @@
import { expect, mockObject } from 'earl'
import { utils } from 'ethers'

import { Bytes } from '../../../utils/Bytes'
import { EthereumAddress } from '../../../utils/EthereumAddress'
import { DiscoveryProvider } from '../../provider/DiscoveryProvider'
import { callMethod } from './callMethod'

describe('callMethod', () => {
const ADDRESS = EthereumAddress.random()
const BLOCK_NUMBER = 1234
const encoder = utils.defaultAbiCoder

it('decodes struct returns', async () => {
const RESULT_VALUE = EthereumAddress.random().toString()

const abi = new utils.Interface([
'function testFunction() view returns (tuple(address r1, uint64 r2, address r3, uint64 r4))',
])

const provider = mockObject<DiscoveryProvider>({
call: async () =>
Bytes.fromHex(
encoder.encode(
['tuple(address r1, uint64 r2, address r3, uint64 r4)'],
[[RESULT_VALUE, 1234, RESULT_VALUE, 5678]],
),
),
})

const result = await callMethod(
provider,
ADDRESS,
abi.getFunction('testFunction'),
[],
BLOCK_NUMBER,
['r1', 'r4'],
)

expect(result.value).toEqual([RESULT_VALUE, 5678])
})

it('picks from multiple return values', async () => {
const RESULT_VALUE = EthereumAddress.random().toString()

const abi = new utils.Interface([
'function testFunction() view returns (address r1, uint64 r2, address r3, uint64 r4)',
])

const provider = mockObject<DiscoveryProvider>({
call: async () =>
Bytes.fromHex(
encoder.encode(
['address r1', 'uint64 r2', 'address r3', 'uint64 r4'],
[RESULT_VALUE, 1234, RESULT_VALUE, 5678],
),
),
})

const result = await callMethod(
provider,
ADDRESS,
abi.getFunction('testFunction'),
[],
BLOCK_NUMBER,
['r1', 'r4'],
)

expect(result.value).toEqual([RESULT_VALUE, 5678])
})

it('decodes multiple return values', async () => {
const RESULT_VALUE = EthereumAddress.random().toString()

const abi = new utils.Interface([
'function testFunction() view returns (address r1, uint64 r2, address r3, uint64 r4)',
])

const provider = mockObject<DiscoveryProvider>({
call: async () =>
Bytes.fromHex(
encoder.encode(
['address r1', 'uint64 r2', 'address r3', 'uint64 r4'],
[RESULT_VALUE, 1234, RESULT_VALUE, 5678],
),
),
})

const result = await callMethod(
provider,
ADDRESS,
abi.getFunction('testFunction'),
[],
BLOCK_NUMBER,
)

expect(result.value).toEqual([RESULT_VALUE, 1234, RESULT_VALUE, 5678])
})

it('picks from array return value', async () => {
const RESULT_VALUES = [
EthereumAddress.random().toString(),
EthereumAddress.random().toString(),
]

const abi = new utils.Interface([
'function testFunction() view returns (address[])',
])

const provider = mockObject<DiscoveryProvider>({
call: async () =>
Bytes.fromHex(encoder.encode(['address[]'], [RESULT_VALUES])),
})

const result = await callMethod(
provider,
ADDRESS,
abi.getFunction('testFunction'),
[],
BLOCK_NUMBER,
[1],
)

expect(result.value).toEqual(RESULT_VALUES[1])
})

it('decodes an array return value', async () => {
const RESULT_VALUES = [
EthereumAddress.random().toString(),
EthereumAddress.random().toString(),
]

const abi = new utils.Interface([
'function testFunction() view returns (address[])',
])

const provider = mockObject<DiscoveryProvider>({
call: async () =>
Bytes.fromHex(encoder.encode(['address[]'], [RESULT_VALUES])),
})

const result = await callMethod(
provider,
ADDRESS,
abi.getFunction('testFunction'),
[],
BLOCK_NUMBER,
)

expect(result.value).toEqual(RESULT_VALUES)
})

it('throws on trying to pick from scalar return value', async () => {
const abi = new utils.Interface([
'function testFunction() view returns (uint256)',
])

const provider = mockObject<DiscoveryProvider>({
call: async () => Bytes.randomOfLength(32),
})

const result = await callMethod(
provider,
ADDRESS,
abi.getFunction('testFunction'),
[],
BLOCK_NUMBER,
['field'],
)

expect(result.error).toEqual(
'Cannot pick fields from a non-struct-like return value',
)
})

it('decodes a scalar return value', async () => {
const RETURN_VALUE = EthereumAddress.random()
const abi = new utils.Interface([
'function testFunction() view returns (address)',
])

const provider = mockObject<DiscoveryProvider>({
call: async () => Bytes.fromHex(RETURN_VALUE.toString()).padStart(32),
})

const result = await callMethod(
provider,
ADDRESS,
abi.getFunction('testFunction'),
[],
BLOCK_NUMBER,
)

expect(result.value).toEqual(RETURN_VALUE.toString())
})
})
21 changes: 15 additions & 6 deletions packages/discovery/src/discovery/handlers/utils/callMethod.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ export async function callMethod(
fragment: utils.FunctionFragment,
parameters: unknown[],
blockNumber: number,
filter?: (string | number)[],
pickFields?: (string | number)[],
) {
const abi = new utils.Interface([fragment])

Expand All @@ -25,7 +25,7 @@ export async function callMethod(
const result = await provider.call(address, callData, blockNumber)

return {
value: decodeMethodResult(abi, fragment, result, filter),
value: decodeMethodResult(abi, fragment, result, pickFields),
}
} catch (e) {
return {
Expand All @@ -39,11 +39,20 @@ export function decodeMethodResult(
abi: utils.Interface,
fragment: utils.FunctionFragment,
result: Bytes,
filter?: (string | number)[],
pickFields?: (string | number)[],
) {
const decoded = abi.decodeFunctionResult(fragment, result.toString())
const filtered = filter
? filter.map((i) => decoded[i] as utils.Result)
let decoded = abi.decodeFunctionResult(fragment, result.toString())

if (decoded.length === 1 && Array.isArray(decoded[0])) {
decoded = decoded[0]
}

if (decoded.length === 1 && pickFields !== undefined) {
throw new Error('Cannot pick fields from a non-struct-like return value')
}

const filtered = pickFields
? pickFields.map((i) => decoded[i] as utils.Result)
: decoded
const mapped = filtered.map(toContractValue)
return mapped.length === 1 ? mapped[0] : mapped
Expand Down
Loading