1
use std::fs::{self, File};
2
use std::path::Path;
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
pub async fn deploy(rpc_url: String, signer: Keypair, deploy_args: DeployArgs) -> Result<()> {
20
    let bar = ProgressBar::new_spinner();
21
    let rpc_client = RpcClient::new_with_commitment(rpc_url.clone(), CommitmentConfig::confirmed());
22
    let SharedDeployArgs {
23
        manifest_path,
24
        auto_confirm,
25
    } = deploy_args.shared_args();
26

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

            
56
            let dest = format!("{}-{}", manifest.name, manifest.image_id);
57
            let store_path = object_store::path::Path::from(dest.clone());
58

            
59
            // Use conventional S3 endpoint URL format
60
            let endpoint_url = endpoint.unwrap_or(format!("https://s3.{}.amazonaws.com", region));
61

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

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

            
101
            bar.finish_and_clear();
102

            
103
            // Create a properly formatted HTTPS URL for the S3 object
104
            // This follows the AWS S3 URL convention
105
            let https_url = format!("https://{}.s3.{}.amazonaws.com/{}", bucket, region, dest);
106
            println!("Image uploaded to S3");
107
            debug!("S3 path: s3://{}/{}", bucket, dest);
108
            debug!("HTTPS URL (used for download): {}", https_url);
109

            
110
            // Return the HTTPS URL for compatibility with the HTTP client
111
            https_url
112
        }
113
        DeployArgs::Url(url_upload) => {
114
            let req = reqwest::get(&url_upload.url).await?;
115
            let bytes = req.bytes().await?;
116
            if bytes != loaded_binary {
117
                return Err(BonsolCliError::OriginBinaryMismatch {
118
                    url: url_upload.url,
119
                    binary_path: manifest.binary_path,
120
                }
121
                .into());
122
            }
123

            
124
            bar.finish_and_clear();
125
            url_upload.url
126
        }
127
    };
128

            
129
    if !auto_confirm {
130
        bar.finish_and_clear();
131
        println!("Deploying to Solana, which will cost real money. Are you sure you want to continue? (y/n)");
132
        let mut input = String::new();
133
        std::io::stdin().read_line(&mut input).unwrap();
134
        let response = input.trim();
135
        if response != "y" {
136
            bar.finish_and_clear();
137
            println!("Response: {response}\nAborting...");
138
            return Ok(());
139
        }
140
    }
141
    let bonsol_client = BonsolClient::with_rpc_client(rpc_client);
142
    let image_id = manifest.image_id;
143
    let deploy = bonsol_client.get_deployment(&image_id).await;
144
    match deploy {
145
        Ok(Some(account)) => {
146
            bar.finish_and_clear();
147
            println!(
148
                "Deployment for account '{}' already exists, deployments are immutable",
149
                account.owner
150
            );
151
            Ok(())
152
        }
153
        Ok(None) => {
154
            let deploy_txn = bonsol_client
155
                .deploy_v1(
156
                    &signer.pubkey(),
157
                    &image_id,
158
                    manifest.size,
159
                    &manifest.name,
160
                    &url,
161
                    manifest
162
                        .input_order
163
                        .iter()
164
                        .map(|i| match i.as_str() {
165
                            "Public" => ProgramInputType::Public,
166
                            "Private" => ProgramInputType::Private,
167
                            _ => ProgramInputType::Unknown,
168
                        })
169
                        .collect(),
170
                )
171
                .await?;
172
            if let Err(err) = bonsol_client.send_txn_standard(signer, deploy_txn).await {
173
                bar.finish_and_clear();
174
                anyhow::bail!(err)
175
            }
176

            
177
            bar.finish_and_clear();
178
            println!("{} deployed", image_id);
179
            Ok(())
180
        }
181
        Err(e) => {
182
            bar.finish_with_message(format!("Error getting deployment: {:?}", e));
183
            Ok(())
184
        }
185
    }
186
}