deso_sdk/
lib.rs

1mod crypto_lib;
2mod errors;
3mod post_lib;
4pub use post_lib::SubmitPostDataBuilder;
5use reqwest;
6use serde::Deserialize;
7use serde::Serialize;
8use serde_json;
9use std::collections::HashMap;
10
11#[derive(Serialize, Deserialize, Debug)]
12struct TransactionFee {
13    #[serde(rename = "PublicKeyBase58Check")]
14    recipient_public_key: String,
15    #[serde(rename = "AmountNanos")]
16    nanos: u64,
17}
18
19#[derive(Serialize, Deserialize, Debug)]
20struct ExtraDataBody {
21    #[serde(rename = "TransactionHex")]
22    transaction_hex: String,
23    #[serde(rename = "ExtraData")]
24    extra_data: HashMap<String, String>,
25}
26#[derive(Serialize, Deserialize, Debug)]
27struct TransactionHex {
28    #[serde(rename = "TransactionHex")]
29    transaction_hex: String,
30}
31
32#[derive(Serialize, Deserialize, Debug)]
33struct TransactionSubmittedHex {
34    #[serde(rename = "TxnHashHex")]
35    txn_hash_hex: String,
36}
37
38#[derive(Serialize, Deserialize, Debug)]
39struct SignatureIndex {
40    #[serde(rename = "SignatureIndex")]
41    signature_index: u32,
42}
43
44#[derive(Serialize, Deserialize, Debug)]
45struct GetTransaction {
46    #[serde(rename = "TxnFound")]
47    txn_found: bool,
48}
49
50/// Determines whether to target the Main node or Test node
51#[derive(Serialize, Deserialize, Debug, Clone, Copy)]
52pub enum Node {
53    MAIN,
54    TEST
55}
56
57impl Node {
58    fn get_endpoint(self, api: &str) -> String {
59        match self {
60            Node::MAIN => format!("https://node.deso.org/{}", api),
61            Node::TEST => format!("https://test.deso.org/{}", api)
62        }
63    }
64}
65
66/// A Deso account that will be used to do any transactions
67#[derive(Serialize, Deserialize, Debug)]
68pub struct DesoAccount {
69    /// The deso account public key
70    public_key: String,
71    /// Either the seed hex or derived private key (recommended)
72    seed_hex_key: String,
73    /// The derived public key (needed if using a derived private key)
74    derived_public_key: Option<String>,
75    /// The Node you are targeting (Main is default)
76    node: Node,
77}
78/// A Deso account builder that will be used to do any transactions
79pub struct DesoAccountBuilder {
80    pub public_key: Option<String>,
81    pub seed_hex_key: Option<String>,
82    pub derived_public_key: Option<String>,
83    pub node: Option<Node>,
84}
85
86impl DesoAccountBuilder {
87    pub fn new() -> Self {
88        DesoAccountBuilder {
89            public_key: None,
90            seed_hex_key: None,
91            derived_public_key: None,
92            node: Some(Node::MAIN)
93        }
94    }
95    /// The deso account public key
96    pub fn public_key(mut self, public_key: String) -> Self {
97        self.public_key = Some(public_key);
98        self
99    }
100    /// Either the seed hex or derived private key (recommended)
101    pub fn seed_hex_key(mut self, seed_hex_key: String) -> Self {
102        self.seed_hex_key = Some(seed_hex_key);
103        self
104    }
105    /// The derived public key (needed if using a derived private key)
106    pub fn derived_public_key(mut self, derived_public_key: String) -> Self {
107        self.derived_public_key = Some(derived_public_key);
108        self
109    }
110    /// The Node you are targeting
111    pub fn node(mut self, node: Node) -> Self {
112        self.node = Some(node);
113        self
114    }
115    /// Builds the DesoAccount
116    pub fn build(self) -> Result<DesoAccount, errors::DesoError> {
117        if self.public_key.is_none() {
118            return Err(errors::DesoError::DesoAccountError(String::from(
119                "Public Key",
120            )));
121        }
122        if self.seed_hex_key.is_none() {
123            return Err(errors::DesoError::DesoAccountError(String::from(
124                "Seed Hex or Derived Private Key",
125            )));
126        }
127        Ok(DesoAccount {
128            public_key: self.public_key.unwrap(),
129            seed_hex_key: self.seed_hex_key.unwrap(),
130            derived_public_key: self.derived_public_key,
131            node: self.node.unwrap()
132        })
133    }
134}
135
136#[allow(non_camel_case_types)]
137#[allow(dead_code)]
138enum TransactionType {
139    POST,
140    MINT,
141    TRANS,
142    ACCEPT,
143    PAYMENT,
144    ACCEPT_BID,
145    MAKE_BID,
146    ACCEPT_TRANSFER,
147    AUTHORIZE,
148    UPDATE,
149    ASSOCIATION,
150}
151
152const DEBUG: bool = false;
153
154pub async fn create_post(
155    publisher_account: &DesoAccount,
156    post_data: &post_lib::SubmitPostData,
157) -> Result<post_lib::SubmittedTransaction, errors::DesoError> {
158    let client = reqwest::Client::new();
159    let post_uri = publisher_account.node.get_endpoint("api/v0/submit-post");
160
161    let post_transaction_response = submit_and_sign(
162        &publisher_account.node,
163        &post_uri,
164        &client,
165        &post_data,
166        1,
167        TransactionType::POST,
168        publisher_account.seed_hex_key.clone(),
169        publisher_account.derived_public_key.clone(),
170    )
171    .await?;
172    let transaction_json: post_lib::SubmittedTransaction =
173        match serde_json::from_str(&post_transaction_response.to_string()) {
174            Ok(j) => j,
175            Err(e) => {
176                return Err(errors::DesoError::JsonError(
177                    String::from("NEW POST ERROR"),
178                    e.to_string(),
179                ))
180            }
181        };
182    let _post_hash_hex = transaction_json.post_entry_response.post_hash_hex.clone();
183
184    return Ok(transaction_json);
185}
186
187async fn get_signature_index(
188    node: &Node,
189    tx_hex: &String,
190    client: &reqwest::Client,
191) -> Result<usize, errors::DesoError> {
192    let uri = node.get_endpoint("api/v0/signature-index");
193    let payload = TransactionHex {
194        transaction_hex: tx_hex.clone(),
195    };
196    let resp = match client.post(uri).json(&payload).send().await {
197        Ok(s) => s,
198        Err(e) => {
199            return Err(errors::DesoError::SigningError(format!(
200                "Problem getting index response: {}",
201                e.to_string()
202            )));
203        }
204    };
205    let text = match resp.text().await {
206        Ok(t) => t,
207        Err(e) => return Err(errors::DesoError::ReqwestError(e.to_string())),
208    };
209    if DEBUG {
210        println!("Response: {}", text);
211    }
212    let json: SignatureIndex = match serde_json::from_str(&text.to_string()) {
213        Ok(j) => j,
214        Err(e) => {
215            return Err(errors::DesoError::SigningError(format!(
216                "Problem parsing index response: {}",
217                e.to_string()
218            )));
219        }
220    };
221    Ok(json.signature_index as usize)
222}
223
224async fn submit_and_sign<T: Serialize + ?Sized>(
225    node: &Node,
226    uri: &str,
227    client: &reqwest::Client,
228    json: &T,
229    retry: u8,
230    tx_type: TransactionType,
231    signer_hex: String,
232    derived_public_key: Option<String>,
233) -> Result<String, errors::DesoError> {
234    let transaction = match tx_type {
235        TransactionType::MINT => "minting",
236        TransactionType::TRANS => "transfer",
237        TransactionType::POST => "posting",
238        TransactionType::ACCEPT => "accepting",
239        TransactionType::PAYMENT => "payment",
240        TransactionType::ACCEPT_BID => "accepting bid",
241        TransactionType::MAKE_BID => "making bid",
242        TransactionType::ACCEPT_TRANSFER => "accept transfer",
243        TransactionType::AUTHORIZE => "authorizing dervied key",
244        TransactionType::UPDATE => "updating nft to be for sale",
245        TransactionType::ASSOCIATION => "associating a new author",
246    };
247    if DEBUG {
248        println!("Logging for: {} transaction.", transaction);
249    }
250    let resp = match client.post(uri).json(&json).send().await {
251        Ok(s) => s,
252        Err(e) => {
253            return Err(errors::DesoError::TransactionError(
254                String::from(transaction),
255                format!("Error on Post: {}", e.to_string()),
256            ));
257        }
258    };
259    let text = match resp.text().await {
260        Ok(t) => t,
261        Err(e) => return Err(errors::DesoError::ReqwestError(e.to_string())),
262    };
263    if DEBUG {
264        println!("Response: {}", text);
265    }
266    let json: TransactionHex = match serde_json::from_str(&text.to_string()) {
267        Ok(j) => j,
268        Err(e) => {
269            return Err(errors::DesoError::TransactionError(
270                String::from(transaction),
271                format!("Problem in Response: {}; {}", text, e.to_string()),
272            ))
273        }
274    };
275    if DEBUG {
276        println!("BEFORE TX: {}", json.transaction_hex);
277    }
278    let mut tx_hex = json;
279    if let Some(key) = derived_public_key {
280        println!("Derived Public Key: {}", key);
281        tx_hex = match append_data(node, &tx_hex, key.to_string(), client).await {
282            Ok(t) => t,
283            Err(e) => {
284                return Err(errors::DesoError::TransactionError(
285                    String::from("Error appending derived public key tx"),
286                    e.to_string(),
287                ));
288            }
289        };
290    }
291    if DEBUG {
292        println!("\nAfter appending data: {}", tx_hex.transaction_hex);
293    }
294
295    // Get signature index
296    let signature_index = get_signature_index(node, &tx_hex.transaction_hex, client).await?;
297
298    let signed_transaction = crypto_lib::sign(tx_hex.transaction_hex, signer_hex, signature_index)?;
299
300    if DEBUG {
301        println!("\nAfter signing: {}", signed_transaction);
302    }
303    let json_transaction_hex: TransactionHex = TransactionHex {
304        transaction_hex: signed_transaction,
305    };
306    let mut i = 0;
307    let mut txn_hash_hex: TransactionSubmittedHex = TransactionSubmittedHex {
308        txn_hash_hex: String::from(""),
309    };
310
311    let mut response_message = String::from("success");
312
313    while i < retry {
314        i += 1;
315        match submit_transaction(node, &json_transaction_hex, client).await {
316            Ok(s) => {
317                response_message = s.clone();
318                txn_hash_hex = match serde_json::from_str(&s) {
319                    Ok(j) => j,
320                    Err(e) => {
321                        return Err(errors::DesoError::JsonError(
322                            String::from("SUBMIT TX"),
323                            e.to_string(),
324                        ))
325                    }
326                };
327                break;
328            }
329            Err(e) => {
330                std::thread::sleep(std::time::Duration::from_secs(1 << i));
331                println!("Error {}", e.to_string());
332            }
333        }
334    }
335
336    if txn_hash_hex.txn_hash_hex == String::from("") {
337        return Err(errors::DesoError::TransactionError(
338            String::from(transaction),
339            String::from("Transaction Failed :/"),
340        ));
341    } else if DEBUG {
342        println!("Txn Hash Hex: {}", txn_hash_hex.txn_hash_hex);
343    }
344
345    // Now we have submitted a transaction successfully, but let's wait and see
346    // if it is through before moving on.
347
348    let transaction_check_uri = node.get_endpoint("api/v0/get-txn");
349    let mut pause_count = 0;
350    while pause_count < 7 {
351        std::thread::sleep(std::time::Duration::from_secs(1 << pause_count));
352        match client
353            .post(&transaction_check_uri)
354            .json(&txn_hash_hex)
355            .send()
356            .await
357        {
358            Ok(resp) => {
359                let text = match resp.text().await {
360                    Ok(t) => t,
361                    Err(_) => {
362                        if DEBUG {
363                            println!("ERROR getting response for {}", transaction);
364                        }
365                        pause_count += 1;
366                        continue;
367                    }
368                };
369                let txn_found_struct: GetTransaction = match serde_json::from_str(&text.to_string())
370                {
371                    Ok(json) => json,
372                    Err(_) => {
373                        if DEBUG {
374                            println!("ERROR in transaction deserialzed for {}", transaction);
375                        }
376                        pause_count += 1;
377                        continue;
378                    }
379                };
380                if txn_found_struct.txn_found {
381                    return Ok(response_message);
382                } else {
383                    pause_count += 1;
384                }
385            }
386            Err(e) => {
387                if DEBUG {
388                    println!("Error for {}: {}", transaction, e);
389                }
390                pause_count += 1;
391            }
392        };
393    }
394    Ok(response_message)
395}
396
397async fn submit_transaction(
398    node: &Node,
399    tx: &TransactionHex,
400    client: &reqwest::Client,
401) -> Result<String, errors::DesoError> {
402    let uri = node.get_endpoint("api/v0/submit-transaction");
403    let resp = match client.post(uri).json(&tx).send().await {
404        Ok(r) => r,
405        Err(e) => return Err(errors::DesoError::ReqwestError(e.to_string())),
406    };
407    let status: bool = resp.status().is_success();
408    let raw_resp = match resp.text().await {
409        Ok(t) => t,
410        Err(e) => return Err(errors::DesoError::ReqwestError(e.to_string())),
411    };
412    println!("Response: {}", status);
413    if status {
414        Ok(raw_resp)
415    } else {
416        return Err(errors::DesoError::DesoError(raw_resp));
417    }
418}
419
420async fn append_data(
421    node: &Node,
422    tx: &TransactionHex,
423    derived_public_key: String,
424    client: &reqwest::Client,
425) -> Result<TransactionHex, errors::DesoError> {
426    let uri = node.get_endpoint("api/v0/append-extra-data");
427
428    let mut extra_data: HashMap<String, String> = HashMap::new();
429
430    extra_data.insert(String::from("DerivedPublicKey"), derived_public_key);
431    let post_data = ExtraDataBody {
432        transaction_hex: tx.transaction_hex.clone(),
433        extra_data: extra_data,
434    };
435    let resp = match client.post(uri).json(&post_data).send().await {
436        Ok(r) => r,
437        Err(e) => return Err(errors::DesoError::ReqwestError(e.to_string())),
438    };
439    let text = match resp.text().await {
440        Ok(t) => t,
441        Err(e) => return Err(errors::DesoError::ReqwestError(e.to_string())),
442    };
443    let json: TransactionHex = match serde_json::from_str(&text.to_string()) {
444        Ok(j) => j,
445        Err(e) => {
446            return Err(errors::DesoError::JsonError(
447                String::from("APPEND DATA"),
448                e.to_string(),
449            ))
450        }
451    };
452    Ok(json)
453}
454
455#[cfg(test)]
456mod tests {
457    use std::env;
458
459    use super::*;
460
461    macro_rules! aw {
462        ($e:expr) => {
463            tokio_test::block_on($e)
464        };
465    }
466    #[test]
467    fn test_create_post() {
468        dotenv::from_filename("src/.env").ok();
469        let deso_account = env::var("DESO_ACCOUNT").ok();
470        let deso_private_key = env::var("PRIVATE_KEY").ok();
471
472        let deso_account = DesoAccountBuilder::new()
473            .public_key(deso_account.unwrap())
474            .seed_hex_key(deso_private_key.unwrap())
475            .node(Node::TEST)
476            .build()
477            .unwrap();
478
479        let mut extra_data_map: HashMap<String, String> = HashMap::new();
480        extra_data_map.insert(String::from("nft_type"), String::from("AUTHOR"));
481
482        let post_data = post_lib::SubmitPostDataBuilder::new()
483            .body(String::from(
484                "Testing the new deso rust library by @Spatium!",
485            ))
486            .public_key(deso_account.public_key.clone())
487            .extra_data(extra_data_map)
488            .build()
489            .unwrap();
490
491        let post_transaction_json = aw!(create_post(&deso_account, &post_data)).unwrap();
492
493        let post_hash_hex = post_transaction_json.post_entry_response.post_hash_hex;
494
495        let comment_post_data = post_lib::SubmitPostDataBuilder::new()
496            .body(String::from("cool comment bro"))
497            .public_key(deso_account.public_key.clone())
498            .parent_post_hash_hex(post_hash_hex)
499            .build()
500            .unwrap();
501
502        let _comment_transaction_json =
503            aw!(create_post(&deso_account, &comment_post_data)).unwrap();
504    }
505}