casper_client/
verification.rs

1use std::{cmp::min, io, path::Path};
2
3use bytes::{BufMut, Bytes, BytesMut};
4use flate2::{write::GzEncoder, Compression};
5use reqwest::{
6    header::{HeaderMap, HeaderValue, CONTENT_TYPE},
7    Client, ClientBuilder, StatusCode,
8};
9use tar::Builder as TarBuilder;
10use tokio::time::{sleep, Duration};
11
12use crate::{
13    verification_types::{
14        VerificationDetails, VerificationRequest, VerificationResult, VerificationStatus,
15    },
16    Error, Verbosity,
17};
18
19const MAX_RETRIES: u32 = 10;
20const BASE_DELAY: Duration = Duration::from_secs(3);
21const MAX_DELAY: Duration = Duration::from_secs(300);
22
23static GIT_DIR_NAME: &str = ".git";
24static TARGET_DIR_NAME: &str = "target";
25
26/// Builds an archive from the specified path.
27///
28/// This function creates a compressed tar archive from the files and directories located at the
29/// specified path. It excludes the `.git` and `target` directories from the archive.
30///
31/// # Arguments
32///
33/// * `path` - The path to the directory containing the files and directories to be archived.
34///
35/// # Returns
36///
37/// The compressed tar archive as a `Bytes` object, or an `std::io::Error` if an error occurs during
38/// the archiving process.
39pub fn build_archive(path: &Path) -> Result<Bytes, io::Error> {
40    let buffer = BytesMut::new().writer();
41    let encoder = GzEncoder::new(buffer, Compression::best());
42    let mut archive = TarBuilder::new(encoder);
43
44    for entry in path.read_dir()?.flatten() {
45        let file_name = entry.file_name();
46        // Skip `.git` and `target`.
47        if file_name == TARGET_DIR_NAME || file_name == GIT_DIR_NAME {
48            continue;
49        }
50        let full_path = entry.path();
51        if full_path.is_dir() {
52            archive.append_dir_all(&file_name, &full_path)?;
53        } else {
54            archive.append_path_with_name(&full_path, &file_name)?;
55        }
56    }
57
58    let encoder = archive.into_inner()?;
59    let buffer = encoder.finish()?;
60    Ok(buffer.into_inner().freeze())
61}
62
63/// Verifies the smart contract code against the one deployed at transaction hash.
64///
65/// Sends a verification request to the specified verification URL base path, including the
66/// transaction hash, public key, and code archive.
67///
68/// # Arguments
69///
70/// * `hash_str` - The hash of the deploy or transaction that installed the contract.
71/// * `base_url` - The base path of the verification URL.
72/// * `code_archive` - Base64-encoded tar-gzipped archive of the source code.
73/// * `verbosity` - The verbosity level of the verification process.
74///
75/// # Returns
76///
77/// The verification details of the contract.
78pub async fn send_verification_request(
79    hash_str: &str,
80    base_url: &str,
81    code_archive: String,
82    verbosity: Verbosity,
83) -> Result<VerificationDetails, Error> {
84    let verification_request = VerificationRequest {
85        hash: hash_str.to_string(),
86        code_archive,
87    };
88
89    fn make_client() -> reqwest::Result<Client> {
90        let mut headers = HeaderMap::new();
91        headers.insert(CONTENT_TYPE, HeaderValue::from_static("application/json"));
92
93        let builder = ClientBuilder::new()
94            .default_headers(headers)
95            .user_agent("casper-client-rs");
96
97        // https://github.com/hyperium/hyper/issues/2136
98        #[cfg(not(target_arch = "wasm32"))]
99        let builder = builder.pool_max_idle_per_host(0);
100
101        builder.build()
102    }
103
104    let Ok(http_client) = make_client() else {
105        eprintln!("Failed to build HTTP client");
106        return Err(Error::FailedToConstructHttpClient);
107    };
108
109    if verbosity == Verbosity::Medium || verbosity == Verbosity::High {
110        println!("Sending verification request");
111    }
112
113    let url = base_url.to_string() + "/verification";
114    let response = match http_client
115        .post(url)
116        .json(&verification_request)
117        .send()
118        .await
119    {
120        Ok(response) => response,
121        Err(error) => {
122            eprintln!("Cannot send verification request: {error:?}");
123            return Err(Error::ContractVerificationFailed);
124        }
125    };
126
127    match response.status() {
128        StatusCode::OK => {
129            if verbosity == Verbosity::Medium || verbosity == Verbosity::High {
130                println!("Sent verification request",);
131            }
132        }
133        status => {
134            eprintln!("Verification failed with status {status}");
135        }
136    }
137
138    wait_for_verification_finished(base_url, &http_client, hash_str, verbosity).await;
139
140    if verbosity == Verbosity::Medium || verbosity == Verbosity::High {
141        println!("Getting verification details...");
142    }
143
144    let details_url = format!("{}/verification/{}/details", base_url, hash_str);
145    match http_client.get(details_url).send().await {
146        Ok(response) => response.json().await.map_err(|err| {
147            eprintln!("Failed to parse JSON {err}");
148            Error::ContractVerificationFailed
149        }),
150        Err(error) => {
151            eprintln!("Cannot get verification details: {error:?}");
152            Err(Error::ContractVerificationFailed)
153        }
154    }
155}
156
157/// Waits for the verification process to finish.
158async fn wait_for_verification_finished(
159    base_url: &str,
160    http_client: &Client,
161    hash_str: &str,
162    verbosity: Verbosity,
163) {
164    let mut retries = MAX_RETRIES;
165    let mut delay = BASE_DELAY;
166
167    while retries != 0 {
168        sleep(delay).await;
169
170        match get_verification_status(base_url, http_client, hash_str).await {
171            Ok(status) => {
172                if verbosity == Verbosity::Medium || verbosity == Verbosity::High {
173                    println!("Verification status: {status:?}");
174                }
175                if status == VerificationStatus::Verified || status == VerificationStatus::Failed {
176                    break;
177                }
178            }
179            Err(error) => {
180                eprintln!("Cannot get verification status: {error:?}");
181                break;
182            }
183        };
184
185        retries -= 1;
186        delay = min(delay * 2, MAX_DELAY);
187    }
188}
189
190/// Gets the verification status of the contract.
191async fn get_verification_status(
192    base_url: &str,
193    http_client: &Client,
194    hash_str: &str,
195) -> Result<VerificationStatus, Error> {
196    let status_url = format!("{}/verification/{}/status", base_url, hash_str);
197    let response = match http_client.get(status_url).send().await {
198        Ok(response) => response,
199        Err(error) => {
200            eprintln!("Failed to fetch verification status: {error:?}");
201            return Err(Error::ContractVerificationFailed);
202        }
203    };
204
205    match response.status() {
206        StatusCode::OK => {
207            let result: VerificationResult = response.json().await.map_err(|err| {
208                eprintln!("Failed to parse JSON for verification status, {err}");
209                Error::ContractVerificationFailed
210            })?;
211            Ok(result.status)
212        }
213        status => {
214            eprintln!("Verification status not found, {status}");
215            Err(Error::ContractVerificationFailed)
216        }
217    }
218}