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
use serde::de::DeserializeOwned;
use std::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 store_files;
// mod upload_multiple_files;

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 store_files::*;

use crate::{
    constants::SHDW_DRIVE_ENDPOINT,
    error::Error,
    models::{FileDataResponse, ShadowDriveResult},
};

/// 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)
    }

    async fn send_shdw_txn<K: DeserializeOwned>(
        &self,
        uri: &str,
        txn_encoded: String,
    ) -> ShadowDriveResult<K> {
        let body = serde_json::to_string(&json!({
           "transaction": txn_encoded,
           "commitment": "finalized"
        }))
        .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))
}