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

Interactive discovery #123

Open
wants to merge 14 commits into
base: main
Choose a base branch
from
5 changes: 5 additions & 0 deletions packages/discovery/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,16 @@
"typecheck": "tsc --noEmit"
},
"dependencies": {
"@inquirer/checkbox": "^1.5.0",
"@inquirer/select": "^1.3.1",
"@l2beat/backend-tools": "^0.5.1",
"@l2beat/discovery-types": "^0.8.0",
"chalk": "^4.1.2",
"comment-json": "^4.2.3",
Copy link
Contributor

Choose a reason for hiding this comment

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

We already import jsonc-parser, either don't use comment-json or replace jsonc-parser with it

"deep-diff": "^1.0.2",
"dotenv": "^16.0.3",
"ethers": "^5.7.2",
"inquirer": "^8.0.0",
"jsonc-parser": "^3.2.0",
"lodash": "^4.17.21",
"mkdirp": "^3.0.0",
Expand All @@ -36,6 +40,7 @@
},
"devDependencies": {
"@types/deep-diff": "^1.0.2",
"@types/inquirer": "^9.0.7",
"@types/lodash": "^4.14.198",
"@types/node-fetch": "^2.6.4"
}
Expand Down
1 change: 1 addition & 0 deletions packages/discovery/src/cli/discoverCommand.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ export function discover(
project: config.project,
chain: config.chain,
dryRun: config.dryRun === true,
interactive: config.interactive === true,
dev: config.dev === true,
sourcesFolder: config.sourcesFolder,
discoveryFilename: config.discoveryFilename,
Expand Down
7 changes: 6 additions & 1 deletion packages/discovery/src/cli/getCliParameters.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ describe(getCliParameters.name, () => {
chain: 'ethereum',
project: 'foo',
dryRun: false,
interactive: false,
dev: false,
sourcesFolder: undefined,
discoveryFilename: undefined,
Expand All @@ -49,6 +50,7 @@ describe(getCliParameters.name, () => {
chain: 'ethereum',
project: 'foo',
dryRun: true,
interactive: false,
dev: false,
sourcesFolder: undefined,
discoveryFilename: undefined,
Expand All @@ -70,6 +72,7 @@ describe(getCliParameters.name, () => {
chain: 'ethereum',
project: 'foo',
dryRun: false,
interactive: false,
dev: true,
sourcesFolder: '.code@1234',
discoveryFilename: 'discovery@1234',
Expand All @@ -87,19 +90,21 @@ describe(getCliParameters.name, () => {
expect(cli).toEqual({ mode: 'help', error: 'Too many arguments' })
})

it('discover ethereum --block-number=5678 foo --dry-run', () => {
it('discover ethereum --block-number=5678 foo --dry-run --interactive', () => {
const cli = getCliParameters([
'discover',
'ethereum',
'--block-number=5678',
'foo',
'--dry-run',
'--interactive',
])
expect(cli).toEqual({
mode: 'discover',
chain: 'ethereum',
project: 'foo',
dryRun: true,
interactive: true,
dev: false,
sourcesFolder: undefined,
discoveryFilename: undefined,
Expand Down
8 changes: 8 additions & 0 deletions packages/discovery/src/cli/getCliParameters.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ export interface DiscoverCliParameters {
project: string
chain: string
dryRun: boolean
interactive: boolean
dev: boolean
sourcesFolder?: string
discoveryFilename?: string
Expand Down Expand Up @@ -63,6 +64,7 @@ export function getCliParameters(args = process.argv.slice(2)): CliParameters {

let dryRun = false
let dev = false
let interactive = false
let blockNumber: number | undefined
let sourcesFolder: string | undefined
let discoveryFilename: string | undefined
Expand All @@ -72,6 +74,11 @@ export function getCliParameters(args = process.argv.slice(2)): CliParameters {
remaining.splice(remaining.indexOf('--dry-run'), 1)
}

if (remaining.includes('--interactive')) {
interactive = true
remaining.splice(remaining.indexOf('--interactive'), 1)
}

if (remaining.includes('--dev')) {
dev = true
remaining.splice(remaining.indexOf('--dev'), 1)
Expand Down Expand Up @@ -125,6 +132,7 @@ export function getCliParameters(args = process.argv.slice(2)): CliParameters {
mode: 'discover',
chain,
project,
interactive,
dryRun,
dev,
sourcesFolder,
Expand Down
1 change: 1 addition & 0 deletions packages/discovery/src/config/config.discovery.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ export function getDiscoveryCliConfig(cli: CliParameters): DiscoveryCliConfig {
project: cli.project,
chain: cli.chain,
dryRun: cli.dryRun,
interactive: cli.interactive,
dev: cli.dev,
blockNumber: cli.blockNumber,
getLogsMaxRange: chain.rpcGetLogsMaxRange,
Expand Down
1 change: 1 addition & 0 deletions packages/discovery/src/config/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ export interface DiscoveryModuleConfig {
readonly project: string
readonly chain: string
readonly dryRun?: boolean
readonly interactive?: boolean
readonly dev?: boolean
readonly blockNumber?: number
readonly getLogsMaxRange?: number
Expand Down
34 changes: 34 additions & 0 deletions packages/discovery/src/discovery/config/ConfigReader.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { assert } from '@l2beat/backend-tools'
import { DiscoveryOutput } from '@l2beat/discovery-types'
import { parse as parseWithComments } from 'comment-json'
import { readdirSync } from 'fs'
import { readFile } from 'fs/promises'
import { parse, ParseError } from 'jsonc-parser'
Expand Down Expand Up @@ -119,4 +120,37 @@ export class ConfigReader {

return projects
}

async readRawConfigWithComments(
name: string,
chain: string,
): Promise<RawDiscoveryConfig> {
assert(
fileExistsCaseSensitive(`discovery/${name}`),
'Project not found, check if case matches',
)
assert(
fileExistsCaseSensitive(`discovery/${name}/${chain}`),
'Chain not found in project, check if case matches',
)

const contents = await readFile(
`discovery/${name}/${chain}/config.jsonc`,
'utf-8',
)
const parsed: unknown = parseWithComments(contents)

// Parsing via Zod would effectively remove symbols and thus comments
assertDiscoveryConfig(parsed)

assert(parsed.chain === chain, 'Chain mismatch in config.jsonc')

return parsed
}
}
Comment on lines +123 to +150
Copy link
Contributor

Choose a reason for hiding this comment

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

There is duplication of reading the config in here now. We should extract the common part, just call readRawConfigWithComments in readConfig and drop the comments there.


function assertDiscoveryConfig(
config: unknown,
): asserts config is RawDiscoveryConfig {
RawDiscoveryConfig.parse(config)
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ export type ContractOverrides = DiscoveryContract & {
}

export class DiscoveryOverrides {
private readonly nameToAddress = new Map<string, EthereumAddress>()
protected readonly nameToAddress = new Map<string, EthereumAddress>()

constructor(
public readonly config: Pick<RawDiscoveryConfig, 'names' | 'overrides'>,
Expand Down Expand Up @@ -43,7 +43,6 @@ export class DiscoveryOverrides {

*[Symbol.iterator](): IterableIterator<ContractOverrides> {
for (const key of Object.keys(this.config.overrides ?? {})) {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
yield this.get(key)
}
}
Expand Down
153 changes: 153 additions & 0 deletions packages/discovery/src/discovery/config/DiscoveryOverridesBuilder.ts
Copy link
Contributor

Choose a reason for hiding this comment

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

Can be easily tested

Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
import { ContractParameters, DiscoveryOutput } from '@l2beat/discovery-types'
import { assign, parse, stringify } from 'comment-json'
import * as fs from 'fs/promises'

import { ContractOverrides } from './DiscoveryOverrides'
import {
MutableDiscoveryOverrides,
MutableOverride,
} from './MutableDiscoveryOverrides'
import { RawDiscoveryConfig } from './RawDiscoveryConfig'

export class DiscoveryOverridesBuilder {
private readonly mutableOverrides: MutableDiscoveryOverrides

constructor(
private readonly output: DiscoveryOutput,
private readonly rawConfigWithComments: RawDiscoveryConfig,
) {
this.mutableOverrides = new MutableDiscoveryOverrides(
this.rawConfigWithComments,
)
}

getContracts(): ContractParameters[] {
return [...this.output.contracts]
Copy link
Contributor

Choose a reason for hiding this comment

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

Why do we need to spread it and later create a new array if the .contracts already is an array

}

getWatchMode(contract: ContractParameters): string[] {
const isDiscoveryIgnored = this.getIgnoreDiscovery(contract)

if (isDiscoveryIgnored) {
return []
}

const overrides = this.getSafeOverride(contract)

const ignoredInWatchMode = overrides?.ignoreInWatchMode ?? []

return ignoredInWatchMode
}

getIgnoredRelatives(contract: ContractParameters): string[] {
const overrides = this.getSafeOverride(contract)

const ignoredRelatives = overrides?.ignoreRelatives ?? []

return ignoredRelatives
}

getIgnoredMethods(contract: ContractParameters): string[] {
const overrides = this.getSafeOverride(contract)
const ignoredMethods = overrides?.ignoreMethods ?? []

return ignoredMethods
}

getIgnoreDiscovery(contract: ContractParameters): boolean {
const overrides = this.getSafeOverride(contract)

return overrides?.ignoreDiscovery ?? false
}

setOverride(contract: ContractParameters, override: MutableOverride): void {
// Optimistically set overrides
this.mutableOverrides.set(contract, override)

const isDiscoveryIgnored = this.getIgnoreDiscovery(contract)
const ignoredInWatchMode = this.getWatchMode(contract)
const ignoredMethods = this.getIgnoredMethods(contract)
const ignoredRelatives = this.getIgnoredRelatives(contract)

// Wipe all overrides if discovery is ignored
if (isDiscoveryIgnored) {
this.mutableOverrides.set(contract, {
ignoreDiscovery: true,
ignoreInWatchMode: [],
ignoreMethods: [],
ignoreRelatives: [],
})
return
}

// Exclude ignoreMethods from watch mode and relatives completely
const validWatchMode = ignoredInWatchMode.filter(
(method) => !ignoredMethods.includes(method),
)

const validRelatives = ignoredRelatives.filter(
(method) => !ignoredMethods.includes(method),
)

this.mutableOverrides.set(contract, {
ignoreInWatchMode: validWatchMode,
ignoreRelatives: validRelatives,
})
}

/**
* Do not replace whole file, just read most-recent raw, replace and save overrides
*/
async flushOverrides(): Promise<void> {
const path = `discovery/${this.output.name}/${this.output.chain}/config.jsonc`
Copy link
Contributor

Choose a reason for hiding this comment

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

Why are we not using ConfigReader here? We implemented reading a config with comments there and just casually reimplemented this inline here


const fileContents = await fs.readFile(path, 'utf8')

const parsed = parse(fileContents) as RawDiscoveryConfig | null

if (this.mutableOverrides.config.overrides) {
assign(parsed, { overrides: this.mutableOverrides.config.overrides })
}

if (this.mutableOverrides.config.names) {
assign(parsed, { names: this.mutableOverrides.config.names })
}

await fs.writeFile(path, stringify(parsed, null, 2))
}

private getOverrideIdentity(contract: ContractParameters): string {
const hasName = Boolean(
this.mutableOverrides.config.names?.[contract.address.toString()],
)

if (hasName) {
return contract.name
}

return contract.address.toString()
}

private getSafeOverride(
contract: ContractParameters,
): ContractOverrides | null {
const addressOrName = this.getOverrideIdentity(contract)

try {
return this.mutableOverrides.get(addressOrName)
} catch {
return null
}
}

public isCustomHandler(
contract: ContractParameters,
property: string,
): boolean {
const addressOrName = this.getOverrideIdentity(contract)

return Object.keys(
this.mutableOverrides.get(addressOrName).fields ?? {},
).includes(property)
}
}
Loading
Loading