use crate::action::WireActionStatus;
use crate::action::WireDelete;
use crate::action::WireNewEntryAction;
use crate::action::WireUpdateRelationship;
use crate::prelude::*;
use error::RecordGroupError;
use error::RecordGroupResult;
use holochain_keystore::KeystoreError;
use holochain_keystore::LairResult;
use holochain_keystore::MetaLairClient;
use holochain_zome_types::entry::EntryHashed;
use std::borrow::Cow;
use std::collections::BTreeSet;
#[allow(missing_docs)]
pub mod error;
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, SerializedBytes, Default)]
pub struct WireRecordOps {
pub action: Option<Judged<SignedAction>>,
pub deletes: Vec<Judged<WireDelete>>,
pub updates: Vec<Judged<WireUpdateRelationship>>,
pub entry: Option<Entry>,
}
impl WireRecordOps {
pub fn new() -> Self {
Self::default()
}
pub fn render(self) -> DhtOpResult<RenderedOps> {
let Self {
action,
deletes,
updates,
entry,
} = self;
let mut ops = Vec::with_capacity(1 + deletes.len() + updates.len());
if let Some(action) = action {
let status = action.validation_status();
let SignedAction(action, signature) = action.data;
let entry_hash = action.entry_hash().cloned();
ops.push(RenderedOp::new(
action,
signature,
status,
DhtOpType::StoreRecord,
)?);
if let Some(entry_hash) = entry_hash {
for op in deletes {
let status = op.validation_status();
let op = op.data;
let signature = op.signature;
let action = Action::Delete(op.delete);
ops.push(RenderedOp::new(
action,
signature,
status,
DhtOpType::RegisterDeletedBy,
)?);
}
for op in updates {
let status = op.validation_status();
let SignedAction(action, signature) =
op.data.into_signed_action(entry_hash.clone());
ops.push(RenderedOp::new(
action,
signature,
status,
DhtOpType::RegisterUpdatedRecord,
)?);
}
}
}
Ok(RenderedOps {
entry: entry.map(EntryHashed::from_content_sync),
ops,
})
}
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, SerializedBytes)]
pub struct WireRecord {
signed_action: SignedAction,
maybe_entry: Option<Entry>,
validation_status: ValidationStatus,
deletes: Vec<WireActionStatus<WireDelete>>,
updates: Vec<WireActionStatus<WireUpdateRelationship>>,
}
#[derive(Debug, Clone)]
pub struct RecordGroup<'a> {
actions: Vec<Cow<'a, SignedActionHashed>>,
rejected: Vec<Cow<'a, SignedActionHashed>>,
entry: Cow<'a, EntryHashed>,
}
#[derive(Debug, Clone, derive_more::Constructor)]
pub struct RecordStatus {
pub record: Record,
pub status: ValidationStatus,
}
impl<'a> RecordGroup<'a> {
pub fn actions_and_hashes(&self) -> impl Iterator<Item = (&ActionHash, &Action)> {
self.actions
.iter()
.map(|shh| shh.action_address())
.zip(self.actions.iter().map(|shh| shh.action()))
}
pub fn is_empty(&self) -> bool {
self.len() == 0
}
pub fn len(&self) -> usize {
self.actions.len()
}
pub fn visibility(&self) -> RecordGroupResult<&EntryVisibility> {
self.actions
.first()
.ok_or(RecordGroupError::Empty)?
.action()
.entry_data()
.map(|(_, et)| et.visibility())
.ok_or(RecordGroupError::MissingEntryData)
}
pub fn entry_hash(&self) -> &EntryHash {
self.entry.as_hash()
}
pub fn entry_hashed(&self) -> EntryHashed {
self.entry.clone().into_owned()
}
pub fn owned_signed_actions(&self) -> impl Iterator<Item = SignedActionHashed> + 'a {
self.actions
.clone()
.into_iter()
.chain(self.rejected.clone().into_iter())
.map(|shh| shh.into_owned())
}
pub fn valid_hashes(&self) -> impl Iterator<Item = &ActionHash> {
self.actions.iter().map(|shh| shh.action_address())
}
pub fn rejected_hashes(&self) -> impl Iterator<Item = &ActionHash> {
self.rejected.iter().map(|shh| shh.action_address())
}
pub fn from_wire_records<I: IntoIterator<Item = WireActionStatus<WireNewEntryAction>>>(
actions_iter: I,
entry_type: EntryType,
entry: Entry,
) -> RecordGroupResult<RecordGroup<'a>> {
let iter = actions_iter.into_iter();
let mut valid = Vec::with_capacity(iter.size_hint().0);
let mut rejected = Vec::with_capacity(iter.size_hint().0);
let entry = entry.into_hashed();
let entry_hash = entry.as_hash().clone();
let entry = Cow::Owned(entry);
for wire in iter {
match wire.validation_status {
ValidationStatus::Valid => valid.push(Cow::Owned(
wire.action
.into_action(entry_type.clone(), entry_hash.clone()),
)),
ValidationStatus::Rejected => rejected.push(Cow::Owned(
wire.action
.into_action(entry_type.clone(), entry_hash.clone()),
)),
ValidationStatus::Abandoned => todo!(),
}
}
Ok(Self {
actions: valid,
rejected,
entry,
})
}
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, SerializedBytes)]
pub enum GetRecordResponse {
GetEntryFull(Option<Box<RawGetEntryResponse>>),
GetEntryPartial,
GetEntryCollapsed,
GetAction(Option<Box<WireRecord>>),
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, SerializedBytes)]
pub struct RawGetEntryResponse {
pub live_actions: BTreeSet<WireActionStatus<WireNewEntryAction>>,
pub deletes: Vec<WireActionStatus<WireDelete>>,
pub updates: Vec<WireActionStatus<WireUpdateRelationship>>,
pub entry: Entry,
pub entry_type: EntryType,
}
impl RawGetEntryResponse {
pub fn from_records<E>(
records: E,
deletes: Vec<WireActionStatus<WireDelete>>,
updates: Vec<WireActionStatus<WireUpdateRelationship>>,
) -> Option<Self>
where
E: IntoIterator<Item = RecordStatus>,
{
let mut records = records.into_iter();
records.next().map(|RecordStatus { record, status }| {
let mut live_actions = BTreeSet::new();
let (new_entry_action, entry_type, entry) = Self::from_record(record);
live_actions.insert(WireActionStatus::new(new_entry_action, status));
let r = Self {
live_actions,
deletes,
updates,
entry,
entry_type,
};
records.fold(r, |mut response, RecordStatus { record, status }| {
let (new_entry_action, entry_type, entry) = Self::from_record(record);
debug_assert_eq!(response.entry, entry);
debug_assert_eq!(response.entry_type, entry_type);
response
.live_actions
.insert(WireActionStatus::new(new_entry_action, status));
response
})
})
}
fn from_record(record: Record) -> (WireNewEntryAction, EntryType, Entry) {
let (shh, entry) = record.into_inner();
let entry = entry
.into_option()
.expect("Get entry responses cannot be created without entries");
let (action, signature) = shh.into_inner();
let (new_entry_action, entry_type) = match action.into_content() {
Action::Create(ec) => {
let et = ec.entry_type.clone();
(WireNewEntryAction::Create((ec, signature).into()), et)
}
Action::Update(eu) => {
let et = eu.entry_type.clone();
(WireNewEntryAction::Update((eu, signature).into()), et)
}
h => panic!(
"Get entry responses cannot be created from actions
other then Create or Update.
Tried to with: {:?}",
h
),
};
(new_entry_action, entry_type, entry)
}
}
#[async_trait::async_trait]
pub trait RecordExt {
async fn validate(&self) -> Result<(), KeystoreError>;
}
#[async_trait::async_trait]
impl RecordExt for Record {
async fn validate(&self) -> Result<(), KeystoreError> {
self.signed_action().validate().await?;
Ok(())
}
}
#[async_trait::async_trait]
pub trait SignedActionHashedExt {
fn from_content_sync(signed_action: SignedAction) -> SignedActionHashed;
#[allow(clippy::new_ret_no_self)]
async fn sign(
keystore: &MetaLairClient,
action: ActionHashed,
) -> LairResult<SignedActionHashed>;
async fn validate(&self) -> Result<(), KeystoreError>;
}
#[allow(missing_docs)]
#[async_trait::async_trait]
impl SignedActionHashedExt for SignedActionHashed {
fn from_content_sync(signed_action: SignedAction) -> Self
where
Self: Sized,
{
let (action, signature) = signed_action.into();
Self::with_presigned(action.into_hashed(), signature)
}
async fn sign(keystore: &MetaLairClient, action_hashed: ActionHashed) -> LairResult<Self> {
let signature = action_hashed
.author()
.sign(keystore, action_hashed.as_content())
.await?;
Ok(Self::with_presigned(action_hashed, signature))
}
async fn validate(&self) -> Result<(), KeystoreError> {
if !self
.action()
.author()
.verify_signature(self.signature(), self.action())
.await
{
return Err(KeystoreError::InvalidSignature(
self.signature().clone(),
format!("action {:?}", self.action_address()),
));
}
Ok(())
}
}
impl WireRecord {
pub fn into_parts(self) -> (RecordStatus, Vec<RecordStatus>, Vec<RecordStatus>) {
let entry_hash = self.signed_action.action().entry_hash().cloned();
let action = Record::new(
SignedActionHashed::from_content_sync(self.signed_action),
self.maybe_entry,
);
let deletes = self
.deletes
.into_iter()
.map(WireActionStatus::<WireDelete>::into_record_status)
.collect();
let updates = self
.updates
.into_iter()
.map(|u| {
let entry_hash = entry_hash
.clone()
.expect("Updates cannot be on actions that do not have entries");
u.into_record_status(entry_hash)
})
.collect();
(
RecordStatus::new(action, self.validation_status),
deletes,
updates,
)
}
pub fn from_record(
e: RecordStatus,
deletes: Vec<WireActionStatus<WireDelete>>,
updates: Vec<WireActionStatus<WireUpdateRelationship>>,
) -> Self {
let RecordStatus { record, status } = e;
let (signed_action, maybe_entry) = record.into_inner();
Self {
signed_action: signed_action.into(),
maybe_entry: maybe_entry.into_option(),
validation_status: status,
deletes,
updates,
}
}
pub fn entry_hash(&self) -> Option<&EntryHash> {
self.signed_action
.action()
.entry_data()
.map(|(hash, _)| hash)
}
}
#[cfg(test)]
mod tests {
use super::SignedAction;
use super::SignedActionHashed;
use crate::prelude::*;
use ::fixt::prelude::*;
use holo_hash::HasHash;
use holo_hash::HoloHashed;
#[tokio::test(flavor = "multi_thread")]
async fn test_signed_action_roundtrip() {
let signature = SignatureFixturator::new(Unpredictable).next().unwrap();
let action = ActionFixturator::new(Unpredictable).next().unwrap();
let signed_action = SignedAction(action, signature);
let hashed: HoloHashed<SignedAction> = HoloHashed::from_content_sync(signed_action);
let HoloHashed {
content: SignedAction(action, signature),
hash,
} = hashed.clone();
let shh = SignedActionHashed {
hashed: ActionHashed::with_pre_hashed(action, hash),
signature,
};
assert_eq!(shh.action_address(), hashed.as_hash());
let round = HoloHashed {
content: SignedAction(shh.action().clone(), shh.signature().clone()),
hash: shh.action_address().clone(),
};
assert_eq!(hashed, round);
}
}