mod helper;
use chrono::{DateTime, Utc};
use helper::{utc, version};
use std::fs;
use url::Url;
macro_rules! DTI_DATA_FILE {
() => {
"DTI_Data_20230701.json"
};
}
#[derive(serde::Deserialize)]
#[serde(rename = "records")]
pub struct DTIData {
pub records: Vec<Record>,
}
impl DTIData {
pub fn new() -> Result<Self, Error> {
serde_json::from_str(include_str!(concat!("../", DTI_DATA_FILE!())))
.map_err(Error::DeserializationError)
}
}
#[derive(serde::Deserialize)]
#[serde(rename_all = "PascalCase")]
pub struct Record {
pub header: Header,
pub informative: Informative,
pub normative: Option<Normative>,
pub metadata: Metadata,
}
#[derive(Clone, Debug, PartialEq, Eq, serde::Deserialize)]
pub struct Header {
#[serde(rename = "DTI")]
pub dti: String,
#[serde(rename = "DTIType")]
pub dti_type: DigitalTokenIdentifierType,
#[serde(rename = "DLTType")]
pub dlt_type: Option<DigitalLedgerTechnologyType>,
#[serde(rename = "templateVersion")]
#[serde(with = "version")]
pub template_version: semver::Version,
}
#[derive(Clone, Debug, PartialEq, Eq, serde_repr::Deserialize_repr)]
#[repr(u8)]
pub enum DigitalTokenIdentifierType {
AuxiliaryDigitalToken = 0,
NativeDigitalToken = 1,
DistributedLedgerWithoutaNativeDigitalToken = 2,
FunctionallyFungibleGroupofDigitalTokens = 3,
}
#[derive(Clone, Debug, PartialEq, Eq, serde_repr::Deserialize_repr)]
#[repr(u8)]
pub enum DigitalLedgerTechnologyType {
Other = 0,
Blockchain = 1,
}
#[derive(Clone, Debug, PartialEq, Eq, serde::Deserialize)]
pub struct Informative {
#[serde(rename = "LongName")]
pub long_name: String,
#[serde(rename = "OrigLangLongName")]
pub orig_lang_long_name: Option<String>,
#[serde(rename = "ShortNames")]
pub short_names: Option<Vec<ShortName>>,
#[serde(rename = "UnitMultiplier")]
pub unit_multiplier: Option<u128>,
#[serde(rename = "URL")]
pub url: Option<Url>,
#[serde(rename = "PublicDistributedLedgerIndication")]
pub public_distributed_ledger_indication: Option<bool>,
#[serde(rename = "UnderlyingAssetExternalIdentifiers")]
pub underlying_asset_external_identifiers: Option<Vec<UnderlyingAssetExternalIdentifier>>,
}
#[derive(Clone, Debug, PartialEq, Eq, serde::Deserialize)]
pub struct ShortName {
#[serde(rename = "ShortName")]
pub short_name: String,
}
#[derive(Clone, Debug, PartialEq, Eq, serde::Deserialize)]
pub struct UnderlyingAssetExternalIdentifier {
#[serde(rename = "UnderlyingAssetExternalIdentifierType")]
pub underlying_asset_external_identifier_type: String,
#[serde(rename = "UnderlyingAssetExternalIdentifierValue")]
pub underlying_asset_external_identifier_value: String,
}
#[derive(Default, Clone, Debug, PartialEq, Eq, serde::Deserialize)]
pub struct Normative {
#[serde(rename = "GenesisBlockHash")]
pub genesis_block_hash: Option<String>,
#[serde(rename = "GenesisBlockHashAlgorithm")]
pub genesis_block_hash_algorithm: Option<String>,
#[serde(rename = "GenesisBlockUTCTimestamp")]
#[serde(default)]
#[serde(with = "utc")]
pub genesis_block_utctimestamp: DateTime<Utc>,
#[serde(rename = "Forks")]
pub forks: Option<Vec<Fork>>,
#[serde(rename = "Functionally fungible DTI")]
pub functionally_fungible_dti: Option<Vec<String>>,
#[serde(rename = "AuxiliaryMechanism")]
pub auxiliary_mechanism: Option<String>,
#[serde(rename = "AuxiliaryDistributedLedger")]
pub auxiliary_distributed_ledger: Option<String>,
#[serde(rename = "AuxiliaryTechnicalReference")]
pub auxiliary_technical_reference: Option<String>,
}
#[derive(Clone, Debug, PartialEq, Eq, serde::Deserialize)]
pub struct Fork {
#[serde(rename = "ForkReferenceDTI")]
pub fork_reference_dti: String,
#[serde(rename = "ForkBlockHeight")]
pub fork_block_height: u128,
#[serde(rename = "ForkBlockUTCTimestamp")]
#[serde(with = "utc")]
pub fork_block_utctimestamp: DateTime<Utc>,
#[serde(rename = "ForkBlockHash")]
pub fork_block_hash: String,
#[serde(rename = "ConsensusMechanismChangeResponse")]
pub consensus_mechanism_change_response: bool,
#[serde(rename = "DigitalTokenCreationResponse")]
pub digital_token_creation_response: bool,
}
#[derive(Clone, Debug, PartialEq, Eq, serde::Deserialize)]
#[allow(clippy::struct_excessive_bools)]
pub struct Metadata {
#[serde(rename = "recVersion")]
pub rec_version: u8,
#[serde(rename = "recDateTime")]
#[serde(with = "utc")]
pub rec_date_time: DateTime<Utc>,
#[serde(rename = "Provisional")]
pub provisional: bool,
#[serde(rename = "Private")]
pub private: bool,
#[serde(rename = "Disputed")]
pub disputed: Option<bool>,
#[serde(rename = "Deleted")]
pub deleted: Option<bool>,
}
pub fn get_latest_dti_data() -> Result<DTIData, Error> {
let dti_file = reqwest::blocking::get("https://download.dtif.org/data.json")?
.error_for_status()?
.text()?;
serde_json::from_str(&dti_file).map_err(Error::DeserializationError)
}
pub fn read_dti_data_from_file(file_path: &str) -> Result<DTIData, Error> {
let dti_file = fs::read_to_string(file_path).map_err(Error::FileError)?;
serde_json::from_str(&dti_file).map_err(Error::DeserializationError)
}
#[must_use]
pub fn by_dti<'a>(data: &'a DTIData, dti: &str) -> Option<&'a Record> {
data.records.iter().find(|&record| record.header.dti == dti)
}
#[must_use]
pub fn get_all_dtis(data: &DTIData) -> Vec<String> {
let mut dtis: Vec<String> = vec![];
for record in &data.records {
dtis.push(record.header.dti.clone());
}
dtis
}
#[derive(thiserror::Error, Debug)]
pub enum Error {
#[error("Error downloading DTI file: {0}")]
HTTPError(#[from] reqwest::Error),
#[error("Error deseralizing JSON: {0}")]
DeserializationError(#[source] serde_json::Error),
#[error("Error opening DTI file: {0}")]
FileError(#[source] std::io::Error),
}
#[cfg(test)]
#[allow(clippy::bool_assert_comparison, clippy::too_many_lines)]
mod tests {
use super::*;
use anyhow::Ok;
use chrono::TimeZone;
#[test]
fn get_all_dtis_check() -> Result<(), anyhow::Error> {
let data = DTIData::new()?;
let dtis = get_all_dtis(&data);
assert!(
dtis.len() >= 1000,
"Number of DTIs below 100 (it's {}). Something must be wrong.",
dtis.len()
);
assert_ne!(dtis.iter().position(|r| r == "4H95J0R2X"), None); Ok(())
}
#[test]
fn by_dti_btc() -> Result<(), anyhow::Error> {
let data = DTIData::new()?;
let r = by_dti(&data, "4H95J0R2X").unwrap();
assert_eq!(r.header.dti, "4H95J0R2X");
assert_eq!(
r.header.dti_type,
DigitalTokenIdentifierType::NativeDigitalToken
);
assert_eq!(
r.header.dlt_type.clone().unwrap(),
DigitalLedgerTechnologyType::Blockchain
);
assert_eq!(r.header.template_version, semver::Version::new(1, 0, 0));
assert_eq!(r.informative.long_name, "Bitcoin");
assert!(r.informative.orig_lang_long_name.is_none()); assert_eq!(r.informative.short_names.as_ref().unwrap().len(), 2);
assert_eq!(
r.informative
.short_names
.as_ref()
.unwrap()
.get(0)
.unwrap()
.short_name,
"BTC"
);
assert_eq!(
r.informative
.short_names
.as_ref()
.unwrap()
.get(1)
.unwrap()
.short_name,
"XBT"
);
assert_eq!(
r.informative.unit_multiplier.unwrap_or_default(),
100_000_000
);
assert_eq!(
r.informative.url.clone().unwrap(),
Url::parse("https://github.com/bitcoin/bitcoin").unwrap()
);
assert_eq!(
r.informative
.public_distributed_ledger_indication
.unwrap_or_default(),
true
);
assert!(r
.informative
.underlying_asset_external_identifiers
.is_none()); if r.informative
.underlying_asset_external_identifiers
.is_some()
&& (!r
.informative
.underlying_asset_external_identifiers
.clone()
.unwrap()
.is_empty())
{
assert_eq!(
r.informative
.underlying_asset_external_identifiers
.clone()
.unwrap()
.get(0)
.unwrap()
.underlying_asset_external_identifier_type,
""
);
assert_eq!(
r.informative
.underlying_asset_external_identifiers
.clone()
.unwrap()
.get(0)
.unwrap()
.underlying_asset_external_identifier_value,
""
);
}
assert_eq!(
r.normative
.as_ref()
.unwrap()
.genesis_block_hash
.clone()
.unwrap_or_default(),
"000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f"
);
assert_eq!(
r.normative
.as_ref()
.unwrap()
.genesis_block_hash_algorithm
.clone()
.unwrap_or_default(),
"Double SHA-256"
);
assert_eq!(
r.normative.as_ref().unwrap().genesis_block_utctimestamp,
Utc.datetime_from_str("2009-01-03T18:15:05", "%Y-%m-%dT%H:%M:%S")
.unwrap()
);
assert_eq!(
r.normative.as_ref().unwrap().forks.as_ref().unwrap().len(),
11
);
assert_eq!(
r.normative
.as_ref()
.unwrap()
.forks
.as_ref()
.unwrap()
.get(0)
.unwrap()
.fork_reference_dti,
"4H95J0R2X"
);
assert_eq!(
r.normative
.as_ref()
.unwrap()
.forks
.as_ref()
.unwrap()
.get(0)
.unwrap()
.fork_block_height,
74638
);
assert_eq!(
r.normative
.as_ref()
.unwrap()
.forks
.as_ref()
.unwrap()
.get(0)
.unwrap()
.fork_block_utctimestamp,
Utc.datetime_from_str("2010-08-15T23:53:0", "%Y-%m-%dT%H:%M:%S")
.unwrap()
);
assert_eq!(
r.normative
.as_ref()
.unwrap()
.forks
.as_ref()
.unwrap()
.get(0)
.unwrap()
.fork_block_hash,
"Double SHA-256"
);
assert_eq!(
r.normative
.as_ref()
.unwrap()
.forks
.as_ref()
.unwrap()
.get(0)
.unwrap()
.consensus_mechanism_change_response,
true
);
assert_eq!(
r.normative
.as_ref()
.unwrap()
.forks
.as_ref()
.unwrap()
.get(0)
.unwrap()
.digital_token_creation_response,
false
);
assert!(r
.normative
.as_ref()
.unwrap()
.functionally_fungible_dti
.is_none()); if r.normative
.as_ref()
.unwrap()
.functionally_fungible_dti
.is_some()
{
assert_eq!(
r.normative
.as_ref()
.unwrap()
.functionally_fungible_dti
.as_ref()
.unwrap()
.get(0)
.unwrap(),
""
);
}
assert!(r.normative.as_ref().unwrap().auxiliary_mechanism.is_none()); assert!(r
.normative
.as_ref()
.unwrap()
.auxiliary_distributed_ledger
.is_none()); assert!(r
.normative
.as_ref()
.unwrap()
.auxiliary_technical_reference
.is_none());
assert_eq!(r.metadata.rec_version, 2);
assert_eq!(
r.metadata.rec_date_time,
Utc.datetime_from_str("2022-09-17T00:00:00", "%Y-%m-%dT%H:%M:%S")
.unwrap()
);
assert_eq!(r.metadata.provisional, false);
assert_eq!(r.metadata.private, false);
assert!(r.metadata.disputed.is_none()); assert!(r.metadata.deleted.is_none()); Ok(())
}
#[test]
fn by_dti_wbtc() -> Result<(), anyhow::Error> {
let data = DTIData::new()?;
let r = by_dti(&data, "PXK9B3H8Z").unwrap();
assert_eq!(r.header.dti, "PXK9B3H8Z");
assert_eq!(
r.header.dti_type,
DigitalTokenIdentifierType::AuxiliaryDigitalToken
);
assert!(r.header.dlt_type.clone().is_none()); assert_eq!(r.header.template_version, semver::Version::new(1, 0, 0));
assert_eq!(r.informative.long_name, "Bitcoin");
assert!(r.informative.orig_lang_long_name.is_none()); assert_eq!(r.informative.short_names.as_ref().unwrap().len(), 2);
assert_eq!(
r.informative
.short_names
.as_ref()
.unwrap()
.get(0)
.unwrap()
.short_name,
"WBTC"
);
assert_eq!(
r.informative
.short_names
.as_ref()
.unwrap()
.get(1)
.unwrap()
.short_name,
"BTC"
);
assert_eq!(
r.informative.unit_multiplier.unwrap_or_default(),
100_000_000
);
assert!(r.informative.url.is_none()); assert!(r.informative.public_distributed_ledger_indication.is_none()); assert!(r
.informative
.underlying_asset_external_identifiers
.is_none());
assert!(r.normative.as_ref().unwrap().genesis_block_hash.is_none()); assert!(r
.normative
.as_ref()
.unwrap()
.genesis_block_hash_algorithm
.is_none()); assert_eq!(
r.normative.as_ref().unwrap().genesis_block_utctimestamp,
DateTime::<Utc>::default()
); assert!(r.normative.as_ref().unwrap().forks.is_none()); assert!(r
.normative
.as_ref()
.unwrap()
.functionally_fungible_dti
.is_none()); assert_eq!(
r.normative
.as_ref()
.unwrap()
.auxiliary_mechanism
.clone()
.unwrap_or_default(),
"ERC-20"
);
assert_eq!(
r.normative
.as_ref()
.unwrap()
.auxiliary_distributed_ledger
.clone()
.unwrap_or_default(),
"WS6BZ8225"
);
assert_eq!(
r.normative
.as_ref()
.unwrap()
.auxiliary_technical_reference
.clone()
.unwrap_or_default(),
"0x321162cd933e2be498cd2267a90534a804051b11"
);
assert_eq!(r.metadata.rec_version, 2);
assert_eq!(
r.metadata.rec_date_time,
Utc.datetime_from_str("2022-12-28T00:00:00", "%Y-%m-%dT%H:%M:%S")
.unwrap()
);
assert_eq!(r.metadata.provisional, false);
assert_eq!(r.metadata.private, false);
assert!(r.metadata.disputed.is_none()); assert!(r.metadata.deleted.is_none()); Ok(())
}
#[test]
fn get_latest_dti_data_check() -> Result<(), anyhow::Error> {
let data = get_latest_dti_data()?;
let r = by_dti(&data, "4H95J0R2X").unwrap();
assert_eq!(r.header.dti, "4H95J0R2X");
assert_eq!(
r.normative
.as_ref()
.unwrap()
.genesis_block_hash
.clone()
.unwrap_or_default(),
"000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f"
);
Ok(())
}
}