1use std::path::Path;
2use std::str::FromStr;
3use std::sync::Arc;
4
5use pyo3::exceptions::{PyRuntimeError, PyValueError};
6use pyo3::prelude::*;
7use pyo3::{
8 pymodule,
9 types::{PyModule, PyString},
10 PyResult, Python,
11};
12use shadow_drive_sdk::constants::SHDW_DRIVE_OBJECT_PREFIX;
13use shadow_drive_sdk::models::{ShadowFile, ShadowUploadResponse};
14use shadow_drive_sdk::{
15 read_keypair_file, Byte, CommitmentConfig, Keypair, Pubkey, RpcClient,
16 ShadowDriveClient as ShadowDriveRustClient, Signer,
17};
18use tokio::runtime::{Builder, Runtime};
19
20#[pymodule]
22fn shadow_drive(_py: Python, m: &PyModule) -> PyResult<()> {
23 const SOLANA_MAINNET_BETA: &'static str = "https://api.mainnet-beta.solana.com";
25 m.add("SOLANA_MAINNET_BETA", SOLANA_MAINNET_BETA)?;
26
27 #[pyclass]
28 pub struct ShadowDriveClient {
29 rust_client: Arc<ShadowDriveRustClient<Keypair>>,
30 runtime: Runtime,
31 current_account: Option<Pubkey>,
32 }
33
34 #[pymethods]
35 impl ShadowDriveClient {
36 #[new]
44 fn new(keypair: &str, account: Option<&str>) -> PyResult<ShadowDriveClient> {
45 let keypair: Keypair = read_keypair_file(keypair)
46 .map_err(|e| PyRuntimeError::new_err(format!("Failed to read keypair file {e}")))?;
47
48 let rust_client = Arc::new(ShadowDriveRustClient::new(
49 keypair,
50 SOLANA_MAINNET_BETA.to_string(),
51 ));
52 let runtime = Builder::new_multi_thread()
53 .worker_threads(2)
54 .enable_time()
55 .enable_io()
56 .build()
57 .unwrap();
58 let current_account: Option<Pubkey> =
59 check_current_account(account, &rust_client, &runtime);
60
61 Ok(ShadowDriveClient {
62 rust_client,
63 runtime,
64 current_account,
65 })
66 }
67
68 fn new_with_commitment(
74 keypair: Py<PyAny>,
75 commitment: &str,
76 account: Option<&str>,
77 ) -> PyResult<ShadowDriveClient> {
78 let commitment_config = extract_commitment(commitment)?;
80
81 let keypair: Keypair = read_keypair_file(keypair.to_string())
82 .map_err(|e| PyRuntimeError::new_err(format!("Failed to read keypair file {e}")))?;
83
84 let rust_client = Arc::new(ShadowDriveRustClient::new_with_rpc(
85 keypair,
86 RpcClient::new_with_commitment(SOLANA_MAINNET_BETA.to_string(), commitment_config),
87 ));
88
89 let runtime = Builder::new_multi_thread()
90 .worker_threads(2)
91 .enable_time()
92 .enable_io()
93 .build()
94 .unwrap();
95 let current_account: Option<Pubkey> =
96 check_current_account(account, &rust_client, &runtime);
97
98 Ok(ShadowDriveClient {
99 rust_client,
100 runtime,
101 current_account,
102 })
103 }
104
105 fn new_with_rpc(
110 keypair: Py<PyAny>,
111 rpc: &str,
112 account: Option<&str>,
113 ) -> PyResult<ShadowDriveClient> {
114 let keypair: Keypair = read_keypair_file(keypair.to_string())
115 .map_err(|e| PyRuntimeError::new_err(format!("Failed to read keypair file {e}")))?;
116
117 let rust_client = Arc::new(ShadowDriveRustClient::new_with_rpc(
118 keypair,
119 RpcClient::new_with_commitment(rpc.to_string(), CommitmentConfig::finalized()),
120 ));
121
122 let runtime = Builder::new_multi_thread()
123 .worker_threads(2)
124 .enable_time()
125 .enable_io()
126 .build()
127 .unwrap();
128 let current_account: Option<Pubkey> =
129 check_current_account(account, &rust_client, &runtime);
130
131 Ok(ShadowDriveClient {
132 rust_client,
133 runtime,
134 current_account,
135 })
136 }
137
138 fn new_with_commitment_and_rpc(
144 keypair: Py<PyAny>,
145 commitment: &str,
146 rpc: &str,
147 account: Option<&str>,
148 py: Python,
149 ) -> PyResult<ShadowDriveClient> {
150 let commitment_config = extract_commitment(commitment)?;
152
153 let keypair: Keypair = read_keypair_file(keypair.to_string())
154 .map_err(|e| PyRuntimeError::new_err(format!("Failed to read keypair file {e}")))?;
155
156 let rust_client = Arc::new(ShadowDriveRustClient::new_with_rpc(
157 keypair,
158 RpcClient::new_with_commitment(rpc.to_string(), commitment_config),
159 ));
160
161 let runtime = Builder::new_multi_thread()
162 .worker_threads(2)
163 .enable_time()
164 .enable_io()
165 .build()
166 .unwrap();
167 let current_account: Option<Pubkey> =
168 check_current_account(account, &rust_client, &runtime);
169
170 Ok(ShadowDriveClient {
171 rust_client,
172 runtime,
173 current_account,
174 })
175 }
176
177 fn create_account(
182 &mut self,
183 name: &str,
184 size: u64,
185 use_account: Option<bool>,
186 py: Python,
187 ) -> PyResult<(Py<PyString>, Py<PyString>)> {
188 let result: PyResult<(Py<PyString>, Py<PyString>)> = self.runtime.block_on(async {
189 self.rust_client
190 .create_storage_account(
191 name,
192 Byte::from(size as u128),
193 shadow_drive_sdk::StorageAccountVersion::V2,
194 )
195 .await
196 .map(|response| {
197 (
198 PyString::new(py, &response.shdw_bucket.unwrap()).into(),
199 PyString::new(py, &response.transaction_signature).into(),
200 )
201 })
202 .map_err(|err| {
203 PyValueError::new_err(format!("failed to create storage account {err:?}"))
204 .into()
205 })
206 });
207 if let Ok((ref bucket, _)) = result {
208 if let Some(true) = use_account {
209 self.current_account = Some(
210 Pubkey::from_str(&bucket.to_string())
211 .expect("sucessful storage account creation"),
212 );
213 }
214 }
215 result
216 }
217
218 fn delete_account(&self, name: &str) -> PyResult<()> {
223 self.runtime
224 .block_on(self.rust_client.delete_storage_account(&try_pubkey(name)?))
225 .map(|response| {
226 println!("deleted storage account {name}: {}", response.txid);
227 })
228 .map_err(|err| {
229 PyValueError::new_err(format!("failed to delete storage account {err:?}"))
230 .into()
231 })
232 }
233
234 fn add_storage(&self, amount: u64) -> PyResult<()> {
239 self.runtime.block_on(async {
240 if let Some(ref account) = self.current_account {
242 let storage_account = self
243 .rust_client
244 .get_storage_account(account)
245 .await
246 .map_err(|e| {
247 PyRuntimeError::new_err(format!(
248 "unable to retrieve storage account {e:?}"
249 ))
250 })?;
251
252 if storage_account.is_immutable() {
253 self.rust_client
254 .add_immutable_storage(account, Byte::from(amount))
255 .await
256 .map(|response| {
257 println!("AddImmutableStorage Response: {}", response.message);
258 })
259 .map_err(|err| {
260 PyValueError::new_err(format!("failed to add storage {err:?}"))
261 .into()
262 })
263 } else {
264 self.rust_client
265 .add_storage(account, Byte::from(amount))
266 .await
267 .map(|response| {
268 println!("AddStorage Response: {}", response.message);
269 })
270 .map_err(|err| {
271 PyValueError::new_err(format!("failed to add storage {err:?}"))
272 .into()
273 })
274 }
275 } else {
276 Err(PyRuntimeError::new_err(
277 "no storage account is set. Set one using the set_account(...) method",
278 ))
279 }
280 })
281 }
282
283 fn reduce_storage(&self, amount: u64) -> PyResult<()> {
288 self.runtime.block_on(async {
289 if let Some(ref account) = self.current_account {
291 let storage_account = self
292 .rust_client
293 .get_storage_account(account)
294 .await
295 .map_err(|e| {
296 PyRuntimeError::new_err(format!(
297 "unable to retrieve storage account {e:?}"
298 ))
299 })?;
300
301 let is_immutable: bool = storage_account.is_immutable();
302 let total_storage = storage_account.storage();
303 if total_storage < amount {
304 return Err(PyRuntimeError::new_err(format!("Account only has {total_storage} bytes, but you attempted to reduce by {amount}")));
305 }
306
307 if is_immutable {
308 Err(PyRuntimeError::new_err(
309 "Account selected is immutable. Cannot remove storage",
310 ))
311 } else {
312 self.rust_client
313 .reduce_storage(account, Byte::from(amount))
314 .await
315 .map(|response| {
316 println!("RemoveStorage Response: {}", response.message);
317 })
318 .map_err(|err| {
319 PyValueError::new_err(format!("failed to add storage {err:?}"))
320 .into()
321 })
322 }
323 } else {
324 Err(PyRuntimeError::new_err(
325 "no storage account is set. Set one using the set_account(...) method",
326 ))
327 }
328 })
329 }
330
331 fn upload_files(&self, files: Vec<&str>, py: Python) -> PyResult<Vec<Py<PyString>>> {
337 if let Some(ref storage_account) = self.current_account {
338 let files: Vec<ShadowFile> = files
340 .into_iter()
341 .map(|file| {
342 let path: &Path = Path::new(file);
343 if let Some(name) = path
344 .file_name()
345 .map(|name| name.to_string_lossy().to_string())
346 {
347 Ok(ShadowFile::file(name, path))
348 } else {
349 Err(PyValueError::new_err(format!(
350 "an invalid file path was provided: {}",
351 path.display()
352 )))
353 }
354 })
355 .collect::<PyResult<Vec<ShadowFile>>>()?;
356
357 let response: ShadowUploadResponse = self
359 .runtime
360 .block_on(self.rust_client.store_files(storage_account, files))
361 .map_err(|err| {
362 PyValueError::new_err(format!("failed to upload files: {err:?}"))
363 })?;
364
365 for error in &response.upload_errors {
367 println!("failed to upload file {}: {}", &error.file, &error.error);
368 }
369
370 let successes = response
372 .finalized_locations
373 .iter()
374 .map(|loc| PyString::new(py, loc).into())
375 .collect();
376 Ok(successes)
377 } else {
378 Err(PyRuntimeError::new_err("No storage account is specified. Create one with create_account, or specify one with set_account"))
379 }
380 }
381
382 fn delete_files(&self, file_urls: Vec<String>) -> PyResult<()> {
387 if let Some(ref storage_account) = self.current_account {
388 self.runtime.block_on(async move {
389 tokio_scoped::scope(|scope| {
390 for url in file_urls {
391 scope.spawn(async move {
392 if let Err(err) = self
393 .rust_client
394 .delete_file(&storage_account, url.clone())
395 .await
396 {
397 println!("failed to delete file {url}: {err:?}");
398 }
399 });
400 }
401 });
402 });
403 Ok(())
404 } else {
405 Err(PyRuntimeError::new_err("No storage account is specified. Create one with create_account, or specify one with set_account"))
406 }
407 }
408
409 fn list_files(&self, account: Option<&str>) -> PyResult<Vec<String>> {
414 let account_to_check = self
415 .current_account
416 .map(Result::Ok)
417 .or(account.map(Pubkey::from_str));
418
419 if let Some(Ok(ref storage_account)) = account_to_check {
420 self.runtime.block_on(async move {
421 self.rust_client
422 .list_objects(storage_account)
423 .await
424 .map_err(|e| {
425 PyRuntimeError::new_err(format!(
426 "failed to gather files for storage account: {e:?}"
427 ))
428 })
429 })
430 } else {
431 Err(PyRuntimeError::new_err("No storage account is specified. Create one with create_account, specify one with set_account, or pass in the 'account' optional arugment"))
432 }
433 }
434
435 fn get_file(&self, file: &str) -> PyResult<Vec<u8>> {
440 let url = if file.contains(SHDW_DRIVE_OBJECT_PREFIX) {
441 file.to_string()
442 } else {
443 if let Some(ref storage_account) = self.current_account {
444 format!("{SHDW_DRIVE_OBJECT_PREFIX}")
445 } else {
446 return Err(PyRuntimeError::new_err("No storage account is specified. Create one with create_account, specify one with set_account, or pass in the 'account' optional arugment"));
447 }
448 };
449 self.runtime.block_on(async move {
450 reqwest::get(url)
451 .await
452 .map(|response| response.bytes())
453 .map_err(|e| PyRuntimeError::new_err(format!("failed to retrieve file {e:?}")))?
454 .await
455 .map(|bytes| bytes.to_vec())
456 .map_err(|e| PyRuntimeError::new_err(format!("failed to retrieve file {e:?}")))
457 })
458 }
459
460 fn cancel_delete_account(&self, account: Option<&str>) -> PyResult<String> {
466 let account_to_check = self
467 .current_account
468 .map(Result::Ok)
469 .or(account.map(Pubkey::from_str));
470
471 if let Some(Ok(ref storage_account)) = account_to_check {
472 self.runtime.block_on(async move {
473 self.rust_client
474 .cancel_delete_storage_account(storage_account)
475 .await
476 .map(|response| {
477 println!("CancelDeleteAccount Response: {}", response.txid);
478 response.txid
479 })
480 .map_err(|err| {
481 PyValueError::new_err(format!("failed to add storage {err:?}")).into()
482 })
483 })
484 } else {
485 Err(PyRuntimeError::new_err("No storage account is specified. Create one with create_account, specify one with set_account, or pass in the 'account' optional arugment"))
486 }
487 }
488
489 fn make_account_immutable(&self, skip_warning: Option<bool>) -> PyResult<()> {
494 if let Some(ref storage_account) = self.current_account {
495 if skip_warning != Some(true) {
497 println!("You are about to make {storage_account} immutable. This is a permanent, irreversible action. Proceed? [y/n]");
498 let mut user_input = String::new();
499 let _ = std::io::stdin().read_line(&mut user_input);
500
501 if !["yes", "y"].contains(&user_input.to_lowercase().as_ref()) {
502 println!("Did not mark account as immutable");
503 return Ok(());
504 }
505 }
506
507 self.runtime.block_on(async move {
508 self.rust_client
509 .make_storage_immutable(storage_account)
510 .await
511 .map(|response| {
512 println!("RemoveStorage Response: {}", response.message);
513 })
514 .map_err(|e| {
515 PyRuntimeError::new_err(format!(
516 "failed to mark account as immutable: {e:?}"
517 ))
518 })
519 })
520 } else {
521 Err(PyRuntimeError::new_err("No storage account is specified. Create one with create_account, specify one with set_account, or pass in the 'account' optional arugment"))
522 }
523 }
524
525 fn claim_stake(&self, account: Option<&str>) -> PyResult<String> {
530 let account_to_check = self
531 .current_account
532 .map(Result::Ok)
533 .or(account.map(Pubkey::from_str));
534
535 if let Some(Ok(ref storage_account)) = account_to_check {
536 self.runtime.block_on(async move {
537 self.rust_client
538 .claim_stake(storage_account)
539 .await
540 .map(|response| {
541 println!("ClaimStake Response: {}", response.txid);
542 response.txid
543 })
544 .map_err(|e| {
545 PyRuntimeError::new_err(format!(
546 "failed to claim stake for storage account: {e:?}"
547 ))
548 })
549 })
550 } else {
551 Err(PyRuntimeError::new_err("No storage account is specified. Create one with create_account, specify one with set_account, or pass in the 'account' optional arugment"))
552 }
553 }
554
555 fn set_account(&mut self, account: &str) -> PyResult<()> {
560 self.current_account = Some(Pubkey::from_str(account).map_err(|err| {
561 PyValueError::new_err(format!(
562 "an invalid Pubkey {} was provided: {}",
563 account, err
564 ))
565 })?);
566
567 Ok(())
568 }
569 }
570
571 m.add_class::<ShadowDriveClient>()?;
572
573 Ok(())
574}
575
576fn extract_commitment(commitment: &str) -> PyResult<CommitmentConfig> {
577 match commitment.to_lowercase().as_ref() {
578 "processed" => Ok(CommitmentConfig::processed()),
579 "confirmed" => Ok(CommitmentConfig::confirmed()),
580 "finalized" => Ok(CommitmentConfig::finalized()),
581 _ => Err(PyValueError::new_err(
582 "the only acceptable commitment values are 'processed', 'confirmed', and 'finalized'.",
583 )),
584 }
585}
586
587fn check_current_account(
588 account: Option<&str>,
589 rust_client: &ShadowDriveRustClient<Keypair>,
590 runtime: &Runtime,
591) -> Option<Pubkey> {
592 if let Some(acct) = account {
593 match Pubkey::from_str(&acct) {
594 Ok(key) => runtime
596 .block_on(rust_client.get_storage_account(&key))
597 .map(|_| key)
598 .map_err(|err| {
599 println!("invalid account pubkey provided: {err:?}");
600 })
601 .ok(),
602
603 Err(err) => {
605 println!("invalid account pubkey provided: {err:?}");
606 None
607 }
608 }
609 } else {
610 None
611 }
612}
613
614fn try_pubkey(key: &str) -> PyResult<Pubkey> {
615 Pubkey::from_str(key).map_err(|err| {
616 PyValueError::new_err(format!("an invalid Pubkey {} was provided: {}", key, err))
617 })
618}