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#[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#[derive(Serialize, Deserialize, Debug)]
68pub struct DesoAccount {
69 public_key: String,
71 seed_hex_key: String,
73 derived_public_key: Option<String>,
75 node: Node,
77}
78pub 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 pub fn public_key(mut self, public_key: String) -> Self {
97 self.public_key = Some(public_key);
98 self
99 }
100 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 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 pub fn node(mut self, node: Node) -> Self {
112 self.node = Some(node);
113 self
114 }
115 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 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 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}