From 854e8d9690f5449dcc7a952a4df518fc644b09e8 Mon Sep 17 00:00:00 2001 From: Omar Date: Fri, 18 Jul 2025 14:22:13 +0300 Subject: [PATCH 1/2] Fix `deserialization error: invalid value: string "0x2d79dd80ff729c000"` (#34) * Introduce a custom kitchensink network * fix formatting * Added `--dev` to `substrate-node` arguments. This commit adds the `--dev` argument to the `substrate-node` to allow the chain to keep advancing as time goes own. We have found that if this option is not added then the chain won't advance forward. * fix clippy warning * fix clippy warning --- Cargo.lock | 1 + crates/node/Cargo.toml | 2 + crates/node/src/kitchensink.rs | 468 ++++++++++++++++++++++++++++++++- 3 files changed, 468 insertions(+), 3 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index eebc456..b7cfb19 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3998,6 +3998,7 @@ dependencies = [ "anyhow", "revive-dt-config", "revive-dt-node-interaction", + "serde", "serde_json", "sp-core", "sp-runtime", diff --git a/crates/node/Cargo.toml b/crates/node/Cargo.toml index 790c5ba..48b5e2d 100644 --- a/crates/node/Cargo.toml +++ b/crates/node/Cargo.toml @@ -17,6 +17,7 @@ tokio = { workspace = true } revive-dt-node-interaction = { workspace = true } revive-dt-config = { workspace = true } +serde = { workspace = true } serde_json = { workspace = true } sp-core = { workspace = true } @@ -24,3 +25,4 @@ sp-runtime = { workspace = true } [dev-dependencies] temp-dir = { workspace = true } +tokio = { workspace = true } diff --git a/crates/node/src/kitchensink.rs b/crates/node/src/kitchensink.rs index 768c1aa..fbb36d4 100644 --- a/crates/node/src/kitchensink.rs +++ b/crates/node/src/kitchensink.rs @@ -12,15 +12,21 @@ use std::{ }; use alloy::{ + consensus::{BlockHeader, TxEnvelope}, hex, - network::EthereumWallet, - primitives::Address, + network::{ + Ethereum, EthereumWallet, Network, TransactionBuilder, TransactionBuilderError, + UnbuiltTransactionError, + }, + primitives::{Address, B64, B256, BlockNumber, Bloom, Bytes, U256}, providers::{Provider, ProviderBuilder, ext::DebugApi}, rpc::types::{ TransactionReceipt, + eth::{Block, Header, Transaction}, trace::geth::{DiffMode, GethDebugTracingOptions, PreStateConfig, PreStateFrame}, }, }; +use serde::{Deserialize, Serialize}; use serde_json::{Value as JsonValue, json}; use sp_core::crypto::Ss58Codec; use sp_runtime::AccountId32; @@ -157,6 +163,7 @@ impl KitchensinkNode { .clone() .open(self.kitchensink_stderr_log_file_path())?; self.process_substrate = Command::new(&self.substrate_binary) + .arg("--dev") .arg("--chain") .arg(chainspec_path) .arg("--base-path") @@ -339,6 +346,7 @@ impl EthereumNode for KitchensinkNode { tracing::info!("Submitting tx to kitchensink"); let receipt = BlockingExecutor::execute(async move { Ok(ProviderBuilder::new() + .network::() .wallet(wallet) .connect(&url) .await? @@ -367,6 +375,7 @@ impl EthereumNode for KitchensinkNode { BlockingExecutor::execute(async move { Ok(ProviderBuilder::new() + .network::() .wallet(wallet) .connect(&url) .await? @@ -491,15 +500,437 @@ impl Drop for KitchensinkNode { } } +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +struct KitchenSinkNetwork; + +impl Network for KitchenSinkNetwork { + type TxType = ::TxType; + + type TxEnvelope = ::TxEnvelope; + + type UnsignedTx = ::UnsignedTx; + + type ReceiptEnvelope = ::ReceiptEnvelope; + + type Header = KitchenSinkHeader; + + type TransactionRequest = ::TransactionRequest; + + type TransactionResponse = ::TransactionResponse; + + type ReceiptResponse = ::ReceiptResponse; + + type HeaderResponse = Header; + + type BlockResponse = Block, Header>; +} + +impl TransactionBuilder for ::TransactionRequest { + fn chain_id(&self) -> Option { + <::TransactionRequest as TransactionBuilder>::chain_id(self) + } + + fn set_chain_id(&mut self, chain_id: alloy::primitives::ChainId) { + <::TransactionRequest as TransactionBuilder>::set_chain_id( + self, chain_id, + ) + } + + fn nonce(&self) -> Option { + <::TransactionRequest as TransactionBuilder>::nonce(self) + } + + fn set_nonce(&mut self, nonce: u64) { + <::TransactionRequest as TransactionBuilder>::set_nonce( + self, nonce, + ) + } + + fn input(&self) -> Option<&alloy::primitives::Bytes> { + <::TransactionRequest as TransactionBuilder>::input(self) + } + + fn set_input>(&mut self, input: T) { + <::TransactionRequest as TransactionBuilder>::set_input( + self, input, + ) + } + + fn from(&self) -> Option
{ + <::TransactionRequest as TransactionBuilder>::from(self) + } + + fn set_from(&mut self, from: Address) { + <::TransactionRequest as TransactionBuilder>::set_from( + self, from, + ) + } + + fn kind(&self) -> Option { + <::TransactionRequest as TransactionBuilder>::kind(self) + } + + fn clear_kind(&mut self) { + <::TransactionRequest as TransactionBuilder>::clear_kind( + self, + ) + } + + fn set_kind(&mut self, kind: alloy::primitives::TxKind) { + <::TransactionRequest as TransactionBuilder>::set_kind( + self, kind, + ) + } + + fn value(&self) -> Option { + <::TransactionRequest as TransactionBuilder>::value(self) + } + + fn set_value(&mut self, value: alloy::primitives::U256) { + <::TransactionRequest as TransactionBuilder>::set_value( + self, value, + ) + } + + fn gas_price(&self) -> Option { + <::TransactionRequest as TransactionBuilder>::gas_price(self) + } + + fn set_gas_price(&mut self, gas_price: u128) { + <::TransactionRequest as TransactionBuilder>::set_gas_price( + self, gas_price, + ) + } + + fn max_fee_per_gas(&self) -> Option { + <::TransactionRequest as TransactionBuilder>::max_fee_per_gas( + self, + ) + } + + fn set_max_fee_per_gas(&mut self, max_fee_per_gas: u128) { + <::TransactionRequest as TransactionBuilder>::set_max_fee_per_gas( + self, max_fee_per_gas + ) + } + + fn max_priority_fee_per_gas(&self) -> Option { + <::TransactionRequest as TransactionBuilder>::max_priority_fee_per_gas( + self, + ) + } + + fn set_max_priority_fee_per_gas(&mut self, max_priority_fee_per_gas: u128) { + <::TransactionRequest as TransactionBuilder>::set_max_priority_fee_per_gas( + self, max_priority_fee_per_gas + ) + } + + fn gas_limit(&self) -> Option { + <::TransactionRequest as TransactionBuilder>::gas_limit(self) + } + + fn set_gas_limit(&mut self, gas_limit: u64) { + <::TransactionRequest as TransactionBuilder>::set_gas_limit( + self, gas_limit, + ) + } + + fn access_list(&self) -> Option<&alloy::rpc::types::AccessList> { + <::TransactionRequest as TransactionBuilder>::access_list( + self, + ) + } + + fn set_access_list(&mut self, access_list: alloy::rpc::types::AccessList) { + <::TransactionRequest as TransactionBuilder>::set_access_list( + self, + access_list, + ) + } + + fn complete_type( + &self, + ty: ::TxType, + ) -> Result<(), Vec<&'static str>> { + <::TransactionRequest as TransactionBuilder>::complete_type( + self, ty, + ) + } + + fn can_submit(&self) -> bool { + <::TransactionRequest as TransactionBuilder>::can_submit( + self, + ) + } + + fn can_build(&self) -> bool { + <::TransactionRequest as TransactionBuilder>::can_build(self) + } + + fn output_tx_type(&self) -> ::TxType { + <::TransactionRequest as TransactionBuilder>::output_tx_type( + self, + ) + } + + fn output_tx_type_checked(&self) -> Option<::TxType> { + <::TransactionRequest as TransactionBuilder>::output_tx_type_checked( + self, + ) + } + + fn prep_for_submission(&mut self) { + <::TransactionRequest as TransactionBuilder>::prep_for_submission( + self, + ) + } + + fn build_unsigned( + self, + ) -> alloy::network::BuildResult<::UnsignedTx, KitchenSinkNetwork> + { + let result = <::TransactionRequest as TransactionBuilder>::build_unsigned( + self, + ); + match result { + Ok(unsigned_tx) => Ok(unsigned_tx), + Err(UnbuiltTransactionError { request, error }) => { + Err(UnbuiltTransactionError:: { + request, + error: match error { + TransactionBuilderError::InvalidTransactionRequest(tx_type, items) => { + TransactionBuilderError::InvalidTransactionRequest(tx_type, items) + } + TransactionBuilderError::UnsupportedSignatureType => { + TransactionBuilderError::UnsupportedSignatureType + } + TransactionBuilderError::Signer(error) => { + TransactionBuilderError::Signer(error) + } + TransactionBuilderError::Custom(error) => { + TransactionBuilderError::Custom(error) + } + }, + }) + } + } + } + + async fn build>( + self, + wallet: &W, + ) -> Result< + ::TxEnvelope, + TransactionBuilderError, + > { + Ok(wallet.sign_request(self).await?) + } +} + +#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct KitchenSinkHeader { + /// The Keccak 256-bit hash of the parent + /// block’s header, in its entirety; formally Hp. + pub parent_hash: B256, + /// The Keccak 256-bit hash of the ommers list portion of this block; formally Ho. + #[serde(rename = "sha3Uncles", alias = "ommersHash")] + pub ommers_hash: B256, + /// The 160-bit address to which all fees collected from the successful mining of this block + /// be transferred; formally Hc. + #[serde(rename = "miner", alias = "beneficiary")] + pub beneficiary: Address, + /// The Keccak 256-bit hash of the root node of the state trie, after all transactions are + /// executed and finalisations applied; formally Hr. + pub state_root: B256, + /// The Keccak 256-bit hash of the root node of the trie structure populated with each + /// transaction in the transactions list portion of the block; formally Ht. + pub transactions_root: B256, + /// The Keccak 256-bit hash of the root node of the trie structure populated with the receipts + /// of each transaction in the transactions list portion of the block; formally He. + pub receipts_root: B256, + /// The Bloom filter composed from indexable information (logger address and log topics) + /// contained in each log entry from the receipt of each transaction in the transactions list; + /// formally Hb. + pub logs_bloom: Bloom, + /// A scalar value corresponding to the difficulty level of this block. This can be calculated + /// from the previous block’s difficulty level and the timestamp; formally Hd. + pub difficulty: U256, + /// A scalar value equal to the number of ancestor blocks. The genesis block has a number of + /// zero; formally Hi. + #[serde(with = "alloy::serde::quantity")] + pub number: BlockNumber, + /// A scalar value equal to the current limit of gas expenditure per block; formally Hl. + // This is the main difference over the Ethereum network implementation. We use u128 here and + // not u64. + #[serde(with = "alloy::serde::quantity")] + pub gas_limit: u128, + /// A scalar value equal to the total gas used in transactions in this block; formally Hg. + #[serde(with = "alloy::serde::quantity")] + pub gas_used: u64, + /// A scalar value equal to the reasonable output of Unix’s time() at this block’s inception; + /// formally Hs. + #[serde(with = "alloy::serde::quantity")] + pub timestamp: u64, + /// An arbitrary byte array containing data relevant to this block. This must be 32 bytes or + /// fewer; formally Hx. + pub extra_data: Bytes, + /// A 256-bit hash which, combined with the + /// nonce, proves that a sufficient amount of computation has been carried out on this block; + /// formally Hm. + pub mix_hash: B256, + /// A 64-bit value which, combined with the mixhash, proves that a sufficient amount of + /// computation has been carried out on this block; formally Hn. + pub nonce: B64, + /// A scalar representing EIP1559 base fee which can move up or down each block according + /// to a formula which is a function of gas used in parent block and gas target + /// (block gas limit divided by elasticity multiplier) of parent block. + /// The algorithm results in the base fee per gas increasing when blocks are + /// above the gas target, and decreasing when blocks are below the gas target. The base fee per + /// gas is burned. + #[serde( + default, + with = "alloy::serde::quantity::opt", + skip_serializing_if = "Option::is_none" + )] + pub base_fee_per_gas: Option, + /// The Keccak 256-bit hash of the withdrawals list portion of this block. + /// + #[serde(default, skip_serializing_if = "Option::is_none")] + pub withdrawals_root: Option, + /// The total amount of blob gas consumed by the transactions within the block, added in + /// EIP-4844. + #[serde( + default, + with = "alloy::serde::quantity::opt", + skip_serializing_if = "Option::is_none" + )] + pub blob_gas_used: Option, + /// A running total of blob gas consumed in excess of the target, prior to the block. Blocks + /// with above-target blob gas consumption increase this value, blocks with below-target blob + /// gas consumption decrease it (bounded at 0). This was added in EIP-4844. + #[serde( + default, + with = "alloy::serde::quantity::opt", + skip_serializing_if = "Option::is_none" + )] + pub excess_blob_gas: Option, + /// The hash of the parent beacon block's root is included in execution blocks, as proposed by + /// EIP-4788. + /// + /// This enables trust-minimized access to consensus state, supporting staking pools, bridges, + /// and more. + /// + /// The beacon roots contract handles root storage, enhancing Ethereum's functionalities. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub parent_beacon_block_root: Option, + /// The Keccak 256-bit hash of the an RLP encoded list with each + /// [EIP-7685] request in the block body. + /// + /// [EIP-7685]: https://eips.ethereum.org/EIPS/eip-7685 + #[serde(default, skip_serializing_if = "Option::is_none")] + pub requests_hash: Option, +} + +impl BlockHeader for KitchenSinkHeader { + fn parent_hash(&self) -> B256 { + self.parent_hash + } + + fn ommers_hash(&self) -> B256 { + self.ommers_hash + } + + fn beneficiary(&self) -> Address { + self.beneficiary + } + + fn state_root(&self) -> B256 { + self.state_root + } + + fn transactions_root(&self) -> B256 { + self.transactions_root + } + + fn receipts_root(&self) -> B256 { + self.receipts_root + } + + fn withdrawals_root(&self) -> Option { + self.withdrawals_root + } + + fn logs_bloom(&self) -> Bloom { + self.logs_bloom + } + + fn difficulty(&self) -> U256 { + self.difficulty + } + + fn number(&self) -> BlockNumber { + self.number + } + + // There's sadly nothing that we can do about this. We're required to implement this trait on + // any type that represents a header and the gas limit type used here is a u64. + fn gas_limit(&self) -> u64 { + self.gas_limit.try_into().unwrap_or(u64::MAX) + } + + fn gas_used(&self) -> u64 { + self.gas_used + } + + fn timestamp(&self) -> u64 { + self.timestamp + } + + fn mix_hash(&self) -> Option { + Some(self.mix_hash) + } + + fn nonce(&self) -> Option { + Some(self.nonce) + } + + fn base_fee_per_gas(&self) -> Option { + self.base_fee_per_gas + } + + fn blob_gas_used(&self) -> Option { + self.blob_gas_used + } + + fn excess_blob_gas(&self) -> Option { + self.excess_blob_gas + } + + fn parent_beacon_block_root(&self) -> Option { + self.parent_beacon_block_root + } + + fn requests_hash(&self) -> Option { + self.requests_hash + } + + fn extra_data(&self) -> &Bytes { + &self.extra_data + } +} + #[cfg(test)] mod tests { + use alloy::rpc::types::TransactionRequest; use revive_dt_config::Arguments; use std::path::PathBuf; use temp_dir::TempDir; use std::fs; - use super::KitchensinkNode; + use super::*; use crate::{GENESIS_JSON, Node}; fn test_config() -> (Arguments, TempDir) { @@ -514,6 +945,37 @@ mod tests { (config, temp_dir) } + #[tokio::test] + async fn node_mines_simple_transfer_transaction_and_returns_receipt() { + // Arrange + let (args, _temp_dir) = test_config(); + let mut node = KitchensinkNode::new(&args); + node.spawn(GENESIS_JSON.to_owned()) + .expect("Failed to spawn the node"); + + let provider = ProviderBuilder::new() + .network::() + .wallet(args.wallet()) + .connect(&node.rpc_url) + .await + .expect("Failed to create provider"); + + let account_address = args.wallet().default_signer().address(); + let transaction = TransactionRequest::default() + .to(account_address) + .value(U256::from(100_000_000_000_000u128)); + + // Act + let receipt = provider.send_transaction(transaction).await; + + // Assert + let _ = receipt + .expect("Failed to send the transfer transaction") + .get_receipt() + .await + .expect("Failed to get the receipt for the transfer"); + } + #[test] fn test_init_generates_chainspec_with_balances() { let genesis_content = r#" From 2bee2d5c8b098b237578d80fa80329d4b08bcd60 Mon Sep 17 00:00:00 2001 From: Omar Date: Fri, 18 Jul 2025 14:22:51 +0300 Subject: [PATCH 2/2] Fix the ABI finding logic (#38) * Fix the ABI finding logic * Implement ABI fix in the compiler trait impl --- crates/compiler/src/lib.rs | 1 + crates/compiler/src/revive_resolc.rs | 57 +++++++++++++++++--- crates/compiler/src/solc.rs | 7 +++ crates/core/src/driver/mod.rs | 77 ++++++++++++---------------- 4 files changed, 92 insertions(+), 50 deletions(-) diff --git a/crates/compiler/src/lib.rs b/crates/compiler/src/lib.rs index e43a527..3c7e173 100644 --- a/crates/compiler/src/lib.rs +++ b/crates/compiler/src/lib.rs @@ -49,6 +49,7 @@ pub struct CompilerInput { } /// The generic compilation output configuration. +#[derive(Debug)] pub struct CompilerOutput { /// The solc standard JSON input. pub input: CompilerInput, diff --git a/crates/compiler/src/revive_resolc.rs b/crates/compiler/src/revive_resolc.rs index 501a2f9..c808c1c 100644 --- a/crates/compiler/src/revive_resolc.rs +++ b/crates/compiler/src/revive_resolc.rs @@ -11,6 +11,7 @@ use revive_dt_config::Arguments; use revive_solc_json_interface::SolcStandardJsonOutput; /// A wrapper around the `resolc` binary, emitting PVM-compatible bytecode. +#[derive(Debug)] pub struct Resolc { /// Path to the `resolc` executable resolc_path: PathBuf, @@ -19,6 +20,7 @@ pub struct Resolc { impl SolidityCompiler for Resolc { type Options = Vec; + #[tracing::instrument(level = "debug", ret)] fn build( &self, input: CompilerInput, @@ -69,12 +71,13 @@ impl SolidityCompiler for Resolc { }); } - let parsed = serde_json::from_slice::(&stdout).map_err(|e| { - anyhow::anyhow!( - "failed to parse resolc JSON output: {e}\nstderr: {}", - String::from_utf8_lossy(&stderr) - ) - })?; + let mut parsed = + serde_json::from_slice::(&stdout).map_err(|e| { + anyhow::anyhow!( + "failed to parse resolc JSON output: {e}\nstderr: {}", + String::from_utf8_lossy(&stderr) + ) + })?; // Detecting if the compiler output contained errors and reporting them through logs and // errors instead of returning the compiler output that might contain errors. @@ -85,6 +88,48 @@ impl SolidityCompiler for Resolc { } } + // We need to do some post processing on the output to make it in the same format that solc + // outputs. More specifically, for each contract, the `.metadata` field should be replaced + // with the `.metadata.solc_metadata` field which contains the ABI and other information + // about the compiled contracts. We do this because we do not want any downstream logic to + // need to differentiate between which compiler is being used when extracting the ABI of the + // contracts. + if let Some(ref mut contracts) = parsed.contracts { + for (contract_path, contracts_map) in contracts.iter_mut() { + for (contract_name, contract_info) in contracts_map.iter_mut() { + let Some(metadata) = contract_info.metadata.take() else { + continue; + }; + + // Get the `solc_metadata` in the metadata of the contract. + let Some(solc_metadata) = metadata + .get("solc_metadata") + .and_then(|metadata| metadata.as_str()) + else { + tracing::error!( + contract_path, + contract_name, + metadata = serde_json::to_string(&metadata).unwrap(), + "Encountered a contract compiled with resolc that has no solc_metadata" + ); + anyhow::bail!( + "Contract {} compiled with resolc that has no solc_metadata", + contract_name + ); + }; + + // Replace the original metadata with the new solc_metadata. + contract_info.metadata = + Some(serde_json::Value::String(solc_metadata.to_string())); + } + } + } + + tracing::debug!( + output = %serde_json::to_string(&parsed).unwrap(), + "Compiled successfully" + ); + Ok(CompilerOutput { input, output: parsed, diff --git a/crates/compiler/src/solc.rs b/crates/compiler/src/solc.rs index 653dd33..8184b83 100644 --- a/crates/compiler/src/solc.rs +++ b/crates/compiler/src/solc.rs @@ -11,6 +11,7 @@ use revive_dt_config::Arguments; use revive_dt_solc_binaries::download_solc; use revive_solc_json_interface::SolcStandardJsonOutput; +#[derive(Debug)] pub struct Solc { solc_path: PathBuf, } @@ -18,6 +19,7 @@ pub struct Solc { impl SolidityCompiler for Solc { type Options = (); + #[tracing::instrument(level = "debug", ret)] fn build( &self, input: CompilerInput, @@ -75,6 +77,11 @@ impl SolidityCompiler for Solc { } } + tracing::debug!( + output = %String::from_utf8_lossy(&output.stdout).to_string(), + "Compiled successfully" + ); + Ok(CompilerOutput { input, output: parsed, diff --git a/crates/core/src/driver/mod.rs b/crates/core/src/driver/mod.rs index d6783af..f4d6acc 100644 --- a/crates/core/src/driver/mod.rs +++ b/crates/core/src/driver/mod.rs @@ -302,51 +302,40 @@ where std::any::type_name::() ); - if let Some(Value::String(metadata_json_str)) = &contract.metadata { - tracing::trace!( - "metadata found for contract {contract_name}, {metadata_json_str}" - ); + let Some(Value::String(metadata)) = &contract.metadata else { + tracing::error!(?contract, "Contract does not have a metadata field"); + anyhow::bail!("Contract does not have a metadata field: {contract:?}"); + }; - match serde_json::from_str::(metadata_json_str) { - Ok(metadata_json) => { - if let Some(abi_value) = - metadata_json.get("output").and_then(|o| o.get("abi")) - { - match serde_json::from_value::(abi_value.clone()) { - Ok(parsed_abi) => { - tracing::trace!( - "ABI found in metadata for contract {}", - &contract_name - ); - self.deployed_abis - .insert(contract_name.clone(), parsed_abi); - } - Err(err) => { - anyhow::bail!( - "Failed to parse ABI from metadata for contract {}: {}", - contract_name, - err - ); - } - } - } else { - anyhow::bail!( - "No ABI found in metadata for contract {}", - contract_name - ); - } - } - Err(err) => { - anyhow::bail!( - "Failed to parse metadata JSON string for contract {}: {}", - contract_name, - err - ); - } - } - } else { - anyhow::bail!("No metadata found for contract {}", contract_name); - } + // Deserialize the solc metadata into a JSON object so we can get the ABI of the + // contracts. If we fail to perform the deserialization then we return an error + // as there's no other way to handle this. + let Ok(metadata) = serde_json::from_str::(metadata) else { + tracing::error!(%metadata, "Failed to parse solc metadata into a structured value"); + anyhow::bail!( + "Failed to parse solc metadata into a structured value {metadata}" + ); + }; + + // Accessing the ABI on the solc metadata and erroring if the accessing failed + let Some(abi) = metadata.get("output").and_then(|value| value.get("abi")) + else { + tracing::error!(%metadata, "Failed to access the .output.abi field of the solc metadata"); + anyhow::bail!( + "Failed to access the .output.abi field of the solc metadata {metadata}" + ); + }; + + // Deserialize the ABI object that we got from the unstructured JSON into a + // structured ABI object and error out if we fail. + let Ok(abi) = serde_json::from_value::(abi.clone()) else { + tracing::error!(%metadata, "Failed to deserialize ABI into a structured format"); + anyhow::bail!( + "Failed to deserialize ABI into a structured format {metadata}" + ); + }; + + self.deployed_abis.insert(contract_name.clone(), abi); } } }