waves-rust 0.2.6

A Rust library for interacting with the Waves blockchain. Supports node interaction, offline transaction signing and creating addresses and keys.
Documentation
use crate::error::{Error, Result};
use crate::model::{Amount, AssetId, Base64String, ByteString};
use crate::util::JsonDeserializer;
use crate::waves_proto::IssueTransactionData;
use serde_json::{Map, Value};

const TYPE: u8 = 3;

#[derive(Clone, Eq, PartialEq, Debug)]
pub struct IssueTransactionInfo {
    asset_id: AssetId,
    name: String,
    description: String,
    quantity: u64,
    decimals: u32,
    is_reissuable: bool,
    script: Option<Base64String>,
}

impl IssueTransactionInfo {
    pub fn new(
        asset_id: AssetId,
        name: String,
        description: String,
        quantity: u64,
        decimals: u32,
        is_reissuable: bool,
        script: Option<Base64String>,
    ) -> Self {
        IssueTransactionInfo {
            asset_id,
            name,
            description,
            quantity,
            decimals,
            is_reissuable,
            script,
        }
    }

    pub fn tx_type() -> u8 {
        TYPE
    }

    pub fn asset_id(&self) -> AssetId {
        self.asset_id.clone()
    }

    pub fn name(&self) -> String {
        self.name.clone()
    }

    pub fn description(&self) -> String {
        self.description.clone()
    }

    pub fn quantity(&self) -> u64 {
        self.quantity
    }

    pub fn decimals(&self) -> u32 {
        self.decimals
    }

    pub fn is_reissuable(&self) -> bool {
        self.is_reissuable
    }

    pub fn script(&self) -> Option<Base64String> {
        self.script.clone()
    }
}

impl TryFrom<&Value> for IssueTransactionInfo {
    type Error = Error;

    fn try_from(value: &Value) -> Result<Self> {
        let issue_transaction: IssueTransaction = value.try_into()?;
        let asset_id = AssetId::from_string(&JsonDeserializer::safe_to_string_from_field(
            value, "assetId",
        )?)?;

        Ok(IssueTransactionInfo {
            asset_id,
            name: issue_transaction.name(),
            description: issue_transaction.description(),
            quantity: issue_transaction.quantity(),
            decimals: issue_transaction.decimals(),
            is_reissuable: issue_transaction.is_reissuable(),
            script: issue_transaction.script(),
        })
    }
}

#[derive(Clone, Eq, PartialEq, Debug)]
pub struct IssueTransaction {
    name: String,
    description: String,
    quantity: u64,
    decimals: u32,
    is_reissuable: bool,
    script: Option<Base64String>,
}

impl IssueTransaction {
    pub fn new(
        name: String,
        description: String,
        quantity: u64,
        decimals: u32,
        is_reissuable: bool,
        script: Option<Base64String>,
    ) -> Self {
        IssueTransaction {
            name,
            description,
            quantity,
            decimals,
            is_reissuable,
            script,
        }
    }

    pub fn tx_type() -> u8 {
        TYPE
    }

    pub fn name(&self) -> String {
        self.name.clone()
    }

    pub fn description(&self) -> String {
        self.description.clone()
    }

    pub fn quantity(&self) -> u64 {
        self.quantity
    }

    pub fn decimals(&self) -> u32 {
        self.decimals
    }

    pub fn is_reissuable(&self) -> bool {
        self.is_reissuable
    }

    pub fn script(&self) -> Option<Base64String> {
        self.script.clone()
    }

    pub fn min_fee(&self) -> Amount {
        let value = if self.is_nft_issue() {
            100_000
        } else {
            100_000_000
        };
        Amount::new(value, None)
    }

    pub fn is_nft_issue(&self) -> bool {
        self.quantity == 1 && self.decimals == 0 && !self.is_reissuable
    }
}

impl TryFrom<&IssueTransaction> for IssueTransactionData {
    type Error = Error;

    fn try_from(issue_tx: &IssueTransaction) -> Result<Self> {
        let script = match issue_tx.script() {
            Some(script) => script.bytes(),
            None => vec![],
        };

        Ok(IssueTransactionData {
            name: issue_tx.name(),
            description: issue_tx.description(),
            amount: issue_tx.quantity() as i64,
            decimals: issue_tx.decimals() as i32,
            reissuable: issue_tx.is_reissuable(),
            script,
        })
    }
}

impl TryFrom<&Value> for IssueTransaction {
    type Error = Error;

    fn try_from(value: &Value) -> Result<Self> {
        let name = JsonDeserializer::safe_to_string_from_field(value, "name")?;
        let description = JsonDeserializer::safe_to_string_from_field(value, "description")?;
        let quantity = JsonDeserializer::safe_to_int_from_field(value, "quantity")? as u64;
        let decimals = JsonDeserializer::safe_to_int_from_field(value, "decimals")? as u32;
        let is_reissuable = JsonDeserializer::safe_to_boolean_from_field(value, "reissuable")?;
        let script = match value["script"].as_str() {
            Some(val) => Some(Base64String::from_string(val)?),
            None => None,
        };

        Ok(IssueTransaction {
            name,
            description,
            quantity,
            decimals,
            is_reissuable,
            script,
        })
    }
}

