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(¶ms),
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(¶ms),
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(¶ms),
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}