1use crate::client::tx_types::{TXResultAsync, TXResultSync, TxFeeResult};
3use crate::core_types::{Coin, StdFee, StdSignMsg, StdSignature};
4use reqwest::header::{HeaderMap, CONTENT_TYPE, USER_AGENT};
5use reqwest::{Client, RequestBuilder};
6use serde::{Deserialize, Serialize};
7
8pub mod auth;
9pub mod auth_types;
11pub mod bank;
13pub mod client_types;
15pub mod core_types;
17pub mod fcd;
18pub mod lcd_types;
19pub mod market;
21pub mod oracle;
23pub mod oracle_types;
25pub mod rpc;
27pub mod rpc_types;
28pub mod staking;
30pub mod staking_types;
32pub mod tendermint;
34pub mod tendermint_types;
36pub mod tx;
38pub mod tx_types;
40pub mod wasm;
42pub mod wasm_types;
43
44use crate::auth_types::AuthAccount;
45use crate::errors::TerraRustAPIError;
46use crate::errors::TerraRustAPIError::{GasPriceError, TxResultError};
47use crate::messages::Message;
48use crate::PrivateKey;
49use crate::{AddressBook, LCDResult};
50
51use rust_decimal_macros::dec;
52use secp256k1::Secp256k1;
53use secp256k1::Signing;
54use std::fs::File;
55
56const VERSION: Option<&'static str> = option_env!("CARGO_PKG_VERSION");
58const NAME: Option<&'static str> = option_env!("CARGO_PKG_NAME");
60
61const NETWORK_PROD_ADDRESS_BOOK: &str = "https://network.terra.dev/addrbook.json";
62const NETWORK_TEST_ADDRESS_BOOK: &str =
63 "https://raw.githubusercontent.com/terra-money/testnet/master/bombay-12/addrbook.json";
64
65#[derive(Clone, Debug)]
69pub struct GasOptions {
70 pub fees: Option<Coin>,
72 pub estimate_gas: bool,
75 pub gas: Option<u64>,
77 pub gas_price: Option<Coin>,
79 pub gas_adjustment: Option<f64>,
81}
82impl GasOptions {
83 pub fn create_with_fees(fees: &str, gas: u64) -> Result<GasOptions, TerraRustAPIError> {
85 Ok(GasOptions {
86 fees: Coin::parse(fees)?,
87 estimate_gas: false,
88 gas: Some(gas),
89 gas_price: None,
90 gas_adjustment: None,
91 })
92 }
93 pub fn create_with_gas_estimate(
96 gas_price: &str,
97 gas_adjustment: f64,
98 ) -> Result<GasOptions, TerraRustAPIError> {
99 Ok(GasOptions {
100 fees: None,
101 estimate_gas: true,
102 gas: None,
103 gas_price: Coin::parse(gas_price)?,
104 gas_adjustment: Some(gas_adjustment),
105 })
106 }
107 pub async fn create_with_fcd(
108 client: &reqwest::Client,
109 fcd_url: &str,
110 gas_denom: &str,
111 gas_adjustment: f64,
112 ) -> Result<GasOptions, TerraRustAPIError> {
113 let prices = fcd::FCD::fetch_gas_prices(client, fcd_url).await?;
114 if let Some(price) = prices.get(gas_denom) {
115 let gas_coin = Coin::create(gas_denom, *price);
116 let gas_price = Some(gas_coin);
117 Ok(GasOptions {
118 fees: None,
119 estimate_gas: true,
120 gas: None,
121 gas_price,
122 gas_adjustment: Some(gas_adjustment),
123 })
124 } else {
125 Err(GasPriceError(gas_denom.into()))
126 }
127 }
128}
129
130#[derive(Clone)]
132pub struct Terra {
133 client: Client,
135 url: String,
137
138 pub chain_id: String,
140 pub gas_options: Option<GasOptions>,
142 pub debug: bool,
143}
144impl Terra {
145 pub fn lcd_client<S: Into<String>>(
147 url: S,
148 chain_id: S,
149 gas_options: &GasOptions,
150 debug: Option<bool>,
151 ) -> Terra {
152 let client = reqwest::Client::new();
153 Terra {
154 client,
155 url: url.into(),
156 chain_id: chain_id.into(),
157 gas_options: Some(gas_options.clone()),
158 debug: debug.unwrap_or(false),
159 }
160 }
161
162 pub fn lcd_client_no_tx<S: Into<String>>(url: S, chain_id: S) -> Terra {
164 let client = reqwest::Client::new();
165 Terra {
166 client,
167 url: url.into(),
168 chain_id: chain_id.into(),
169 gas_options: None,
170 debug: false,
171 }
172 }
173
174 pub fn auth(&self) -> auth::Auth {
176 auth::Auth::create(self)
177 }
178 pub fn bank(&self) -> bank::Bank {
180 bank::Bank::create(self)
181 }
182 pub fn staking(&self) -> staking::Staking {
184 staking::Staking::create(self)
185 }
186 pub fn market(&self) -> market::Market {
188 market::Market::create(self)
189 }
190 pub fn oracle(&self) -> oracle::Oracle {
192 oracle::Oracle::create(self)
193 }
194 pub fn tendermint(&self) -> tendermint::Tendermint {
196 tendermint::Tendermint::create(self)
197 }
198 pub fn tx(&self) -> tx::TX {
200 tx::TX::create(self)
201 }
202 pub fn rpc<'a>(&'a self, tendermint_url: &'a str) -> rpc::RPC {
204 rpc::RPC::create(self, tendermint_url)
205 }
206 pub fn fcd<'a>(&'a self, fcd_url: &'a str) -> fcd::FCD {
208 fcd::FCD::create(self, fcd_url)
209 }
210 pub fn wasm(&self) -> wasm::Wasm {
212 wasm::Wasm::create(self)
213 }
214
215 pub fn construct_headers() -> HeaderMap {
216 let mut headers = HeaderMap::new();
217
218 headers.insert(
219 USER_AGENT,
220 format!(
221 "PFC-{}/{}",
222 NAME.unwrap_or("terra-rust-api"),
223 VERSION.unwrap_or("-?-")
224 )
225 .parse()
226 .unwrap(),
227 );
228 headers.insert(CONTENT_TYPE, "application/json".parse().unwrap());
229 headers
230 }
231
232 pub async fn send_cmd<T: for<'de> Deserialize<'de>>(
234 &self,
235 path: &str,
236 args: Option<&str>,
237 ) -> Result<T, TerraRustAPIError> {
238 self.send_cmd_url(&self.url, path, args).await
239 }
240
241 pub async fn send_cmd_url<T: for<'de> Deserialize<'de>>(
243 &self,
244 url: &str,
245 path: &str,
246 args: Option<&str>,
247 ) -> Result<T, TerraRustAPIError> {
248 let request_url = match args {
249 Some(a) => format!("{}{}{}", url.to_owned(), path, a),
250 None => format!("{}{}", url.to_owned(), path),
251 };
252
253 if self.debug {
254 log::debug!("URL={}", &request_url);
255 }
256 let req = self
257 .client
258 .get(&request_url)
259 .headers(Terra::construct_headers());
260
261 Terra::resp::<T>(&request_url, req).await
262 }
263
264 pub async fn fetch_url<T: for<'de> Deserialize<'de>>(
265 client: &reqwest::Client,
266 url: &str,
267 path: &str,
268 args: Option<&str>,
269 ) -> Result<T, TerraRustAPIError> {
270 let request_url = match args {
271 Some(a) => format!("{}{}{}", url.to_owned(), path, a),
272 None => format!("{}{}", url.to_owned(), path),
273 };
274
275 let req = client.get(&request_url).headers(Terra::construct_headers());
276
277 Terra::resp::<T>(&request_url, req).await
278 }
279
280 pub async fn post_cmd<R: for<'de> Serialize, T: for<'de> Deserialize<'de>>(
282 &self,
283 path: &str,
284 args: &R,
285 ) -> Result<T, TerraRustAPIError> {
286 let request_url = format!("{}{}", self.url.to_owned(), path);
287
288 if self.debug {
289 log::debug!("URL={}", &request_url);
290 }
291
292 let req = self
293 .client
294 .post(&request_url)
295 .headers(Terra::construct_headers())
296 .json::<R>(args);
297
298 Terra::resp::<T>(&request_url, req).await
299 }
300
301 async fn resp<T: for<'de> Deserialize<'de>>(
302 request_url: &str,
303 req: RequestBuilder,
304 ) -> Result<T, TerraRustAPIError> {
305 let response = req.send().await?;
306 let status = response.status();
307 if !&status.is_success() {
308 let status_text = response.text().await?;
309 log::debug!("URL={} - {}", &request_url, &status_text);
311 Err(TerraRustAPIError::TerraLCDResponse(status, status_text))
312 } else {
313 let struct_response: T = response.json::<T>().await?;
314 Ok(struct_response)
315 }
316 }
317
318 pub async fn calc_fees(
322 &self,
323 auth_account: &AuthAccount,
324 messages: &[Message],
325 ) -> Result<StdFee, TerraRustAPIError> {
326 match &self.gas_options {
327 None => Err(TerraRustAPIError::NoGasOpts),
328
329 Some(gas) => {
330 match &gas.fees {
331 Some(f) => {
332 let fee_coin: Coin = Coin::create(&f.denom, f.amount);
333 Ok(StdFee::create(vec![fee_coin], gas.gas.unwrap_or(0)))
334 }
335
336 None => {
337 let fee: StdFee = match &gas.estimate_gas {
338 true => {
339 let default_gas_coin = Coin::create("ukrw", dec!(1.0));
340 let gas_coin = match &gas.gas_price {
341 Some(c) => c,
342 None => &default_gas_coin,
343 };
344 let res: LCDResult<TxFeeResult> = self
345 .tx()
346 .estimate_fee(
347 &auth_account.address,
348 messages,
349 gas.gas_adjustment.unwrap_or(1.0),
350 &[gas_coin],
351 )
352 .await?;
353 let mut fees: Vec<Coin> = vec![];
355 for fee in res.result.fee.amount {
356 fees.push(Coin::create(&fee.denom, fee.amount))
357 }
358 StdFee::create(fees, res.result.fee.gas as u64)
359 }
360 false => {
361 let mut fees: Vec<Coin> = vec![];
362 match &gas.fees {
363 Some(fee) => {
364 fees.push(Coin::create(&fee.denom, fee.amount));
365 }
366 None => {}
367 }
368
369 StdFee::create(fees, gas.gas.unwrap_or(0))
370 }
371 };
372 Ok(fee)
373 }
374 }
375 }
376 }
377 }
378
379 #[allow(clippy::too_many_arguments)]
381 fn generate_transaction_to_broadcast_fees<C: Signing + secp256k1::Context>(
382 chain_id: &str,
383 auth_account: &AuthAccount,
384 fee: StdFee,
385 secp: &Secp256k1<C>,
386 from: &PrivateKey,
387 messages: Vec<Message>,
388 memo: Option<String>,
389 ) -> Result<(StdSignMsg, Vec<StdSignature>), TerraRustAPIError> {
390 let account_number = auth_account.account_number;
391 let sequence = auth_account.sequence.unwrap_or(0);
392 let messages_len = messages.len();
393 let std_sign_msg = StdSignMsg {
394 chain_id: chain_id.to_string(), account_number,
396 sequence,
397 fee,
398 msgs: messages,
399 memo: memo.unwrap_or(format!(
400 "PFC-{}/{}",
401 NAME.unwrap_or("TERRA-RUST"),
402 VERSION.unwrap_or("dev")
403 )),
404 };
405 let js = serde_json::to_string(&std_sign_msg)?;
406 if js.len() > 1000 {
407 log::debug!(
408 "TO SIGN - {} {} {} #messages {}",
409 chain_id,
410 account_number,
411 sequence,
412 messages_len
413 );
414 } else {
415 log::debug!("TO SIGN - {}", js);
416 }
417
418 let sig = from.sign(secp, &js)?;
420 let sigs: Vec<StdSignature> = vec![sig];
421
422 Ok((std_sign_msg, sigs))
423 }
424
425 pub async fn generate_transaction_to_broadcast<C: secp256k1::Signing + secp256k1::Context>(
428 &self,
429 secp: &Secp256k1<C>,
430 from: &PrivateKey,
431 messages: Vec<Message>,
432 memo: Option<String>,
433 ) -> Result<(StdSignMsg, Vec<StdSignature>), TerraRustAPIError> {
434 let from_public = from.public_key(secp);
435 let from_account = from_public.account()?;
436 let auth = self.auth().account(&from_account).await?;
437 let fees = self.calc_fees(&auth.result.value, &messages).await?;
438 Terra::generate_transaction_to_broadcast_fees(
439 &self.chain_id,
440 &auth.result.value,
441 fees,
442 secp,
443 from,
444 messages,
445 memo,
446 )
447 }
448 pub async fn submit_transaction_sync<C: Signing + secp256k1::Context>(
450 &self,
451 secp: &Secp256k1<C>,
452 from: &PrivateKey,
453 messages: Vec<Message>,
454 memo: Option<String>,
455 ) -> Result<TXResultSync, TerraRustAPIError> {
456 let (std_sign_msg, sigs) = self
457 .generate_transaction_to_broadcast(secp, from, messages, memo)
458 .await?;
459 let resp = self.tx().broadcast_sync(&std_sign_msg, &sigs).await?;
460
461 match resp.code {
462 Some(code) => Err(TxResultError(code, resp.txhash, resp.raw_log)),
463 None => Ok(resp),
464 }
465 }
466 pub async fn submit_transaction_async<C: Signing + secp256k1::Context>(
468 &self,
469 secp: &Secp256k1<C>,
470 from: &PrivateKey,
471 messages: Vec<Message>,
472 memo: Option<String>,
473 ) -> Result<TXResultAsync, TerraRustAPIError> {
474 let (std_sign_msg, sigs) = self
475 .generate_transaction_to_broadcast(secp, from, messages, memo)
476 .await?;
477 let resp = self.tx().broadcast_async(&std_sign_msg, &sigs).await?;
478 Ok(resp)
479 }
480
481 pub async fn production_address_book() -> Result<AddressBook, TerraRustAPIError> {
483 Self::address_book(NETWORK_PROD_ADDRESS_BOOK).await
484 }
485 pub async fn testnet_address_book() -> Result<AddressBook, TerraRustAPIError> {
487 Self::address_book(NETWORK_TEST_ADDRESS_BOOK).await
488 }
489 pub async fn address_book(addr_url: &str) -> Result<AddressBook, TerraRustAPIError> {
491 if let Some(file_name) = addr_url.strip_prefix("file://") {
492 let file = File::open(file_name).unwrap();
493 let add: AddressBook = serde_json::from_reader(file)?;
494 Ok(add)
495 } else {
496 let client = reqwest::Client::new();
497
498 let req = client.get(addr_url).headers(Self::construct_headers());
499 Ok(Self::resp::<AddressBook>(addr_url, req).await?)
500 }
501 }
502}
503#[cfg(test)]
504mod tst {
505 use super::*;
506 use crate::core_types::{Coin, StdTx};
508 use crate::messages::MsgSend;
509 use crate::{PrivateKey, Terra};
510 use bitcoin::secp256k1::Secp256k1;
511
512 #[test]
513 pub fn test_send() -> Result<(), TerraRustAPIError> {
514 let str_1 = "island relax shop such yellow opinion find know caught erode blue dolphin behind coach tattoo light focus snake common size analyst imitate employ walnut";
515 let secp = Secp256k1::new();
516 let pk = PrivateKey::from_words(&secp, str_1, 0, 0)?;
517 let pub_k = pk.public_key(&secp);
518 let from_address = pub_k.account()?;
519 assert_eq!(from_address, "terra1n3g37dsdlv7ryqftlkef8mhgqj4ny7p8v78lg7");
520
521 let send = MsgSend::create_single(
522 from_address,
523 "terra1usws7c2c6cs7nuc8vma9qzaky5pkgvm2uag6rh".into(),
524 Coin::parse("100000uluna")?.unwrap(),
525 )?;
526 let json = serde_json::to_string(&send)?;
527 let json_eq = r#"{"type":"bank/MsgSend","value":{"amount":[{"amount":"100000","denom":"uluna"}],"from_address":"terra1n3g37dsdlv7ryqftlkef8mhgqj4ny7p8v78lg7","to_address":"terra1usws7c2c6cs7nuc8vma9qzaky5pkgvm2uag6rh"}}"#;
528
529 assert_eq!(json, json_eq);
530 let std_fee = StdFee::create_single(Coin::parse("50000uluna")?.unwrap(), 90000);
531
532 let messages: Vec<Message> = vec![send];
533 let auth_account = AuthAccount {
534 address: "terra1n3g37dsdlv7ryqftlkef8mhgqj4ny7p8v78lg7".to_string(),
535 public_key: None,
536 account_number: 43045,
537 sequence: Some(3),
538 };
539 let (sign_message, signatures) = Terra::generate_transaction_to_broadcast_fees(
540 "tequila-0004".into(),
541 &auth_account,
542 std_fee,
543 &secp,
544 &pk,
545 messages,
546 Some("PFC-terra-rust/0.1.5".into()),
547 )?;
548 let json_sign_message = serde_json::to_string(&sign_message)?;
549 let json_sign_message_eq = r#"{"account_number":"43045","chain_id":"tequila-0004","fee":{"amount":[{"amount":"50000","denom":"uluna"}],"gas":"90000"},"memo":"PFC-terra-rust/0.1.5","msgs":[{"type":"bank/MsgSend","value":{"amount":[{"amount":"100000","denom":"uluna"}],"from_address":"terra1n3g37dsdlv7ryqftlkef8mhgqj4ny7p8v78lg7","to_address":"terra1usws7c2c6cs7nuc8vma9qzaky5pkgvm2uag6rh"}}],"sequence":"3"}"#;
550 assert_eq!(json_sign_message, json_sign_message_eq);
551 let json_sig = serde_json::to_string(&signatures)?;
552 let json_sig_eq = r#"[{"signature":"f1wYTzbSyAYqN2tGR0A4PGmfyNYBUExpuoU7UOiBDpNoRlChF/BMtE7h6pdgbpu/V7jNzitu1Eb0fO35dxVkWA==","pub_key":{"type":"tendermint/PubKeySecp256k1","value":"AiMzHaA2bvnDXfHzkjMM+vkSE/p0ymBtAFKUnUtQAeXe"}}]"#;
553 assert_eq!(json_sig, json_sig_eq);
554 let std_tx: StdTx = StdTx::from_StdSignMsg(&sign_message, &signatures, "sync");
555 let js_sig = serde_json::to_string(&std_tx)?;
556 let js_sig_eq = r#"{"tx":{"msg":[{"type":"bank/MsgSend","value":{"amount":[{"amount":"100000","denom":"uluna"}],"from_address":"terra1n3g37dsdlv7ryqftlkef8mhgqj4ny7p8v78lg7","to_address":"terra1usws7c2c6cs7nuc8vma9qzaky5pkgvm2uag6rh"}}],"fee":{"amount":[{"amount":"50000","denom":"uluna"}],"gas":"90000"},"signatures":[{"signature":"f1wYTzbSyAYqN2tGR0A4PGmfyNYBUExpuoU7UOiBDpNoRlChF/BMtE7h6pdgbpu/V7jNzitu1Eb0fO35dxVkWA==","pub_key":{"type":"tendermint/PubKeySecp256k1","value":"AiMzHaA2bvnDXfHzkjMM+vkSE/p0ymBtAFKUnUtQAeXe"}}],"memo":"PFC-terra-rust/0.1.5"},"mode":"sync"}"#;
557 assert_eq!(js_sig, js_sig_eq);
558 Ok(())
559 }
560
561 #[test]
562 pub fn test_wasm() -> Result<(), TerraRustAPIError> {
563 let key_words = "sell raven long age tooth still predict idea quit march gasp bamboo hurdle problem voyage east tiger divide machine brain hole tiger find smooth";
564 let secp = Secp256k1::new();
565 let private = PrivateKey::from_words(&secp, key_words, 0, 0)?;
566 let public_key = private.public_key(&secp);
567 let account = public_key.account()?;
568 assert_eq!(account, "terra1vr0e7kylhu9am44v0s3gwkccmz7k3naxysrwew");
569 Ok(())
615 }
616
617 #[tokio::test]
618 pub async fn test_address_book() -> Result<(), TerraRustAPIError> {
619 let prod = Terra::production_address_book().await?;
620 assert!(prod.addrs.len() > 0);
621 let test = Terra::testnet_address_book().await?;
622 assert!(test.addrs.len() > 0);
623 let file_version = Terra::address_book("file://resources/addressbook.json").await?;
624 assert_eq!(file_version.key, "775cf30a073ca5e97fb07a00");
625 assert!(file_version.addrs.len() > 1);
626 assert_eq!(
627 file_version.addrs[0].addr.id,
628 "ebca6b5d3cc2da9dfdfe4b1c045043fce686f143"
629 );
630
631 Ok(())
632 }
633}