Skip to content

Commit 20029a4

Browse files
committed
gnd: Normalize ABI artifacts during build to support Hardhat/Truffle formats
Extract normalize_abi_json() to a shared abi module and use it in copy_abi_to_dir (build.rs) so that Hardhat artifacts ({"abi":[...]}) and Truffle artifacts ({"compilerOutput":{"abi":[...]}}) are stripped down to the bare ABI array before being written to the build output. Previously, a raw fs::copy would pass through the metadata wrapper, causing "expected a valid JSON ABI sequence" failures.
1 parent 69913fe commit 20029a4

4 files changed

Lines changed: 118 additions & 100 deletions

File tree

gnd/src/abi.rs

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
//! ABI normalization utilities.
2+
//!
3+
//! Handles extraction of bare ABI arrays from various artifact formats
4+
//! (raw arrays, Hardhat/Foundry, Truffle).
5+
6+
use anyhow::{anyhow, Context, Result};
7+
8+
/// Normalize ABI JSON to extract the actual ABI array from various artifact formats.
9+
///
10+
/// Supports:
11+
/// - Raw ABI array: `[{...}]`
12+
/// - Foundry/Hardhat format: `{"abi": [...], ...}`
13+
/// - Truffle format: `{"compilerOutput": {"abi": [...], ...}, ...}`
14+
pub fn normalize_abi_json(abi_str: &str) -> Result<serde_json::Value> {
15+
let value: serde_json::Value =
16+
serde_json::from_str(abi_str).context("Failed to parse ABI JSON")?;
17+
18+
// Case 1: Already an array - return as-is
19+
if value.is_array() {
20+
return Ok(value);
21+
}
22+
23+
// Case 2: Object with "abi" field (Foundry/Hardhat format)
24+
if let Some(abi) = value.get("abi") {
25+
if abi.is_array() {
26+
return Ok(abi.clone());
27+
}
28+
}
29+
30+
// Case 3: Object with "compilerOutput.abi" field (Truffle format)
31+
if let Some(compiler_output) = value.get("compilerOutput") {
32+
if let Some(abi) = compiler_output.get("abi") {
33+
if abi.is_array() {
34+
return Ok(abi.clone());
35+
}
36+
}
37+
}
38+
39+
Err(anyhow!(
40+
"Invalid ABI format: expected an array or an object with 'abi' field"
41+
))
42+
}
43+
44+
#[cfg(test)]
45+
mod tests {
46+
use super::*;
47+
48+
#[test]
49+
fn test_normalize_abi_json_raw_array() {
50+
let raw_abi = r#"[{"type": "event", "name": "Transfer"}]"#;
51+
let result = normalize_abi_json(raw_abi).unwrap();
52+
assert!(result.is_array());
53+
assert_eq!(result.as_array().unwrap().len(), 1);
54+
}
55+
56+
#[test]
57+
fn test_normalize_abi_json_hardhat_format() {
58+
let hardhat_abi = r#"{
59+
"_format": "hh-sol-artifact-1",
60+
"contractName": "MyContract",
61+
"abi": [{"type": "event", "name": "Transfer"}],
62+
"bytecode": "0x..."
63+
}"#;
64+
let result = normalize_abi_json(hardhat_abi).unwrap();
65+
assert!(result.is_array());
66+
assert_eq!(result.as_array().unwrap().len(), 1);
67+
assert_eq!(
68+
result.as_array().unwrap()[0].get("name").unwrap(),
69+
"Transfer"
70+
);
71+
}
72+
73+
#[test]
74+
fn test_normalize_abi_json_truffle_format() {
75+
let truffle_abi = r#"{
76+
"contractName": "MyContract",
77+
"compilerOutput": {
78+
"abi": [{"type": "event", "name": "Transfer"}]
79+
}
80+
}"#;
81+
let result = normalize_abi_json(truffle_abi).unwrap();
82+
assert!(result.is_array());
83+
assert_eq!(result.as_array().unwrap().len(), 1);
84+
assert_eq!(
85+
result.as_array().unwrap()[0].get("name").unwrap(),
86+
"Transfer"
87+
);
88+
}
89+
90+
#[test]
91+
fn test_normalize_abi_json_invalid_format() {
92+
let invalid_abi = r#"{"contractName": "MyContract"}"#;
93+
let result = normalize_abi_json(invalid_abi);
94+
assert!(result.is_err());
95+
assert!(result
96+
.unwrap_err()
97+
.to_string()
98+
.contains("Invalid ABI format"));
99+
}
100+
}

gnd/src/commands/build.rs

Lines changed: 16 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ use anyhow::{anyhow, Context, Result};
1212
use clap::Parser;
1313
use sha1::{Digest, Sha1};
1414

15+
use crate::abi::normalize_abi_json;
1516
use crate::compiler::{compile_mapping, find_graph_ts, AscCompileOptions};
1617
use crate::config::{apply_network_config, get_network_config, load_networks_config};
1718
use crate::manifest::{load_manifest, resolve_path, DataSource, Manifest, Template};
@@ -649,10 +650,10 @@ fn copy_schema(schema_path: &Path, output_dir: &Path) -> Result<()> {
649650
Ok(())
650651
}
651652

652-
/// Copy an ABI file to the specified output directory.
653-
///
654-
/// This unified function handles both data source and template ABIs.
655-
/// The caller specifies the output directory.
653+
/// Copy an ABI file to the specified output directory, normalizing it to a bare
654+
/// ABI array. This handles Hardhat/Foundry artifacts (`{"abi": [...]}`) and
655+
/// Truffle artifacts (`{"compilerOutput": {"abi": [...]}}`) in addition to raw
656+
/// ABI arrays.
656657
fn copy_abi_to_dir(abi_name: &str, abi_path: &Path, output_subdir: &Path) -> Result<()> {
657658
fs::create_dir_all(output_subdir)?;
658659

@@ -664,13 +665,17 @@ fn copy_abi_to_dir(abi_name: &str, abi_path: &Path, output_subdir: &Path) -> Res
664665
&format!("Copy ABI {} to {}", abi_name, output_path.display()),
665666
);
666667