impl TryFrom<&IssueTransaction> for Map<String, Value> {
    type Error = Error;

    fn try_from(issue_tx: &IssueTransaction) -> Result<Self> {
        let mut json = Map::new();
        json.insert("name".to_string(), issue_tx.name().into());
        json.insert("description".to_string(), issue_tx.description().into());
        json.insert("quantity".to_string(), issue_tx.quantity().into());
        json.insert("decimals".to_string(), issue_tx.decimals().into());
        json.insert("reissuable".to_string(), issue_tx.is_reissuable().into());
        json.insert(
            "script".to_string(),
            issue_tx.script().map(|it| it.encoded()).into(),
        );
        Ok(json)
    }
}

#[cfg(test)]
mod tests {
    use crate::error::Result;
    use crate::model::{Base64String, ByteString, IssueTransaction, IssueTransactionInfo};
    use crate::waves_proto::IssueTransactionData;
    use serde_json::{json, Map, Value};
    use std::borrow::Borrow;
    use std::fs;

    #[test]
    fn test_json_to_issue_transaction() {
        let data = fs::read_to_string("./tests/resources/issue_transaction_rs.json")
            .expect("Unable to read file");
        let json: Value = serde_json::from_str(&data).expect("failed to generate json from str");

        let issue_tx_from_json: IssueTransactionInfo = json.borrow().try_into().unwrap();

        assert_eq!(
            "5HCFX88m6Xxws4SunQuW9ghvYBmk8rK8b6xVCRL8PyAw",
            issue_tx_from_json.asset_id().encoded()
        );
        assert_eq!("test asset", issue_tx_from_json.name());
        assert_eq!(32, issue_tx_from_json.quantity());
        assert_eq!(false, issue_tx_from_json.is_reissuable());
        assert_eq!(3, issue_tx_from_json.decimals());
        assert_eq!("this is test asset", issue_tx_from_json.description());

        let script = "base64:AgQAAAAHbWFzdGVyMQkBAAAAEWFkZHJlc3NGcm9tU3RyaW5nAAAAAQIAAAAQMzMzbWFzdGVyQWRkcmVzcwQAAAAHJG1hdGNoMAUAAAACdHgDCQAAAQAAAAIFAAAAByRtYXRjaDACAAAAE1RyYW5zZmVyVHJhbnNhY3Rpb24EAAAAAXQFAAAAByRtYXRjaDADCQAAAAAAAAIIBQAAAAF0AAAABnNlbmRlcgUAAAAHbWFzdGVyMQYJAAAAAAAAAggFAAAAAXQAAAAJcmVjaXBpZW50BQAAAAdtYXN0ZXIxAwkAAAEAAAACBQAAAAckbWF0Y2gwAgAAABdNYXNzVHJhbnNmZXJUcmFuc2FjdGlvbgQAAAACbXQFAAAAByRtYXRjaDAJAAAAAAAAAggFAAAAAm10AAAABnNlbmRlcgUAAAAHbWFzdGVyMQMJAAABAAAAAgUAAAAHJG1hdGNoMAIAAAATRXhjaGFuZ2VUcmFuc2FjdGlvbgcGFLbwIw==";

        assert_eq!(
            script,
            issue_tx_from_json.script().unwrap().encoded_with_prefix()
        );
    }

    #[test]
    fn test_issue_transaction_to_proto() -> Result<()> {
        let issue_tx = &IssueTransaction::new(
            "name".to_owned(),
            "descr".to_owned(),
            32,
            0,
            false,
            Some(Base64String::from_bytes(vec![1, 2, 3])),
        );
        let proto: IssueTransactionData = issue_tx.try_into()?;
        assert_eq!(proto.name, issue_tx.name());
        assert_eq!(proto.description, issue_tx.description());
        assert_eq!(proto.amount as u64, issue_tx.quantity());
        assert_eq!(proto.decimals as u32, issue_tx.decimals());
        assert_eq!(proto.reissuable, issue_tx.is_reissuable());
        assert_eq!(proto.script, issue_tx.script().unwrap().bytes());
        Ok(())
    }

    #[test]
    fn test_issue_tx_to_json() -> Result<()> {
        let issue_tx = &IssueTransaction::new(
            "test asset".to_owned(),
            "this is test asset".to_owned(),
            32,
            3,
            false,
            Some(Base64String::from_bytes(vec![1, 2, 3])),
        );

        let map: Map<String, Value> = issue_tx.try_into()?;
        let json: Value = map.into();
        let expected_json = json!({
            "name": "test asset",
            "quantity": 32,
            "reissuable": false,
            "decimals": 3,
            "description": "this is test asset",
            "script": "AQID",
        });
        assert_eq!(expected_json, json);
        Ok(())
    }

    #[test]
    fn test_min_fee_for_issue_transaction() {
        let issue_transaction =
            IssueTransaction::new("name".into(), "description".into(), 123, 8, true, None);

        let nft_issue_transaction =
            IssueTransaction::new("nft".into(), "description".into(), 1, 0, false, None);

        assert_eq!(issue_transaction.min_fee().value(), 100_000_000);

        assert!(nft_issue_transaction.is_nft_issue());
        assert_eq!(nft_issue_transaction.min_fee().value(), 100_000);
    }
}