diff --git a/packages/discovery/src/discovery/config/ConfigReader.ts b/packages/discovery/src/discovery/config/ConfigReader.ts index 4c6210f3..fae6cde2 100644 --- a/packages/discovery/src/discovery/config/ConfigReader.ts +++ b/packages/discovery/src/discovery/config/ConfigReader.ts @@ -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' @@ -119,4 +120,37 @@ export class ConfigReader { return projects } + + async readRawConfigWithComments( + name: string, + chain: string, + ): Promise { + 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 + } +} + +function assertDiscoveryConfig( + config: unknown, +): asserts config is RawDiscoveryConfig { + RawDiscoveryConfig.parse(config) } diff --git a/packages/discovery/src/discovery/config/MutableDiscoveryOverrides.ts b/packages/discovery/src/discovery/config/MutableDiscoveryOverrides.ts index 3e1dc343..4aec48f4 100644 --- a/packages/discovery/src/discovery/config/MutableDiscoveryOverrides.ts +++ b/packages/discovery/src/discovery/config/MutableDiscoveryOverrides.ts @@ -1,13 +1,19 @@ import { ContractParameters } from '@l2beat/discovery-types' +import { assign } from 'comment-json' import { EthereumAddress } from '../../utils/EthereumAddress' import { ContractOverrides, DiscoveryOverrides } from './DiscoveryOverrides' export type MutableOverride = Pick< ContractOverrides, - 'ignoreDiscovery' | 'ignoreInWatchMode' | 'ignoreMethods' + 'ignoreDiscovery' | 'ignoreInWatchMode' | 'ignoreMethods' | 'ignoreRelatives' > +/** + * In-place overrides map with intention to be mutable + * since it is easier to do that this way instead of modification squash + * @notice Re-assignments made via comments-json `assign` which supports both entries with comments (JSONC) and with out them. + */ export class MutableDiscoveryOverrides extends DiscoveryOverrides { public set(contract: ContractParameters, override: MutableOverride): void { const nameOrAddress = this.updateNameToAddress(contract) @@ -22,7 +28,9 @@ export class MutableDiscoveryOverrides extends DiscoveryOverrides { if (override.ignoreInWatchMode.length === 0) { delete originalOverride.ignoreInWatchMode } else { - originalOverride.ignoreInWatchMode = override.ignoreInWatchMode + assign(originalOverride, { + ignoreInWatchMode: override.ignoreInWatchMode, + }) } } @@ -30,7 +38,15 @@ export class MutableDiscoveryOverrides extends DiscoveryOverrides { if (override.ignoreMethods.length === 0) { delete originalOverride.ignoreMethods } else { - originalOverride.ignoreMethods = override.ignoreMethods + assign(originalOverride, { ignoreMethods: override.ignoreMethods }) + } + } + + if (override.ignoreRelatives !== undefined) { + if (override.ignoreRelatives.length === 0) { + delete originalOverride.ignoreRelatives + } else { + assign(originalOverride, { ignoreRelatives: override.ignoreRelatives }) } } @@ -38,15 +54,25 @@ export class MutableDiscoveryOverrides extends DiscoveryOverrides { if (!override.ignoreDiscovery) { delete originalOverride.ignoreDiscovery } else { - originalOverride.ignoreDiscovery = override.ignoreDiscovery + assign(originalOverride, { ignoreRelatives: override.ignoreRelatives }) } } + // Pre-set overrides if they are not set if (this.config.overrides === undefined) { this.config.overrides = {} } - this.config.overrides[identifier ?? nameOrAddress] = originalOverride + // Set override only if it is not empty + if (Object.keys(originalOverride).length > 0) { + assign(this.config.overrides, { + [identifier ?? nameOrAddress]: originalOverride, + }) + // Remove override if it is empty + } else { + // eslint-disable-next-line @typescript-eslint/no-dynamic-delete + delete this.config.overrides[identifier ?? nameOrAddress] + } } private getIdentifier( diff --git a/packages/discovery/src/discovery/interactive/InteractiveOverrides.ts b/packages/discovery/src/discovery/interactive/InteractiveOverrides.ts index 55e4bc04..1a20fbe5 100644 --- a/packages/discovery/src/discovery/interactive/InteractiveOverrides.ts +++ b/packages/discovery/src/discovery/interactive/InteractiveOverrides.ts @@ -12,29 +12,29 @@ export class InteractiveOverrides { async run(): Promise { console.log(chalk.blue.bold('### Interactive mode ###')) - for (;;) { const message = 'Options: ' const choices = [ { - name: 'Configure contract overrides', + name: '⚙️ Configure contract overrides', value: 'configure', }, - { name: 'Flush overrides', value: 'flush' }, + { name: '💾 Flush overrides & exit', value: 'flush' }, + { name: '🚪 Exit', value: 'exit' }, ] as const - const choice = await select({ message, choices, }) - if (choice === 'configure') { await this.configureContract() } - if (choice === 'flush') { await this.iom.flushOverrides() - break + return + } + if (choice === 'exit') { + return } } } @@ -70,14 +70,16 @@ export class InteractiveOverrides { name: `🔍 Watch Mode (${chalk.gray('ignoreInWatchMode')})`, value: 'watchmode', }, + { + name: `📝 Ignore relatives (${chalk.gray('ignoreRelatives')})`, + value: 'relatives', + }, { name: `⏭️ Ignore methods (${chalk.gray('ignoreMethods')})`, value: 'methods', }, { - name: `🛑 Ignore Discovery completely (${chalk.gray( - 'ignoreDiscovery', - )})`, + name: `🛑 Ignore discovery (${chalk.gray('ignoreDiscovery')})`, value: 'ignore', }, ] as const @@ -88,6 +90,10 @@ export class InteractiveOverrides { await this.configureWatchMode(contract) } + if (choice === 'relatives') { + await this.configureIgnoredRelatives(contract) + } + if (choice === 'methods') { await this.configureIgnoredMethods(contract) } @@ -116,7 +122,7 @@ export class InteractiveOverrides { })) if (choices.length === 0) { - noValuesWarning(true) + noValuesWarning({ withMethods: true }) return } @@ -130,16 +136,41 @@ export class InteractiveOverrides { this.iom.setOverride(contract, { ignoreInWatchMode }) } + async configureIgnoredRelatives(contract: ContractParameters): Promise { + const { possible, ignored } = this.iom.getIgnoredRelatives(contract) + + const message = 'Ignored relatives (values in ignoreMethods are excluded):' + + const choices = possible.map((property) => ({ + name: property, + value: property, + checked: ignored.includes(property), + })) + + if (choices.length === 0) { + noValuesWarning({ withMethods: true }) + return + } + + const ignoredRelatives = await checkbox({ + loop: false, + pageSize: InteractiveOverrides.MAX_PAGE_SIZE, + message, + choices, + }) + + this.iom.setOverride(contract, { ignoreRelatives: ignoredRelatives }) + } + async configureIgnoredMethods(contract: ContractParameters): Promise { - const ignoredMethods = this.iom.getIgnoredMethods(contract) + const { possible, ignored } = this.iom.getIgnoredMethods(contract) const message = 'Ignored methods: ' - const choices = ignoredMethods.all.map((property) => ({ + const choices = possible.map((property) => ({ name: property, value: property, - // Sync already present configuration - checked: ignoredMethods.ignored.includes(property), + checked: ignored.includes(property), })) if (choices.length === 0) { @@ -174,6 +205,7 @@ export class InteractiveOverrides { choices, }) + // Checkbox with only one value, yet array is returned const ignoreDiscovery = Boolean(choice.length > 0) this.iom.setOverride(contract, { ignoreDiscovery }) @@ -203,7 +235,7 @@ async function selectWithBack( const choicesWithBack = [ ...choices, { - name: 'Back', + name: '🏃 Back', value: 'back', } as const, ] @@ -218,13 +250,13 @@ async function selectWithBack( return answer } -function noValuesWarning(full?: boolean): void { +function noValuesWarning(opts?: { withMethods: boolean }): void { let msg = ` ⚠️ OOPS - no values to manage - check following cases: - Discovery is set to ignore this contract - Contract has no values discovered` - if (full) { + if (opts?.withMethods) { msg += '\n - All values are ignored via ignoreMethods' } diff --git a/packages/discovery/src/discovery/interactive/InteractiveOverridesManager.ts b/packages/discovery/src/discovery/interactive/InteractiveOverridesManager.ts index dc9cbb31..01619ec3 100644 --- a/packages/discovery/src/discovery/interactive/InteractiveOverridesManager.ts +++ b/packages/discovery/src/discovery/interactive/InteractiveOverridesManager.ts @@ -1,27 +1,29 @@ -import { assert } from '@l2beat/backend-tools' import { ContractParameters, DiscoveryOutput } from '@l2beat/discovery-types' -import { parse, stringify } from 'comment-json' +import { assign, parse, stringify } from 'comment-json' import * as fs from 'fs/promises' -import { DiscoveryConfig } from '../config/DiscoveryConfig' import { ContractOverrides } from '../config/DiscoveryOverrides' import { MutableDiscoveryOverrides, MutableOverride, } from '../config/MutableDiscoveryOverrides' -import { - DiscoveryContract, - RawDiscoveryConfig, -} from '../config/RawDiscoveryConfig' +import { RawDiscoveryConfig } from '../config/RawDiscoveryConfig' + +interface IgnoreResult { + possible: string[] + ignored: string[] +} export class InteractiveOverridesManager { private readonly mutableOverrides: MutableDiscoveryOverrides constructor( private readonly output: DiscoveryOutput, - private readonly config: DiscoveryConfig, + private readonly rawConfigWithComments: RawDiscoveryConfig, ) { - this.mutableOverrides = new MutableDiscoveryOverrides(this.config.raw) + this.mutableOverrides = new MutableDiscoveryOverrides( + this.rawConfigWithComments, + ) } getContracts(): ContractParameters[] { @@ -50,7 +52,7 @@ export class InteractiveOverridesManager { // All discovered keys + look ahead for all ignored methods const possibleMethods = [ - ...new Set([...allProperties, ...ignoredMethods.all]), + ...new Set([...allProperties, ...ignoredMethods.possible]), ] .filter((method) => !this.isCustomHandler(contract, method)) .filter((method) => !ignoredMethods.ignored.includes(method)) @@ -61,15 +63,42 @@ export class InteractiveOverridesManager { } } - getIgnoredMethods(contract: ContractParameters): { - all: string[] - ignored: string[] - } { + getIgnoredRelatives(contract: ContractParameters): IgnoreResult { const isDiscoveryIgnored = this.getIgnoreDiscovery(contract) + const ignoredMethods = this.getIgnoredMethods(contract) if (isDiscoveryIgnored) { return { - all: [], + possible: [], + ignored: [], + } + } + + const overrides = this.getSafeOverride(contract) + + const allProperties = Object.keys(contract.values ?? {}) + + const ignoredRelatives = overrides?.ignoreRelatives ?? [] + + // All discovered keys + look ahead for all ignored methods + const possibleMethods = [ + ...new Set([...allProperties, ...ignoredMethods.possible]), + ] + .filter((method) => !this.isCustomHandler(contract, method)) + .filter((method) => !ignoredMethods.ignored.includes(method)) + + return { + possible: possibleMethods, + ignored: ignoredRelatives, + } + } + + getIgnoredMethods(contract: ContractParameters): IgnoreResult { + const isDiscoveryIgnored = this.getIgnoreDiscovery(contract) + + if (isDiscoveryIgnored) { + return { + possible: [], ignored: [], } } @@ -84,13 +113,9 @@ export class InteractiveOverridesManager { ...new Set([...allProperties, ...ignoredMethods]), ].filter((method) => !this.isCustomHandler(contract, method)) - const ignored = possibleMethods.filter((method) => - ignoredMethods.includes(method), - ) - return { - all: possibleMethods, - ignored, + possible: possibleMethods, + ignored: ignoredMethods, } } @@ -107,6 +132,7 @@ export class InteractiveOverridesManager { 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) { @@ -114,17 +140,23 @@ export class InteractiveOverridesManager { ignoreDiscovery: true, ignoreInWatchMode: [], ignoreMethods: [], + ignoreRelatives: [], }) return } - // Exclude ignoreMethods from watch mode completely + // Exclude ignoreMethods from watch mode and relatives completely const validWatchMode = ignoredInWatchMode.ignored.filter( (method) => !ignoredMethods.ignored.includes(method), ) + const validRelatives = ignoredRelatives.ignored.filter( + (method) => !ignoredMethods.ignored.includes(method), + ) + this.mutableOverrides.set(contract, { ignoreInWatchMode: validWatchMode, + ignoreRelatives: validRelatives, }) } @@ -138,31 +170,17 @@ export class InteractiveOverridesManager { const parsed = parse(fileContents) as RawDiscoveryConfig | null - assert(parsed, 'Cannot parse file') - if (this.mutableOverrides.config.overrides) { - parsed.overrides = this.stripEmptyOverrides( - this.mutableOverrides.config.overrides, - ) + assign(parsed, { overrides: this.mutableOverrides.config.overrides }) } if (this.mutableOverrides.config.names) { - parsed.names = this.mutableOverrides.config.names + assign(parsed, { names: this.mutableOverrides.config.names }) } await fs.writeFile(path, stringify(parsed, null, 2)) } - private stripEmptyOverrides( - overrides: Record, - ): Record { - return Object.fromEntries( - Object.entries(overrides).filter( - ([_, override]) => Object.keys(override).length > 0, - ), - ) - } - private getOverrideIdentity(contract: ContractParameters): string { const hasName = Boolean( this.mutableOverrides.config.names?.[contract.address.toString()], diff --git a/packages/discovery/src/discovery/runDiscovery.ts b/packages/discovery/src/discovery/runDiscovery.ts index a568e509..f798a32c 100644 --- a/packages/discovery/src/discovery/runDiscovery.ts +++ b/packages/discovery/src/discovery/runDiscovery.ts @@ -67,7 +67,7 @@ export async function dryRunDiscovery( const BLOCKS_PER_DAY = 86400 / 12 const blockNumberYesterday = blockNumber - BLOCKS_PER_DAY - const projectConfig = await configReader.readConfig( + const rawConfigWitComments = await configReader.readRawConfigWithComments( config.project, config.chain, ) @@ -104,7 +104,10 @@ export async function dryRunDiscovery( } if (config.interactive) { - const iom = new InteractiveOverridesManager(discovered, projectConfig) + const iom = new InteractiveOverridesManager( + discovered, + rawConfigWitComments, + ) const interactiveOverrides = new InteractiveOverrides(iom) await interactiveOverrides.run() }