667-
fs::copy(abi_path, &output_path).with_context(|| {
668-
format!(
669-
"Failed to copy ABI from {} to {}",
670-
abi_path.display(),
671-
output_path.display()
672-
)
673-
})?;
668+
let abi_str = fs::read_to_string(abi_path)
669+
.with_context(|| format!("Failed to read ABI file: {}", abi_path.display()))?;
670+
671+
let normalized = normalize_abi_json(&abi_str)
672+
.with_context(|| format!("Failed to normalize ABI: {}", abi_path.display()))?;
673+
674+
let output_str =
675+
serde_json::to_string_pretty(&normalized).context("Failed to serialize normalized ABI")?;
676+
677+
fs::write(&output_path, output_str)
678+
.with_context(|| format!("Failed to write ABI to {}", output_path.display()))?;
674679

675680
Ok(())
676681
}

gnd/src/commands/codegen.rs

Lines changed: 1 addition & 89 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ use ethabi::Contract;
1515
use graphql_parser::schema as gql;
1616
use semver::Version;
1717

18+
use crate::abi::normalize_abi_json;
1819
use crate::codegen::{
1920
AbiCodeGenerator, Class, ModuleImports, SchemaCodeGenerator, Template as CodegenTemplate,
2021
TemplateCodeGenerator, TemplateKind, GENERATED_FILE_NOTE,
@@ -220,42 +221,6 @@ fn generate_schema_types(
220221
Ok(true)
221222
}
222223

223-
/// Normalize ABI JSON to extract the actual ABI array from various artifact formats.
224-
///
225-
/// Supports:
226-
/// - Raw ABI array: `[{...}]`
227-
/// - Foundry/Hardhat format: `{"abi": [...], ...}`
228-
/// - Truffle format: `{"compilerOutput": {"abi": [...], ...}, ...}`
229-
fn normalize_abi_json(abi_str: &str) -> Result<serde_json::Value> {
230-
let value: serde_json::Value =
231-
serde_json::from_str(abi_str).context("Failed to parse ABI JSON")?;
232-
233-
// Case 1: Already an array - return as-is
234-
if value.is_array() {
235-
return Ok(value);
236-
}
237-
238-
// Case 2: Object with "abi" field (Foundry/Hardhat format)
239-
if let Some(abi) = value.get("abi") {
240-
if abi.is_array() {
241-
return Ok(abi.clone());
242-
}
243-
}
244-
245-
// Case 3: Object with "compilerOutput.abi" field (Truffle format)
246-
if let Some(compiler_output) = value.get("compilerOutput") {
247-
if let Some(abi) = compiler_output.get("abi") {
248-
if abi.is_array() {
249-
return Ok(abi.clone());
250-
}
251-
}
252-
}
253-
254-
Err(anyhow!(
255-
"Invalid ABI format: expected an array or an object with 'abi' field"
256-
))
257-
}
258-
259224
/// Preprocess ABI JSON to add default names for unnamed parameters.
260225
/// The ethabi crate requires all parameters to have names, but Solidity ABIs
261226
/// can have unnamed parameters. This function adds `param0`, `param1`, etc.
@@ -853,57 +818,4 @@ dataSources:
853818
"Contract should have static bind method"
854819
);
855820
}
856-
857-
#[test]
858-
fn test_normalize_abi_json_raw_array() {
859-
let raw_abi = r#"[{"type": "event", "name": "Transfer"}]"#;
860-
let result = normalize_abi_json(raw_abi).unwrap();
861-
assert!(result.is_array());
862-
assert_eq!(result.as_array().unwrap().len(), 1);
863-
}
864-
865-
#[test]
866-
fn test_normalize_abi_json_hardhat_format() {
867-
let hardhat_abi = r#"{
868-
"_format": "hh-sol-artifact-1",
869-
"contractName": "MyContract",
870-
"abi": [{"type": "event", "name": "Transfer"}],
871-
"bytecode": "0x..."
872-
}"#;
873-
let result = normalize_abi_json(hardhat_abi).unwrap();
874-
assert!(result.is_array());
875-
assert_eq!(result.as_array().unwrap().len(), 1);
876-
assert_eq!(
877-
result.as_array().unwrap()[0].get("name").unwrap(),
878-
"Transfer"
879-
);
880-
}
881-
882-
#[test]
883-
fn test_normalize_abi_json_truffle_format() {
884-
let truffle_abi = r#"{
885-
"contractName": "MyContract",
886-
"compilerOutput": {
887-
"abi": [{"type": "event", "name": "Transfer"}]
888-
}
889-
}"#;
890-
let result = normalize_abi_json(truffle_abi).unwrap();
891-
assert!(result.is_array());
892-
assert_eq!(result.as_array().unwrap().len(), 1);
893-
assert_eq!(
894-
result.as_array().unwrap()[0].get("name").unwrap(),
895-
"Transfer"
896-
);
897-
}
898-
899-
#[test]
900-
fn test_normalize_abi_json_invalid_format() {
901-
let invalid_abi = r#"{"contractName": "MyContract"}"#;
902-
let result = normalize_abi_json(invalid_abi);
903-
assert!(result.is_err());
904-
assert!(result
905-
.unwrap_err()
906-
.to_string()
907-
.contains("Invalid ABI format"));
908-
}
909821
}

gnd/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
pub mod abi;
12
pub mod codegen;
23
pub mod commands;
34
pub mod compiler;

0 commit comments

Comments
 (0)