1
use std::fs::{self, File};
2
use std::path::Path as StdPath;
3

            
4
use anyhow::Result;
5
use bonsol_sdk::{BonsolClient, ProgramInputType};
6
use indicatif::ProgressBar;
7
use log::debug;
8
use object_store::aws::AmazonS3Builder;
9
use object_store::ObjectStore;
10
use solana_rpc_client::nonblocking::rpc_client::RpcClient;
11
use solana_sdk::commitment_config::CommitmentConfig;
12
use solana_sdk::signature::Keypair;
13
use solana_sdk::signer::Signer;
14

            
15
use crate::command::{DeployArgs, S3UploadArgs, SharedDeployArgs};
16
use crate::common::ZkProgramManifest;
17
use crate::error::{BonsolCliError, S3ClientError, ZkManifestError};
18

            
19
fn get_s3_path(manifest_name: &str, image_id: &str) -> String {
20
    format!("{}-{}", manifest_name, image_id)
21
}
22

            
23
pub async fn deploy(rpc_url: String, signer: Keypair, deploy_args: DeployArgs) -> Result<()> {
24
    let bar = ProgressBar::new_spinner();
25
    let rpc_client = RpcClient::new_with_commitment(rpc_url.clone(), CommitmentConfig::confirmed());
26
    let SharedDeployArgs {
27
        manifest_path,
28
        auto_confirm,
29
    } = deploy_args.shared_args();
30

            
31
    let manifest_file = File::open(StdPath::new(&manifest_path)).map_err(|err| {
32
        BonsolCliError::ZkManifestError(ZkManifestError::FailedToOpen {
33
            manifest_path: manifest_path.clone(),
34
            err,
35
        })
36
    })?;
37
    let manifest: ZkProgramManifest = serde_json::from_reader(manifest_file).map_err(|err| {
38
        BonsolCliError::ZkManifestError(ZkManifestError::FailedDeserialization {
39
            manifest_path,
40
            err,
41
        })
42
    })?;
43
    let loaded_binary = fs::read(&manifest.binary_path).map_err(|err| {
44
        BonsolCliError::ZkManifestError(ZkManifestError::FailedToLoadBinary {
45
            binary_path: manifest.binary_path.clone(),
46
            err,
47
        })
48
    })?;
49
    let url: String = match deploy_args {
50
        DeployArgs::S3(s3_upload) => {
51
            let S3UploadArgs {
52
                bucket,
53
                access_key,
54
                secret_key,
55
                region,
56
                endpoint,
57
                ..
58
            } = s3_upload;
59

            
60
            let dest = format!("{}-{}", manifest.name, manifest.image_id);
61
            let store_path = object_store::path::Path::from(dest.clone());
62
            
63
            // Use conventional S3 endpoint URL format
64
            let endpoint_url = endpoint.clone().unwrap_or(format!(
65
                "https://s3.{}.amazonaws.com",
66
                region
67
            ));
68
            
69
            // Create the S3 client with the proper configuration
70
            let s3_client = AmazonS3Builder::new()
71
                .with_bucket_name(&bucket)
72
                .with_region(&region)
73
                .with_access_key_id(&access_key)
74
                .with_secret_access_key(&secret_key)
75
                .with_endpoint(&endpoint_url)
76
                .build()
77
                .map_err(|err| {
78
                    BonsolCliError::S3ClientError(S3ClientError::FailedToBuildClient {
79
                        args: vec![
80
                            format!("bucket: {bucket}"),
81
                            format!("access_key: {access_key}"),
82
                            format!(
83
                                "secret_key: {}..{}",
84
                                &secret_key[..4],
85
                                &secret_key[secret_key.len() - 4..]
86
                            ),
87
                            format!("region: {region}"),
88
                        ],
89
                        err,
90
                    })
91
                })?;
92

            
93
            // get the file to see if it exists
94
            if s3_client.head(&store_path).await.is_ok() {
95
                bar.set_message("File already exists, skipping upload");
96
            } else {
97
                s3_client
98
                    .put(&store_path, loaded_binary.into())
99
                    .await
100
                    .map_err(|err| {
101
                        BonsolCliError::S3ClientError(S3ClientError::UploadFailed { dest: store_path.clone(), err })
102
                    })?;
103
            }
104

            
105
            bar.finish_and_clear();
106

            
107
            // Create the download URL using the provided endpoint or AWS S3 URL convention
108
            let https_url = if let Some(ep) = endpoint {
109
                format!("{}/{}/{}", ep, bucket, dest)
110
            } else {
111
                format!("https://{}.s3.{}.amazonaws.com/{}", bucket, region, dest)
112
            };
113
            println!("Image uploaded to S3");
114
            debug!("S3 path: s3://{}/{}", bucket, dest);
115
            debug!("HTTPS URL (used for download): {}", https_url);
116
            // Return the HTTPS URL for compatibility with the HTTP client
117
            https_url
118
        }
119
        DeployArgs::Url(url_upload) => {
120
            let formatted_url = format!(
121
                "{}/{}-{}",
122
                url_upload.url.trim_end_matches("/"),
123
                manifest.name,
124
                manifest.image_id
125
            );
126

            
127
            let url = if !url_upload.no_post {
128
                // Post the binary to the URL endpoint
129
                let client = reqwest::Client::new();
130
                client
131
                    .post(&formatted_url)
132
                    .header("Content-Type", "application/octet-stream")
133
                    .body(loaded_binary.clone())
134
                    .send()
135
                    .await?;
136

            
137
                formatted_url
138
            } else {
139
                // Not posting assumes the data is already at the URL, check it
140
                let req = reqwest::get(&formatted_url).await?;
141
                let bytes = req.bytes().await?;
142
                if bytes != loaded_binary {
143
                    return Err(BonsolCliError::OriginBinaryMismatch {
144
                        url: formatted_url,
145
                        binary_path: manifest.binary_path,
146
                    }
147
                    .into());
148
                }
149

            
150
                formatted_url
151
            };
152

            
153
            bar.finish_and_clear();
154
            println!("Program available at URL {}", url);
155
            url
156
        }
157
    };
158

            
159
    if !auto_confirm {
160
        bar.finish_and_clear();
161
        println!("Deploying to Solana, which will cost real money. Are you sure you want to continue? (y/n)");
162
        let mut input = String::new();
163
        std::io::stdin().read_line(&mut input).unwrap();
164
        let response = input.trim();
165
        if response != "y" {
166
            bar.finish_and_clear();
167
            println!("Response: {response}\nAborting...");
168
            return Ok(());
169
        }
170
    }
171
    let bonsol_client = BonsolClient::with_rpc_client(rpc_client);
172
    let image_id = manifest.image_id;
173
    let deploy = bonsol_client.get_deployment(&image_id).await;
174
    match deploy {
175
        Ok(Some(account)) => {
176
            bar.finish_and_clear();
177
            println!(
178
                "Deployment for account '{}' already exists, deployments are immutable",
179
                account.owner
180
            );
181
            Ok(())
182
        }
183
        Ok(None) => {
184
            let deploy_txn = bonsol_client
185
                .deploy_v1(
186
                    &signer.pubkey(),
187
                    &image_id,
188
                    manifest.size,
189
                    &manifest.name,
190
                    &url,
191
                    manifest
192
                        .input_order
193
                        .iter()
194
                        .map(|i| match i.as_str() {
195
                            "Public" => ProgramInputType::Public,
196
                            "Private" => ProgramInputType::Private,
197
                            _ => ProgramInputType::Unknown,
198
                        })
199
                        .collect(),
200
                )
201
                .await?;
202
            if let Err(err) = bonsol_client.send_txn_standard(signer, deploy_txn).await {
203
                bar.finish_and_clear();
204
                anyhow::bail!(err)
205
            }
206

            
207
            bar.finish_and_clear();
208
            println!("{} deployed", image_id);
209
            Ok(())
210
        }
211
        Err(e) => {
212
            bar.finish_with_message(format!("Error getting deployment: {:?}", e));
213
            Ok(())
214
        }
215
    }
216
}