Recent Changes for hApp Devs

Here are some recent changes that will improve hApp development. Even though they may create some friction at first if you have to change already existing code, they are all steps in the right direction in the long run.

HDK

In the recent weeks, there have been a lot of breaking changes happening in the HDK. This is mainly an effort on the part of the holochain development team to introduce as many features and change possible before the release cycle for the HDK and holochain itself is stablished, and with it we arrive to a stabilization of the HDK API.

hdk3 is now hdk

The crate hdk3 is now renamed as hdk. This means that ALL ZOMES WRITTEN BEFORE THIS CHANGE WILL BREAK, but fortunately the changes are really easy to do with a find and replace all:

Before:

hdk3 = {git = "https://github.com/holochain/holochain.git", rev = "5f1d6f4", package = "hdk3"}
use hdk3::prelude::*;

After:

hdk = {git = "https://github.com/holochain/holochain.git", rev = "5f1d6f4", package = "hdk"}
use hdk::prelude::*;

Serialization

  • The macro SerializedBytes is no longer necessary with input/output structs, but now Debug is

You’ll need to add the Debug macro in all input/output structs, and the SerializedBytes is optional.

  • Any rust types that implement Debug can be returned

You can now freely use Vecs, Options, Strings as input or output types for your functions.

  • Better output on serialization errors

We now get useful messages whenever a serialization error occurs instead of the usual Unknown error.

  • ElementVec has been removed

You can simply replace the ElementVec with Vec<Element>:

let query_result: Vec<Element> = query(header_filter)?;

Example

Before the serialization changes:

#[derive(Clone, Serialize, Deserialize, SerializedBytes)]
pub struct SearchProfilesInput {
    nickname_prefix: String,
}
#[derive(Clone, Serialize, Deserialize, SerializedBytes)]
pub struct GetProfilesOutput(Vec<AgentProfile>);
#[hdk_extern]
pub fn search_profiles(
    search_profiles_input: SearchProfilesInput,
) -> ExternResult<GetProfilesOutput> {
    let agent_profiles = profile::search_profiles(search_profiles_input.nickname_prefix)?;

    Ok(GetProfilesOutput(agent_profiles))
}

After the serialization changes:

#[derive(Clone, Serialize, Deserialize, Debug)]
pub struct SearchProfilesInput {
    nickname_prefix: String,
}
#[hdk_extern]
pub fn search_profiles(
    search_profiles_input: SearchProfilesInput,
) -> ExternResult<Vec<AgentProfile>> {
    let agent_profiles = profile::search_profiles(search_profiles_input.nickname_prefix)?;

    Ok(agent_profiles)
}

ExternResult has now WasmError as its result: one less nested struct

Before, if I wanted to return a string error, I had to return all these nested structs:

return Err(HdkError::Wasm(WasmError::Zome("Error Message".into())));

Now, HdkError no longer exists: now you can simply return a WasmError::Guest:

return Err(WasmError::Guest("Error Message".into()));

entry_def_index! makes query easier

If you’ve written a query function call in RSM, you know it can get tedious, because of the entry definition index. Now with this new macro, we can just write query like this:

fn query_all_offers() -> ExternResult<Vec<Element>> {
    let offer_entry_type = EntryType::App(AppEntryType::new(
        entry_def_index!(Offer),
        zome_info()?.zome_id,
        EntryVisibility::Private,
    ));
    let filter = ChainQueryFilter::new()
        .entry_type(offer_entry_type)
        .include_entries(true);
    let query_result = query(filter)?;

    Ok(query_result.0)
}

EntryDefRegistration trait

There is now the EntryDefRegistration trait that is automatically generated whenever we use #[hdk_entry()] to define an entry. This means that we can write a generic function that takes trait objects for that trait, and call the necessary methods on that:

#[hdk_entry(id = "profile")]
struct Post(String);

fn query_and_convert_entries<T: EntryDefRegistration>() -> ExternResult<Vec<T>> {
    let entry_def_id = T::entry_def_id();
  
    let filter = ChainQueryFilter::new()
        .entry_type(EntryType::App(AppEntryType::new(
            entry_def_id,
            zome_info!()?.zome_id,
            EntryVisibility::Private,
        )))
        .header_type(HeaderType::Create)
        .include_entries(true);
    let query_result: ElementVec = query!(filter)?;
    Ok(query_result.0)
}

Better support for timestamps

Timestamps have been improved by a big margin: they are now easily convertible to and from chronos types, and also you can do easy math on them.

To see all changes and new goodies, check here.

sign_raw and verify_signature_raw

The recently introduced sign and verify_signature have been joined by sign_raw and verify_signature_raw:

  • Raw versions directly accept an array of bytes as its input
  • “Normal” versions serializes the input struct before signing the bytes

Debugging

There is a bunch of changes in the way that tracing is done at the conductor/wasm interface level. The biggest win we have as holochain devs is the ability to add the WASM_LOG to our tests and runs to control the output that we get. This allows us to turn off the logs that holochain itself outputs, and just concentrate on the logs that our application is producing.

The WASM_LOG variable can take the same values as the RUST_LOG one: error, info, debug and trace. The two variables can also be combined together to control the output of holochain.

Example use when running holochain:

WASM_LOG=debug holochain

Example use with the package.json of tryorama:

  "scripts": {
    "test": "WASM_LOG=debug RUST_LOG=error RUST_BACKTRACE=1 TRYORAMA_HOLOCHAIN_PATH=\"holochain\" ts-node src/index.ts"
  },

With this, we now also get different levels of debugging output enabled in our zomes:

#[hdk_extern]
fn debug(_: ()) -> ExternResult<()> {
    trace!("tracing {}", "works!");
    debug!("debug works");
    info!("info works");
    warn!("warn works");
    error!("error works");
    debug!(foo = "fields", bar = "work", "too");

    Ok(())
}
Written by Guillem Córdoba on February 13, 2021