@openzeppelin/upgrades-core 1.44.2 → 1.45.0-alpha.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.
@@ -129,7 +129,7 @@ async function exists(dir: string) {
129
129
 
130
130
  async function getJsonFiles(dir: string): Promise<string[]> {
131
131
  const files = await fs.readdir(dir);
132
- const jsonFiles = files.filter(file => file.endsWith('.json'));
132
+ const jsonFiles = files.filter(file => file.endsWith('.json') && !file.endsWith('.output.json'));
133
133
  return jsonFiles.map(file => path.join(dir, file));
134
134
  }
135
135
 
@@ -137,44 +137,169 @@ async function readBuildInfo(buildInfoFilePaths: string[], dirShortName: string)
137
137
  const buildInfoFiles: BuildInfoFile[] = [];
138
138
 
139
139
  for (const buildInfoFilePath of buildInfoFilePaths) {
140
- const buildInfoJson = await readJSON(buildInfoFilePath);
141
- if (
142
- buildInfoJson.input === undefined ||
143
- buildInfoJson.output === undefined ||
144
- buildInfoJson.solcVersion === undefined
145
- ) {
140
+ const buildInfo = await loadBuildInfo(buildInfoFilePath);
141
+
142
+ checkOutputSelection(buildInfo.input, buildInfoFilePath);
143
+
144
+ buildInfoFiles.push({
145
+ input: buildInfo.input,
146
+ output: buildInfo.output,
147
+ solcVersion: buildInfo.solcVersion,
148
+ dirShortName,
149
+ });
150
+ }
151
+ return buildInfoFiles;
152
+ }
153
+
154
+ const FOUNDRY_BUILD_INFO_HELP = `\
155
+ Foundry build-info files must contain Solidity compiler input, output, and solcVersion.
156
+ Ensure foundry.toml has:
157
+ [profile.default]
158
+ build_info = true
159
+ extra_output = ["storageLayout"]
160
+ Then run: ${FOUNDRY_COMPILE_COMMAND}`;
161
+
162
+ const HH3_BUILD_INFO_HELP = `\
163
+ Hardhat 3's build-info uses a main .json file (input, solcVersion) and a separate .output.json (output).
164
+ Ensure you compile with Hardhat 3 so that artifacts/build-info/ contains both the main files and the .output.json files.
165
+ Run: ${HARDHAT_COMPILE_COMMAND}`;
166
+
167
+ const GENERIC_BUILD_INFO_HELP = `\
168
+ If using Foundry, ensure foundry.toml has build_info = true and extra_output = ["storageLayout"], then compile with '${FOUNDRY_COMPILE_COMMAND}'.
169
+ If using Hardhat, compile with '${HARDHAT_COMPILE_COMMAND}'.`;
170
+
171
+ async function loadBuildInfo(buildInfoFilePath: string): Promise<{
172
+ input: SolcInput;
173
+ output: SolcOutput;
174
+ solcVersion: string;
175
+ }> {
176
+ const buildInfoJson = await readJSON(buildInfoFilePath);
177
+
178
+ if (
179
+ buildInfoJson.input !== undefined &&
180
+ buildInfoJson.output !== undefined &&
181
+ buildInfoJson.solcVersion !== undefined
182
+ ) {
183
+ return {
184
+ input: buildInfoJson.input,
185
+ output: buildInfoJson.output,
186
+ solcVersion: buildInfoJson.solcVersion,
187
+ };
188
+ }
189
+
190
+ const format = buildInfoJson._format as string | undefined;
191
+
192
+ // Foundry (ethers-rs-sol-build-info-1) normally has input, output, solcVersion at top level.
193
+ // If we reach here with Foundry format, a field is missing — suggest recompiling.
194
+ if (typeof format === 'string' && format.startsWith('ethers-rs-sol-build-info')) {
195
+ throw new ValidateCommandError(
196
+ `Build info file ${buildInfoFilePath} must contain Solidity compiler input, output, and solcVersion. Got format: ${format}.`,
197
+ () => FOUNDRY_BUILD_INFO_HELP,
198
+ );
199
+ }
200
+
201
+ if (typeof format === 'string' && format.startsWith('hh3-sol-build-info')) {
202
+ if (buildInfoJson.input === undefined || buildInfoJson.solcVersion === undefined) {
146
203
  throw new ValidateCommandError(
147
- `Build info file ${buildInfoFilePath} must contain Solidity compiler input, output, and solcVersion.`,
204
+ `Build info file ${buildInfoFilePath} (Hardhat 3 format) must contain input and solcVersion. Got format: ${format}.`,
205
+ () => HH3_BUILD_INFO_HELP,
148
206
  );
149
- } else {
150
- checkOutputSelection(buildInfoJson, buildInfoFilePath);
151
-
152
- buildInfoFiles.push({
153
- input: buildInfoJson.input,
154
- output: buildInfoJson.output,
155
- solcVersion: buildInfoJson.solcVersion,
156
- dirShortName: dirShortName,
157
- });
158
207
  }
208
+ let inputData: SolcInput = buildInfoJson.input;
209
+ let outputData: SolcOutput | undefined = buildInfoJson.output;
210
+
211
+ if (outputData === undefined) {
212
+ const { dir, name } = path.parse(buildInfoFilePath);
213
+ const outputFilePath = path.join(dir, `${name}.output.json`);
214
+
215
+ try {
216
+ const outputJson = await readJSON(outputFilePath);
217
+ outputData = outputJson.output ?? outputJson;
218
+ } catch (error) {
219
+ throw new ValidateCommandError(
220
+ `Build info file ${buildInfoFilePath} does not contain output, and output file ${outputFilePath} could not be read.`,
221
+ () => HH3_BUILD_INFO_HELP,
222
+ );
223
+ }
224
+
225
+ if (outputData === undefined) {
226
+ throw new ValidateCommandError(
227
+ `Build info file ${buildInfoFilePath} does not contain output, and output file ${outputFilePath} is missing Solidity compiler output.`,
228
+ () => HH3_BUILD_INFO_HELP,
229
+ );
230
+ }
231
+ }
232
+
233
+ const userSourceNameMap: Record<string, string> | undefined = buildInfoJson.userSourceNameMap;
234
+ if (userSourceNameMap !== undefined) {
235
+ const canonicalToUser: Record<string, string> = {};
236
+ for (const [userSource, canonicalSource] of Object.entries(userSourceNameMap)) {
237
+ canonicalToUser[canonicalSource] = userSource;
238
+ }
239
+
240
+ if (inputData.sources !== undefined) {
241
+ inputData = {
242
+ ...inputData,
243
+ sources: remapKeys(inputData.sources, canonicalToUser),
244
+ };
245
+ }
246
+
247
+ if (outputData.sources !== undefined) {
248
+ outputData = {
249
+ ...outputData,
250
+ sources: remapKeys(outputData.sources, canonicalToUser),
251
+ contracts:
252
+ outputData.contracts !== undefined
253
+ ? remapKeys(outputData.contracts, canonicalToUser)
254
+ : outputData.contracts,
255
+ };
256
+ } else if (outputData.contracts !== undefined) {
257
+ outputData = {
258
+ ...outputData,
259
+ contracts: remapKeys(outputData.contracts, canonicalToUser),
260
+ };
261
+ }
262
+ }
263
+
264
+ return {
265
+ input: inputData,
266
+ output: outputData,
267
+ solcVersion: buildInfoJson.solcVersion,
268
+ };
159
269
  }
160
- return buildInfoFiles;
270
+
271
+ throw new ValidateCommandError(
272
+ `Build info from ${buildInfoFilePath} must include Solidity compiler input, output, and solcVersion. Got format: ${format ?? 'unknown'}.`,
273
+ () => GENERIC_BUILD_INFO_HELP,
274
+ );
161
275
  }
162
276
 
163
277
  /**
164
278
  * Gives an error if there is empty output selection for any contract, or a contract does not have storage layout.
165
279
  */
166
- function checkOutputSelection(buildInfoJson: any, buildInfoFilePath: string) {
167
- const o = buildInfoJson.input.settings?.outputSelection;
280
+ function checkOutputSelection(input: SolcInput, buildInfoFilePath: string) {
281
+ const outputSelection = input.settings?.outputSelection;
282
+
283
+ if (outputSelection === undefined) {
284
+ throw new ValidateCommandError(
285
+ `Build info file ${buildInfoFilePath} does not define compiler outputSelection.`,
286
+ () => STORAGE_LAYOUT_HELP,
287
+ );
288
+ }
289
+
290
+ Object.keys(outputSelection).forEach(item => {
291
+ const selection = outputSelection[item] ?? {};
292
+ const unnamed = Array.isArray(selection['']) ? selection[''] : [];
293
+ const wildcard = Array.isArray(selection['*']) ? selection['*'] : [];
168
294
 
169
- return Object.keys(o).forEach((item: any) => {
170
- if ((o[item][''] === undefined || o[item][''].length === 0) && o[item]['*'].length === 0) {
295
+ if (unnamed.length === 0 && wildcard.length === 0) {
171
296
  // No outputs at all for this contract e.g. if there were no changes since the last compile in Foundry.
172
297
  // This is not supported for now, since it leads to AST nodes that reference node ids in other build-info files.
173
298
  throw new ValidateCommandError(
174
299
  `Build info file ${buildInfoFilePath} is not from a full compilation.`,
175
300
  () => PARTIAL_COMPILE_HELP,
176
301
  );
177
- } else if (!o[item]['*'].includes('storageLayout')) {
302
+ } else if (!wildcard.includes('storageLayout')) {
178
303
  throw new ValidateCommandError(
179
304
  `Build info file ${buildInfoFilePath} does not contain storage layout for all contracts.`,
180
305
  () => STORAGE_LAYOUT_HELP,
@@ -186,3 +311,12 @@ function checkOutputSelection(buildInfoJson: any, buildInfoFilePath: string) {
186
311
  async function readJSON(path: string) {
187
312
  return JSON.parse(await fs.readFile(path, 'utf8'));
188
313
  }
314
+
315
+ function remapKeys<T>(original: Record<string, T>, canonicalToUser: Record<string, string>): Record<string, T> {
316
+ const remapped: Record<string, T> = {};
317
+ for (const [key, value] of Object.entries(original)) {
318
+ const mappedKey = canonicalToUser[key] ?? key;
319
+ remapped[mappedKey] = value;
320
+ }
321
+ return remapped;
322
+ }
@@ -20,13 +20,18 @@ export class InvalidBeacon extends UpgradesError {}
20
20
  * @throws {InvalidBeacon} If the implementation() function could not be called or does not return an address.
21
21
  */
22
22
  export async function getImplementationAddressFromBeacon(
23
- provider: EthereumProvider,
23
+ provider: EthereumProvider, // v2 may differ from v3
24
24
  beaconAddress: string,
25
25
  ): Promise<string> {
26
26
  const impl = await callOptionalSignature(provider, beaconAddress, 'implementation()');
27
+
27
28
  let parsedImplAddress;
28
29
  if (impl !== undefined) {
29
- parsedImplAddress = parseAddress(impl);
30
+ try {
31
+ parsedImplAddress = parseAddress(impl);
32
+ } catch (parseErr) {
33
+ throw new InvalidBeacon(`Contract at ${beaconAddress} doesn't look like a beacon`);
34
+ }
30
35
  }
31
36
 
32
37
  if (parsedImplAddress === undefined) {