Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions yarn-project/aztec.js/src/api/abi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ export {
isWrappedFieldStruct,
isFunctionSelectorStruct,
loadContractArtifact,
loadContractArtifactWithValidation,
loadContractArtifactForPublic,
getAllFunctionAbis,
contractArtifactToBuffer,
Expand Down
4 changes: 2 additions & 2 deletions yarn-project/cli/src/utils/aztec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import {
type FunctionAbi,
FunctionType,
getAllFunctionAbis,
loadContractArtifact,
loadContractArtifactWithValidation,
} from '@aztec/aztec.js/abi';
import { EthAddress } from '@aztec/aztec.js/addresses';
import type { L1ContractsConfig } from '@aztec/ethereum/config';
Expand Down Expand Up @@ -132,7 +132,7 @@ export async function getContractArtifact(fileDir: string, log: LogFn) {
}

try {
return loadContractArtifact(JSON.parse(contents));
return loadContractArtifactWithValidation(JSON.parse(contents));
} catch (err) {
log('Invalid file used. Please try again.');
throw err;
Expand Down
18 changes: 17 additions & 1 deletion yarn-project/ethereum/src/utils.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { Abi } from 'viem';

import { mergeAbis } from './utils.js';
import { FormattedViemError, formatViemError, mergeAbis } from './utils.js';

describe('mergeAbis', () => {
it('dedupes identical function items', () => {
Expand Down Expand Up @@ -59,3 +59,19 @@ describe('mergeAbis', () => {
expect(merged).toHaveLength(2);
});
});

describe('formatViemError', () => {
it('formats an error whose cause carries non-cloneable function-valued context', () => {
// viem RPC errors routinely attach plain-object context holding functions (e.g. transport
// request methods). structuredClone throws DataCloneError on these, so formatViemError must
// not let the clone failure mask the underlying error.
const error = new Error('rpc request failed');
(error as any).cause = { code: -32000, request: { send: () => undefined } };

const formatted = formatViemError(error);

expect(formatted).toBeInstanceOf(FormattedViemError);
expect(formatted.message).toContain('rpc request failed');
expect(formatted.cause).toBe(error);
});
});
22 changes: 11 additions & 11 deletions yarn-project/ethereum/src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -235,18 +235,18 @@ export function formatViemError(error: any, abi: Abi = ErrorsAbi): FormattedViem
// If decoding fails, we fall back to the original formatting
}

// Strip ABI from the error object before formatting
// Strip ABI from the error object before formatting. We clone first to avoid mutating the
// caller's error, but structuredClone throws DataCloneError on values it cannot clone (e.g.
// viem RPC errors carrying function-valued request context). If cloning fails, fall back to
// formatting the original error untouched rather than letting the clone failure mask it.
if (error && typeof error === 'object') {
// Create a clone to avoid modifying the original
const errorClone = structuredClone(error);

// Helper function to recursively remove ABI properties

// Strip ABIs from the clone
stripAbis(errorClone);

// Use the cleaned clone for further processing
error = errorClone;
try {
const errorClone = structuredClone(error);
stripAbis(errorClone);
error = errorClone;
} catch {
// Leave `error` as the original; we skip stripAbis to avoid mutating the caller's object.
}
}

// If it's a regular Error instance, return it with its message
Expand Down
25 changes: 24 additions & 1 deletion yarn-project/stdlib/src/abi/contract_artifact.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
import { getBenchmarkContractArtifact } from '../tests/fixtures.js';
import { contractArtifactFromBuffer, contractArtifactToBuffer } from './contract_artifact.js';
import {
contractArtifactFromBuffer,
contractArtifactToBuffer,
loadContractArtifactWithValidation,
} from './contract_artifact.js';

describe('contract_artifact', () => {
it('serializes and deserializes an instance', () => {
Expand All @@ -8,4 +12,23 @@ describe('contract_artifact', () => {
const deserialized = contractArtifactFromBuffer(serialized);
expect(deserialized).toEqual(artifact);
});

describe('loadContractArtifactWithValidation', () => {
// The wire form of an already-processed artifact (hex/base64 strings) is what reaches the
// loader from a JSON file, e.g. via the CLI deploy command.
const wireForm = () => JSON.parse(contractArtifactToBuffer(getBenchmarkContractArtifact()).toString('utf-8'));

it('accepts a valid already-processed artifact', () => {
const loaded = loadContractArtifactWithValidation(wireForm());
expect(loaded.name).toEqual(getBenchmarkContractArtifact().name);
});

it('rejects an artifact that passes the shallow shape check but violates the schema', () => {
const input = wireForm();
// functionType stays a string, so the shallow isContractArtifact() heuristic still passes,
// but it is not a valid FunctionType enum value, so full schema validation must reject it.
input.functions[0].functionType = 'not-a-real-type';
expect(() => loadContractArtifactWithValidation(input)).toThrow();
});
});
});
19 changes: 19 additions & 0 deletions yarn-project/stdlib/src/abi/contract_artifact.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,25 @@ export function loadContractArtifact(input: NoirCompiledContract): ContractArtif
return generateContractArtifact(input);
}

/**
* Like {@link loadContractArtifact}, but fully validates an already-processed artifact against the
* contract artifact schema before returning it. Use when loading an artifact from untrusted or
* external JSON (e.g. a file path passed to the CLI), so a malformed artifact is rejected up-front
* with a clear schema error instead of surfacing as an opaque failure later during deployment.
*
* `loadContractArtifact` only runs the shallow `isContractArtifact` shape check on already-processed
* artifacts; raw nargo output is validated via `generateContractArtifact` regardless. The returned
* object is identical to `loadContractArtifact`'s; the schema parse is used purely for validation.
* @param input - Input object as generated by nargo compile, or an already-processed artifact.
* @returns A valid contract artifact instance.
*/
export function loadContractArtifactWithValidation(input: NoirCompiledContract): ContractArtifact {
if (isContractArtifact(input)) {
ContractArtifactSchema.parse(input);
}
return loadContractArtifact(input);
}

/**
* Gets nargo build output and returns a valid contract artifact instance.
* Differs from loadContractArtifact() by retaining all bytecode.
Expand Down
Loading