hardhat-external-artifacts 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,346 @@
1
+ import type {ExternalArtifact, RichArtifact, LinkReferences} from './types.js';
2
+ import {isRichArtifact} from './types.js';
3
+
4
+ export interface SyntheticCompilation {
5
+ solcVersion: string;
6
+ compilerInput: CompilerInput;
7
+ compilerOutput: CompilerOutput;
8
+ }
9
+
10
+ interface CompilerInput {
11
+ language: string;
12
+ sources: Record<string, {content: string}>;
13
+ settings: {
14
+ optimizer: {enabled: boolean; runs?: number};
15
+ outputSelection: Record<string, Record<string, string[]>>;
16
+ remappings?: string[];
17
+ metadata?: {useLiteralContent?: boolean; bytecodeHash?: string};
18
+ };
19
+ }
20
+
21
+ interface CompilerOutput {
22
+ sources: Record<string, {id: number; ast: object}>;
23
+ contracts: Record<
24
+ string,
25
+ Record<
26
+ string,
27
+ {
28
+ abi: readonly any[];
29
+ evm: {
30
+ bytecode: BytecodeOutput;
31
+ deployedBytecode: BytecodeOutput;
32
+ methodIdentifiers: Record<string, string>;
33
+ };
34
+ metadata?: string;
35
+ devdoc?: any;
36
+ userdoc?: any;
37
+ storageLayout?: any;
38
+ }
39
+ >
40
+ >;
41
+ }
42
+
43
+ interface BytecodeOutput {
44
+ object: string;
45
+ opcodes: string;
46
+ sourceMap: string;
47
+ linkReferences: LinkReferences;
48
+ immutableReferences?: Record<string, Array<{start: number; length: number}>>;
49
+ generatedSources?: any[];
50
+ functionDebugData?: Record<string, any>;
51
+ }
52
+
53
+ /**
54
+ * Creates a minimal valid AST for a source file.
55
+ * This is needed because Hardhat's contract decoder expects a valid AST structure.
56
+ */
57
+ function createMinimalAst(
58
+ sourceName: string,
59
+ sourceId: number,
60
+ contracts?: Array<{name: string; nodeId: number}>,
61
+ ): object {
62
+ const nodes: object[] = [];
63
+ const exportedSymbols: Record<string, number[]> = {};
64
+
65
+ // Add contract definition nodes if provided
66
+ if (contracts) {
67
+ for (const contract of contracts) {
68
+ nodes.push({
69
+ nodeType: 'ContractDefinition',
70
+ id: contract.nodeId,
71
+ src: `0:0:${sourceId}`,
72
+ name: contract.name,
73
+ contractKind: 'contract',
74
+ abstract: false,
75
+ fullyImplemented: true,
76
+ linearizedBaseContracts: [contract.nodeId],
77
+ nodes: [],
78
+ scope: sourceId,
79
+ });
80
+ exportedSymbols[contract.name] = [contract.nodeId];
81
+ }
82
+ }
83
+
84
+ return {
85
+ nodeType: 'SourceUnit',
86
+ src: `0:0:${sourceId}`,
87
+ id: sourceId,
88
+ absolutePath: sourceName,
89
+ exportedSymbols,
90
+ nodes,
91
+ };
92
+ }
93
+
94
+ /**
95
+ * Convert artifacts to compilation format.
96
+ * If artifacts are "rich" (have solcInput), use the embedded data.
97
+ * Otherwise, synthesize a minimal compilation.
98
+ */
99
+ export function artifactsToCompilations(
100
+ artifacts: ExternalArtifact[],
101
+ defaultSolcVersion: string,
102
+ ): SyntheticCompilation[] {
103
+ // Group artifacts by whether they have solcInput
104
+ const richArtifacts = artifacts.filter(isRichArtifact);
105
+ const simpleArtifacts = artifacts.filter((a) => !isRichArtifact(a));
106
+
107
+ const compilations: SyntheticCompilation[] = [];
108
+
109
+ // Process rich artifacts - these have embedded solcInput
110
+ for (const artifact of richArtifacts) {
111
+ const compilation = richArtifactToCompilation(artifact);
112
+ if (compilation) {
113
+ compilations.push(compilation);
114
+ }
115
+ }
116
+
117
+ // Process simple artifacts - synthesize compilation
118
+ if (simpleArtifacts.length > 0) {
119
+ compilations.push(
120
+ synthesizeCompilation(simpleArtifacts, defaultSolcVersion),
121
+ );
122
+ }
123
+
124
+ return compilations;
125
+ }
126
+
127
+ /**
128
+ * Convert a rich artifact (with embedded solcInput) to a compilation.
129
+ * Uses the embedded solcInput directly for maximum fidelity.
130
+ */
131
+ function richArtifactToCompilation(
132
+ artifact: RichArtifact,
133
+ ): SyntheticCompilation | null {
134
+ if (!artifact.solcInput) {
135
+ return null;
136
+ }
137
+
138
+ // Parse the embedded solcInput
139
+ const compilerInput: CompilerInput = JSON.parse(artifact.solcInput);
140
+
141
+ // Extract solc version from metadata
142
+ let solcVersion = '0.8.20'; // Default
143
+ if (artifact.metadata) {
144
+ try {
145
+ const metadata = JSON.parse(artifact.metadata);
146
+ if (metadata.compiler?.version) {
147
+ // Format: "0.8.10+commit.fc410830" -> extract "0.8.10"
148
+ solcVersion = metadata.compiler.version.split('+')[0];
149
+ }
150
+ } catch {
151
+ // Ignore parsing errors, use default
152
+ }
153
+ }
154
+
155
+ // Build compiler output from the artifact
156
+ // Only include sources that we have contract data for
157
+ // Including empty sources/contracts can cause EDR selector fixup issues
158
+ const compilerOutput: CompilerOutput = {
159
+ sources: {},
160
+ contracts: {},
161
+ };
162
+
163
+ const sourceName = artifact.sourceName;
164
+
165
+ // Track source IDs for all input sources (needed for consistent source indexing)
166
+ let sourceId = 0;
167
+ const sourceIds: Record<string, number> = {};
168
+ for (const srcName of Object.keys(compilerInput.sources)) {
169
+ sourceIds[srcName] = sourceId++;
170
+ }
171
+
172
+ // Only include the source that contains our contract
173
+ const contractSourceId = sourceIds[sourceName] ?? sourceId++;
174
+ const contractNodeId = contractSourceId + 1000; // Use an offset to avoid ID conflicts
175
+
176
+ compilerOutput.sources[sourceName] = {
177
+ id: contractSourceId,
178
+ ast: createMinimalAst(sourceName, contractSourceId, [
179
+ {name: artifact.contractName, nodeId: contractNodeId},
180
+ ]),
181
+ };
182
+ compilerOutput.contracts[sourceName] = {};
183
+
184
+ // Ensure the source is in compilerInput as well
185
+ if (!compilerInput.sources[sourceName]) {
186
+ compilerInput.sources[sourceName] = {content: ''};
187
+ }
188
+
189
+ // Build bytecode output, ensuring proper format
190
+ // Standard solc output has bytecode.object without 0x prefix
191
+ const bytecode: BytecodeOutput = {
192
+ object: stripHexPrefix(
193
+ artifact.evm?.bytecode?.object ?? artifact.bytecode ?? '0x',
194
+ ),
195
+ opcodes: artifact.evm?.bytecode?.opcodes ?? '',
196
+ sourceMap: artifact.evm?.bytecode?.sourceMap ?? '',
197
+ linkReferences:
198
+ artifact.evm?.bytecode?.linkReferences ?? artifact.linkReferences ?? {},
199
+ generatedSources: artifact.evm?.bytecode?.generatedSources,
200
+ functionDebugData: artifact.evm?.bytecode?.functionDebugData,
201
+ };
202
+
203
+ const deployedBytecode: BytecodeOutput = {
204
+ object: stripHexPrefix(
205
+ artifact.evm?.deployedBytecode?.object ??
206
+ artifact.deployedBytecode ??
207
+ '0x',
208
+ ),
209
+ opcodes: artifact.evm?.deployedBytecode?.opcodes ?? '',
210
+ sourceMap: artifact.evm?.deployedBytecode?.sourceMap ?? '',
211
+ linkReferences:
212
+ artifact.evm?.deployedBytecode?.linkReferences ??
213
+ artifact.deployedLinkReferences ??
214
+ {},
215
+ immutableReferences:
216
+ artifact.evm?.deployedBytecode?.immutableReferences ?? {},
217
+ generatedSources: artifact.evm?.deployedBytecode?.generatedSources,
218
+ functionDebugData: artifact.evm?.deployedBytecode?.functionDebugData,
219
+ };
220
+
221
+ // IMPORTANT: Do NOT provide method identifiers - let EDR compute them
222
+ // EDR has internal "selector fixup" logic for function overloading that can fail
223
+ // if we provide method identifiers that it then tries to reconcile with AST info.
224
+ // The error "Failed to fix up the selector for ... #supportsInterface" happens
225
+ // when EDR can't match provided selectors with overloaded functions.
226
+ // By providing an empty object, EDR will compute selectors from the ABI directly.
227
+ const methodIdentifiers: Record<string, string> = {};
228
+
229
+ compilerOutput.contracts[sourceName][artifact.contractName] = {
230
+ abi: artifact.abi,
231
+ evm: {
232
+ bytecode,
233
+ deployedBytecode,
234
+ methodIdentifiers,
235
+ },
236
+ metadata: artifact.metadata,
237
+ devdoc: artifact.devdoc,
238
+ userdoc: artifact.userdoc,
239
+ storageLayout: artifact.storageLayout,
240
+ };
241
+
242
+ return {
243
+ solcVersion,
244
+ compilerInput,
245
+ compilerOutput,
246
+ };
247
+ }
248
+
249
+ /**
250
+ * Synthesize a minimal compilation from simple artifacts.
251
+ * Used when artifacts don't have embedded solcInput.
252
+ */
253
+ function synthesizeCompilation(
254
+ artifacts: ExternalArtifact[],
255
+ solcVersion: string,
256
+ ): SyntheticCompilation {
257
+ const sources: CompilerInput['sources'] = {};
258
+ const outputSources: CompilerOutput['sources'] = {};
259
+ const contracts: CompilerOutput['contracts'] = {};
260
+
261
+ // First, group artifacts by source
262
+ const contractsBySource: Record<
263
+ string,
264
+ Array<{name: string; artifact: ExternalArtifact}>
265
+ > = {};
266
+ for (const artifact of artifacts) {
267
+ if (!contractsBySource[artifact.sourceName]) {
268
+ contractsBySource[artifact.sourceName] = [];
269
+ }
270
+ contractsBySource[artifact.sourceName].push({
271
+ name: artifact.contractName,
272
+ artifact,
273
+ });
274
+ }
275
+
276
+ // Track IDs for AST nodes
277
+ let nextId = 0;
278
+
279
+ // Create sources with contract definitions in AST
280
+ for (const [sourceName, sourceContracts] of Object.entries(
281
+ contractsBySource,
282
+ )) {
283
+ const sourceId = nextId++;
284
+ sources[sourceName] = {content: ''};
285
+ contracts[sourceName] = {};
286
+
287
+ // Create contract nodes for AST
288
+ const contractNodes: Array<{name: string; nodeId: number}> = [];
289
+ for (const {name} of sourceContracts) {
290
+ const nodeId = nextId++;
291
+ contractNodes.push({name, nodeId});
292
+ }
293
+
294
+ outputSources[sourceName] = {
295
+ id: sourceId,
296
+ ast: createMinimalAst(sourceName, sourceId, contractNodes),
297
+ };
298
+
299
+ // Add contract outputs
300
+ for (const {name, artifact} of sourceContracts) {
301
+ contracts[sourceName][name] = {
302
+ abi: artifact.abi,
303
+ evm: {
304
+ bytecode: {
305
+ object: stripHexPrefix(artifact.bytecode),
306
+ opcodes: '',
307
+ sourceMap: '',
308
+ linkReferences: artifact.linkReferences ?? {},
309
+ },
310
+ deployedBytecode: {
311
+ object: stripHexPrefix(artifact.deployedBytecode),
312
+ opcodes: '',
313
+ sourceMap: '',
314
+ linkReferences: artifact.deployedLinkReferences ?? {},
315
+ immutableReferences: {},
316
+ },
317
+ // Empty object - let EDR compute selectors to avoid selector fixup issues
318
+ // with overloaded functions (consistent with richArtifactToCompilation)
319
+ methodIdentifiers: {},
320
+ },
321
+ };
322
+ }
323
+ }
324
+
325
+ return {
326
+ solcVersion,
327
+ compilerInput: {
328
+ language: 'Solidity',
329
+ sources,
330
+ settings: {
331
+ optimizer: {enabled: false},
332
+ outputSelection: {
333
+ '*': {'*': ['abi', 'evm.bytecode', 'evm.deployedBytecode']},
334
+ },
335
+ },
336
+ },
337
+ compilerOutput: {
338
+ sources: outputSources,
339
+ contracts,
340
+ },
341
+ };
342
+ }
343
+
344
+ function stripHexPrefix(hex: string): string {
345
+ return hex.startsWith('0x') ? hex.slice(2) : hex;
346
+ }
@@ -0,0 +1,148 @@
1
+ import type {
2
+ ExternalArtifact,
3
+ RichArtifact,
4
+ ExternalArtifactsConfig,
5
+ } from './types.js';
6
+ import {
7
+ readJsonFile,
8
+ getAllFilesMatching,
9
+ } from '@nomicfoundation/hardhat-utils/fs';
10
+ import path from 'node:path';
11
+ import fs from 'node:fs/promises';
12
+
13
+ export class ArtifactLoader {
14
+ readonly #config: ExternalArtifactsConfig;
15
+ readonly #projectRoot: string;
16
+
17
+ constructor(config: ExternalArtifactsConfig, projectRoot: string) {
18
+ this.#config = config;
19
+ this.#projectRoot = projectRoot;
20
+ }
21
+
22
+ async loadAll(): Promise<ExternalArtifact[]> {
23
+ const artifacts: ExternalArtifact[] = [];
24
+
25
+ // Load from paths
26
+ if (this.#config.paths) {
27
+ for (const pathOrGlob of this.#config.paths) {
28
+ const absolutePath = path.resolve(this.#projectRoot, pathOrGlob);
29
+ artifacts.push(...(await this.#loadFromPath(absolutePath)));
30
+ }
31
+ }
32
+
33
+ // Load from resolver function
34
+ if (this.#config.resolver) {
35
+ const resolvedArtifacts = await this.#config.resolver();
36
+ artifacts.push(...resolvedArtifacts);
37
+ }
38
+
39
+ return artifacts;
40
+ }
41
+
42
+ async #loadFromPath(absolutePath: string): Promise<ExternalArtifact[]> {
43
+ let stat: Awaited<ReturnType<typeof fs.stat>>;
44
+
45
+ try {
46
+ stat = await fs.stat(absolutePath);
47
+ } catch {
48
+ // Path doesn't exist, return empty array
49
+ if (this.#config.warnOnInvalidArtifacts !== false) {
50
+ console.warn(
51
+ `[hardhat-external-artifacts] Path not found: ${absolutePath}`,
52
+ );
53
+ }
54
+ return [];
55
+ }
56
+
57
+ if (stat.isFile()) {
58
+ try {
59
+ return [await this.#loadArtifactFile(absolutePath)];
60
+ } catch (error) {
61
+ if (this.#config.warnOnInvalidArtifacts !== false) {
62
+ console.warn(
63
+ `[hardhat-external-artifacts] Failed to load artifact: ${absolutePath}`,
64
+ error,
65
+ );
66
+ }
67
+ return [];
68
+ }
69
+ }
70
+
71
+ if (stat.isDirectory()) {
72
+ // Support both .json and .ts artifact files
73
+ const files = await getAllFilesMatching(absolutePath, (p) =>
74
+ p.endsWith('.json'),
75
+ );
76
+
77
+ const loadedArtifacts: ExternalArtifact[] = [];
78
+ for (const file of files) {
79
+ try {
80
+ loadedArtifacts.push(await this.#loadArtifactFile(file));
81
+ } catch (error) {
82
+ if (this.#config.warnOnInvalidArtifacts !== false) {
83
+ console.warn(
84
+ `[hardhat-external-artifacts] Failed to load artifact: ${file}`,
85
+ error,
86
+ );
87
+ }
88
+ }
89
+ }
90
+ return loadedArtifacts;
91
+ }
92
+
93
+ return [];
94
+ }
95
+
96
+ async #loadArtifactFile(filePath: string): Promise<ExternalArtifact> {
97
+ const content = await readJsonFile(filePath);
98
+ return this.#normalizeArtifact(content, filePath);
99
+ }
100
+
101
+ #normalizeArtifact(raw: any, source: string): ExternalArtifact {
102
+ // Validate required fields - only ABI is truly required
103
+ if (!raw.abi || !Array.isArray(raw.abi)) {
104
+ throw new Error(
105
+ `Artifact from ${source} is missing required field 'abi' or it's not an array`,
106
+ );
107
+ }
108
+
109
+ // Infer contractName from filename if not provided
110
+ // e.g., "/path/to/EIP173Proxy.json" -> "EIP173Proxy"
111
+ let contractName = raw.contractName;
112
+ if (!contractName || typeof contractName !== 'string') {
113
+ const filename = path.basename(source, '.json');
114
+ contractName = filename;
115
+ }
116
+
117
+ // Infer sourceName from filename if not provided
118
+ // e.g., "EIP173Proxy" -> "external/EIP173Proxy.sol"
119
+ let sourceName = raw.sourceName;
120
+ if (!sourceName || typeof sourceName !== 'string') {
121
+ sourceName = `external/${contractName}.sol`;
122
+ }
123
+
124
+ // Base artifact fields - required
125
+ const artifact: ExternalArtifact = {
126
+ contractName,
127
+ sourceName,
128
+ abi: raw.abi,
129
+ bytecode: raw.bytecode ?? '0x',
130
+ deployedBytecode: raw.deployedBytecode ?? '0x',
131
+ linkReferences: raw.linkReferences ?? {},
132
+ deployedLinkReferences: raw.deployedLinkReferences ?? {},
133
+ };
134
+
135
+ // Check if this is a "rich" artifact with embedded solcInput
136
+ if (raw.solcInput) {
137
+ // Rich artifact - has full compilation data
138
+ (artifact as RichArtifact).solcInput = raw.solcInput;
139
+ (artifact as RichArtifact).metadata = raw.metadata;
140
+ (artifact as RichArtifact).evm = raw.evm;
141
+ (artifact as RichArtifact).devdoc = raw.devdoc;
142
+ (artifact as RichArtifact).userdoc = raw.userdoc;
143
+ (artifact as RichArtifact).storageLayout = raw.storageLayout;
144
+ }
145
+
146
+ return artifact;
147
+ }
148
+ }
@@ -0,0 +1,132 @@
1
+ /**
2
+ * A minimal artifact format that can be provided externally.
3
+ * Supports both Hardhat v2 and v3 formats.
4
+ */
5
+ export interface ExternalArtifact {
6
+ /** Contract name */
7
+ contractName: string;
8
+
9
+ /** Source file path/identifier */
10
+ sourceName: string;
11
+
12
+ /** Contract ABI */
13
+ abi: readonly any[];
14
+
15
+ /** Deployment bytecode (0x-prefixed) */
16
+ bytecode: string;
17
+
18
+ /** Deployed/runtime bytecode (0x-prefixed) */
19
+ deployedBytecode: string;
20
+
21
+ /** Library link references for deployment bytecode */
22
+ linkReferences?: LinkReferences;
23
+
24
+ /** Library link references for deployed bytecode */
25
+ deployedLinkReferences?: LinkReferences;
26
+ }
27
+
28
+ /**
29
+ * A rich artifact that includes embedded solcInput and full compilation data.
30
+ * This is the format from hardhat-deploy or other tools that preserve
31
+ * the full compilation output.
32
+ */
33
+ export interface RichArtifact extends ExternalArtifact {
34
+ /** The full solc compiler input JSON (stringified) */
35
+ solcInput?: string;
36
+
37
+ /** Contract metadata JSON (contains solc version, settings, etc.) */
38
+ metadata?: string;
39
+
40
+ /** Full EVM output including generated sources, source maps, etc. */
41
+ evm?: {
42
+ bytecode: {
43
+ object: string;
44
+ opcodes: string;
45
+ sourceMap: string;
46
+ linkReferences: LinkReferences;
47
+ generatedSources?: any[];
48
+ functionDebugData?: Record<string, any>;
49
+ };
50
+ deployedBytecode: {
51
+ object: string;
52
+ opcodes: string;
53
+ sourceMap: string;
54
+ linkReferences: LinkReferences;
55
+ immutableReferences?: Record<
56
+ string,
57
+ Array<{start: number; length: number}>
58
+ >;
59
+ generatedSources?: any[];
60
+ functionDebugData?: Record<string, any>;
61
+ };
62
+ methodIdentifiers?: Record<string, string>;
63
+ gasEstimates?: any;
64
+ };
65
+
66
+ /** Developer documentation */
67
+ devdoc?: any;
68
+
69
+ /** User documentation */
70
+ userdoc?: any;
71
+
72
+ /** Storage layout */
73
+ storageLayout?: any;
74
+ }
75
+
76
+ export interface LinkReferences {
77
+ [sourceName: string]: {
78
+ [libraryName: string]: Array<{start: number; length: number}>;
79
+ };
80
+ }
81
+
82
+ /**
83
+ * Type guard to check if an artifact is a rich artifact
84
+ */
85
+ export function isRichArtifact(
86
+ artifact: ExternalArtifact,
87
+ ): artifact is RichArtifact {
88
+ return 'solcInput' in artifact && artifact.solcInput !== undefined;
89
+ }
90
+
91
+ /**
92
+ * Function type for dynamically resolving artifacts
93
+ */
94
+ export type ArtifactResolver = () =>
95
+ | Promise<ExternalArtifact[]>
96
+ | ExternalArtifact[];
97
+
98
+ /**
99
+ * Configuration for external artifacts
100
+ */
101
+ export interface ExternalArtifactsConfig {
102
+ /**
103
+ * Paths to artifact files or directories
104
+ * - File path: loads single artifact JSON
105
+ * - Directory path: loads all .json files recursively
106
+ */
107
+ paths?: string[];
108
+
109
+ /**
110
+ * Function that resolves and returns artifacts dynamically
111
+ * Useful for loading from APIs, databases, or complex logic
112
+ */
113
+ resolver?: ArtifactResolver;
114
+
115
+ /**
116
+ * Solc version to use when creating synthetic compilations
117
+ * @default "0.8.20"
118
+ */
119
+ solcVersion?: string;
120
+
121
+ /**
122
+ * Whether to log warnings for malformed artifacts
123
+ * @default true
124
+ */
125
+ warnOnInvalidArtifacts?: boolean;
126
+
127
+ /**
128
+ * Enable debug logging to diagnose issues
129
+ * @default false
130
+ */
131
+ debug?: boolean;
132
+ }
@@ -0,0 +1,43 @@
1
+ import type {ConfigHooks} from 'hardhat/types/hooks';
2
+ import type {ExternalArtifactsConfig} from '../artifacts/types.js';
3
+
4
+ export default async (): Promise<Partial<ConfigHooks>> => {
5
+ const handlers: Partial<ConfigHooks> = {
6
+ resolveUserConfig: async (
7
+ userConfig,
8
+ resolveConfigurationVariable,
9
+ next,
10
+ ) => {
11
+ const resolvedConfig = await next(
12
+ userConfig,
13
+ resolveConfigurationVariable,
14
+ );
15
+
16
+ // Get user's external artifacts config
17
+ const externalArtifactsUserConfig = userConfig.externalArtifacts as
18
+ | ExternalArtifactsConfig
19
+ | undefined;
20
+
21
+ // Apply defaults
22
+ const externalArtifacts: Required<
23
+ Omit<ExternalArtifactsConfig, 'resolver'>
24
+ > & {
25
+ resolver?: ExternalArtifactsConfig['resolver'];
26
+ } = {
27
+ paths: externalArtifactsUserConfig?.paths ?? [],
28
+ resolver: externalArtifactsUserConfig?.resolver,
29
+ solcVersion: externalArtifactsUserConfig?.solcVersion ?? '0.8.20',
30
+ warnOnInvalidArtifacts:
31
+ externalArtifactsUserConfig?.warnOnInvalidArtifacts ?? true,
32
+ debug: externalArtifactsUserConfig?.debug ?? false,
33
+ };
34
+
35
+ return {
36
+ ...resolvedConfig,
37
+ externalArtifacts,
38
+ };
39
+ },
40
+ };
41
+
42
+ return handlers;
43
+ };