tenderly_rs/services/
contracts.rs

1use std::collections::HashMap;
2
3use serde::{
4    Deserialize,
5    Serialize,
6};
7
8use crate::{
9    core::api_client::{
10        ApiClient,
11        ApiClientProvider,
12        ApiVersion,
13    },
14    errors::{
15        BytecodeMismatchError,
16        CompilationError,
17        GeneralError,
18        NotFoundError,
19        UnexpectedVerificationError,
20    },
21    services::contracts::types::{
22        Contract,
23        ContractRequest,
24        ContractResponse,
25        GetByParams,
26        LibraryAddresses,
27        SolcConfig,
28        SourceContent,
29        UpdateContractRequest,
30        VerificationMode,
31        VerificationRequest,
32        VerificationResponse,
33    },
34    types::{
35        Network,
36        Path,
37        TenderlyConfiguration,
38    },
39};
40
41pub mod types;
42
43#[derive(Debug, Clone)]
44pub struct ContractData {
45    pub display_name: Option<String>,
46}
47
48fn map_contract_response_to_contract_model(contract_response: ContractResponse) -> Contract {
49    let network = contract_response
50        .contract
51        .network_id
52        .parse::<u64>()
53        .ok()
54        .and_then(|id| match id {
55            1 => Some(Network::Mainnet),
56            10 => Some(Network::Optimistic),
57            30 => Some(Network::Rsk),
58            31 => Some(Network::RskTestnet),
59            56 => Some(Network::Binance),
60            97 => Some(Network::RialtoBinance),
61            100 => Some(Network::GnosisChain),
62            111 => Some(Network::BobSepolia),
63            137 => Some(Network::Polygon),
64            252 => Some(Network::Fraxtal),
65            288 => Some(Network::BobaEthereum),
66            480 => Some(Network::Worldchain),
67            919 => Some(Network::ModeSepolia),
68            1135 => Some(Network::Lisk),
69            1284 => Some(Network::Moonbeam),
70            1285 => Some(Network::Moonriver),
71            1287 => Some(Network::MoonbaseAlpha),
72            1328 => Some(Network::SeiAtlantic2),
73            1329 => Some(Network::SeiPacific1),
74            2522 => Some(Network::FraxtalHolesky),
75            2810 => Some(Network::MorphHolesky),
76            4202 => Some(Network::LiskSepolia),
77            4653 => Some(Network::Gold),
78            4801 => Some(Network::WorldchainSepolia),
79            5000 => Some(Network::Mantle),
80            5003 => Some(Network::MantleSepolia),
81            7000 => Some(Network::Zetachain),
82            7001 => Some(Network::ZetachainTestnet),
83            7887 => Some(Network::Kinto),
84            8008 => Some(Network::Polynomial),
85            8453 => Some(Network::Base),
86            9728 => Some(Network::BobaBinanceRialto),
87            11069 => Some(Network::IntervalTestnet),
88            13371 => Some(Network::Immutable),
89            13473 => Some(Network::ImmutableTestnet),
90            17000 => Some(Network::Holesky),
91            18233 => Some(Network::Unreal),
92            18291 => Some(Network::ConcreteTestnet),
93            28882 => Some(Network::BobaSepolia),
94            33111 => Some(Network::ApechainCurtis),
95            33139 => Some(Network::Apechain),
96            34443 => Some(Network::Mode),
97            42161 => Some(Network::ArbitrumOne),
98            42170 => Some(Network::ArbitrumNova),
99            43113 => Some(Network::AvalancheFuji),
100            43114 => Some(Network::Avalanche),
101            56288 => Some(Network::BobaBinance),
102            59141 => Some(Network::LineaSepolia),
103            59144 => Some(Network::Linea),
104            60808 => Some(Network::Bob),
105            80002 => Some(Network::PolygonAmoy),
106            80008 => Some(Network::PolynomialSepolia),
107            81457 => Some(Network::Blast),
108            84532 => Some(Network::BaseSepolia),
109            111188 => Some(Network::Real),
110            167000 => Some(Network::Taiko),
111            167009 => Some(Network::TaikoHekla),
112            421614 => Some(Network::ArbitrumSepolia),
113            7777777 => Some(Network::Zora),
114            11155111 => Some(Network::Sepolia),
115            11155420 => Some(Network::OptimisticSepolia),
116            999999999 => Some(Network::ZoraSepolia),
117            _ => None,
118        })
119        .unwrap_or(Network::Mainnet);
120
121    let mut contract = Contract {
122        address: contract_response.contract.address,
123        network,
124        display_name: None,
125        tags: None,
126    };
127
128    if !contract_response.display_name.is_empty() {
129        contract.display_name = Some(contract_response.display_name);
130    }
131
132    if let Some(tags) = contract_response.tags {
133        contract.tags = Some(tags.iter().map(|t| t.tag.clone()).collect());
134    }
135
136    contract
137}
138
139fn map_contract_model_to_contract_request(contract: &Contract) -> ContractRequest {
140    ContractRequest {
141        address: contract.address.clone(),
142        network_id: format!("{}", contract.network as u64),
143        display_name: contract.display_name.clone(),
144    }
145}
146
147/// Contract service for managing contracts in your Tenderly project
148///
149/// Provides functionality to add, update, remove, and query contracts.
150/// Supports contract verification, metadata management, and filtering by tags or display names.
151///
152/// # Example
153///
154/// ```rust,no_run
155/// use tenderly_rs::{
156///     services::ContractData,
157///     Network,
158///     Tenderly,
159///     TenderlyConfiguration,
160/// };
161///
162/// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
163/// let tenderly = Tenderly::new(TenderlyConfiguration::new(
164///     "account".to_string(),
165///     "project".to_string(),
166///     "key".to_string(),
167///     Network::Mainnet,
168/// ))?;
169///
170/// // Add a contract
171/// let contract = tenderly
172///     .contracts
173///     .add(
174///         "0x6b175474e89094c44da98b954eedeac495271d0f",
175///         Some(&ContractData {
176///             display_name: Some("DAI Token".to_string()),
177///         }),
178///     )
179///     .await?;
180///
181/// // Get a contract
182/// let contract = tenderly
183///     .contracts
184///     .get("0x6b175474e89094c44da98b954eedeac495271d0f")
185///     .await?;
186/// # Ok(())
187/// # }
188/// ```
189pub struct ContractRepository {
190    api_v1: ApiClient,
191    api_v2: ApiClient,
192    configuration: TenderlyConfiguration,
193}
194
195impl ContractRepository {
196    pub fn new(
197        api_provider: ApiClientProvider,
198        configuration: TenderlyConfiguration,
199    ) -> Result<Self, GeneralError> {
200        Ok(Self {
201            api_v1: api_provider.get_api_client(ApiVersion::V1)?,
202            api_v2: api_provider.get_api_client(ApiVersion::V2)?,
203            configuration,
204        })
205    }
206
207    /// Get a contract by address
208    ///
209    /// Retrieves contract information from your Tenderly project for the configured network.
210    ///
211    /// # Arguments
212    ///
213    /// * `address` - Contract address (hex string with or without 0x prefix)
214    ///
215    /// # Returns
216    ///
217    /// Returns [`Contract`] if found, or [`GeneralError::NotFound`] if the contract
218    /// is not in your project.
219    ///
220    /// # Example
221    ///
222    /// ```rust,no_run
223    /// use tenderly_rs::{
224    ///     Network,
225    ///     Tenderly,
226    ///     TenderlyConfiguration,
227    /// };
228    ///
229    /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
230    /// let tenderly = Tenderly::new(TenderlyConfiguration::new(
231    ///     "account".to_string(),
232    ///     "project".to_string(),
233    ///     "key".to_string(),
234    ///     Network::Mainnet,
235    /// ))?;
236    /// let contracts = &tenderly.contracts;
237    ///
238    /// let contract = contracts
239    ///     .get("0x6b175474e89094c44da98b954eedeac495271d0f")
240    ///     .await?;
241    /// println!("Contract: {}", contract.address);
242    /// # Ok(())
243    /// # }
244    /// ```
245    ///
246    /// # Errors
247    ///
248    /// Returns [`GeneralError::NotFound`] if the contract is not found,
249    /// or [`GeneralError::ApiError`] if the API request fails.
250    pub async fn get(&self, address: &str) -> Result<Contract, GeneralError> {
251        let mut params = HashMap::new();
252        params.insert("addresses[]".to_string(), vec![address.to_string()]);
253        params.insert(
254            "networkIDs[]".to_string(),
255            vec![format!("{}", self.configuration.network as u64)],
256        );
257        params.insert(
258            "types[]".to_string(),
259            vec!["contract".to_string(), "unverified_contract".to_string()],
260        );
261
262        #[derive(Deserialize)]
263        struct Response {
264            accounts: Vec<ContractResponse>,
265        }
266
267        let result = self
268            .api_v2
269            .get::<Response>(
270                &format!(
271                    "/accounts/{}/projects/{}/accounts",
272                    self.configuration.account_name, self.configuration.project_name
273                ),
274                Some(&params),
275            )
276            .await?;
277
278        if result.accounts.is_empty() {
279            return Err(GeneralError::NotFound(NotFoundError::new(format!(
280                "Contract with address {} not found",
281                address
282            ))));
283        }
284
285        Ok(map_contract_response_to_contract_model(
286            result.accounts[0].clone(),
287        ))
288    }
289
290    /// Add a contract to the project
291    ///
292    /// Adds a contract address to your Tenderly project for monitoring and tracking.
293    /// Optionally includes metadata like display name for easier identification.
294    ///
295    /// # Arguments
296    ///
297    /// * `address` - Contract address (hex string with or without 0x prefix)
298    /// * `contract_data` - Optional metadata including display name
299    ///
300    /// # Returns
301    ///
302    /// Returns the added [`Contract`] object.
303    ///
304    /// # Example
305    ///
306    /// ```rust,no_run
307    /// use tenderly_rs::{
308    ///     services::ContractData,
309    ///     Network,
310    ///     Tenderly,
311    ///     TenderlyConfiguration,
312    /// };
313    ///
314    /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
315    /// let tenderly = Tenderly::new(TenderlyConfiguration::new(
316    ///     "account".to_string(),
317    ///     "project".to_string(),
318    ///     "key".to_string(),
319    ///     Network::Mainnet,
320    /// ))?;
321    /// let contracts = &tenderly.contracts;
322    ///
323    /// let contract = contracts
324    ///     .add(
325    ///         "0x6b175474e89094c44da98b954eedeac495271d0f",
326    ///         Some(&ContractData {
327    ///             display_name: Some("DAI Token".to_string()),
328    ///         }),
329    ///     )
330    ///     .await?;
331    /// # Ok(())
332    /// # }
333    /// ```
334    ///
335    /// # Errors
336    ///
337    /// Returns [`GeneralError::ApiError`] if the API request fails.
338    pub async fn add(
339        &self,
340        address: &str,
341        contract_data: Option<&ContractData>,
342    ) -> Result<Contract, GeneralError> {
343        let contract = Contract {
344            address: address.to_string(),
345            network: self.configuration.network,
346            display_name: contract_data.and_then(|d| d.display_name.clone()),
347            tags: None,
348        };
349
350        let request = map_contract_model_to_contract_request(&contract);
351
352        // Try to parse POST response first, fallback to GET if needed
353        let response_result = self
354            .api_v1
355            .post::<ContractRequest, ContractResponse>(
356                &format!(
357                    "/account/{}/project/{}/address",
358                    self.configuration.account_name, self.configuration.project_name
359                ),
360                Some(&request),
361            )
362            .await;
363
364        match response_result {
365            Ok(contract_response) => {
366                // Successfully parsed POST response, use it directly
367                Ok(map_contract_response_to_contract_model(contract_response))
368            }
369            Err(_) => {
370                // If POST response parsing fails or is empty, try GET as fallback
371                // If GET also fails (e.g., 503), assume POST was successful and return the
372                // contract
373                match self.get(address).await {
374                    Ok(contract) => Ok(contract),
375                    Err(_) => {
376                        // POST was successful (status 2xx), but response couldn't be parsed
377                        // and GET failed. Return the contract we tried to add.
378                        Ok(contract)
379                    }
380                }
381            }
382        }
383    }
384
385    /// Remove a contract from the project
386    ///
387    /// Removes a contract address from your Tenderly project. This does not delete
388    /// contract verification data, only removes it from your project's tracked contracts.
389    ///
390    /// # Arguments
391    ///
392    /// * `address` - Contract address to remove
393    ///
394    /// # Example
395    ///
396    /// ```rust,no_run
397    /// use tenderly_rs::{
398    ///     Network,
399    ///     Tenderly,
400    ///     TenderlyConfiguration,
401    /// };
402    ///
403    /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
404    /// let tenderly = Tenderly::new(TenderlyConfiguration::new(
405    ///     "account".to_string(),
406    ///     "project".to_string(),
407    ///     "key".to_string(),
408    ///     Network::Mainnet,
409    /// ))?;
410    /// let contracts = &tenderly.contracts;
411    ///
412    /// contracts
413    ///     .remove("0x6b175474e89094c44da98b954eedeac495271d0f")
414    ///     .await?;
415    /// # Ok(())
416    /// # }
417    /// ```
418    ///
419    /// # Errors
420    ///
421    /// Returns [`GeneralError::ApiError`] if the API request fails.
422    pub async fn remove(&self, address: &str) -> Result<(), GeneralError> {
423        self.api_v1
424            .delete::<()>(
425                &format!(
426                    "/account/{}/project/{}/contract/{}/{}",
427                    self.configuration.account_name,
428                    self.configuration.project_name,
429                    self.configuration.network as u64,
430                    address
431                ),
432                None,
433            )
434            .await
435    }
436
437    /// Update a contract's metadata
438    ///
439    /// Updates contract metadata including display name and tags. You can update
440    /// display name, add tags, or both in a single call.
441    ///
442    /// # Arguments
443    ///
444    /// * `address` - Contract address to update
445    /// * `payload` - Update request containing display name and/or tags to append
446    ///
447    /// # Returns
448    ///
449    /// Returns the updated [`Contract`] object.
450    ///
451    /// # Example
452    ///
453    /// ```rust,no_run
454    /// use tenderly_rs::{
455    ///     services::contracts::types::UpdateContractRequest,
456    ///     Network,
457    ///     Tenderly,
458    ///     TenderlyConfiguration,
459    /// };
460    ///
461    /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
462    /// let tenderly = Tenderly::new(TenderlyConfiguration::new(
463    ///     "account".to_string(),
464    ///     "project".to_string(),
465    ///     "key".to_string(),
466    ///     Network::Mainnet,
467    /// ))?;
468    /// let contracts = &tenderly.contracts;
469    ///
470    /// let update_request = UpdateContractRequest {
471    ///     display_name: Some("Updated Name".to_string()),
472    ///     append_tags: Some(vec!["defi".to_string(), "token".to_string()]),
473    /// };
474    ///
475    /// let updated = contracts.update("0x...", &update_request).await?;
476    /// # Ok(())
477    /// # }
478    /// ```
479    ///
480    /// # Errors
481    ///
482    /// Returns [`GeneralError::ApiError`] if the API request fails.
483    pub async fn update(
484        &self,
485        address: &str,
486        payload: &UpdateContractRequest,
487    ) -> Result<Contract, GeneralError> {
488        if let Some(append_tags) = &payload.append_tags {
489            for tag in append_tags {
490                #[derive(Serialize)]
491                struct TagRequest {
492                    contract_ids: Vec<String>,
493                    tag: String,
494                }
495
496                let tag_request = TagRequest {
497                    contract_ids: vec![format!(
498                        "eth:{}:{}",
499                        self.configuration.network as u64, address
500                    )],
501                    tag: tag.clone(),
502                };
503
504                self.api_v1
505                    .post_ignore_response::<TagRequest>(
506                        &format!(
507                            "/account/{}/project/{}/tag",
508                            self.configuration.account_name, self.configuration.project_name
509                        ),
510                        Some(&tag_request),
511                    )
512                    .await?;
513            }
514        }
515
516        if let Some(display_name) = &payload.display_name {
517            #[derive(Serialize)]
518            struct RenameRequest {
519                display_name: String,
520            }
521
522            let rename_request = RenameRequest {
523                display_name: display_name.clone(),
524            };
525
526            self.api_v1
527                .post_ignore_response::<RenameRequest>(
528                    &format!(
529                        "/account/{}/project/{}/contract/{}/{}/rename",
530                        self.configuration.account_name,
531                        self.configuration.project_name,
532                        self.configuration.network as u64,
533                        address
534                    ),
535                    Some(&rename_request),
536                )
537                .await?;
538        }
539
540        self.get(address).await
541    }
542
543    /// Get all contracts in the project
544    ///
545    /// Retrieves all contracts currently tracked in your Tenderly project for the
546    /// configured network.
547    ///
548    /// # Returns
549    ///
550    /// Returns a vector of [`Contract`] objects.
551    ///
552    /// # Example
553    ///
554    /// ```rust,no_run
555    /// use tenderly_rs::{
556    ///     Network,
557    ///     Tenderly,
558    ///     TenderlyConfiguration,
559    /// };
560    ///
561    /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
562    /// let tenderly = Tenderly::new(TenderlyConfiguration::new(
563    ///     "account".to_string(),
564    ///     "project".to_string(),
565    ///     "key".to_string(),
566    ///     Network::Mainnet,
567    /// ))?;
568    /// let contracts = &tenderly.contracts;
569    ///
570    /// let all_contracts: Vec<_> = contracts.get_all().await?;
571    /// println!("Found {} contracts", all_contracts.len());
572    /// # Ok(())
573    /// # }
574    /// ```
575    ///
576    /// # Errors
577    ///
578    /// Returns [`GeneralError::ApiError`] if the API request fails.
579    pub async fn get_all(&self) -> Result<Vec<Contract>, GeneralError> {
580        let mut params = HashMap::new();
581        params.insert("types[]".to_string(), vec!["contract".to_string()]);
582
583        #[derive(Deserialize)]
584        struct Response {
585            accounts: Vec<ContractResponse>,
586        }
587
588        let result = self
589            .api_v2
590            .get::<Response>(
591                &format!(
592                    "/accounts/{}/projects/{}/accounts",
593                    self.configuration.account_name, self.configuration.project_name
594                ),
595                Some(&params),
596            )
597            .await?;
598
599        Ok(result
600            .accounts
601            .into_iter()
602            .map(map_contract_response_to_contract_model)
603            .collect::<Vec<Contract>>())
604    }
605
606    /// Get contracts by query parameters
607    ///
608    /// Retrieves contracts filtered by display names and/or tags. Useful for
609    /// finding specific contracts in large projects.
610    ///
611    /// # Arguments
612    ///
613    /// * `query_object` - Query parameters containing optional display names and tags filters
614    ///
615    /// # Returns
616    ///
617    /// Returns a vector of [`Contract`] objects matching the query.
618    ///
619    /// # Example
620    ///
621    /// ```rust,no_run
622    /// use tenderly_rs::{
623    ///     services::contracts::types::GetByParams,
624    ///     Network,
625    ///     Tenderly,
626    ///     TenderlyConfiguration,
627    /// };
628    ///
629    /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
630    /// let tenderly = Tenderly::new(TenderlyConfiguration::new(
631    ///     "account".to_string(),
632    ///     "project".to_string(),
633    ///     "key".to_string(),
634    ///     Network::Mainnet,
635    /// ))?;
636    /// let contracts = &tenderly.contracts;
637    ///
638    /// let query = GetByParams {
639    ///     display_names: Some(vec!["DAI Token".to_string()]),
640    ///     tags: Some(vec!["defi".to_string()]),
641    /// };
642    ///
643    /// let filtered = contracts.get_by(&query).await?;
644    /// # Ok(())
645    /// # }
646    /// ```
647    ///
648    /// # Errors
649    ///
650    /// Returns [`GeneralError::ApiError`] if the API request fails.
651    pub async fn get_by(&self, query_object: &GetByParams) -> Result<Vec<Contract>, GeneralError> {
652        let mut params = HashMap::new();
653        params.insert(
654            "networkIDs[]".to_string(),
655            vec![format!("{}", self.configuration.network as u64)],
656        );
657        params.insert("types[]".to_string(), vec!["contract".to_string()]);
658
659        if let Some(display_names) = &query_object.display_names {
660            if !display_names.is_empty() {
661                params.insert("display_names[]".to_string(), display_names.clone());
662            }
663        }
664
665        if let Some(tags) = &query_object.tags {
666            if !tags.is_empty() {
667                params.insert("tags[]".to_string(), tags.clone());
668            }
669        }
670
671        #[derive(Deserialize)]
672        struct Response {
673            accounts: Vec<ContractResponse>,
674        }
675
676        let result = self
677            .api_v2
678            .get::<Response>(
679                &format!(
680                    "/accounts/{}/projects/{}/accounts",
681                    self.configuration.account_name, self.configuration.project_name
682                ),
683                Some(&params),
684            )
685            .await?;
686
687        Ok(result
688            .accounts
689            .into_iter()
690            .map(map_contract_response_to_contract_model)
691            .collect::<Vec<Contract>>())
692    }
693
694    /// Verify a contract's source code
695    ///
696    /// Verifies a contract by submitting its source code and compiler settings.
697    /// After verification, the contract's source code becomes viewable on Tenderly,
698    /// and you can interact with verified functions.
699    ///
700    /// # Arguments
701    ///
702    /// * `address` - Contract address to verify
703    /// * `verification_request` - Verification request containing source code, compiler config, and
704    ///   verification mode
705    ///
706    /// # Returns
707    ///
708    /// Returns the verified [`Contract`] object.
709    ///
710    /// # Example
711    ///
712    /// ```rust,no_run
713    /// use tenderly_rs::{Network, Tenderly, TenderlyConfiguration};
714    /// use tenderly_rs::services::contracts::types::*;
715    /// use std::collections::HashMap;
716    ///
717    /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
718    /// let tenderly = Tenderly::new(TenderlyConfiguration::new(
719    ///     "account".to_string(),
720    ///     "project".to_string(),
721    ///     "key".to_string(),
722    ///     Network::Sepolia,
723    /// ))?;
724    /// let contracts = &tenderly.contracts;
725    ///
726    /// let mut sources = HashMap::new();
727    /// sources.insert("Counter.sol".to_string(), SourceContent {
728    ///     content: "pragma solidity ^0.8.0; contract Counter { }".to_string(),
729    /// });
730    ///
731    /// let solc_config = SolcConfig {
732    ///     version: "v0.8.18".to_string(),
733    ///     sources,
734    ///     settings: serde_json::json!({"optimizer": {"enabled": false}}),
735    /// };
736    ///
737    /// let request = VerificationRequest {
738    ///     contract_to_verify: "Counter.sol:Counter".to_string(),
739    ///     solc: solc_config,
740    ///     config: VerificationConfig {
741    ///         mode: VerificationMode::Public,
742    ///     },
743    /// };
744    ///
745    /// let verified = contracts.verify("0x...", &request).await?;
746    /// # Ok(())
747    /// # }
748    /// ```
749    ///
750    /// # Errors
751    ///
752    /// Returns [`GeneralError::Compilation`] if source code compilation fails,
753    /// [`GeneralError::BytecodeMismatch`] if bytecode doesn't match,
754    /// [`GeneralError::ApiError`] if the API request fails, or
755    /// [`GeneralError::InvalidArguments`] if contract name format is invalid.
756    pub async fn verify(
757        &self,
758        address: &str,
759        verification_request: &VerificationRequest,
760    ) -> Result<Contract, GeneralError> {
761        if !is_fully_qualified_contract_name(&verification_request.contract_to_verify) {
762            return Err(GeneralError::InvalidArguments(
763                crate::errors::InvalidArgumentsError::new(format!(
764                    "The contract name '{}' is not a fully qualified name. Please use the fully \
765                     qualified name (e.g. path/to/file.sol:ContractName)",
766                    verification_request.contract_to_verify
767                )),
768            ));
769        }
770
771        let solc_config = repack_libraries(&verification_request.solc);
772        let sources = map_solc_sources_to_tenderly_sources(&verification_request.solc.sources);
773
774        #[derive(Serialize)]
775        struct VerificationPayload {
776            contracts: Vec<ContractVerification>,
777        }
778
779        #[derive(Serialize)]
780        struct ContractVerification {
781            compiler: serde_json::Value,
782            sources: HashMap<String, serde_json::Value>,
783            networks: HashMap<String, NetworkAddress>,
784            #[serde(rename = "contractToVerify")]
785            contract_to_verify: String,
786        }
787
788        #[derive(Serialize)]
789        struct NetworkAddress {
790            address: String,
791        }
792
793        let mut networks = HashMap::new();
794        networks.insert(
795            format!("{}", self.configuration.network as u64),
796            NetworkAddress {
797                address: address.to_string(),
798            },
799        );
800
801        let payload = VerificationPayload {
802            contracts: vec![ContractVerification {
803                compiler: serde_json::to_value(&solc_config).unwrap(),
804                sources,
805                networks,
806                contract_to_verify: verification_request.contract_to_verify.clone(),
807            }],
808        };
809
810        let path = if verification_request.config.mode == VerificationMode::Private {
811            format!(
812                "/accounts/{}/projects/{}/contracts/verify",
813                self.configuration.account_name, self.configuration.project_name
814            )
815        } else {
816            "/public/contracts/verify".to_string()
817        };
818
819        let response: VerificationResponse = self.api_v1.post(&path, Some(&payload)).await?;
820
821        if let Some(compilation_errors) = &response.compilation_errors {
822            if !compilation_errors.is_empty() {
823                return Err(GeneralError::Compilation(CompilationError::new(
824                    "There has been a compilation error while trying to verify contracts.",
825                    Some(
826                        serde_json::to_value(
827                            compilation_errors
828                                .iter()
829                                .map(|e| {
830                                    serde_json::json!({
831                                        "source_location": {
832                                            "file": e.source_location.file,
833                                            "start": e.source_location.start,
834                                            "end": e.source_location.end,
835                                        },
836                                        "error_type": e.error_type,
837                                        "component": e.component,
838                                        "message": e.message,
839                                        "formatted_message": e.formatted_message,
840                                    })
841                                })
842                                .collect::<Vec<_>>(),
843                        )
844                        .unwrap(),
845                    ),
846                )));
847            }
848        }
849
850        if response.results.is_none() || response.results.as_ref().unwrap().is_empty() {
851            return Err(GeneralError::UnexpectedVerification(
852                UnexpectedVerificationError::new(
853                    "There has been an unexpected verification error during the verification \
854                     process. Please check your contract's source code and try again.",
855                ),
856            ));
857        }
858
859        if let Some(bytecode_mismatch_error) =
860            &response.results.as_ref().unwrap()[0].bytecode_mismatch_error
861        {
862            let mut error_data = serde_json::json!({
863                "contract_id": bytecode_mismatch_error.contract_id,
864            });
865            if !bytecode_mismatch_error.assumed_reason.is_empty() {
866                error_data["assumed_reason"] =
867                    serde_json::json!(bytecode_mismatch_error.assumed_reason);
868            }
869            if let Some(expected) = &bytecode_mismatch_error.expected {
870                error_data["expected"] = serde_json::json!(expected);
871            }
872            if let Some(got) = &bytecode_mismatch_error.got {
873                error_data["got"] = serde_json::json!(got);
874            }
875            if let Some(similarity) = &bytecode_mismatch_error.similarity {
876                error_data["similarity"] = serde_json::json!(similarity);
877            }
878            return Err(GeneralError::BytecodeMismatch(BytecodeMismatchError::new(
879                "There has been a bytecode mismatch error while trying to verify contracts.",
880                Some(error_data),
881            )));
882        }
883
884        self.add(address, None).await
885    }
886}
887
888fn map_solc_sources_to_tenderly_sources(
889    sources: &HashMap<Path, SourceContent>,
890) -> HashMap<String, serde_json::Value> {
891    let mut tenderly_sources = HashMap::new();
892    for (path, source) in sources {
893        tenderly_sources.insert(
894            path.clone(),
895            serde_json::json!({
896                "code": source.content
897            }),
898        );
899    }
900    tenderly_sources
901}
902
903fn repack_libraries(solc_config: &SolcConfig) -> serde_json::Value {
904    let mut tenderly_solc_config = serde_json::json!({
905        "version": solc_config.version,
906        "settings": {}
907    });
908
909    if let Some(settings) = solc_config.settings.as_object() {
910        if let Some(libraries) = settings.get("libraries") {
911            if let Some(libs_obj) = libraries.as_object() {
912                let mut tenderly_libraries: HashMap<String, LibraryAddresses> = HashMap::new();
913
914                for (file_name, lib_val) in libs_obj {
915                    if let Some(lib_map) = lib_val.as_object() {
916                        for (lib_name, lib_address) in lib_map {
917                            if let Some(addr_str) = lib_address.as_str() {
918                                tenderly_libraries
919                                    .entry(file_name.clone())
920                                    .or_insert_with(|| LibraryAddresses {
921                                        addresses: HashMap::new(),
922                                    })
923                                    .addresses
924                                    .insert(lib_name.clone(), addr_str.to_string());
925                            }
926                        }
927                    }
928                }
929
930                tenderly_solc_config["settings"]["libraries"] =
931                    serde_json::to_value(tenderly_libraries).unwrap();
932            }
933        }
934    }
935
936    tenderly_solc_config
937}
938
939fn is_fully_qualified_contract_name(contract_name: &str) -> bool {
940    let pattern = regex::Regex::new(r"^(.+)\.sol:([a-zA-Z_][a-zA-Z_0-9]*)$").unwrap();
941    pattern.is_match(contract_name)
942}