1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
use serde::de::DeserializeOwned;
use std::{collections::HashMap, time::Duration};

use serde_json::{json, Value};
use solana_client::nonblocking::rpc_client::RpcClient;
use solana_sdk::{commitment_config::CommitmentConfig, signer::Signer, transaction::Transaction};

mod add_immutable_storage;
mod add_storage;
mod cancel_delete_storage_account;
mod claim_stake;
mod create_storage_account;
mod delete_file;
mod delete_storage_account;
mod edit_file;
mod get_storage_account;
mod list_objects;
mod make_storage_immutable;
mod migrate;
mod redeem_rent;
mod reduce_storage;
mod refresh_stake;
mod store_files;
mod top_up;
// mod upload_multiple_files;

use crate::{
    constants::SHDW_DRIVE_ENDPOINT,
    error::Error,
    models::{FileDataResponse, GetBucketSizeResponse, ShadowDriveResult},
};
pub use add_immutable_storage::*;
pub use add_storage::*;
pub use cancel_delete_storage_account::*;
pub use claim_stake::*;
pub use create_storage_account::*;
pub use delete_file::*;
pub use delete_storage_account::*;
pub use edit_file::*;
pub use get_storage_account::*;
pub use list_objects::*;
pub use make_storage_immutable::*;
pub use migrate::*;
pub use redeem_rent::*;
pub use reduce_storage::*;
pub use refresh_stake::*;
pub use store_files::*;
pub use top_up::*;

/// Client that allows a user to interact with the Shadow Drive.
pub struct ShadowDriveClient<T>
where
    T: Signer,
{
    wallet: T,
    rpc_client: RpcClient,
    http_client: reqwest::Client,
}

impl<T> ShadowDriveClient<T>
where
    T: Signer,
{
    /// Creates a new [`ShadowDriveClient`] from the given [`Signer`] and URL.
    /// * `wallet` - A [`Signer`] that for signs all transactions generated by the client. Typically this is a user's keypair.
    /// * `rpc_url` - An HTTP URL of a Solana RPC provider.
    ///
    /// The underlying Solana RPC client is configured with 120s timeout and a [commitment level][cl] of [`confirmed`](solana_sdk::commitment_config::CommitmentLevel::Confirmed).
    ///
    /// [cl]: https://docs.solana.com/developing/clients/jsonrpc-api#configuring-state-commitment
    ///
    /// To customize [`RpcClient`] settings see [`new_with_rpc`](Self::new_with_rpc).
    ///
    /// # Example
    /// ```
    /// use solana_sdk::signer::keypair::Keypair;    
    ///
    /// let wallet = Keypair::generate();
    /// let shdw_drive = ShadowDriveClient::new(wallet, "https://ssc-dao.genesysgo.net");
    /// ```
    pub fn new<U: ToString>(wallet: T, rpc_url: U) -> Self {
        let rpc_client = RpcClient::new_with_timeout_and_commitment(
            rpc_url.to_string(),
            Duration::from_secs(120),
            CommitmentConfig::confirmed(),
        );
        Self {
            wallet,
            rpc_client,
            http_client: reqwest::Client::new(),
        }
    }

    /// Creates a new [`ShadowDriveClient`] from the given [`Signer`] and [`RpcClient`].
    /// * `wallet` - A [`Signer`] that for signs all transactions generated by the client. Typically this is a user's keypair.
    /// * `rpc_client` - A Solana [`RpcClient`] that handles sending transactions and reading accounts from the blockchain.
    ///
    /// Providng the [`RpcClient`] allows customization of timeout and committment level.
    ///
    /// # Example
    /// ```
    /// use solana_client::rpc_client::RpcClient;
    /// use solana_sdk::signer::keypair::Keypair;    
    /// use solana_sdk::commitment_config::CommitmentConfig;
    ///
    /// let wallet = Keypair::generate();
    /// let solana_rpc = RpcClient::new_with_commitment("https://ssc-dao.genesysgo.net", CommitmentConfig::confirmed());
    /// let shdw_drive = ShadowDriveClient::new_with_rpc(wallet, solana_rpc);
    /// ```
    pub fn new_with_rpc(wallet: T, rpc_client: RpcClient) -> Self {
        Self {
            wallet,
            rpc_client,
            http_client: reqwest::Client::new(),
        }
    }

    pub async fn get_object_data(&self, location: &str) -> ShadowDriveResult<FileDataResponse> {
        let response = self
            .http_client
            .post(format!("{}/get-object-data", SHDW_DRIVE_ENDPOINT))
            .header("Content-Type", "application/json")
            .json(&json!({ "location": location }))
            .send()
            .await?;

        if !response.status().is_success() {
            return Err(Error::ShadowDriveServerError {
                status: response.status().as_u16(),
                message: response.json::<Value>().await?,
            });
        }

        let response = response.json::<FileDataResponse>().await?;

        Ok(response)
    }
    pub async fn get_storage_account_size(
        &self,
        storage_account_key: &str,
    ) -> ShadowDriveResult<GetBucketSizeResponse> {
        let mut bucket_query = HashMap::new();
        bucket_query.insert("storageAccount", storage_account_key.to_string());
        let response = self
            .http_client
            .get(format!("{}/storage-account-size", SHDW_DRIVE_ENDPOINT))
            .query(&bucket_query)
            .header("Content-Type", "application/json")
            .send()
            .await?;

        if !response.status().is_success() {
            return Err(Error::ShadowDriveServerError {
                status: response.status().as_u16(),
                message: response.json::<Value>().await?,
            });
        }

        let response = response.json::<GetBucketSizeResponse>().await?;

        Ok(response)
    }

    async fn send_shdw_txn<K: DeserializeOwned>(
        &self,
        uri: &str,
        txn_encoded: String,
        storage_used: Option<u64>,
    ) -> ShadowDriveResult<K> {
        let body = serde_json::to_string(&json!({
           "transaction": txn_encoded,
           "commitment": "finalized",
           "storageUsed": Some(storage_used)
        }))
        .map_err(Error::InvalidJson)?;

        let response = self
            .http_client
            .post(format!("{}/{}", SHDW_DRIVE_ENDPOINT, uri))
            .header("Content-Type", "application/json")
            .body(body)
            .send()
            .await?;

        if !response.status().is_success() {
            return Err(Error::ShadowDriveServerError {
                status: response.status().as_u16(),
                message: response.json::<Value>().await?,
            });
        }

        let response = response.json::<K>().await?;

        Ok(response)
    }
}

pub(crate) fn serialize_and_encode(txn: &Transaction) -> ShadowDriveResult<String> {
    let serialized = bincode::serialize(txn)
        .map_err(|error| Error::TransactionSerializationFailed(format!("{:?}", error)))?;
    Ok(base64::encode(serialized))
}