hdk_extensions/
lib.rs

1pub use hdi_extensions::hdi;
2pub use hdi_extensions::holo_hash;
3pub use hdk;
4pub use hdi_extensions;
5
6use core::convert::{ TryFrom, TryInto };
7use hdi_extensions::{
8    summon_action,
9    summon_entry,
10};
11use hdk::prelude::{
12    get, get_details, agent_info,
13    debug, wasm_error,
14    Serialize, Deserialize,
15    ExternResult, WasmError, WasmErrorInner, GetOptions,
16    Record, Action, Details, RecordDetails, SignedHashed,
17    LinkTypeFilter, LinkTypeFilterExt, LinkTag,
18};
19use holo_hash::{
20    AgentPubKey, ActionHash, AnyDhtHash, AnyLinkableHash,
21    AnyDhtHashPrimitive, AnyLinkableHashPrimitive,
22};
23use thiserror::Error;
24use hdi_extensions::*;
25
26
27
28//
29// General Structs
30//
31/// A distinct state within the context of a life-cycle
32///
33/// A [`MorphAddr`] and its entry content represent a chain in the entity's life-cycle.
34///
35/// ##### Example: Basic Usage
36/// ```
37/// # use hdk::prelude::{
38///     ActionHash, TryFrom,
39/// };
40/// # use hdk_extensions::{
41///     Entity, MorphAddr,
42/// };
43///
44/// #[derive(Clone)]
45/// struct Content {
46///     pub message: String,
47/// }
48///
49/// let identity_addr = "uhCkkrVjqWkvcFoq2Aw4LOSe6Yx9OgQLMNG-DiXqtT0nLx8uIM2j7";
50/// let revision_addr = "uhCkknDrZjzEgzf8iIQ6aEzbqEYrYBBg1pv_iTNUGAFJovhxOJqu0";
51///
52/// Entity::<Content>(
53///     MorphAddr(
54///         ActionHash::try_from(identity_addr).unwrap(),
55///         ActionHash::try_from(revision_addr).unwrap(),
56///     ),
57///     Content {
58///         message: String::from("Hello world"),
59///     }
60/// );
61/// ```
62#[derive(Clone, Serialize, Deserialize, Debug)]
63pub struct Entity<T>(
64    /// The Metamorphic address relevant to `T` (the content)
65    pub MorphAddr,
66    /// The content that belong's to the [`MorphAddr`]'s revision address
67    pub T,
68)
69where
70    T: Clone;
71
72impl<T> Entity<T>
73where
74    T: Clone,
75{
76    /// See [`MorphAddr::is_origin`]
77    pub fn identity(&self) -> &ActionHash {
78        self.0.identity()
79    }
80
81    /// See [`MorphAddr::is_origin`]
82    pub fn revision(&self) -> &ActionHash {
83        self.0.revision()
84    }
85
86    /// See [`MorphAddr::is_origin`]
87    pub fn is_origin(&self) -> bool {
88        self.0.is_origin()
89    }
90}
91
92/// An address representing a precise phase in an entities life-cycle (short for: Metamorphic
93/// Address)
94///
95/// Together the pair of identity/revision addresses act as coordinates that can be used to
96/// determine the entity's identity (identity addr) and a phase in its life-cycle (revision addr).
97///
98/// ##### Example: Basic Usage
99/// ```
100/// # use hdk::prelude::{
101///     ActionHash, TryFrom,
102/// };
103/// # use hdk_extensions::{
104///     Entity, MorphAddr,
105/// };
106///
107/// let identity_addr = "uhCkkrVjqWkvcFoq2Aw4LOSe6Yx9OgQLMNG-DiXqtT0nLx8uIM2j7";
108/// let revision_addr = "uhCkknDrZjzEgzf8iIQ6aEzbqEYrYBBg1pv_iTNUGAFJovhxOJqu0";
109///
110/// MorphAddr(
111///     ActionHash::try_from(identity_addr).unwrap(),
112///     ActionHash::try_from(revision_addr).unwrap(),
113/// );
114/// ```
115#[derive(Clone, Serialize, Deserialize, Debug)]
116pub struct MorphAddr(
117    /// The create action of the entities life-cycle
118    pub ActionHash,
119    /// Any entry creation action in the entity's life-cycle
120    pub ActionHash,
121);
122
123impl MorphAddr {
124    /// A reference to the tuple's index 0
125    pub fn identity(&self) -> &ActionHash {
126        &self.0
127    }
128
129    /// A reference to the tuple's index 1
130    pub fn revision(&self) -> &ActionHash {
131        &self.1
132    }
133
134    /// This is an origin metamorphic address if the identity and revision are the same
135    pub fn is_origin(&self) -> bool {
136        self.0 == self.1
137    }
138}
139
140
141
142//
143// Custom Errors
144//
145#[derive(Debug, Error)]
146pub enum HdkExtError<'a> {
147    #[error("Record not found @ address {0}")]
148    RecordNotFound(&'a AnyDhtHash),
149    #[error("No entry in record ({0})")]
150    RecordHasNoEntry(&'a ActionHash),
151    #[error("Expected an action hash, not an entry hash: {0}")]
152    ExpectedRecordNotEntry(&'a ActionHash),
153}
154
155impl<'a> From<HdkExtError<'a>> for WasmError {
156    fn from(error: HdkExtError) -> Self {
157        wasm_error!(WasmErrorInner::Guest( format!("{}", error ) ))
158    }
159}
160
161
162
163//
164// Agent
165//
166/// Get this Agent's initial pubkey from zome info
167pub fn agent_id() -> ExternResult<AgentPubKey> {
168    Ok( agent_info()?.agent_initial_pubkey )
169}
170
171
172
173//
174// Get Helpers
175//
176/// Get a [`Record`] or return a "not found" error
177///
178/// The difference between this `must_get` and `hdk`'s `get` is that this one replaces a `None` response
179/// with [`HdkExtError::RecordNotFound`] so that an ok result will always be a [`Record`].
180///
181/// **NOTE:** Not to be confused with the `hdi`'s meaning of 'must'.  This 'must' will not retrieve
182/// deleted records.
183pub fn must_get<T>(addr: &T) -> ExternResult<Record>
184where
185    T: Clone + std::fmt::Debug,
186    AnyDhtHash: From<T>,
187{
188    Ok(
189        get( addr.to_owned(), GetOptions::network() )?
190            .ok_or(HdkExtError::RecordNotFound(&addr.to_owned().into()))?
191    )
192}
193
194
195/// Get the [`RecordDetails`] for a given [`ActionHash`]
196///
197/// This method provides a more deterministic result by unwrapping the [`get_details`] result.
198pub fn must_get_record_details(action: &ActionHash) -> ExternResult<RecordDetails> {
199    let details = get_details( action.to_owned(), GetOptions::network() )?
200        .ok_or(HdkExtError::RecordNotFound(&action.to_owned().into()))?;
201
202    match details {
203        Details::Record(record_details) => Ok( record_details ),
204        Details::Entry(_) => Err(HdkExtError::ExpectedRecordNotEntry(action))?,
205    }
206}
207
208
209/// Check if a DHT address can be fetched
210pub fn exists<T>(addr: &T) -> ExternResult<bool>
211where
212    T: Clone + std::fmt::Debug,
213    AnyDhtHash: From<T>,
214{
215    debug!("Checking if address {:?} exists", addr );
216    Ok(
217        match AnyDhtHash::from(addr.to_owned()).into_primitive() {
218            AnyDhtHashPrimitive::Action(addr) => summon_action( &addr ).is_ok(),
219            AnyDhtHashPrimitive::Entry(addr) => summon_entry( &addr ).is_ok(),
220        }
221    )
222}
223
224
225/// Check if a DHT address can be fetched and is not deleted
226pub fn available<T>(addr: &T) -> ExternResult<bool>
227where
228    T: Clone + std::fmt::Debug,
229    AnyDhtHash: From<T>,
230{
231    debug!("Checking if address {:?} is available", addr );
232    Ok( get( addr.to_owned(), GetOptions::network() )?.is_some() )
233}
234
235
236
237//
238// Tracing Actions
239//
240/// Resolve an [`AnyLinkableHash`] into an [`ActionHash`]
241///
242/// If the linkable's primitive is a
243/// - `Action` - the action hash is simply returned
244/// - `Entry` - the action hash is pulled from the result of a `get`
245/// - `External` - results in an error
246pub fn resolve_action_addr<T>(addr: &T) -> ExternResult<ActionHash>
247where
248    T: Into<AnyLinkableHash> + Clone,
249{
250    let addr : AnyLinkableHash = addr.to_owned().into();
251    match addr.into_primitive() {
252        AnyLinkableHashPrimitive::Entry(entry_hash) => {
253            Ok(
254                must_get( &entry_hash )?.action_address().to_owned()
255            )
256        },
257        AnyLinkableHashPrimitive::Action(action_hash) => Ok( action_hash ),
258        AnyLinkableHashPrimitive::External(external_hash) => Err(guest_error!(
259            format!("External hash ({}) will not have a corresponding action", external_hash )
260        )),
261    }
262}
263
264
265/// Collect the chain of evolutions forward
266///
267/// When there are multiple updates the lowest action's timestamp is selected.
268///
269/// The first item of the returned [`Vec`] will always be the given [`ActionHash`]
270pub fn follow_evolutions(action_address: &ActionHash) -> ExternResult<Vec<ActionHash>> {
271    let mut evolutions = vec![];
272    let mut next_addr = Some(action_address.to_owned());
273
274    while let Some(addr) = next_addr {
275        let details = must_get_record_details( &addr )?;
276        let maybe_next_update = details.updates.iter()
277            .min_by_key(|sa| sa.action().timestamp() );
278
279        next_addr = match maybe_next_update {
280            Some(signed_action) => Some(signed_action.hashed.hash.to_owned()),
281            None => None,
282        };
283
284        evolutions.push( addr );
285    }
286
287    Ok( evolutions )
288}
289
290
291/// Collect the chain of evolutions forward filtering updates
292pub fn follow_evolutions_selector<F>(
293    action_address: &ActionHash,
294    selector: F
295) -> ExternResult<Vec<ActionHash>>
296where
297    F: Fn(Vec<SignedHashed<Action>>) -> ExternResult<Option<ActionHash>>,
298{
299    let mut evolutions = vec![];
300    let mut next_addr = Some(action_address.to_owned());
301
302    while let Some(addr) = next_addr {
303        let details = must_get_record_details( &addr )?;
304        next_addr = selector( details.updates )?;
305
306        evolutions.push( addr );
307    }
308
309    Ok( evolutions )
310}
311
312
313/// Collect the chain of evolutions forward filtering authorized updates
314pub fn follow_evolutions_using_authorities(
315    action_address: &ActionHash,
316    authors: &Vec<AgentPubKey>
317) -> ExternResult<Vec<ActionHash>> {
318    let evolutions = follow_evolutions_selector( action_address, |updates| {
319        let updates_count = updates.len();
320        let valid_updates : Vec<SignedHashed<Action>> = updates
321            .into_iter()
322            .filter(|sa| {
323                debug!(
324                    "Checking authorities for author '{}': {:?}",
325                    sa.action().author(),
326                    authors
327                );
328                authors.contains( sa.action().author() )
329            })
330            .collect();
331
332        debug!(
333            "Filtered {}/{} updates",
334            updates_count - valid_updates.len(),
335            updates_count
336        );
337        let maybe_next_update = valid_updates.iter()
338            .min_by_key(|sa| sa.action().timestamp() );
339
340        Ok(
341            match maybe_next_update {
342                Some(signed_action) => Some(signed_action.hashed.hash.to_owned()),
343                None => None,
344            }
345        )
346    })?;
347
348    Ok( evolutions )
349}
350
351
352/// Collect the chain of evolutions forward filtering authorized updates with exceptions
353pub fn follow_evolutions_using_authorities_with_exceptions(
354    action_address: &ActionHash,
355    authors: &Vec<AgentPubKey>,
356    exceptions: &Vec<ActionHash>
357) -> ExternResult<Vec<ActionHash>> {
358    let evolutions = follow_evolutions_selector( action_address, |updates| {
359        let updates_count = updates.len();
360        let valid_updates : Vec<SignedHashed<Action>> = updates
361            .into_iter()
362            .filter(|sa| {
363                debug!(
364                    "Checking authorities for author '{}' or an action exception '{}'",
365                    sa.action().author(),
366                    sa.action_address()
367                );
368                authors.contains( sa.action().author() ) || exceptions.contains( sa.action_address() )
369            })
370            .collect();
371
372        debug!(
373            "Filtered {}/{} updates",
374            updates_count - valid_updates.len(),
375            updates_count
376        );
377        let maybe_next_update = valid_updates.iter()
378            .min_by_key(|sa| sa.action().timestamp() );
379
380        Ok(
381            match maybe_next_update {
382                Some(signed_action) => Some(signed_action.hashed.hash.to_owned()),
383                None => None,
384            }
385        )
386    })?;
387
388    Ok( evolutions )
389}
390
391
392/// Indicates the update filtering pattern when following content evolution
393#[derive(Clone, Serialize, Debug)]
394#[serde(untagged)]
395pub enum EvolutionFilteringStrategy {
396    /// Variant used for [`follow_evolutions`]
397    Unfiltered,
398    /// Variant used for [`follow_evolutions_using_authorities`]
399    AuthoritiesFilter(Vec<AgentPubKey>),
400    /// Variant used for [`follow_evolutions_using_authorities_with_exceptions`]
401    AuthoritiesExceptionsFilter(Vec<AgentPubKey>, Vec<ActionHash>),
402    /// Not used yet
403    ExceptionsFilter(Vec<ActionHash>),
404}
405
406impl Default for EvolutionFilteringStrategy {
407    fn default() -> Self {
408        EvolutionFilteringStrategy::Unfiltered
409    }
410}
411
412impl<'de> serde::Deserialize<'de> for EvolutionFilteringStrategy {
413    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
414    where
415        D: serde::Deserializer<'de>,
416    {
417        let buffer : FollowEvolutionsInputBuffer = Deserialize::deserialize(deserializer)?;
418
419        Ok( buffer.into() )
420    }
421}
422
423/// A deserializing buffer for [`EvolutionFilteringStrategy`]
424#[derive(Clone, Serialize, Deserialize, Debug)]
425pub struct FollowEvolutionsInputBuffer {
426    pub authors: Option<Vec<AgentPubKey>>,
427    pub exceptions: Option<Vec<ActionHash>>,
428}
429
430impl From<FollowEvolutionsInputBuffer> for EvolutionFilteringStrategy {
431    fn from(buffer: FollowEvolutionsInputBuffer) -> Self {
432        match (buffer.authors, buffer.exceptions) {
433            (None, None) => Self::Unfiltered,
434            (Some(authors), None) => Self::AuthoritiesFilter(authors),
435            (None, Some(exceptions)) => Self::ExceptionsFilter(exceptions),
436            (Some(authors), Some(exceptions)) => Self::AuthoritiesExceptionsFilter(authors, exceptions),
437        }
438    }
439}
440
441
442//
443// Standard Inputs
444//
445/// Input required for calling the [`follow_evolutions`] method
446#[derive(Clone, Serialize, Deserialize, Debug)]
447pub struct GetEntityInput {
448    pub id: ActionHash,
449    #[serde(default)]
450    pub follow_strategy: EvolutionFilteringStrategy,
451}
452
453/// Input required for calling the [`hdk::prelude::update_entry`] method
454#[derive(Clone, Serialize, Deserialize, Debug)]
455pub struct UpdateEntryInput<T> {
456    pub base: ActionHash,
457    pub entry: T,
458}
459
460/// A simpler deserializable buffer for [`GetLinksInput`]
461#[derive(Clone, Serialize, Deserialize, Debug)]
462pub struct GetLinksInputBuffer {
463    pub base: AnyLinkableHash,
464    pub target: AnyLinkableHash,
465    pub link_type: String,
466    pub tag: Option<String>,
467}
468
469/// Input required for calling the [`hdk::prelude::get_links`] method
470#[derive(Clone, Serialize, Debug)]
471pub struct GetLinksInput<T>
472where
473    T: LinkTypeFilterExt + TryFrom<String, Error = WasmError> + Clone,
474{
475    pub base: AnyLinkableHash,
476    pub target: AnyLinkableHash,
477    pub link_type_filter: LinkTypeFilter,
478    pub tag: Option<LinkTag>,
479    pub link_type: Option<T>,
480}
481
482impl<T> TryFrom<GetLinksInputBuffer> for GetLinksInput<T>
483where
484    T: LinkTypeFilterExt + TryFrom<String, Error = WasmError> + Clone,
485{
486    type Error = WasmError;
487
488    fn try_from(buffer: GetLinksInputBuffer) -> Result<Self, Self::Error> {
489        let (link_type, link_type_filter) = match buffer.link_type.as_str() {
490            ".." => ( None, (..).try_into_filter()? ),
491            name => {
492                let link_type = T::try_from( name.to_string() )?;
493                ( Some(link_type.clone()), link_type.try_into_filter()? )
494            },
495        };
496
497        Ok(Self {
498            base: buffer.base,
499            target: buffer.target,
500            tag: buffer.tag.map(|text| text.into_bytes().into() ),
501            link_type,
502            link_type_filter,
503        })
504    }
505}
506
507impl<'de,T> serde::Deserialize<'de> for GetLinksInput<T>
508where
509    T: LinkTypeFilterExt + TryFrom<String, Error = WasmError> + Clone,
510{
511    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
512    where
513        D: serde::Deserializer<'de>,
514    {
515        let buffer : GetLinksInputBuffer = Deserialize::deserialize(deserializer)?;
516        let error_msg = format!("Buffer could not be converted: {:#?}", buffer );
517
518        Ok(
519            buffer.try_into()
520                .or(Err(serde::de::Error::custom(error_msg)))?
521        )
522    }
523}