use chrono::NaiveDateTime;
use core::convert::{TryFrom, TryInto};
use core::ops::{Add, AddAssign};
use diesel::prelude::*;
#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};
use std::collections::BTreeMap;
use std::str::FromStr;
use amplify::Wrapper;
use lnpbp::bitcoin::{self, OutPoint, Txid};
use lnpbp::bp::{self, TaggedHash};
use lnpbp::hex::FromHex;
use lnpbp::rgb::prelude::*;
use lnpbp::rgb::seal::WitnessVoutError;
use lnpbp::secp256k1zkp::{key::SecretKey, Secp256k1};
use super::schema::{self, FieldType, OwnedRightsType};
use crate::contracts::fungible::cache::models::{
read_allocations, read_inflation, SqlAllocation, SqlAllocationUtxo,
SqlAsset, SqlIssue,
};
use crate::contracts::fungible::cache::SqlCacheError;
use crate::error::ServiceErrorDomain;
pub type AccountingValue = f64;
#[derive(
Clone,
Copy,
PartialEq,
Eq,
Hash,
Debug,
Display,
Default,
StrictEncode,
StrictDecode,
)]
#[display(Debug)]
#[cfg_attr(
feature = "serde",
derive(Serialize, Deserialize),
serde(crate = "serde_crate")
)]
pub struct AccountingAmount(AtomicValue, u8);
impl AccountingAmount {
#[inline]
pub fn transmutate(
fractional_bits: u8,
accounting_value: AccountingValue,
) -> AtomicValue {
AccountingAmount::from_fractioned_accounting_value(
fractional_bits,
accounting_value,
)
.atomic_value()
}
#[inline]
pub fn from_asset_accounting_value(
asset: &Asset,
accounting_value: AccountingValue,
) -> Self {
let bits = asset.fractional_bits;
let full = (accounting_value.trunc() as u64) << bits as u64;
let fract = accounting_value.fract() as u64;
Self(full + fract, asset.fractional_bits)
}
#[inline]
pub fn from_fractioned_atomic_value(
fractional_bits: u8,
atomic_value: AtomicValue,
) -> Self {
Self(atomic_value, fractional_bits)
}
#[inline]
pub fn from_fractioned_accounting_value(
fractional_bits: u8,
accounting_value: AccountingValue,
) -> Self {
let fract = (accounting_value.fract()
* 10u64.pow(fractional_bits as u32) as AccountingValue)
as u64;
Self(accounting_value.trunc() as u64 + fract, fractional_bits)
}
#[inline]
pub fn from_asset_atomic_value(
asset: &Asset,
atomic_value: AtomicValue,
) -> Self {
Self(atomic_value, asset.fractional_bits)
}
#[inline]
pub fn accounting_value(&self) -> AccountingValue {
let full = self.0 >> self.1;
let fract = self.0 ^ (full << self.1);
full as AccountingValue
+ fract as AccountingValue
/ 10u64.pow(self.1 as u32) as AccountingValue
}
#[inline]
pub fn atomic_value(&self) -> AtomicValue {
self.0
}
#[inline]
pub fn fractional_bits(&self) -> u8 {
self.1
}
}
impl Add for AccountingAmount {
type Output = AccountingAmount;
fn add(self, rhs: Self) -> Self::Output {
if self.fractional_bits() != rhs.fractional_bits() {
panic!("Addition of amounts with different fractional bits")
} else {
AccountingAmount::from_fractioned_atomic_value(
self.fractional_bits(),
self.atomic_value() + rhs.atomic_value(),
)
}
}
}
impl AddAssign for AccountingAmount {
fn add_assign(&mut self, rhs: Self) {
if self.fractional_bits() != rhs.fractional_bits() {
panic!("Addition of amounts with different fractional bits")
} else {
self.0 += rhs.0
}
}
}
#[derive(
Clone, Getters, PartialEq, Debug, Display, StrictEncode, StrictDecode,
)]
#[cfg_attr(
feature = "serde",
derive(Serialize, Deserialize),
serde(crate = "serde_crate")
)]
#[display(Debug)]
pub struct Asset {
id: ContractId,
ticker: String,
name: String,
description: Option<String>,
supply: Supply,
chain: bp::Chain,
fractional_bits: u8,
date: NaiveDateTime,
known_issues: Vec<Issue>,
known_inflation: BTreeMap<bitcoin::OutPoint, AccountingAmount>,
unknown_inflation: AccountingAmount,
known_allocations: BTreeMap<bitcoin::OutPoint, Vec<Allocation>>,
}
impl Asset {
pub fn from_sql_asset(
table_value: &SqlAsset,
connection: &SqliteConnection,
) -> Result<Self, SqlCacheError> {
let (known_inflation, unknown_inflation) =
read_inflation(table_value, connection)?;
let known_table_issues =
SqlIssue::belonging_to(table_value).load::<SqlIssue>(connection)?;
let mut known_issues = vec![];
for issue in known_table_issues {
known_issues.push(Issue::from_sql_issue(
issue,
table_value.fractional_bits[0],
)?)
}
Ok(Self {
id: ContractId::from_str(&table_value.contract_id[..])?,
ticker: table_value.ticker.clone(),
name: table_value.asset_name.clone(),
description: table_value.asset_description.clone(),
supply: Supply::from_sql_asset(&table_value),
chain: bp::Chain::from_str(&table_value.chain[..])?,
fractional_bits: table_value.fractional_bits[0],
date: table_value.asset_date,
known_issues: known_issues,
known_inflation: known_inflation,
unknown_inflation: unknown_inflation,
known_allocations: read_allocations(&table_value, connection)?,
})
}
}
#[derive(
Clone, Getters, PartialEq, Debug, Display, StrictEncode, StrictDecode,
)]
#[display(Debug)]
#[cfg_attr(
feature = "serde",
derive(Serialize, Deserialize,),
serde(crate = "serde_crate")
)]
pub struct Allocation {
node_id: NodeId,
index: u16,
outpoint: bitcoin::OutPoint,
value: value::Revealed,
}
impl Allocation {
pub fn from_sql_allocation(
table_value: &SqlAllocation,
outpoint: &SqlAllocationUtxo,
) -> Result<Self, SqlCacheError> {
Ok(Self {
node_id: NodeId::from_hex(&table_value.node_id[..])?,
index: table_value.assignment_index as u16,
outpoint: OutPoint {
txid: Txid::from_hex(&outpoint.txid[..])?,
vout: outpoint.vout as u32,
},
value: value::Revealed {
value: table_value.amount as AtomicValue,
blinding: SecretKey::from_slice(
&Secp256k1::new(),
&Vec::<u8>::from_hex(&table_value.blinding[..])?[..],
)?,
},
})
}
}
#[derive(
Clone,
Copy,
Getters,
PartialEq,
Eq,
Hash,
Debug,
Display,
Default,
StrictEncode,
StrictDecode,
)]
#[display(Debug)]
#[cfg_attr(
feature = "serde",
derive(Serialize, Deserialize,),
serde(crate = "serde_crate")
)]
pub struct Supply {
known_circulating: AccountingAmount,
is_issued_known: Option<bool>,
max_cap: AccountingAmount,
}
impl Supply {
pub fn total_circulating(&self) -> Option<AccountingAmount> {
if self.is_issued_known.unwrap_or(false) {
Some(self.known_circulating)
} else {
None
}
}
pub fn from_sql_asset(table_value: &SqlAsset) -> Self {
Self {
known_circulating:
AccountingAmount::from_fractioned_accounting_value(
table_value.fractional_bits[0],
table_value.known_circulating_supply as AccountingValue,
),
is_issued_known: table_value.is_issued_known,
max_cap: AccountingAmount::from_fractioned_accounting_value(
table_value.fractional_bits[0],
table_value.max_cap as AccountingValue,
),
}
}
}
#[derive(
Clone,
Copy,
Getters,
Debug,
PartialEq,
Eq,
Hash,
Display,
StrictEncode,
StrictDecode,
)]
#[display(Debug)]
#[cfg_attr(
feature = "serde",
derive(Serialize, Deserialize,),
serde(crate = "serde_crate")
)]
pub struct Issue {
id: NodeId,
asset_id: ContractId,
amount: AccountingAmount,
origin: Option<bitcoin::OutPoint>,
}
impl Issue {
pub fn is_primary(&self) -> bool {
self.origin.is_none()
}
pub fn is_secondary(&self) -> bool {
self.origin.is_some()
}
pub fn from_sql_issue(
table_value: SqlIssue,
fraction_bits: u8,
) -> Result<Issue, SqlCacheError> {
Ok(Issue {
id: NodeId::from_hex(&table_value.node_id[..])?,
asset_id: ContractId::from_hex(&table_value.contract_id[..])?,
amount: AccountingAmount::from_fractioned_accounting_value(
fraction_bits,
table_value.amount as AccountingValue,
),
origin: match (table_value.origin_txid, table_value.origin_vout) {
(Some(txid), Some(vout)) => Some(OutPoint {
txid: Txid::from_hex(&txid[..])?,
vout: vout as u32,
}),
_ => None,
},
})
}
}
impl Asset {
pub fn add_issue(&self, _issue: Transition) -> Supply {
unimplemented!()
}
#[inline]
pub fn allocations(
&self,
seal: &bitcoin::OutPoint,
) -> Option<&Vec<Allocation>> {
self.known_allocations.get(seal)
}
pub fn add_allocation(
&mut self,
outpoint: bitcoin::OutPoint,
node_id: NodeId,
index: u16,
value: value::Revealed,
) -> bool {
let new_allocation = Allocation {
node_id,
index,
outpoint,
value,
};
let allocations =
self.known_allocations.entry(outpoint).or_insert(vec![]);
if !allocations.contains(&new_allocation) {
allocations.push(new_allocation);
true
} else {
false
}
}
pub fn remove_allocation(
&mut self,
outpoint: bitcoin::OutPoint,
node_id: NodeId,
index: u16,
value: value::Revealed,
) -> bool {
let old_allocation = Allocation {
node_id,
index,
outpoint,
value,
};
let allocations =
self.known_allocations.entry(outpoint).or_insert(vec![]);
if let Some(index) =
allocations.iter().position(|a| *a == old_allocation)
{
allocations.remove(index);
true
} else {
false
}
}
}
#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug, Display, From, Error)]
#[display(doc_comments)]
pub enum Error {
#[from]
Schema(schema::Error),
#[from(WitnessVoutError)]
GenesisSeal,
}
impl From<Error> for ServiceErrorDomain {
fn from(err: Error) -> Self {
ServiceErrorDomain::Schema(format!("{}", err))
}
}
impl TryFrom<Genesis> for Asset {
type Error = Error;
fn try_from(genesis: Genesis) -> Result<Self, Self::Error> {
if genesis.schema_id() != schema::schema().schema_id() {
Err(schema::Error::WrongSchemaId)?;
}
let genesis_meta = genesis.metadata();
let fractional_bits = *genesis_meta
.u8(*FieldType::Precision)
.first()
.ok_or(schema::Error::NotAllFieldsPresent)?;
let supply = AccountingAmount::from_fractioned_atomic_value(
fractional_bits,
*genesis_meta
.u64(*FieldType::IssuedSupply)
.first()
.ok_or(schema::Error::NotAllFieldsPresent)?,
);
let mut known_inflation = BTreeMap::<_, _>::default();
let mut unknown_inflation = AccountingAmount::default();
for assignment in
genesis.owned_rights_by_type(*OwnedRightsType::Inflation)
{
for state in assignment.to_custom_state() {
match state {
OwnedState::Revealed {
seal_definition,
assigned_state,
} => {
known_inflation.insert(
seal_definition.try_into()?,
AccountingAmount::from_fractioned_atomic_value(
fractional_bits,
assigned_state.u64().ok_or(
schema::Error::NotAllFieldsPresent,
)?,
),
);
}
OwnedState::ConfidentialSeal { assigned_state, .. } => {
if unknown_inflation.atomic_value() < core::u64::MAX {
unknown_inflation +=
AccountingAmount::from_fractioned_atomic_value(
fractional_bits,
assigned_state.u64().ok_or(
schema::Error::NotAllFieldsPresent,
)?,
)
};
}
_ => {
unknown_inflation =
AccountingAmount::from_fractioned_atomic_value(
fractional_bits,
core::u64::MAX,
);
}
}
}
}
let node_id = NodeId::from_inner(genesis.contract_id().into_inner());
let issue = Issue {
id: genesis.node_id(),
asset_id: genesis.contract_id(),
amount: supply.clone(),
origin: None,
};
let mut known_allocations =
BTreeMap::<bitcoin::OutPoint, Vec<Allocation>>::default();
for assignment in genesis.owned_rights_by_type(*OwnedRightsType::Assets)
{
assignment
.to_discrete_state()
.into_iter()
.enumerate()
.for_each(|(index, assign)| {
if let OwnedState::Revealed {
seal_definition:
seal::Revealed::TxOutpoint(outpoint_reveal),
assigned_state,
} = assign
{
known_allocations
.entry(outpoint_reveal.clone().into())
.or_insert(vec![])
.push(Allocation {
node_id,
index: index as u16,
outpoint: outpoint_reveal.into(),
value: assigned_state,
})
}
});
}
Ok(Self {
id: genesis.contract_id(),
chain: genesis.chain().clone(),
ticker: genesis_meta
.string(*FieldType::Ticker)
.first()
.ok_or(schema::Error::NotAllFieldsPresent)?
.clone(),
name: genesis_meta
.string(*FieldType::Name)
.first()
.ok_or(schema::Error::NotAllFieldsPresent)?
.clone(),
description: genesis_meta
.string(*FieldType::ContractText)
.first()
.cloned(),
supply: Supply {
known_circulating: supply,
is_issued_known: None,
max_cap: genesis
.owned_rights_by_type(*OwnedRightsType::Inflation)
.map(|assignments| {
AccountingAmount::from_fractioned_atomic_value(
fractional_bits,
assignments
.known_state_data()
.into_iter()
.map(|data| match data {
data::Revealed::U64(cap) => *cap,
_ => 0,
})
.sum(),
)
})
.unwrap_or(supply),
},
fractional_bits,
date: NaiveDateTime::from_timestamp(
*genesis_meta
.i64(*FieldType::Timestamp)
.first()
.ok_or(schema::Error::NotAllFieldsPresent)?,
0,
),
known_inflation,
unknown_inflation,
known_issues: vec![issue],
known_allocations,
})
}
}