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
65
                .clone()
66
                .unwrap_or(format!("https://s3.{}.amazonaws.com", region));
67

            
68
            // Create the S3 client with the proper configuration
69
            let s3_client = AmazonS3Builder::new()
70
                .with_bucket_name(&bucket)
71
                .with_region(&region)
72
                .with_access_key_id(&access_key)
73
                .with_secret_access_key(&secret_key)
74
                .with_endpoint(&endpoint_url)
75
                .build()
76
                .map_err(|err| {
77
                    BonsolCliError::S3ClientError(S3ClientError::FailedToBuildClient {
78
                        args: vec![
79
                            format!("bucket: {bucket}"),
80
                            format!("access_key: {access_key}"),
81
                            format!(
82
                                "secret_key: {}..{}",
83
                                &secret_key[..4],
84
                                &secret_key[secret_key.len() - 4..]
85
                            ),
86
                            format!("region: {region}"),
87
                        ],
88
                        err,
89
                    })
90
                })?;
91

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

            
107
            bar.finish_and_clear();
108

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

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

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

            
152
                formatted_url
153
            };
154

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

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

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