use std::borrow::Borrow;
use std::collections::{BTreeSet, HashMap, HashSet};
use invoice::{Allocation, Amount};
use rgb::bitcoin::OutPoint as Outpoint;
use rgb::{
AssignmentType, ContractId, GlobalStateType, OpId, OutputSeal, RevealedData, RevealedValue,
Schema, Txid, VoidState,
};
use strict_encoding::{FieldName, StrictDecode, StrictDumb, StrictEncode};
use strict_types::{StrictVal, TypeSystem};
use crate::contract::{AssignmentsFilter, KnownState, OutputAssignment, WitnessInfo};
use crate::info::ContractInfo;
use crate::persistence::ContractStateRead;
use crate::LIB_NAME_RGB_OPS;
#[derive(Clone, Eq, PartialEq, Debug, Display, Error, From)]
#[display(doc_comments)]
pub enum ContractError {
FieldNameUnknown(FieldName),
}
#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Debug, Display, From)]
#[derive(StrictType, StrictDumb, StrictEncode, StrictDecode)]
#[strict_type(lib = LIB_NAME_RGB_OPS, tags = custom)]
#[display(inner)]
#[cfg_attr(
feature = "serde",
derive(Serialize, Deserialize),
serde(crate = "serde_crate", rename_all = "camelCase")
)]
pub enum AllocatedState {
#[from(())]
#[from(VoidState)]
#[display("~")]
#[strict_type(tag = 0, dumb)]
Void,
#[from]
#[from(Amount)]
#[strict_type(tag = 1)]
Amount(RevealedValue),
#[from]
#[from(Allocation)]
#[strict_type(tag = 2)]
Data(RevealedData),
}
impl KnownState for AllocatedState {
const IS_FUNGIBLE: bool = false;
}
impl AllocatedState {
fn unwrap_fungible(&self) -> Amount {
match self {
AllocatedState::Amount(revealed_value) => (*revealed_value).into(),
_ => panic!("unwrapping non-fungible state"),
}
}
}
pub type OwnedAllocation = OutputAssignment<AllocatedState>;
pub type RightsAllocation = OutputAssignment<VoidState>;
pub type FungibleAllocation = OutputAssignment<Amount>;
pub type DataAllocation = OutputAssignment<RevealedData>;
#[derive(Copy, Clone, Eq, PartialEq, Hash, Debug, Display)]
#[cfg_attr(
feature = "serde",
derive(Serialize, Deserialize),
serde(crate = "serde_crate", rename_all = "camelCase")
)]
#[display(lowercase)]
pub enum OpDirection {
Issued,
Received,
Sent,
}
#[derive(Clone, Eq, PartialEq, Hash, Debug)]
#[cfg_attr(
feature = "serde",
derive(Serialize, Deserialize),
serde(crate = "serde_crate", rename_all = "camelCase", tag = "type")
)]
pub struct ContractOp {
pub direction: OpDirection,
pub ty: AssignmentType,
pub opids: BTreeSet<OpId>,
pub state: AllocatedState,
pub to: BTreeSet<OutputSeal>,
pub witness: Option<WitnessInfo>,
}
fn reduce_to_ty(allocations: impl IntoIterator<Item = OwnedAllocation>) -> AssignmentType {
allocations
.into_iter()
.map(|a| a.opout.ty)
.reduce(|ty1, ty2| {
assert_eq!(ty1, ty2);
ty1
})
.expect("empty list of allocations")
}
impl ContractOp {
fn non_fungible_genesis(
our_allocations: HashSet<OwnedAllocation>,
) -> impl ExactSizeIterator<Item = Self> {
our_allocations.into_iter().map(|a| Self {
direction: OpDirection::Issued,
ty: a.opout.ty,
opids: bset![a.opout.op],
state: a.state,
to: bset![a.seal],
witness: None,
})
}
fn non_fungible_sent(
witness: WitnessInfo,
ext_allocations: HashSet<OwnedAllocation>,
) -> impl ExactSizeIterator<Item = Self> {
ext_allocations.into_iter().map(move |a| Self {
direction: OpDirection::Sent,
ty: a.opout.ty,
opids: bset![a.opout.op],
state: a.state,
to: bset![a.seal],
witness: Some(witness),
})
}
fn non_fungible_received(
witness: WitnessInfo,
our_allocations: HashSet<OwnedAllocation>,
) -> impl ExactSizeIterator<Item = Self> {
our_allocations.into_iter().map(move |a| Self {
direction: OpDirection::Received,
ty: a.opout.ty,
opids: bset![a.opout.op],
state: a.state,
to: bset![a.seal],
witness: Some(witness),
})
}
fn fungible_genesis(our_allocations: HashSet<OwnedAllocation>) -> Self {
let to = our_allocations.iter().map(|a| a.seal).collect();
let opids = our_allocations.iter().map(|a| a.opout.op).collect();
let issued: Amount = our_allocations
.iter()
.map(|a| a.state.unwrap_fungible())
.sum();
Self {
direction: OpDirection::Issued,
ty: reduce_to_ty(our_allocations),
opids,
state: AllocatedState::Amount(issued.into()),
to,
witness: None,
}
}
fn fungible_sent(witness: WitnessInfo, ext_allocations: HashSet<OwnedAllocation>) -> Self {
let opids = ext_allocations.iter().map(|a| a.opout.op).collect();
let to = ext_allocations.iter().map(|a| a.seal).collect();
let amount: Amount = ext_allocations
.iter()
.map(|a| a.state.unwrap_fungible())
.sum();
Self {
direction: OpDirection::Sent,
ty: reduce_to_ty(ext_allocations),
opids,
state: AllocatedState::Amount(amount.into()),
to,
witness: Some(witness),
}
}
fn fungible_received(witness: WitnessInfo, our_allocations: HashSet<OwnedAllocation>) -> Self {
let opids = our_allocations.iter().map(|a| a.opout.op).collect();
let to = our_allocations.iter().map(|a| a.seal).collect();
let amount: Amount = our_allocations
.iter()
.map(|a| a.state.unwrap_fungible())
.sum();
Self {
direction: OpDirection::Received,
ty: reduce_to_ty(our_allocations),
opids,
state: AllocatedState::Amount(amount.into()),
to,
witness: Some(witness),
}
}
}
#[derive(Clone, Eq, PartialEq, Debug)]
pub struct ContractData<S: ContractStateRead> {
pub state: S,
pub schema: Schema,
pub types: TypeSystem,
pub info: ContractInfo,
}
impl<S: ContractStateRead> ContractData<S> {
pub fn contract_id(&self) -> ContractId { self.state.contract_id() }
pub fn global(&self, name: impl Into<FieldName>) -> impl Iterator<Item = StrictVal> + '_ {
self.global_raw(self.schema.global_type(name))
}
pub fn global_raw(&self, type_id: GlobalStateType) -> impl Iterator<Item = StrictVal> + '_ {
let global_details = self
.schema
.global_types
.get(&type_id)
.expect("cannot find type ID in schema global types");
self.state
.global(type_id)
.expect("cannot find type ID in global state")
.map(|entry| {
self.types
.strict_deserialize_type(
global_details.global_state_schema.sem_id,
entry.borrow().data().as_slice(),
)
.expect("unvalidated contract data in stash")
.unbox()
})
}
fn extract_state<'c, A, U>(
&'c self,
state: impl IntoIterator<Item = &'c OutputAssignment<A>> + 'c,
type_id: AssignmentType,
filter: impl AssignmentsFilter + 'c,
) -> Result<impl Iterator<Item = OutputAssignment<U>> + 'c, ContractError>
where
A: Clone + KnownState + 'c,
U: From<A> + KnownState + 'c,
{
Ok(self
.extract_state_unfiltered(state, type_id)?
.filter(move |outp| filter.should_include(outp.seal, outp.witness)))
}
fn extract_state_unfiltered<'c, A, U>(
&'c self,
state: impl IntoIterator<Item = &'c OutputAssignment<A>> + 'c,
type_id: AssignmentType,
) -> Result<impl Iterator<Item = OutputAssignment<U>> + 'c, ContractError>
where
A: Clone + KnownState + 'c,
U: From<A> + KnownState + 'c,
{
Ok(state
.into_iter()
.filter(move |outp| outp.opout.ty == type_id)
.cloned()
.map(OutputAssignment::<A>::transmute))
}
pub fn rights<'c>(
&'c self,
name: impl Into<FieldName>,
filter: impl AssignmentsFilter + 'c,
) -> Result<impl Iterator<Item = RightsAllocation> + 'c, ContractError> {
let type_id = self.schema.assignment_type(name);
self.rights_raw(type_id, filter)
}
pub fn rights_raw<'c>(
&'c self,
type_id: AssignmentType,
filter: impl AssignmentsFilter + 'c,
) -> Result<impl Iterator<Item = RightsAllocation> + 'c, ContractError> {
self.extract_state(self.state.rights_all(), type_id, filter)
}
pub fn fungible<'c>(
&'c self,
name: impl Into<FieldName>,
filter: impl AssignmentsFilter + 'c,
) -> Result<impl Iterator<Item = FungibleAllocation> + 'c, ContractError> {
let type_id = self.schema.assignment_type(name);
self.fungible_raw(type_id, filter)
}
pub fn fungible_raw<'c>(
&'c self,
type_id: AssignmentType,
filter: impl AssignmentsFilter + 'c,
) -> Result<impl Iterator<Item = FungibleAllocation> + 'c, ContractError> {
self.extract_state(self.state.fungible_all(), type_id, filter)
}
pub fn data<'c>(
&'c self,
name: impl Into<FieldName>,
filter: impl AssignmentsFilter + 'c,
) -> Result<impl Iterator<Item = DataAllocation> + 'c, ContractError> {
let type_id = self.schema.assignment_type(name);
self.data_raw(type_id, filter)
}
pub fn data_raw<'c>(
&'c self,
type_id: AssignmentType,
filter: impl AssignmentsFilter + 'c,
) -> Result<impl Iterator<Item = DataAllocation> + 'c, ContractError> {
self.extract_state(self.state.data_all(), type_id, filter)
}
pub fn allocations<'c>(
&'c self,
filter: impl AssignmentsFilter + Copy + 'c,
) -> impl Iterator<Item = OwnedAllocation> + 'c {
fn f<'a, S, U>(
filter: impl AssignmentsFilter + 'a,
state: impl IntoIterator<Item = &'a OutputAssignment<S>> + 'a,
) -> impl Iterator<Item = OutputAssignment<U>> + 'a
where
S: Clone + KnownState + 'a,
U: From<S> + KnownState + 'a,
{
state
.into_iter()
.filter(move |outp| filter.should_include(outp.seal, outp.witness))
.cloned()
.map(OutputAssignment::<S>::transmute)
}
f(filter, self.state.rights_all())
.chain(f(filter, self.state.fungible_all()))
.chain(f(filter, self.state.data_all()))
}
pub fn outpoint_allocations(
&self,
outpoint: Outpoint,
) -> impl Iterator<Item = OwnedAllocation> + '_ {
self.allocations(outpoint)
}
pub fn history(
&self,
filter_outpoints: impl AssignmentsFilter + Clone,
filter_witnesses: impl AssignmentsFilter + Clone,
) -> Vec<ContractOp> {
self.history_fungible(filter_outpoints.clone(), filter_witnesses.clone())
.into_iter()
.chain(self.history_rights(filter_outpoints.clone(), filter_witnesses.clone()))
.chain(self.history_data(filter_outpoints.clone(), filter_witnesses.clone()))
.collect()
}
fn operations<'c, T: KnownState + 'c, I: Iterator<Item = &'c OutputAssignment<T>>>(
&'c self,
state: impl Fn(&'c S) -> I,
filter_outpoints: impl AssignmentsFilter,
filter_witnesses: impl AssignmentsFilter,
) -> Vec<ContractOp>
where
AllocatedState: From<T>,
{
let mut allocations_our_outpoint = state(&self.state)
.filter(move |outp| filter_outpoints.should_include(outp.seal, outp.witness))
.fold(HashMap::<_, HashSet<_>>::new(), |mut map, a| {
map.entry(a.witness)
.or_default()
.insert(a.clone().transmute::<AllocatedState>());
map
});
let mut allocations_our_witness = state(&self.state)
.filter(move |outp| filter_witnesses.should_include(outp.seal, outp.witness))
.fold(HashMap::<_, HashSet<_>>::new(), |mut map, a| {
let witness = a.witness.expect(
"all empty witnesses must be already filtered out by wallet.filter_witness()",
);
map.entry(witness)
.or_default()
.insert(a.clone().transmute::<AllocatedState>());
map
});
let mut witness_ids = allocations_our_witness
.keys()
.cloned()
.collect::<BTreeSet<_>>();
witness_ids.extend(allocations_our_outpoint.keys().filter_map(|x| *x));
let mut ops = Vec::with_capacity(witness_ids.len() + 1);
if let Some(genesis_allocations) = allocations_our_outpoint.remove(&None) {
if T::IS_FUNGIBLE {
ops.push(ContractOp::fungible_genesis(genesis_allocations));
} else {
ops.extend(ContractOp::non_fungible_genesis(genesis_allocations));
}
}
for witness_id in witness_ids {
let our_outpoint = allocations_our_outpoint.remove(&Some(witness_id));
let our_witness = allocations_our_witness.remove(&witness_id);
let witness_info = self.witness_info(witness_id).expect(
"witness id was returned from the contract state above, so it must be there",
);
match (our_outpoint, our_witness) {
(Some(our_allocations), Some(all_allocations)) => {
let ext_allocations = all_allocations
.difference(&our_allocations)
.cloned()
.collect::<HashSet<_>>();
if ext_allocations.is_empty() {
continue;
}
if T::IS_FUNGIBLE {
ops.push(ContractOp::fungible_sent(witness_info, ext_allocations))
} else {
ops.extend(ContractOp::non_fungible_sent(witness_info, ext_allocations))
}
}
(None, Some(ext_allocations)) => {
if T::IS_FUNGIBLE {
ops.push(ContractOp::fungible_sent(witness_info, ext_allocations))
} else {
ops.extend(ContractOp::non_fungible_sent(witness_info, ext_allocations))
}
}
(Some(our_allocations), None) => {
if T::IS_FUNGIBLE {
ops.push(ContractOp::fungible_received(witness_info, our_allocations))
} else {
ops.extend(ContractOp::non_fungible_received(witness_info, our_allocations))
}
}
(None, None) => unreachable!("broken allocation filters"),
};
}
ops
}
pub fn history_fungible(
&self,
filter_outpoints: impl AssignmentsFilter,
filter_witnesses: impl AssignmentsFilter,
) -> Vec<ContractOp> {
self.operations(|state| state.fungible_all(), filter_outpoints, filter_witnesses)
}
pub fn history_rights(
&self,
filter_outpoints: impl AssignmentsFilter,
filter_witnesses: impl AssignmentsFilter,
) -> Vec<ContractOp> {
self.operations(|state| state.rights_all(), filter_outpoints, filter_witnesses)
}
pub fn history_data(
&self,
filter_outpoints: impl AssignmentsFilter,
filter_witnesses: impl AssignmentsFilter,
) -> Vec<ContractOp> {
self.operations(|state| state.data_all(), filter_outpoints, filter_witnesses)
}
pub fn witness_info(&self, witness_id: Txid) -> Option<WitnessInfo> {
let ord = self.state.witness_ord(witness_id)?;
Some(WitnessInfo {
id: witness_id,
ord,
})
}
}