sugar_cli/upload/methods/
shdw.rs

1use std::{fs, ops::Deref, sync::Arc};
2
3use async_trait::async_trait;
4use data_encoding::HEXLOWER;
5use reqwest::{
6    multipart::{Form, Part},
7    StatusCode,
8};
9use ring::digest::{Context, SHA256};
10use solana_program::pubkey;
11use tokio::task::JoinHandle;
12
13use crate::{
14    common::*,
15    config::*,
16    upload::{
17        assets::{get_updated_metadata, AssetPair, DataType},
18        uploader::{AssetInfo, ParallelUploader, Prepare, MOCK_URI_SIZE},
19        UploadError,
20    },
21    utils::*,
22};
23
24// Shadow Drive program id.
25const SHADOW_DRIVE_PROGRAM_ID: Pubkey = pubkey!("2e1wdyNhUvE76y6yUCvah2KaviavMJYKoRun8acMRBZZ");
26// Shadow Drive mainnet endpoint.
27const MAINNET_ENDPOINT: &str = "https://shadow-storage.genesysgo.net";
28// Shadow Drive devnet endpoint.
29const DEVNET_ENDPOINT: &str = "https://shadow-storage-dev.genesysgo.net";
30// Shadow Drive files location.
31const SHDW_DRIVE_LOCATION: &str = "https://shdw-drive.genesysgo.net";
32
33#[derive(Debug, Deserialize, Default)]
34#[serde(default)]
35pub struct StorageInfo {
36    pub reserved_bytes: u64,
37    pub current_usage: u64,
38    pub immutable: bool,
39    pub owner1: Option<String>,
40    pub owner2: Option<String>,
41}
42
43pub struct Config {
44    endpoint: String,
45    keypair: Keypair,
46    storage_account: Pubkey,
47    storage_info: StorageInfo,
48}
49
50pub struct SHDWMethod(Arc<Config>);
51
52impl Deref for SHDWMethod {
53    type Target = Arc<Config>;
54    fn deref(&self) -> &Self::Target {
55        &self.0
56    }
57}
58
59impl SHDWMethod {
60    pub async fn new(sugar_config: &SugarConfig, config_data: &ConfigData) -> Result<Self> {
61        if let Some(pubkey) = &config_data.shdw_storage_account {
62            let client = setup_client(sugar_config)?;
63            let program = client.program(SHADOW_DRIVE_PROGRAM_ID);
64            let miraland_cluster: Cluster = get_cluster(program.rpc())?;
65
66            let endpoint = match miraland_cluster {
67                Cluster::Devnet => DEVNET_ENDPOINT,
68                Cluster::Mainnet => MAINNET_ENDPOINT,
69                Cluster::Unknown | Cluster::Localnet => {
70                    return Err(anyhow!(
71                        "ShadowDrive is only supported on devnet or mainnet"
72                    ));
73                }
74            };
75
76            let http_client = reqwest::Client::new();
77            let mut json = HashMap::new();
78            json.insert("storage_account", pubkey);
79
80            let response = http_client
81                .post(format!("{endpoint}/storage-account-info"))
82                .json(&json)
83                .send()
84                .await?;
85
86            let key_bytes = sugar_config.keypair.to_bytes();
87            let keypair = Keypair::from_bytes(&key_bytes)?;
88
89            match response.status() {
90                StatusCode::OK => {
91                    let body = response.json::<Value>().await?;
92                    let storage_info: StorageInfo = serde_json::from_value(body)?;
93
94                    Ok(Self(Arc::new(Config {
95                        endpoint: endpoint.to_string(),
96                        keypair,
97                        storage_account: Pubkey::from_str(pubkey)?,
98                        storage_info,
99                    })))
100                }
101                code => Err(anyhow!("Could not initialize storage account: {code}")),
102            }
103        } else {
104            Err(anyhow!(
105                "Missing 'shdwStorageAccount' value in config file."
106            ))
107        }
108    }
109}
110
111#[async_trait]
112impl Prepare for SHDWMethod {
113    async fn prepare(
114        &self,
115        _sugar_config: &SugarConfig,
116        assets: &HashMap<isize, AssetPair>,
117        asset_indices: Vec<(DataType, &[isize])>,
118    ) -> Result<()> {
119        // calculates the size of the files to upload, this assumes that the total
120        // storage has enough space to hold the collection as assets might already
121        // exist and therefore will be replaced
122        let mut total_size = 0;
123
124        for (data_type, indices) in asset_indices {
125            match data_type {
126                DataType::Image => {
127                    for index in indices {
128                        let item = assets.get(index).unwrap();
129                        let path = Path::new(&item.image);
130                        total_size += fs::metadata(path)?.len();
131                    }
132                }
133                DataType::Animation => {
134                    for index in indices {
135                        let item = assets.get(index).unwrap();
136
137                        if let Some(animation) = &item.animation {
138                            let path = Path::new(animation);
139                            total_size += fs::metadata(path)?.len();
140                        }
141                    }
142                }
143                DataType::Metadata => {
144                    let mock_uri = "x".repeat(MOCK_URI_SIZE);
145
146                    for index in indices {
147                        let item = assets.get(index).unwrap();
148                        let animation = if item.animation.is_some() {
149                            Some(mock_uri.clone())
150                        } else {
151                            None
152                        };
153
154                        total_size +=
155                            get_updated_metadata(&item.metadata, &mock_uri.clone(), &animation)?
156                                .into_bytes()
157                                .len() as u64;
158                    }
159                }
160            }
161        }
162
163        if self.storage_info.reserved_bytes < total_size {
164            let required = total_size - self.storage_info.reserved_bytes;
165            return Err(anyhow!(
166                "Insufficient storage space (additional {required} bytes required)"
167            ));
168        }
169
170        Ok(())
171    }
172}
173
174#[async_trait]
175impl ParallelUploader for SHDWMethod {
176    fn upload_asset(&self, asset_info: AssetInfo) -> JoinHandle<Result<(String, String)>> {
177        let config = self.0.clone();
178        tokio::spawn(async move { config.send(asset_info).await })
179    }
180}
181
182impl Config {
183    async fn send(&self, asset_info: AssetInfo) -> Result<(String, String)> {
184        let data = match asset_info.data_type {
185            DataType::Image => fs::read(&asset_info.content)?,
186            DataType::Metadata => asset_info.content.into_bytes(),
187            DataType::Animation => fs::read(&asset_info.content)?,
188        };
189
190        let mut context = Context::new(&SHA256);
191        context.update(asset_info.name.as_bytes());
192        let hash = HEXLOWER.encode(context.finish().as_ref());
193
194        let message = format!(
195            "Shadow Drive Signed Message:\n\
196            Storage Account: {}\n\
197            Upload files with hash: {hash}",
198            self.storage_account
199        );
200
201        let signature = self.keypair.sign_message(message.as_bytes()).to_string();
202
203        let mut form = Form::new();
204        let file = Part::bytes(data)
205            .file_name(asset_info.name.clone())
206            .mime_str(asset_info.content_type.as_str())?;
207        form = form
208            .part("file", file)
209            .text("message", signature)
210            .text("overwrite", "true")
211            .text("signer", self.keypair.pubkey().to_string())
212            .text("storage_account", self.storage_account.to_string())
213            .text("fileNames", asset_info.name.to_string());
214
215        let http_client = reqwest::Client::new();
216        let response = http_client
217            .post(format!("{}/upload", self.endpoint))
218            .multipart(form)
219            .send()
220            .await?;
221        let status = response.status();
222
223        if status.is_success() {
224            Ok((
225                asset_info.asset_id,
226                format!(
227                    "{SHDW_DRIVE_LOCATION}/{}/{}",
228                    self.storage_account, asset_info.name
229                ),
230            ))
231        } else {
232            Err(anyhow!(UploadError::SendDataFailed(format!(
233                "Error uploading file ({}): {}",
234                status,
235                response.text().await?,
236            ))))
237        }
238    }
239}