elrond_rust/
transaction.rs

1//! Logic for constructing transactions on the Elrond network.
2
3use serde::Serialize;
4use super::{Account, ElrondAddress, ElrondClientError, Result};
5use bigdecimal::BigDecimal;
6use std::str::FromStr;
7
8/// Network representation (e.g., MainNet or TestNet)
9pub enum Network {
10    MainNet,
11    Custom(String)
12}
13
14impl Network {
15    /// Get chain id associated with network
16    pub fn chain_id(&self) -> String {
17        match self {
18            Network::MainNet => "1".to_string(),
19            Network::Custom(s) => s.clone()
20        }
21    }
22}
23
24/// eGLD representation. "1 eGLD" is 10^18 on the blockchain
25#[derive(Clone, Debug, PartialEq)]
26pub struct ElrondCurrencyAmount {
27    inner: String
28}
29
30impl ElrondCurrencyAmount {
31    /// Create an amount of eGLD from a human input, e.g., "2 eGLD"
32    pub fn new(amount: &str) -> Result<Self> {
33        let amount = BigDecimal::from_str(amount).map_err(|_| {
34            ElrondClientError::new("could not parse amount as bigdecimal")
35        })?;
36        let multiplier = BigDecimal::from_str("1000000000000000000").unwrap(); // safe
37        let converted_amount = (amount * multiplier).with_scale(0);
38        let inner = format!("{}", converted_amount);
39        Ok(Self { inner })
40    }
41    /// Parse blockchain representation of eGLD into human readable form
42    pub fn from_blockchain_precision(blockchain_amount: &str) -> Result<Self> {
43        let amount = BigDecimal::from_str(blockchain_amount).map_err(|_| {
44            ElrondClientError::new("could not parse amount as bigdecimal")
45        })?;
46        let divisor = BigDecimal::from_str("1000000000000000000").unwrap(); // safe
47        let converted_amount = amount / divisor;
48        let inner = format!("{}", converted_amount);
49        Ok(Self { inner })
50    }
51    /// Convert to string for serialization
52    pub fn to_string(&self) -> String {
53        self.inner.clone()
54    }
55}
56
57/// Transaction representation before it has been signed by an account
58#[derive(Clone, Debug, Serialize)]
59#[serde(rename_all = "camelCase")] 
60pub struct UnsignedTransaction{
61    nonce: u64,
62    value: String,
63    receiver: String,
64    sender: String,
65    gas_price: u64,
66    gas_limit: u64,
67    // 'chain_ID' needs to be weirdly cased due to requirements of Elrond API
68    chain_ID: String,
69    version: u64
70}
71
72impl UnsignedTransaction {
73    /// Create new unsigned transaction
74    pub fn new(
75        nonce: u64,
76        value: &str,
77        receiver: &str,
78        sender: &str,
79        network: Network,
80    ) -> Result<Self> {
81        Ok(Self {
82            nonce,
83            value: ElrondCurrencyAmount::new(value)?.to_string(),
84            receiver: ElrondAddress::new(receiver)?.to_string(),
85            sender: ElrondAddress::new(sender)?.to_string(),
86            gas_price: 1000000000,
87            gas_limit: 50000,
88            chain_ID: network.chain_id(),
89            version: 1
90        })
91    }
92    /// Serialize transaction for signing
93    pub fn serialize(&self) -> Result<String> {
94        serde_json::to_string(self).map_err(|_| {
95            ElrondClientError::new("could not serialize unsigned transaction")
96        })
97    }
98    /// Sign the transaction with an `Account` to produce a `SignedTransaction`
99    pub fn sign(&self, account: &Account) -> Result<SignedTransaction> {
100        let serialized_tx = self.serialize()?;
101        let signature = account.sign(&serialized_tx)?;
102        Ok(SignedTransaction {
103            nonce: self.nonce,
104            value: self.value.clone(),
105            receiver: self.receiver.clone(),
106            sender: self.sender.clone(),
107            gas_price: self.gas_price,
108            gas_limit: self.gas_limit,
109            chain_ID: self.chain_ID.clone(),
110            version: self.version,
111            // signed transaction requires empty data field if no data
112            data: "".to_string(),
113            signature
114        })
115    }
116}
117
118/// Representation of a signed transaction. Differs from unsigned transaction only by the
119/// addition of a signature field
120#[derive(Clone, Debug, Serialize)]
121#[serde(rename_all = "camelCase")] 
122pub struct SignedTransaction{
123    nonce: u64,
124    value: String,
125    receiver: String,
126    sender: String,
127    gas_price: u64,
128    gas_limit: u64,
129    data: String,
130    // 'chain_ID' needs to be weirdly cased due to requirements of Elrond API
131    chain_ID: String,
132    version: u64,
133    signature: String
134}
135
136impl SignedTransaction {
137    pub fn serialize(&self) -> Result<String> {
138        serde_json::to_string(self).map_err(|_| {
139            ElrondClientError::new("could not serialize signed transaction")
140        })
141    }
142}
143
144#[cfg(test)]
145mod tests {
146    use super::{UnsignedTransaction, Network, ElrondCurrencyAmount};
147    use super::super::account::Account;
148    #[test]
149    fn create_serialize_and_sign_tx(){
150        let private_key = "a4b36a5d97176618b5a7fcc9228d2fd98ee2f14ddd3d6462ae03e40eb487d15b";
151        let account = Account::from_string(private_key).unwrap();
152        let tx = UnsignedTransaction::new(
153            0,
154            "0.001",
155            "erd16jats393r8rnut88yhvu5wvxxje57qzlj3tqk7n6jnf7f6cxs4uqfeh65k",
156            &account.address.to_string(),
157            Network::MainNet
158        ).unwrap();
159        let signed_tx = tx.sign(&account).unwrap();
160        let serialized_correct = "{\"nonce\":0,\"value\":\"1000000000000000\",\"receiver\":\"erd16jats393r8rnut88yhvu5wvxxje57qzlj3tqk7n6jnf7f6cxs4uqfeh65k\",\"sender\":\"erd146apxa83wr7paz3gsg07dhcpg98ascjtpg9p8l8g5rpmg6chhchq9ccvmc\",\"gasPrice\":1000000000,\"gasLimit\":50000,\"data\":\"\",\"chainID\":\"1\",\"version\":1,\"signature\":\"78b7a59aeb9ff1a51637e23f29e0e22528a92a3508d69479806af93114496977ee1eacf045146fdbae89efd2cd3d9e0bcb2c52406515fa0548e2873554a0ac0d\"}";
161        assert_eq!(serialized_correct, signed_tx.serialize().unwrap());
162    }
163
164    #[test]
165    fn test_currency_precision(){
166        let amount = ElrondCurrencyAmount::new("0.001").unwrap();
167        assert_eq!(amount.to_string(), "1000000000000000")
168    }
169}