From acc9db9926e9957cf4d97b94890e98f23f2a9e59 Mon Sep 17 00:00:00 2001 From: Vagabond Date: Wed, 7 Oct 2026 01:56:41 +0700 Subject: [PATCH 1/2] feat(cli): add shared diamond introspection adapter --- cli/src/adapters/IDiamondAdapter/abi.ts | 37 +++++++++ cli/src/adapters/IDiamondAdapter/adapter.ts | 35 ++++++++ cli/src/adapters/IDiamondAdapter/interface.ts | 20 +++++ cli/src/modules/bytecodeValidation/module.ts | 19 ++--- cli/src/modules/inspect/diamondInspectAbi.ts | 22 ----- cli/src/modules/inspect/module.ts | 70 ++++++---------- .../pipelines/bytecodeValidationPipeline.ts | 13 ++- cli/src/pipelines/inspectPipeline.ts | 16 +++- cli/src/resolver/dependencyKey.ts | 1 + cli/src/resolver/dependencyRegistry.ts | 5 ++ .../adapters/IDiamondAdapter/adapter.test.ts | 82 +++++++++++++++++++ .../modules/bytecodeValidation/module.test.ts | 44 +++++----- cli/test/modules/inspect/module.test.ts | 55 +++++-------- .../inspectPipeline/inspectPipeline.test.ts | 17 +++- .../validatePipelineVSL.test.ts | 6 ++ cli/test/resolver/dependencyResolver.test.ts | 6 ++ 16 files changed, 301 insertions(+), 147 deletions(-) create mode 100644 cli/src/adapters/IDiamondAdapter/abi.ts create mode 100644 cli/src/adapters/IDiamondAdapter/adapter.ts create mode 100644 cli/src/adapters/IDiamondAdapter/interface.ts delete mode 100644 cli/src/modules/inspect/diamondInspectAbi.ts create mode 100644 cli/test/adapters/IDiamondAdapter/adapter.test.ts diff --git a/cli/src/adapters/IDiamondAdapter/abi.ts b/cli/src/adapters/IDiamondAdapter/abi.ts new file mode 100644 index 00000000..dedb3dc7 --- /dev/null +++ b/cli/src/adapters/IDiamondAdapter/abi.ts @@ -0,0 +1,37 @@ +/** The four introspection functions shared by ERC-8153 and ERC-2535 diamonds. */ +export const DIAMOND_INSPECT_ABI = [ + { + name: "facets", + type: "function", + stateMutability: "view", + inputs: [], + outputs: [{ + type: "tuple[]", + components: [ + { name: "facet", type: "address" }, + { name: "functionSelectors", type: "bytes4[]" }, + ], + }], + }, + { + name: "facetAddresses", + type: "function", + stateMutability: "view", + inputs: [], + outputs: [{ type: "address[]" }], + }, + { + name: "facetFunctionSelectors", + type: "function", + stateMutability: "view", + inputs: [{ name: "facet", type: "address" }], + outputs: [{ type: "bytes4[]" }], + }, + { + name: "facetAddress", + type: "function", + stateMutability: "view", + inputs: [{ name: "selector", type: "bytes4" }], + outputs: [{ type: "address" }], + }, +] as const; diff --git a/cli/src/adapters/IDiamondAdapter/adapter.ts b/cli/src/adapters/IDiamondAdapter/adapter.ts new file mode 100644 index 00000000..d701ab8f --- /dev/null +++ b/cli/src/adapters/IDiamondAdapter/adapter.ts @@ -0,0 +1,35 @@ +import type { IRPCAdapter } from "../IRPCAdapter/interface"; +import { DIAMOND_INSPECT_ABI } from "./abi"; +import type { IDiamondAdapter } from "./interface"; + +/** Adapt generic RPC reads to the common diamond introspection interface. */ +export function createDiamondAdapter(rpc: IRPCAdapter): IDiamondAdapter { + return { + facets: (diamond, options) => rpc.readContract({ + address: diamond, + abi: DIAMOND_INSPECT_ABI, + functionName: "facets", + blockNumber: options?.blockNumber, + }, { verifyCode: true }), + facetAddresses: (diamond, options) => rpc.readContract({ + address: diamond, + abi: DIAMOND_INSPECT_ABI, + functionName: "facetAddresses", + blockNumber: options?.blockNumber, + }, { verifyCode: true }), + facetFunctionSelectors: (diamond, facet, options) => rpc.readContract({ + address: diamond, + abi: DIAMOND_INSPECT_ABI, + functionName: "facetFunctionSelectors", + args: [facet], + blockNumber: options?.blockNumber, + }, { verifyCode: true }), + facetAddress: (diamond, selector, options) => rpc.readContract({ + address: diamond, + abi: DIAMOND_INSPECT_ABI, + functionName: "facetAddress", + args: [selector], + blockNumber: options?.blockNumber, + }, { verifyCode: true }), + }; +} diff --git a/cli/src/adapters/IDiamondAdapter/interface.ts b/cli/src/adapters/IDiamondAdapter/interface.ts new file mode 100644 index 00000000..63ea9d17 --- /dev/null +++ b/cli/src/adapters/IDiamondAdapter/interface.ts @@ -0,0 +1,20 @@ +import type { Address, Hex } from "viem"; + +/** An entry returned by the shared ERC-8153/ERC-2535 diamond introspection ABI. */ +export type DiamondFacet = { + facet: Address; + functionSelectors: Hex[]; +}; + +/** Pin related introspection reads to the same chain state when needed. */ +export type DiamondReadOptions = { + blockNumber?: bigint; +}; + +/** Read-only diamond introspection boundary. */ +export interface IDiamondAdapter { + facets(diamond: Address, options?: DiamondReadOptions): Promise; + facetAddresses(diamond: Address, options?: DiamondReadOptions): Promise; + facetFunctionSelectors(diamond: Address, facet: Address, options?: DiamondReadOptions): Promise; + facetAddress(diamond: Address, selector: Hex, options?: DiamondReadOptions): Promise
; +} diff --git a/cli/src/modules/bytecodeValidation/module.ts b/cli/src/modules/bytecodeValidation/module.ts index 4c2b0667..efdff41c 100644 --- a/cli/src/modules/bytecodeValidation/module.ts +++ b/cli/src/modules/bytecodeValidation/module.ts @@ -1,11 +1,11 @@ -import { getAddress, type Address } from "viem"; +import { getAddress } from "viem"; import type { BytecodeValidationReport, IBytecodeValidatorAdapter, } from "../../adapters/IBytecodeValidatorAdapter/interface"; import type { IRPCAdapter } from "../../adapters/IRPCAdapter/interface"; +import type { IDiamondAdapter } from "../../adapters/IDiamondAdapter/interface"; import type { ComposeContext, ModuleState } from "../../context/types"; -import { DIAMOND_INSPECT_ABI } from "../inspect/diamondInspectAbi"; import type { VirtualStorageLayoutRecord } from "../validation/types"; import type { BytecodeValidationSummary, @@ -14,14 +14,10 @@ import type { type DeploymentDependencies = { rpc: IRPCAdapter; + diamond: IDiamondAdapter; validator: IBytecodeValidatorAdapter; }; -type InspectedFacet = { - facet: Address; - functionSelectors: `0x${string}`[]; -}; - function storageRecords(ctx: ComposeContext): VirtualStorageLayoutRecord[] { return (ctx.param.virtualStorageRecords as VirtualStorageLayoutRecord[] | undefined) ?? []; } @@ -80,14 +76,9 @@ export const BytecodeValidationModule = { const chainKey = String(ctx.param.chainKey); const diamondAddress = getAddress(String(ctx.param.diamondAddress)); const blockNumber = await dependencies.rpc.getBlockNumber(); - let rawFacets: InspectedFacet[]; + let rawFacets; try { - rawFacets = await dependencies.rpc.readContract({ - address: diamondAddress, - abi: DIAMOND_INSPECT_ABI, - functionName: "facets", - blockNumber, - }, { verifyCode: true }); + rawFacets = await dependencies.diamond.facets(diamondAddress, { blockNumber }); } catch (error) { const message = error instanceof Error ? error.message : "Diamond introspection failed."; ctx.state.bytecodeDeploymentValidation = { diff --git a/cli/src/modules/inspect/diamondInspectAbi.ts b/cli/src/modules/inspect/diamondInspectAbi.ts deleted file mode 100644 index 5a514a7a..00000000 --- a/cli/src/modules/inspect/diamondInspectAbi.ts +++ /dev/null @@ -1,22 +0,0 @@ -/** - * ABI for the Diamond `facets()` introspection function. - * - * ERC-8153 and ERC-2535 expose the same response shape at this boundary. - */ -export const DIAMOND_INSPECT_ABI = [ - { - name: "facets", - type: "function", - stateMutability: "view", - inputs: [], - outputs: [ - { - type: "tuple[]", - components: [ - { name: "facet", type: "address" }, - { name: "functionSelectors", type: "bytes4[]" }, - ], - }, - ], - }, -] as const; diff --git a/cli/src/modules/inspect/module.ts b/cli/src/modules/inspect/module.ts index 73517c5e..962d075f 100644 --- a/cli/src/modules/inspect/module.ts +++ b/cli/src/modules/inspect/module.ts @@ -1,68 +1,46 @@ import path from "node:path"; -import { isAddress, type Address, type Hex } from "viem"; +import { isAddress, type Address } from "viem"; import { ComposeContext } from "../../context/types"; -import { DependencyKey } from "../../resolver/dependencyKey"; -import { DependencyResolver } from "../../resolver/dependencyResolver"; -import { resolveChainConfig } from "../../utils/chainConfig"; +import type { IDiamondAdapter } from "../../adapters/IDiamondAdapter/interface"; +import type { ResolvedChainConfig } from "../../utils/chainConfig"; import { findFileAncestor } from "../../utils/files"; import { RPCAdapterError } from "../../adapters/IRPCAdapter/errors"; import { showInspect } from "./output"; -import { DIAMOND_INSPECT_ABI } from "./diamondInspectAbi"; import { toFacetInfo } from "./facetFormatter"; import { mergeProjectSignatures } from "./selectorDecoder"; import type { InspectResult, FacetInfo } from "./types"; +/** Validate the command address before resolving chain dependencies. */ +export function inspectAddress(value: unknown): Address { + if (typeof value !== "string" || !isAddress(value, { strict: false })) { + throw new RPCAdapterError( + "RPC_INVALID_ADDRESS", + `Invalid diamond address: ${String(value ?? "")}`, + { operation: "inspect" }, + ); + } + return value as Address; +} + export const InspectModule = { /** * Inspects an on-chain Diamond and displays its facets and selectors. * - * Validates the diamond address, resolves the RPC adapter for the target - * chain, fetches facets via Diamond introspection, and decodes each selector + * Validates the diamond address, fetches facets via the resolved Diamond + * adapter, and decodes each selector * using a combination of common signatures and project ABI files. * * @param ctx - The compose context with `address` and optional `chain` params. + * @param diamond - Resolved Diamond adapter for the selected chain. + * @param chain - Resolved chain identity for the result. * @returns The updated context with inspect result stored in * `ctx.state.inspect` as {@link ModuleState}\<{@link InspectResult}\>. * @throws {RPCAdapterError} If the address is invalid or no contract code is * found. */ - async inspect(ctx: ComposeContext): Promise { - const addressValue = ctx.param.address; - if (typeof addressValue !== "string" || !isAddress(addressValue, { strict: false })) { - throw new RPCAdapterError( - "RPC_INVALID_ADDRESS", - `Invalid diamond address: ${String(addressValue ?? "")}`, - { operation: "inspect" }, - ); - } - const diamondAddress = addressValue as Address; - - const chainKey = typeof ctx.param.chain === "string" ? ctx.param.chain : "local"; - const configuredChain = await resolveChainConfig({ chainKey }); - - const dependencies = await DependencyResolver.resolve([{ - key: DependencyKey.RPC, - params: { chainKey: configuredChain.chainKey }, - }]); - const rpc = dependencies[DependencyKey.RPC]; - if (!rpc) throw new Error("RPC dependency was not resolved"); - - const code = await rpc.getCode(diamondAddress); - if (!code || code === "0x") { - throw new RPCAdapterError( - "RPC_CONTRACT_NOT_FOUND", - `No contract code found at ${diamondAddress}`, - { operation: "inspect", chainId: configuredChain.chainId }, - ); - } - - const rawFacets = await rpc.readContract< - { facet: Address; functionSelectors: Hex[] }[] - >({ - address: diamondAddress, - abi: DIAMOND_INSPECT_ABI, - functionName: "facets", - }); + async inspect(ctx: ComposeContext, diamond: IDiamondAdapter, chain: ResolvedChainConfig): Promise { + const diamondAddress = inspectAddress(ctx.param.address); + const rawFacets = await diamond.facets(diamondAddress); const composePath = await findFileAncestor(process.cwd(), "compose.json"); const projectRoot = composePath ? path.dirname(composePath) : null; @@ -74,8 +52,8 @@ export const InspectModule = { const result: InspectResult = { diamond: diamondAddress, - chainKey: configuredChain.chainKey, - chainId: configuredChain.chainId, + chainKey: chain.chainKey, + chainId: chain.chainId, facets, }; diff --git a/cli/src/pipelines/bytecodeValidationPipeline.ts b/cli/src/pipelines/bytecodeValidationPipeline.ts index 7176c542..d95d4875 100644 --- a/cli/src/pipelines/bytecodeValidationPipeline.ts +++ b/cli/src/pipelines/bytecodeValidationPipeline.ts @@ -1,6 +1,7 @@ import type { ComposeContext, ModuleState } from "../context/types"; import type { IBytecodeValidatorAdapter } from "../adapters/IBytecodeValidatorAdapter/interface"; import type { IRPCAdapter } from "../adapters/IRPCAdapter/interface"; +import type { IDiamondAdapter } from "../adapters/IDiamondAdapter/interface"; import { BytecodeValidationModule } from "../modules/bytecodeValidation/module"; import { DependencyKey } from "../resolver/dependencyKey"; import { DependencyResolver } from "../resolver/dependencyResolver"; @@ -27,13 +28,21 @@ export const BytecodeValidationPipeline = { projectRoot: String(ctx.param.projectRoot), }, }, + { + key: DependencyKey.Diamond, + params: { + chainKey: ctx.param.chainKey, + projectRoot: String(ctx.param.projectRoot), + }, + }, { key: DependencyKey.BytecodeValidator }, ]); const rpc = dependencies[DependencyKey.RPC] as IRPCAdapter | undefined; + const diamond = dependencies[DependencyKey.Diamond] as IDiamondAdapter | undefined; const validator = dependencies[DependencyKey.BytecodeValidator] as IBytecodeValidatorAdapter | undefined; - if (!rpc || !validator) throw new Error("Bytecode validation dependencies were not resolved."); + if (!rpc || !diamond || !validator) throw new Error("Bytecode validation dependencies were not resolved."); - ctx = await BytecodeValidationModule.validateDeployment(ctx, { rpc, validator }); + ctx = await BytecodeValidationModule.validateDeployment(ctx, { rpc, diamond, validator }); const state = ctx.state.bytecodeDeploymentValidation as ModuleState; if (!state.success) { ctx.status = { diff --git a/cli/src/pipelines/inspectPipeline.ts b/cli/src/pipelines/inspectPipeline.ts index b86afcbc..84487dc2 100644 --- a/cli/src/pipelines/inspectPipeline.ts +++ b/cli/src/pipelines/inspectPipeline.ts @@ -1,9 +1,21 @@ import { ComposeContext } from "../context/types"; -import { InspectModule } from "../modules/inspect/module"; +import { InspectModule, inspectAddress } from "../modules/inspect/module"; +import { DependencyKey } from "../resolver/dependencyKey"; +import { DependencyResolver } from "../resolver/dependencyResolver"; +import { resolveChainConfig } from "../utils/chainConfig"; /** Diamond inspect pipeline for querying on-chain facets and selectors. */ export const InspectPipeline = { async execute(ctx: ComposeContext): Promise { - return InspectModule.inspect(ctx); + inspectAddress(ctx.param.address); + const chainKey = typeof ctx.param.chain === "string" ? ctx.param.chain : "local"; + const chain = await resolveChainConfig({ chainKey }); + const dependencies = await DependencyResolver.resolve([{ + key: DependencyKey.Diamond, + params: { chainKey: chain.chainKey }, + }]); + const diamond = dependencies[DependencyKey.Diamond]; + if (!diamond) throw new Error("Diamond dependency was not resolved"); + return InspectModule.inspect(ctx, diamond, chain); }, }; diff --git a/cli/src/resolver/dependencyKey.ts b/cli/src/resolver/dependencyKey.ts index 818c6000..095d0ede 100644 --- a/cli/src/resolver/dependencyKey.ts +++ b/cli/src/resolver/dependencyKey.ts @@ -7,6 +7,7 @@ export enum DependencyKey { Hashing = "hashing", RPC = "rpc", + Diamond = "diamond", Foundry = "foundry", Hardhat = "hardhat", BytecodeValidator = "bytecodeValidator", diff --git a/cli/src/resolver/dependencyRegistry.ts b/cli/src/resolver/dependencyRegistry.ts index 27a812c4..b0fe811d 100644 --- a/cli/src/resolver/dependencyRegistry.ts +++ b/cli/src/resolver/dependencyRegistry.ts @@ -7,6 +7,8 @@ import { foundryAdapter } from "../adapters/IFrameworkAdapter/foundryAdapter/ada import { hardhatAdapter } from "../adapters/IFrameworkAdapter/hardhatAdapter/adapter"; import type { IRPCAdapter } from "../adapters/IRPCAdapter/interface"; import { createRPCAdapter } from "../adapters/IRPCAdapter/adapter"; +import type { IDiamondAdapter } from "../adapters/IDiamondAdapter/interface"; +import { createDiamondAdapter } from "../adapters/IDiamondAdapter/adapter"; import { resolveChainConfig } from "../utils/chainConfig"; import { DependencyKey } from "./dependencyKey"; import { BytecodeValidatorAdapter } from "../adapters/IBytecodeValidatorAdapter/adapter"; @@ -29,6 +31,7 @@ export type DependencyFactory = ( export type DependencyMap = { [DependencyKey.Hashing]: IHashingAdapter; [DependencyKey.RPC]: IRPCAdapter; + [DependencyKey.Diamond]: IDiamondAdapter; [DependencyKey.Foundry]: IFrameworkAdapter; [DependencyKey.Hardhat]: IFrameworkAdapter; [DependencyKey.BytecodeValidator]: IBytecodeValidatorAdapter; @@ -52,6 +55,8 @@ export const DependencyRegistry: { const resolved = await resolveChainConfig(params as RPCDependencyParams | undefined); return createRPCAdapter({ rpcUrl: resolved.rpcUrl, chainId: resolved.chainId }); }, + [DependencyKey.Diamond]: async (params) => + createDiamondAdapter(await DependencyRegistry[DependencyKey.RPC](params)), [DependencyKey.Foundry]: () => foundryAdapter, [DependencyKey.Hardhat]: () => hardhatAdapter, [DependencyKey.BytecodeValidator]: () => BytecodeValidatorAdapter, diff --git a/cli/test/adapters/IDiamondAdapter/adapter.test.ts b/cli/test/adapters/IDiamondAdapter/adapter.test.ts new file mode 100644 index 00000000..de3d6bb8 --- /dev/null +++ b/cli/test/adapters/IDiamondAdapter/adapter.test.ts @@ -0,0 +1,82 @@ +import { describe, expect, it, vi } from "vitest"; +import { encodeFunctionData, type Address, type Hex } from "viem"; +import type { IRPCAdapter } from "../../../src/adapters/IRPCAdapter/interface"; +import { createDiamondAdapter } from "../../../src/adapters/IDiamondAdapter/adapter"; +import { DIAMOND_INSPECT_ABI } from "../../../src/adapters/IDiamondAdapter/abi"; + +const diamondAddress = "0x0000000000000000000000000000000000000001" as Address; +const facetAddress = "0x0000000000000000000000000000000000000002" as Address; +const selector = "0x12345678" as Hex; + +function setup() { + const readContract = vi.fn(); + const rpc = { readContract } as unknown as IRPCAdapter; + return { adapter: createDiamondAdapter(rpc), readContract }; +} + +describe("IDiamondAdapter", () => { + it("uses the selectors exposed by DiamondInspectFacet", () => { + expect(encodeFunctionData({ abi: DIAMOND_INSPECT_ABI, functionName: "facets" })).toBe("0x7a0ed627"); + expect(encodeFunctionData({ abi: DIAMOND_INSPECT_ABI, functionName: "facetAddresses" })).toBe("0x52ef6b2c"); + expect(encodeFunctionData({ abi: DIAMOND_INSPECT_ABI, functionName: "facetFunctionSelectors", args: [facetAddress] }).slice(0, 10)).toBe("0xadfca15e"); + expect(encodeFunctionData({ abi: DIAMOND_INSPECT_ABI, functionName: "facetAddress", args: [selector] }).slice(0, 10)).toBe("0xcdffacc6"); + }); + + it("reads facets with the shared tuple shape and verifies diamond code", async () => { + const { adapter, readContract } = setup(); + const facets = [{ facet: facetAddress, functionSelectors: [selector] }]; + readContract.mockResolvedValue(facets); + + await expect(adapter.facets(diamondAddress, { blockNumber: 100n })).resolves.toEqual(facets); + expect(readContract).toHaveBeenCalledWith({ + address: diamondAddress, + abi: expect.arrayContaining([expect.objectContaining({ name: "facets" })]), + functionName: "facets", + blockNumber: 100n, + }, { verifyCode: true }); + }); + + it("reads all facet addresses", async () => { + const { adapter, readContract } = setup(); + readContract.mockResolvedValue([facetAddress]); + + await expect(adapter.facetAddresses(diamondAddress)).resolves.toEqual([facetAddress]); + expect(readContract).toHaveBeenCalledWith(expect.objectContaining({ + address: diamondAddress, + functionName: "facetAddresses", + }), { verifyCode: true }); + }); + + it("reads the selectors owned by one facet", async () => { + const { adapter, readContract } = setup(); + readContract.mockResolvedValue([selector]); + + await expect(adapter.facetFunctionSelectors(diamondAddress, facetAddress)).resolves.toEqual([selector]); + expect(readContract).toHaveBeenCalledWith(expect.objectContaining({ + address: diamondAddress, + functionName: "facetFunctionSelectors", + args: [facetAddress], + }), { verifyCode: true }); + }); + + it("reads selector ownership, including an unowned selector", async () => { + const { adapter, readContract } = setup(); + const empty = "0x0000000000000000000000000000000000000000"; + readContract.mockResolvedValue(empty); + + await expect(adapter.facetAddress(diamondAddress, selector)).resolves.toBe(empty); + expect(readContract).toHaveBeenCalledWith(expect.objectContaining({ + address: diamondAddress, + functionName: "facetAddress", + args: [selector], + }), { verifyCode: true }); + }); + + it("passes through missing-code and RPC errors from the RPC adapter", async () => { + const { adapter, readContract } = setup(); + const error = Object.assign(new Error("No contract code found"), { code: "RPC_CONTRACT_NOT_FOUND" }); + readContract.mockRejectedValue(error); + + await expect(adapter.facets(diamondAddress)).rejects.toBe(error); + }); +}); diff --git a/cli/test/modules/bytecodeValidation/module.test.ts b/cli/test/modules/bytecodeValidation/module.test.ts index 3dbadc62..9bce9d5a 100644 --- a/cli/test/modules/bytecodeValidation/module.test.ts +++ b/cli/test/modules/bytecodeValidation/module.test.ts @@ -3,6 +3,7 @@ import type { Address, Hex } from "viem"; import { Context } from "../../../src/context/context"; import type { IBytecodeValidatorAdapter } from "../../../src/adapters/IBytecodeValidatorAdapter/interface"; import type { IRPCAdapter } from "../../../src/adapters/IRPCAdapter/interface"; +import type { IDiamondAdapter } from "../../../src/adapters/IDiamondAdapter/interface"; import { BytecodeValidationModule } from "../../../src/modules/bytecodeValidation/module"; const diamond = "0x0000000000000000000000000000000000000001" as Address; @@ -28,38 +29,37 @@ function setup() { }; const rpc: IRPCAdapter = { getBlockNumber: vi.fn().mockResolvedValue(100n), - readContract: vi.fn().mockResolvedValue([{ + readContract: vi.fn(), + getCode: vi.fn().mockResolvedValue("0x6000" as Hex), + }; + const diamondAdapter = { + facets: vi.fn().mockResolvedValue([{ facet, functionSelectors: ["0x12345678"], }]), - getCode: vi.fn().mockResolvedValue("0x6000" as Hex), - }; + } as unknown as IDiamondAdapter; const validator: IBytecodeValidatorAdapter = { validate: vi.fn().mockReturnValue(report()), }; - return { ctx, rpc, validator }; + return { ctx, rpc, diamondAdapter, validator }; } describe("BytecodeValidationModule", () => { it("uses Diamond introspection and one pinned block", async () => { - const { ctx, rpc, validator } = setup(); - const result = await BytecodeValidationModule.validateDeployment(ctx, { rpc, validator }); - - expect(rpc.readContract).toHaveBeenCalledWith(expect.objectContaining({ - address: diamond, - blockNumber: 100n, - functionName: "facets", - }), { verifyCode: true }); + const { ctx, rpc, diamondAdapter, validator } = setup(); + const result = await BytecodeValidationModule.validateDeployment(ctx, { rpc, diamond: diamondAdapter, validator }); + + expect(diamondAdapter.facets).toHaveBeenCalledWith(diamond, { blockNumber: 100n }); expect(rpc.getCode).toHaveBeenCalledWith(facet, 100n); expect(validator.validate).toHaveBeenCalledOnce(); expect(result.state.bytecodeDeploymentValidation.success).toBe(true); }); it("reports introspection errors without blocking validation", async () => { - const { ctx, rpc, validator } = setup(); - vi.mocked(rpc.readContract).mockRejectedValue(new Error("packed selectors are invalid")); + const { ctx, rpc, diamondAdapter, validator } = setup(); + vi.mocked(diamondAdapter.facets).mockRejectedValue(new Error("packed selectors are invalid")); - const result = await BytecodeValidationModule.validateDeployment(ctx, { rpc, validator }); + const result = await BytecodeValidationModule.validateDeployment(ctx, { rpc, diamond: diamondAdapter, validator }); const state = result.state.bytecodeDeploymentValidation; expect(state.success).toBe(true); @@ -72,7 +72,7 @@ describe("BytecodeValidationModule", () => { }); it("blocks only when the validator proves a collision", async () => { - const { ctx, rpc, validator } = setup(); + const { ctx, rpc, diamondAdapter, validator } = setup(); ctx.param.virtualStorageRecords = [{ id: "0x01", virtualPath: "example.storage", @@ -95,7 +95,7 @@ describe("BytecodeValidationModule", () => { reason: "type mismatch", }])); - const result = await BytecodeValidationModule.validateDeployment(ctx, { rpc, validator }); + const result = await BytecodeValidationModule.validateDeployment(ctx, { rpc, diamond: diamondAdapter, validator }); expect(result.state.bytecodeDeploymentValidation.success).toBe(false); expect(result.state.bytecodeDeploymentValidation.error?.code).toBe( @@ -110,10 +110,10 @@ describe("BytecodeValidationModule", () => { ["an undefined response", undefined], ["empty runtime bytecode", "0x" as Hex], ])("marks validation incomplete without failing for %s", async (_case, bytecode) => { - const { ctx, rpc, validator } = setup(); + const { ctx, rpc, diamondAdapter, validator } = setup(); vi.mocked(rpc.getCode).mockResolvedValue(bytecode); - const result = await BytecodeValidationModule.validateDeployment(ctx, { rpc, validator }); + const result = await BytecodeValidationModule.validateDeployment(ctx, { rpc, diamond: diamondAdapter, validator }); const state = result.state.bytecodeDeploymentValidation; expect(state.success).toBe(true); @@ -124,9 +124,9 @@ describe("BytecodeValidationModule", () => { }); it("continues after an individual facet cannot be analyzed", async () => { - const { ctx, rpc, validator } = setup(); + const { ctx, rpc, diamondAdapter, validator } = setup(); const secondFacet = "0x0000000000000000000000000000000000000003" as Address; - vi.mocked(rpc.readContract).mockResolvedValue([ + vi.mocked(diamondAdapter.facets).mockResolvedValue([ { facet, functionSelectors: [] }, { facet: secondFacet, functionSelectors: [] }, ] as never); @@ -134,7 +134,7 @@ describe("BytecodeValidationModule", () => { .mockRejectedValueOnce(new Error("code unavailable")) .mockResolvedValueOnce("0x6000"); - const result = await BytecodeValidationModule.validateDeployment(ctx, { rpc, validator }); + const result = await BytecodeValidationModule.validateDeployment(ctx, { rpc, diamond: diamondAdapter, validator }); const state = result.state.bytecodeDeploymentValidation; expect(state.success).toBe(false); diff --git a/cli/test/modules/inspect/module.test.ts b/cli/test/modules/inspect/module.test.ts index 13c7c9a0..bcbc08a6 100644 --- a/cli/test/modules/inspect/module.test.ts +++ b/cli/test/modules/inspect/module.test.ts @@ -1,25 +1,26 @@ import { describe, expect, it, vi } from "vitest"; import { Context } from "../../../src/context/context"; -import { DependencyKey } from "../../../src/resolver/dependencyKey"; import { decodeSelector } from "../../../src/modules/inspect/selectorDecoder"; +import type { IDiamondAdapter } from "../../../src/adapters/IDiamondAdapter/interface"; const mocks = vi.hoisted(() => ({ - resolveChainConfig: vi.fn(), - resolve: vi.fn(), showInspect: vi.fn(), })); -vi.mock("../../../src/utils/chainConfig", () => ({ resolveChainConfig: mocks.resolveChainConfig })); -vi.mock("../../../src/resolver/dependencyResolver", () => ({ DependencyResolver: { resolve: mocks.resolve } })); vi.mock("../../../src/modules/inspect/output", () => ({ showInspect: mocks.showInspect })); import { InspectModule } from "../../../src/modules/inspect/module"; const VALID_ADDRESS = "0x0000000000000000000000000000000000000001"; +const chain = { chainKey: "sepolia", rpcUrl: "https://rpc.example", chainId: 11155111 }; + +function diamondAdapter(facets: ReturnType): IDiamondAdapter { + return { facets } as unknown as IDiamondAdapter; +} describe("InspectModule", () => { it("inspects a diamond and displays facets", async () => { - const readContract = vi.fn().mockResolvedValue([ + const facets = vi.fn().mockResolvedValue([ { facet: "0x0000000000000000000000000000000000000002", functionSelectors: ["0x313ce567", "0x18160ddd"], @@ -29,24 +30,13 @@ describe("InspectModule", () => { functionSelectors: ["0x095ea7b3"], }, ]); - const getCode = vi.fn().mockResolvedValue("0x6000"); - mocks.resolveChainConfig.mockResolvedValue({ chainKey: "sepolia", rpcUrl: "https://rpc.example", chainId: 11155111 }); - mocks.resolve.mockResolvedValue({ [DependencyKey.RPC]: { readContract, getCode } }); const ctx = Context.create(); ctx.param = { address: VALID_ADDRESS, chain: "sepolia" }; - const result = await InspectModule.inspect(ctx); + const result = await InspectModule.inspect(ctx, diamondAdapter(facets), chain); - expect(mocks.resolve).toHaveBeenCalledWith([{ - key: DependencyKey.RPC, - params: { chainKey: "sepolia" }, - }]); - expect(readContract).toHaveBeenCalledWith({ - address: VALID_ADDRESS, - abi: expect.any(Array), - functionName: "facets", - }); + expect(facets).toHaveBeenCalledWith(VALID_ADDRESS); expect(result.state.inspect).toMatchObject({ success: true, result: { @@ -66,36 +56,34 @@ describe("InspectModule", () => { const ctx = Context.create(); ctx.param = { address: "not-an-address", chain: "sepolia" }; - await expect(InspectModule.inspect(ctx)).rejects.toMatchObject({ + await expect(InspectModule.inspect(ctx, diamondAdapter(vi.fn()), chain)).rejects.toMatchObject({ code: "RPC_INVALID_ADDRESS", }); }); it("throws RPC_CONTRACT_NOT_FOUND when no bytecode exists", async () => { - const getCode = vi.fn().mockResolvedValue("0x"); - mocks.resolveChainConfig.mockResolvedValue({ chainKey: "local", rpcUrl: "https://rpc.example", chainId: 31337 }); - mocks.resolve.mockResolvedValue({ [DependencyKey.RPC]: { getCode, readContract: vi.fn() } }); + const facets = vi.fn().mockRejectedValue(Object.assign(new Error("No contract code found"), { + code: "RPC_CONTRACT_NOT_FOUND", + })); const ctx = Context.create(); ctx.param = { address: VALID_ADDRESS, chain: "local" }; - await expect(InspectModule.inspect(ctx)).rejects.toMatchObject({ + await expect(InspectModule.inspect(ctx, diamondAdapter(facets), chain)).rejects.toMatchObject({ code: "RPC_CONTRACT_NOT_FOUND", }); }); it("defaults chain to local when not provided", async () => { - const readContract = vi.fn().mockResolvedValue([]); - const getCode = vi.fn().mockResolvedValue("0x6000"); - mocks.resolveChainConfig.mockResolvedValue({ chainKey: "local", rpcUrl: "https://rpc.example", chainId: 31337 }); - mocks.resolve.mockResolvedValue({ [DependencyKey.RPC]: { readContract, getCode } }); + const facets = vi.fn().mockResolvedValue([]); const ctx = Context.create(); ctx.param = { address: VALID_ADDRESS }; - const result = await InspectModule.inspect(ctx); + const result = await InspectModule.inspect(ctx, diamondAdapter(facets), { + ...chain, chainKey: "local", chainId: 31337, + }); - expect(mocks.resolveChainConfig).toHaveBeenCalledWith({ chainKey: "local" }); expect(result.state.inspect).toMatchObject({ success: true, result: { chainKey: "local", facets: [] }, @@ -103,15 +91,12 @@ describe("InspectModule", () => { }); it("propagates RPC readContract failures", async () => { - const getCode = vi.fn().mockResolvedValue("0x6000"); - const readContract = vi.fn().mockRejectedValue(new Error("execution reverted")); - mocks.resolveChainConfig.mockResolvedValue({ chainKey: "local", rpcUrl: "https://rpc.example", chainId: 31337 }); - mocks.resolve.mockResolvedValue({ [DependencyKey.RPC]: { readContract, getCode } }); + const facets = vi.fn().mockRejectedValue(new Error("execution reverted")); const ctx = Context.create(); ctx.param = { address: VALID_ADDRESS, chain: "local" }; - await expect(InspectModule.inspect(ctx)).rejects.toThrow("execution reverted"); + await expect(InspectModule.inspect(ctx, diamondAdapter(facets), chain)).rejects.toThrow("execution reverted"); }); }); diff --git a/cli/test/pipelines/inspectPipeline/inspectPipeline.test.ts b/cli/test/pipelines/inspectPipeline/inspectPipeline.test.ts index f0ae1c21..2d21d5ee 100644 --- a/cli/test/pipelines/inspectPipeline/inspectPipeline.test.ts +++ b/cli/test/pipelines/inspectPipeline/inspectPipeline.test.ts @@ -15,13 +15,21 @@ vi.mock("../../../src/modules/inspect/output", () => ({ showInspect: mocks.showI import { InspectPipeline } from "../../../src/pipelines/inspectPipeline"; describe("InspectPipeline", () => { + it("rejects an invalid address before resolving a chain", async () => { + const ctx = Context.create(); + ctx.param = { address: "not-an-address" }; + + await expect(InspectPipeline.execute(ctx)).rejects.toMatchObject({ code: "RPC_INVALID_ADDRESS" }); + expect(mocks.resolveChainConfig).not.toHaveBeenCalled(); + expect(mocks.resolve).not.toHaveBeenCalled(); + }); + it("delegates to InspectModule with valid address", async () => { - const readContract = vi.fn().mockResolvedValue([ + const facets = vi.fn().mockResolvedValue([ { facet: "0x0000000000000000000000000000000000000002", functionSelectors: ["0x313ce567"] }, ]); - const getCode = vi.fn().mockResolvedValue("0x6000"); mocks.resolveChainConfig.mockResolvedValue({ chainKey: "sepolia", rpcUrl: "https://rpc.example", chainId: 11155111 }); - mocks.resolve.mockResolvedValue({ [DependencyKey.RPC]: { readContract, getCode } }); + mocks.resolve.mockResolvedValue({ [DependencyKey.Diamond]: { facets } }); const ctx = Context.create(); ctx.param = { @@ -32,9 +40,10 @@ describe("InspectPipeline", () => { const result = await InspectPipeline.execute(ctx); expect(mocks.resolve).toHaveBeenCalledWith([{ - key: DependencyKey.RPC, + key: DependencyKey.Diamond, params: { chainKey: "sepolia" }, }]); + expect(facets).toHaveBeenCalledWith(ctx.param.address); expect(result.state.inspect).toMatchObject({ success: true, result: { chainKey: "sepolia", chainId: 11155111 }, diff --git a/cli/test/pipelines/validatePipeline/validatePipelineVSL.test.ts b/cli/test/pipelines/validatePipeline/validatePipelineVSL.test.ts index 8f7fa144..af89f603 100644 --- a/cli/test/pipelines/validatePipeline/validatePipelineVSL.test.ts +++ b/cli/test/pipelines/validatePipeline/validatePipelineVSL.test.ts @@ -8,6 +8,7 @@ import { createValidatePipelineHarness } from "./harness"; import { DependencyResolver } from "../../../src/resolver/dependencyResolver"; import { DependencyKey } from "../../../src/resolver/dependencyKey"; import type { IRPCAdapter } from "../../../src/adapters/IRPCAdapter/interface"; +import { createDiamondAdapter } from "../../../src/adapters/IDiamondAdapter/adapter"; import type { IBytecodeValidatorAdapter } from "../../../src/adapters/IBytecodeValidatorAdapter/interface"; import type { Address, Hex } from "viem"; import { BytecodeValidatorAdapter } from "../../../src/adapters/IBytecodeValidatorAdapter/adapter"; @@ -99,6 +100,7 @@ describe("validate pipeline", () => { if (requests.some((request) => request.key === DependencyKey.BytecodeValidator)) { return { [DependencyKey.RPC]: rpc, + [DependencyKey.Diamond]: createDiamondAdapter(rpc), [DependencyKey.BytecodeValidator]: validator, }; } @@ -150,6 +152,7 @@ describe("validate pipeline", () => { if (requests.some((request) => request.key === DependencyKey.BytecodeValidator)) { return { [DependencyKey.RPC]: rpc, + [DependencyKey.Diamond]: createDiamondAdapter(rpc), [DependencyKey.BytecodeValidator]: validator, }; } @@ -204,6 +207,7 @@ describe("validate pipeline", () => { if (requests.some((request) => request.key === DependencyKey.BytecodeValidator)) { return { [DependencyKey.RPC]: rpc, + [DependencyKey.Diamond]: createDiamondAdapter(rpc), [DependencyKey.BytecodeValidator]: validator, }; } @@ -258,6 +262,7 @@ describe("validate pipeline", () => { if (chainKey === "broken") throw new Error("RPC unavailable"); return { [DependencyKey.RPC]: rpc, + [DependencyKey.Diamond]: createDiamondAdapter(rpc), [DependencyKey.BytecodeValidator]: validator, }; } @@ -310,6 +315,7 @@ describe("validate pipeline", () => { if (requests.some((request) => request.key === DependencyKey.BytecodeValidator)) { return { [DependencyKey.RPC]: rpc, + [DependencyKey.Diamond]: createDiamondAdapter(rpc), [DependencyKey.BytecodeValidator]: BytecodeValidatorAdapter, }; } diff --git a/cli/test/resolver/dependencyResolver.test.ts b/cli/test/resolver/dependencyResolver.test.ts index 050ec941..67de5585 100644 --- a/cli/test/resolver/dependencyResolver.test.ts +++ b/cli/test/resolver/dependencyResolver.test.ts @@ -8,4 +8,10 @@ describe("DependencyResolver RPC dependency", () => { code: "RPC_INVALID_CONFIGURATION", }); }); + + it("resolves the Diamond adapter through the same chain configuration", async () => { + await expect(DependencyResolver.resolve([{ key: DependencyKey.Diamond, params: { projectRoot: "/path/that/does/not/exist" } }])).rejects.toMatchObject({ + code: "RPC_INVALID_CONFIGURATION", + }); + }); }); From 60ba0c8863b3d50bf9d1d821ea24e7f4f983b492 Mon Sep 17 00:00:00 2001 From: Vagabond Date: Wed, 7 Oct 2026 01:56:49 +0700 Subject: [PATCH 2/2] chore: add CLI diamond adapter changeset --- .changeset/diamond-adapter.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/diamond-adapter.md diff --git a/.changeset/diamond-adapter.md b/.changeset/diamond-adapter.md new file mode 100644 index 00000000..1f2f4be2 --- /dev/null +++ b/.changeset/diamond-adapter.md @@ -0,0 +1,5 @@ +--- +"@perfect-abstractions/compose-cli": patch +--- + +Add a shared Diamond introspection adapter for ERC-8153 and ERC-2535, and use it for inspect and bytecode validation.