sugar_cli/upload/methods/
shdw.rs1use 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
24const SHADOW_DRIVE_PROGRAM_ID: Pubkey = pubkey!("2e1wdyNhUvE76y6yUCvah2KaviavMJYKoRun8acMRBZZ");
26const MAINNET_ENDPOINT: &str = "https://shadow-storage.genesysgo.net";
28const DEVNET_ENDPOINT: &str = "https://shadow-storage-dev.genesysgo.net";
30const 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 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}