use crate::traits::Validate;
use crate::transactions::common::{TransactionHeader, TransactionValidationError};
use crate::utils::{is_false_opt, is_zero, is_zero_addr_opt, is_zero_opt};
use crate::{Address, Transaction};
use derive_builder::Builder;
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use serde_with::{Bytes, serde_as, skip_serializing_none};
#[serde_as]
#[skip_serializing_none]
#[derive(Serialize, Deserialize, Debug, PartialEq, Clone, Builder)]
struct AssetParams {
#[serde(rename = "t")]
#[serde(skip_serializing_if = "is_zero_opt")]
#[serde(default)]
pub total: Option<u64>,
#[serde(rename = "dc")]
#[serde(skip_serializing_if = "is_zero_opt")]
#[serde(default)]
pub decimals: Option<u32>,
#[serde(rename = "df")]
#[serde(skip_serializing_if = "is_false_opt")]
#[serde(default)]
pub default_frozen: Option<bool>,
#[serde(rename = "an")]
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub asset_name: Option<String>,
#[serde(rename = "un")]
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub unit_name: Option<String>,
#[serde(rename = "au")]
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub url: Option<String>,
#[serde(rename = "am")]
#[serde_as(as = "Option<Bytes>")]
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub metadata_hash: Option<[u8; 32]>,
#[serde(rename = "m")]
#[serde(skip_serializing_if = "is_zero_addr_opt")]
#[serde(default)]
pub manager: Option<Address>,
#[serde(rename = "r")]
#[serde(skip_serializing_if = "is_zero_addr_opt")]
#[serde(default)]
pub reserve: Option<Address>,
#[serde(rename = "f")]
#[serde(skip_serializing_if = "is_zero_addr_opt")]
#[serde(default)]
pub freeze: Option<Address>,
#[serde(rename = "c")]
#[serde(skip_serializing_if = "is_zero_addr_opt")]
#[serde(default)]
pub clawback: Option<Address>,
}
#[derive(Debug, PartialEq, Clone, Builder)]
#[builder(
name = "AssetConfigTransactionBuilder",
setter(strip_option),
build_fn(name = "build_fields")
)]
pub struct AssetConfigTransactionFields {
pub header: TransactionHeader,
pub asset_id: u64,
#[builder(default)]
pub total: Option<u64>,
#[builder(default)]
pub decimals: Option<u32>,
#[builder(default)]
pub default_frozen: Option<bool>,
#[builder(default)]
pub asset_name: Option<String>,
#[builder(default)]
pub unit_name: Option<String>,
#[builder(default)]
pub url: Option<String>,
#[builder(default)]
pub metadata_hash: Option<[u8; 32]>,
#[builder(default)]
pub manager: Option<Address>,
#[builder(default)]
pub reserve: Option<Address>,
#[builder(default)]
pub freeze: Option<Address>,
#[builder(default)]
pub clawback: Option<Address>,
}
#[serde_as]
#[derive(Serialize, Deserialize)]
struct AssetConfigTransactionFieldsSerde {
#[serde(flatten)]
header: TransactionHeader,
#[serde(rename = "caid")]
#[serde(skip_serializing_if = "is_zero")]
#[serde(default)]
asset_id: u64,
#[serde(rename = "apar")]
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
asset_params: Option<AssetParams>,
}
pub fn asset_config_serializer<S>(
fields: &AssetConfigTransactionFields,
serializer: S,
) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
let fields = fields.clone();
let has_asset_params = fields.total.is_some()
|| fields.decimals.is_some()
|| fields.default_frozen.is_some()
|| fields.asset_name.is_some()
|| fields.unit_name.is_some()
|| fields.url.is_some()
|| fields.metadata_hash.is_some()
|| fields.manager.is_some()
|| fields.reserve.is_some()
|| fields.freeze.is_some()
|| fields.clawback.is_some();
let asset_params = match has_asset_params {
true => Some(AssetParams {
total: fields.total,
decimals: fields.decimals,
default_frozen: fields.default_frozen,
asset_name: fields.asset_name,
unit_name: fields.unit_name,
url: fields.url,
metadata_hash: fields.metadata_hash,
manager: fields.manager,
reserve: fields.reserve,
freeze: fields.freeze,
clawback: fields.clawback,
}),
false => None,
};
let serde_struct = AssetConfigTransactionFieldsSerde {
header: fields.header,
asset_id: fields.asset_id,
asset_params,
};
serde_struct.serialize(serializer)
}
pub fn asset_config_deserializer<'de, D>(
deserializer: D,
) -> Result<AssetConfigTransactionFields, D::Error>
where
D: Deserializer<'de>,
{
let deserialised_fields = AssetConfigTransactionFieldsSerde::deserialize(deserializer)?;
let (
total,
decimals,
default_frozen,
asset_name,
unit_name,
url,
metadata_hash,
manager,
reserve,
freeze,
clawback,
) = match deserialised_fields.asset_params {
Some(params) => (
params.total,
params.decimals,
params.default_frozen,
params.asset_name,
params.unit_name,
params.url,
params.metadata_hash,
params.manager,
params.reserve,
params.freeze,
params.clawback,
),
None => (
None, None, None, None, None, None, None, None, None, None, None,
),
};
Ok(AssetConfigTransactionFields {
header: deserialised_fields.header,
asset_id: deserialised_fields.asset_id,
total,
decimals,
default_frozen,
asset_name,
unit_name,
url,
metadata_hash,
manager,
reserve,
freeze,
clawback,
})
}
impl AssetConfigTransactionFields {
pub fn validate_for_creation(&self) -> Result<(), Vec<TransactionValidationError>> {
let mut errors = Vec::new();
if self.total.is_none() {
errors.push(TransactionValidationError::RequiredField(
"Total".to_string(),
));
}
if let Some(decimals) = self.decimals {
if decimals > 19 {
errors.push(TransactionValidationError::FieldTooLong {
field: "Decimals".to_string(),
actual: decimals as usize,
max: 19,
unit: "decimal places".to_string(),
});
}
}
if let Some(ref unit_name) = self.unit_name {
if unit_name.len() > 8 {
errors.push(TransactionValidationError::FieldTooLong {
field: "Unit name".to_string(),
actual: unit_name.len(),
max: 8,
unit: "bytes".to_string(),
});
}
}
if let Some(ref asset_name) = self.asset_name {
if asset_name.len() > 32 {
errors.push(TransactionValidationError::FieldTooLong {
field: "Asset name".to_string(),
actual: asset_name.len(),
max: 32,
unit: "bytes".to_string(),
});
}
}
if let Some(ref url) = self.url {
if url.len() > 96 {
errors.push(TransactionValidationError::FieldTooLong {
field: "URL".to_string(),
actual: url.len(),
max: 96,
unit: "bytes".to_string(),
});
}
}
match errors.is_empty() {
true => Ok(()),
false => Err(errors),
}
}
pub fn validate_for_reconfigure(&self) -> Result<(), Vec<TransactionValidationError>> {
let mut errors = Vec::new();
if self.total.is_some() {
errors.push(TransactionValidationError::ImmutableField(
"total".to_string(),
));
}
if self.decimals.is_some() {
errors.push(TransactionValidationError::ImmutableField(
"decimals".to_string(),
));
}
if self.default_frozen.is_some() {
errors.push(TransactionValidationError::ImmutableField(
"default_frozen".to_string(),
));
}
if self.asset_name.is_some() {
errors.push(TransactionValidationError::ImmutableField(
"asset_name".to_string(),
));
}
if self.unit_name.is_some() {
errors.push(TransactionValidationError::ImmutableField(
"unit_name".to_string(),
));
}
if self.url.is_some() {
errors.push(TransactionValidationError::ImmutableField(
"url".to_string(),
));
}
if self.metadata_hash.is_some() {
errors.push(TransactionValidationError::ImmutableField(
"metadata_hash".to_string(),
));
}
match errors.is_empty() {
true => Ok(()),
false => Err(errors),
}
}
}
impl AssetConfigTransactionBuilder {
pub fn build(&self) -> Result<Transaction, AssetConfigTransactionBuilderError> {
let d = self.build_fields()?;
d.validate().map_err(|errors| {
AssetConfigTransactionBuilderError::ValidationError(format!(
"Asset config validation failed: {}",
errors.join("\n")
))
})?;
Ok(Transaction::AssetConfig(d))
}
}
impl Validate for AssetConfigTransactionFields {
fn validate(&self) -> Result<(), Vec<String>> {
match self.asset_id {
0 => {
self.validate_for_creation()
.map_err(|errors| errors.iter().map(|e| e.to_string()).collect())
}
_ => {
let has_asset_params = self.total.is_some()
|| self.decimals.is_some()
|| self.default_frozen.is_some()
|| self.asset_name.is_some()
|| self.unit_name.is_some()
|| self.url.is_some()
|| self.metadata_hash.is_some()
|| self.manager.is_some()
|| self.reserve.is_some()
|| self.freeze.is_some()
|| self.clawback.is_some();
match has_asset_params {
true => self
.validate_for_reconfigure()
.map_err(|errors| errors.iter().map(|e| e.to_string()).collect()),
false => Ok(()),
}
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::test_utils::TransactionHeaderMother;
fn create_test_address() -> Address {
"JB3K6HTAXODO4THESLNYTSG6GQUFNEVIQG7A6ZYVDACR6WA3ZF52TKU5NA"
.parse()
.unwrap()
}
#[test]
fn test_validate_asset_creation_multiple_errors() {
let long_url = "https://".to_string() + &"a".repeat(100); let asset_config = AssetConfigTransactionFields {
header: TransactionHeaderMother::example().build().unwrap(),
asset_id: 0, total: None, decimals: Some(20), default_frozen: Some(false),
asset_name: Some(
"ThisIsAVeryLongAssetNameThatExceedsTheMaximumLengthOf32Bytes".to_string(),
), unit_name: Some("VERYLONGUNITNAME".to_string()), url: Some(long_url), metadata_hash: None,
manager: Some(create_test_address()),
reserve: None,
freeze: None,
clawback: None,
};
let result = asset_config.validate();
assert!(result.is_err());
let errors = result.unwrap_err();
assert_eq!(errors.len(), 5);
let error_text = errors.join("\n");
assert!(error_text.contains("Total is required"));
assert!(error_text.contains("Decimals cannot exceed 19 decimal places"));
assert!(error_text.contains("Asset name cannot exceed 32 bytes"));
assert!(error_text.contains("Unit name cannot exceed 8 bytes"));
assert!(error_text.contains("URL cannot exceed 96 bytes"));
}
#[test]
fn test_validate_valid_asset_creation() {
let asset_config = AssetConfigTransactionFields {
header: TransactionHeaderMother::example().build().unwrap(),
asset_id: 0,
total: Some(1000),
decimals: Some(2),
default_frozen: Some(false),
asset_name: Some("TestAsset".to_string()),
unit_name: Some("TA".to_string()),
url: Some("https://example.com".to_string()),
metadata_hash: Some([1; 32]),
manager: Some(create_test_address()),
reserve: Some(create_test_address()),
freeze: Some(create_test_address()),
clawback: Some(create_test_address()),
};
let result = asset_config.validate();
assert!(result.is_ok());
}
#[test]
fn test_validate_valid_asset_reconfigure() {
let asset_config = AssetConfigTransactionFields {
header: TransactionHeaderMother::example().build().unwrap(),
asset_id: 123, total: None, decimals: None, default_frozen: None, asset_name: None, unit_name: None, url: None, metadata_hash: None, manager: Some(create_test_address()), reserve: Some(create_test_address()), freeze: Some(create_test_address()), clawback: Some(create_test_address()), };
let result = asset_config.validate();
assert!(result.is_ok());
}
#[test]
fn test_validate_valid_asset_destroy() {
let asset_config = AssetConfigTransactionFields {
header: TransactionHeaderMother::example().build().unwrap(),
asset_id: 123, total: None, decimals: None,
default_frozen: None,
asset_name: None,
unit_name: None,
url: None,
metadata_hash: None,
manager: None,
reserve: None,
freeze: None,
clawback: None,
};
let result = asset_config.validate();
assert!(result.is_ok());
}
#[test]
fn test_validate_asset_reconfigure_multiple_immutable_field_errors() {
let asset_config = AssetConfigTransactionFields {
header: TransactionHeaderMother::example().build().unwrap(),
asset_id: 123, total: Some(2000), decimals: Some(3), default_frozen: Some(true), asset_name: Some("NewName".to_string()), unit_name: Some("NEW".to_string()), url: Some("https://newurl.com".to_string()), metadata_hash: Some([2; 32]), manager: Some(create_test_address()), reserve: None,
freeze: None,
clawback: None,
};
let result = asset_config.validate();
assert!(result.is_err());
let errors = result.unwrap_err();
assert_eq!(errors.len(), 7);
let error_text = errors.join("\n");
assert!(error_text.contains("total") && error_text.contains("immutable"));
assert!(error_text.contains("decimals") && error_text.contains("immutable"));
assert!(error_text.contains("default_frozen") && error_text.contains("immutable"));
assert!(error_text.contains("asset_name") && error_text.contains("immutable"));
assert!(error_text.contains("unit_name") && error_text.contains("immutable"));
assert!(error_text.contains("url") && error_text.contains("immutable"));
assert!(error_text.contains("metadata_hash") && error_text.contains("immutable"));
}
}