Hello Me

Time & Level

Time: ~4 hours | Level: Beginner

Welcome back to another tutorial in the Core Concepts series.

The app we have built so far returns a constant value however for more complex applications it would be useful to be able to store some data.

This tutorial builds on the previous tutorial so go back and complete that if you haven't already.

What will you learn

Learn how to add an entry type to your zome. An entry is a piece of data in your source chain that has been validated. How to define and validate an entry type that represents a person. Then how to create and read this data through zome calls. You will also setup tests and your GUI.

Why it matters

Storing data is at the core of Holochain. The most valuable job Holochain does is ensuring agents handle and store data according to the rules of you application.

Test first

Start by writing a test so it's easy to see when your app is working:

Open up cc_tuts/test/index.js.

This is how we left the testing scenario in the Hello Test tutorial:

orchestrator.registerScenario("Test hello holo", async (s, t) => {
  const { alice } = await s.players({alice: config}, true)

  const result = await alice.call('cc_tuts', "hello", "hello_holo", {});
  t.ok(result.Ok);
  t.deepEqual(result, { Ok: 'Hello Holo' })

  // <---- Put your new tests here
})
The new tests go below t.deepEqual(result, { Ok: 'Hello Holo' }).

The following test will create an entry with the name "Alice", retrieve the same entry and check that it has the name "Alice".

Add a call to the create_person function with a person whose name is Alice:

  const create_result = await alice.call('cc_tuts', "hello", "create_person", {"person": { "name" : "Alice" }});

Check that the result of the call is Ok:

  t.ok(create_result.Ok);
  const alice_person_address = create_result.Ok;

Tell the test to wait for the dht to become consistent.

  await s.consistency()

Add a call to the retrieve_person function with the address from the last call:

  const retrieve_result = await alice.call('cc_tuts', "hello", "retrieve_person", {"address": alice_person_address });

Check that this call is Ok as well:

  t.ok(retrieve_result.Ok);
This is the actual result we want at the end of the test. Check that the entry at the address is indeed named Alice:

  t.deepEqual(retrieve_result, { Ok: {"name": "Alice"} })

Running the test

Your test should now look like this:

Check your code
const path = require('path')
const tape = require('tape')

const { Config, Orchestrator, tapeExecutor, singleConductor, combine, callSync } = require('@holochain/try-o-rama')

process.on('unhandledRejection', error => {
  // Will print "unhandledRejection err is not defined"
  console.error('got unhandledRejection:', error);
});

const orchestrator = new Orchestrator({
  globalConfig: {logger: false,  
    network: {
      type: 'sim2h',
      sim2h_url: 'wss://0.0.0.0:9001',
    }
  },
  middleware: combine(singleConductor, tapeExecutor(tape))
})

const config = {
  instances: {
    cc_tuts: Config.dna('dist/cc_tuts.dna.json', 'cc_tuts')
  }
}
orchestrator.registerScenario("Test hello holo", async (s, t) => {
  const { alice } = await s.players({alice: config}, true)

  const result = await alice.call('cc_tuts', "hello", "hello_holo", {});
  t.ok(result.Ok);
  t.deepEqual(result, { Ok: 'Hello Holo' })

  const create_result = await alice.call('cc_tuts', "hello", "create_person", {"person": { "name" : "Alice" }});
  t.ok(create_result.Ok);
  const alice_person_address = create_result.Ok;

  await s.consistency()

  const retrieve_result = await alice.call('cc_tuts', "hello", "retrieve_person", {"address": alice_person_address });
  t.ok(retrieve_result.Ok);
  t.deepEqual(retrieve_result, { Ok: {"name": "Alice"} })

})

orchestrator.run()

Obviously these tests will fail right now. Can you guess what the first failure will be? Let's have a look.

Enter the nix-shell if you don't have it open already:

nix-shell https://holochain.love

Run the test:

Run in nix-shell https://holochain.love

hc test

The test fails on the create_person function because it doesn't exist yet:

"Holochain Instance Error: Zome function 'create_person' not found in Zome 'hello'"

Note that this test might actually get stuck because we haven't put in the required functions yet. Press ctrl-c to exit a stuck test.

Add the entry

Open up your zomes/hello/code/src/lib.rs file.
To add an entry into your source chain start by telling Holochain what kinds of entry exist.
First we'll create a struct to define the shape of the data.

In a moment we will add a Person struct, but this is where to put it:

// <---- Add the person struct here.

#[zome]
mod hello_zome {

Add the following lines.

Allow this struct to be easily converted to and from JSON:

#[derive(Serialize, Deserialize, Debug, DefaultJson, Clone)]

Represent a person as a struct:

pub struct Person {

Represent their name as a String:

    name: String,
}

Look for the following lines inside the hello_zome mod.

#[zome]
mod hello_zome {
  /* --- Lines omitted -- */
  #[zome_fn("hc_public")]
  fn hello_holo() -> ZomeApiResult<String> {
      Ok("Hello Holo".into())
  }

  // <---- Add the following lines here.

Add the person_entry_def function, which tells Holochain about the person entry type:

    #[entry_def]
    fn person_entry_def() -> ValidatingEntryType {

Add the entry! macro that lets you easily create a ValidatingEntryType:

        entry!(

Give it the same name as the Person struct, just to be consistent. Entry types are usually in lowercase.

Add the name and description of the entry:

            name: "person",
            description: "Person to say hello to",

Entries of this type are just for this agent's eyes only, so set the entry sharing to private:

            sharing: Sharing::Private,

Add the validation_package function that says what is needed to validate this entry:

            validation_package: || {
                hdk::ValidationPackageDefinition::Entry
            },

Add the validation function that validates this entry.

It returns that this entry is always Ok as long as it fits the shape of the Person struct:

            validation: | _validation_data: hdk::EntryValidationData<Person>| {
                Ok(())
            }
        )
    }

Now you can create actual person entries and store them on your source chain.

A note on validation: Validation is very important. It is the "rules of the game" for your Holochain app. It is meaningful to emphasize that although we are returning Ok(()) that we are still validating that the data type checks as a Person with a name property containing a String. Essentially this rule says the person entry must be in this format.

Add some use statements

In the above code we have used a few types and macros that are not mentioned anywhere else. So the Rust compiler doesn't know where to find them yet.

Add the following use statements:

#![feature(proc_macro_hygiene)]
+#[macro_use]
extern crate hdk;
extern crate hdk_proc_macros;
extern crate serde;
#[macro_use]
extern crate serde_derive;
extern crate serde_json;
+#[macro_use]
extern crate holochain_json_derive;

use hdk::{
+    entry_definition::ValidatingEntryType,
    error::ZomeApiResult,
};

+use hdk::holochain_core_types::{
+    entry::Entry,
+    dna::entry_types::Sharing,
+};
+
+use hdk::holochain_json_api::{
+    json::JsonString,
+    error::JsonError,
+};
+
+  use hdk::holochain_persistence_api::{
+    cas::content::Address
+};

use hdk_proc_macros::zome;
Check your code
#![feature(proc_macro_hygiene)]
#[macro_use]
extern crate hdk;
extern crate hdk_proc_macros;
extern crate serde;
#[macro_use]
extern crate serde_derive;
extern crate serde_json;
#[macro_use]
extern crate holochain_json_derive;

use hdk::{
    entry_definition::ValidatingEntryType,
    error::ZomeApiResult,
};

use hdk::holochain_core_types::{
    entry::Entry,
    dna::entry_types::Sharing,
};

use hdk::holochain_json_api::{
    json::JsonString,
    error::JsonError,
};

use hdk::holochain_persistence_api::{
    cas::content::Address
};

use hdk_proc_macros::zome;
#[derive(Serialize, Deserialize, Debug, DefaultJson, Clone)]
pub struct Person {
    name: String,
}
#[zome]
mod hello_zome {
    #[init]
    fn init() {
        Ok(())
    }

    #[validate_agent]
    pub fn validate_agent(validation_data: EntryValidationData<AgentId>) {
        Ok(())
    }
  /* --- Lines omitted -- */
  #[zome_fn("hc_public")]
  fn hello_holo() -> ZomeApiResult<String> {
      Ok("Hello Holo".into())
  }

  // <---- Add the following lines here.
    #[entry_def]
    fn person_entry_def() -> ValidatingEntryType {
        entry!(
            name: "person",
            description: "Person to say hello to",
            sharing: Sharing::Private,
            validation_package: || {
                hdk::ValidationPackageDefinition::Entry
            },
            validation: | _validation_data: hdk::EntryValidationData<Person>| {
                Ok(())
            }
        )
    }
}

Create a person

Now you need a way for your UI to actually create a person entry. Holochain has a concept called hc_public which is a way of telling the runtime make this function available to call from outside this zome.

Add the following lines below the previous person_entry_def function.

Add a public function that takes a Person and returns a result with an Address:

    #[zome_fn("hc_public")]
    pub fn create_person(person: Person) -> ZomeApiResult<Address> {

Create an entry from the person argument:

        let entry = Entry::App("person".into(), person.into());

Commit the entry to your local source chain:

        let address = hdk::commit_entry(&entry)?;

Return the Ok result with the new person entry's address:

        Ok(address)
    }

Compile

Check your code
#![feature(proc_macro_hygiene)]
#[macro_use]
extern crate hdk;
extern crate hdk_proc_macros;
extern crate serde;
#[macro_use]
extern crate serde_derive;
extern crate serde_json;
#[macro_use]
extern crate holochain_json_derive;

use hdk::{
    entry_definition::ValidatingEntryType,
    error::ZomeApiResult,
};

use hdk::holochain_core_types::{
    entry::Entry,
    dna::entry_types::Sharing,
};

use hdk::holochain_json_api::{
    json::JsonString,
    error::JsonError,
};

use hdk::holochain_persistence_api::{
    cas::content::Address
};

use hdk_proc_macros::zome;
#[derive(Serialize, Deserialize, Debug, DefaultJson, Clone)]
pub struct Person {
    name: String,
}
#[zome]
mod hello_zome {
    #[init]
    fn init() {
        Ok(())
    }

    #[validate_agent]
    pub fn validate_agent(validation_data: EntryValidationData<AgentId>) {
        Ok(())
    }
  /* --- Lines omitted -- */
  #[zome_fn("hc_public")]
  fn hello_holo() -> ZomeApiResult<String> {
      Ok("Hello Holo".into())
  }

  // <---- Add the following lines here.
    #[entry_def]
    fn person_entry_def() -> ValidatingEntryType {
        entry!(
            name: "person",
            description: "Person to say hello to",
            sharing: Sharing::Private,
            validation_package: || {
                hdk::ValidationPackageDefinition::Entry
            },
            validation: | _validation_data: hdk::EntryValidationData<Person>| {
                Ok(())
            }
        )
    }
    #[zome_fn("hc_public")]
    pub fn create_person(person: Person) -> ZomeApiResult<Address> {
        let entry = Entry::App("person".into(), person.into());
        let address = hdk::commit_entry(&entry)?;
        Ok(address)
    }
}

Check for compile errors again:

Run in nix-shell https://holochain.love

hc package

Retrieve person

Lastly you need a way for your UI to get a person entry back from the source chain.

Add the following lines below the create_person function.

Add a public retrieve_person function that takes an Address and returns a Person:

    #[zome_fn("hc_public")]
    fn retrieve_person(address: Address) -> ZomeApiResult<Person> {

Get the entry from your local storage, asking for it by address, and convert it to a Person type:

        hdk::utils::get_as_type(address)
    }

In Rust the last line is always returned. You do not need to explicitly say return. Just leave off the ;.

Test

Check your code
#![feature(proc_macro_hygiene)]
#[macro_use]
extern crate hdk;
extern crate hdk_proc_macros;
extern crate serde;
#[macro_use]
extern crate serde_derive;
extern crate serde_json;
#[macro_use]
extern crate holochain_json_derive;

use hdk::{
    entry_definition::ValidatingEntryType,
    error::ZomeApiResult,
};

use hdk::holochain_core_types::{
    entry::Entry,
    dna::entry_types::Sharing,
};

use hdk::holochain_json_api::{
    json::JsonString,
    error::JsonError,
};

use hdk::holochain_persistence_api::{
    cas::content::Address
};

use hdk_proc_macros::zome;
#[derive(Serialize, Deserialize, Debug, DefaultJson, Clone)]
pub struct Person {
    name: String,
}
#[zome]
mod hello_zome {
    #[init]
    fn init() {
        Ok(())
    }

    #[validate_agent]
    pub fn validate_agent(validation_data: EntryValidationData<AgentId>) {
        Ok(())
    }
  /* --- Lines omitted -- */
  #[zome_fn("hc_public")]
  fn hello_holo() -> ZomeApiResult<String> {
      Ok("Hello Holo".into())
  }

  // <---- Add the following lines here.
    #[entry_def]
    fn person_entry_def() -> ValidatingEntryType {
        entry!(
            name: "person",
            description: "Person to say hello to",
            sharing: Sharing::Private,
            validation_package: || {
                hdk::ValidationPackageDefinition::Entry
            },
            validation: | _validation_data: hdk::EntryValidationData<Person>| {
                Ok(())
            }
        )
    }
    #[zome_fn("hc_public")]
    pub fn create_person(person: Person) -> ZomeApiResult<Address> {
        let entry = Entry::App("person".into(), person.into());
        let address = hdk::commit_entry(&entry)?;
        Ok(address)
    }
    #[zome_fn("hc_public")]
    fn retrieve_person(address: Address) -> ZomeApiResult<Person> {
        hdk::utils::get_as_type(address)
    }
}

Instead of directly compiling, you can run the test you wrote at the start (the test always compiles before it runs):

Run in nix-shell https://holochain.love

hc test

If everything went smoothly you will see:

# tests 5
# pass  5

# ok

UI

Now that the backend is working you can modify the UI to interact with zome functions you created. First let's do some housekeeping and move the JavaScript from the previous tutorial into its own file.

Go to the GUI project folder that you created in the Hello GUI tutorial:

cd holochain/coreconcepts/gui

Create a new hello.js file, open it in your favorite editor, and open the index.html alongside it.

Move the everything inside the <script> tag into the hello.js:

--- index.html
<script type="text/javascript">
-    var holochain_connection = holochainclient.connect({ url: "ws://localhost:3401"});
-    
-    function hello() {
-      holochain_connection.then(({callZome, close}) => {
-        callZome('test-instance', 'hello', 'hello_holo')({"args": {} }).then((result) => update_span(result))
-      })
-    }
-    function show_output(result) {
-      var span = document.getElementById('output');
-      var output = JSON.parse(result);
-      span.textContent = " " + output.Ok;
-    }
</script>

+++ hello.js
+var holochain_connection = holochainclient.connect({ url: "ws://localhost:3401"});
+
+function hello() {
+  holochain_connection.then(({callZome, close}) => {
+    callZome('test-instance', 'hello', 'hello_holo')({"args": {} }).then((result) => update_span(result))
+  })
+}
+
+function show_output(result) {
+  var span = document.getElementById('output');
+  var output = JSON.parse(result);
+  span.textContent = " " + output.Ok;
+}

Add the src attribute to the <script> tag:

<script type="text/javascript" src="hello.js"></script>

Create person UI widget

In your index.html start by adding the HTML elements to create a person.

Look for the previous 'say hello' elements.

    <button onclick="hello()" type="button">Say Hello</button>
    <div>Response:</span><span id="output"></div>
    <!-- Put the following lines here -->

Below them, give the section a heading:

    <h3>Create a person</h3>

Add a text box so the user can enter their name:

    <input type="text" id="name" placeholder="Enter your name :)">

Add a button that calls a (yet to be written) JavaScript function called create_person:

    <button onclick="create_person()" type="button">Submit Name</button>

Add a span with the id address_output so you can render the result of this call:

    <div>Address: <span id="address_output"></span></div>
Check your code
<!DOCTYPE html>

<html lang="en">
  <head>
    <meta charset="utf-8" />

    <title>Hello GUI</title>
    <meta name="description" content="GUI for a Holochain app" />
    <link
      rel="stylesheet"
      href="https://cdn.jsdelivr.net/gh/kognise/water.css@latest/dist/dark.min.css"
    />
  </head>

  <body>
    <button onclick="hello()" type="button">Say Hello</button>
    <div>Response: <span id="output"></span></div>
    <h3>Create a person</h3>
    <input type="text" id="name" placeholder="Enter your name :)">
    <button onclick="create_person()" type="button">Submit Name</button>
    <div>Address: <span id="address_output"></span></div>
    <script
      type="text/javascript"
      src="hc-web-client/hc-web-client-0.5.1.browser.min.js"
    ></script>
    <script type="text/javascript" src="hello.js"></script>
  </body>
</html>

Switch to your hello.js file

Let's write the create_person function that will call your zome.

Add the create_person function:

function create_person() {

Get the text box by its ID name and save the current text value into the name variable:

  const name = document.getElementById('name').value;

Wait for the connection and then make a zome call:

  holochain_connection.then(({callZome, close}) => {

Call create_person in your hello zome and pass in the name variable as part of a person structure, then write the result to the console:

    callZome('test-instance', 'hello', 'create_person')({
      person: {name: name},
    }).then(result => console.log(result));
  });
}
Check your code
// Connect
var holochain_connection = holochainclient.connect({
  url: 'ws://localhost:3401',
});

// Render functions
function show_output(result, id) {
  var el = document.getElementById(id);
  var output = JSON.parse(result);
  if (output.Ok) {
    el.textContent = ' ' + output.Ok;
  } else {
    alert(output.Err.Internal);
  }
}

function show_person(result) {
  var person = document.getElementById('person_output');
  var output = JSON.parse(result);
  person.textContent = ' ' + output.Ok.name;
}

function show_posts(result) {
  var list = document.getElementById('posts_output');
  list.innerHTML = '';
  var output = JSON.parse(result);
  if (!output.Ok) {
    console.log(output);
  }
  var posts = output.Ok.sort((a, b) => a.timestamp - b.timestamp);
  for (post of posts) {
    var node = document.createElement('LI');
    var textnode = document.createTextNode(post.message);
    node.appendChild(textnode);
    list.appendChild(node);
  }
}

// Zome calls

function hello() {
  holochain_connection.then(({callZome, close}) => {
    callZome('test-instance', 'hello', 'hello_holo')({args: {}}).then(result =>
      show_output(result, 'output'),
    );
  });
}

function create_person() {
  const name = document.getElementById('name').value;
  holochain_connection.then(({callZome, close}) => {
    callZome('test-instance', 'hello', 'create_person')({
      person: {name: name},
    }).then(result => console.log(result));
  });
}

Run the server and open a browser

Go ahead and test your first call.

Open a new terminal window and enter the nix-shell:

cd holochain/coreconcepts/gui
nix-shell https://holochain.love

Run the server:

Run in nix-shell https://holochain.love

python -m SimpleHTTPServer

In your other terminal window (the one with your backend code) package and run your zome:

Run in nix-shell https://holochain.love

hc package
hc run -p 3401

Now that both your UI server and your Holochain conductor server are running, open up a browser and go to 0.0.0.0:8000. You should see the HTML elements you created:

Open the developer console, enter your name, and press the "Submit Name" button. You should something similar to this:

The address you see will probably be different, because you typed in your own name.

Show the new entry's address

Now we're going to show the address on the page rather than the developer console.

But first, a bit of refactoring. If you make the show_ouput function more generic, then you can reuse it for each element that shows the output for a zome function.

Pass in the element's ID so that the function can be reused:

-function show_output(result) {
+function show_output(result, id) {
-  var span = document.getElementById('output');
+  var el = document.getElementById(id);
  var output = JSON.parse(result);
-  span.textContent = ' ' + output.Ok;
+  el.textContent = ' ' + output.Ok;
}

function hello() {
  holochain_connection.then(({callZome, close}) => {
    callZome('test-instance', 'hello', 'hello_holo')({args: {}}).then(result =>
-      show_output(result),
+      show_output(result, id),
    );
  });
}

function create_person() {
  const name = document.getElementById('name').value;
  holochain_connection.then(({callZome, close}) => {
    callZome('test-instance', 'hello', 'create_person')({
      person: {name: name},
-    }).then(result => console.log(result));
+    }).then(result => show_output(result, 'address_output'));
  });
}

Enter the browser

Go back to your browser and refresh the page. This time when you enter your name and press Submit Name, you will see the address show up:

Retrieve a person entry and show it in the UI

Back in the index.html file now and under the create person section, add a new header:

    <h3>Retrieve Person</h3>

Add a text box so the user can enter the address that is returned from the create_person function:

    <input type="text" id="address_in" placeholder="Enter the entry address">

Add a button that calls the (yet to be written) retrieve_person JavaScript function:

    <button onclick="retrieve_person()" type="button">Get Person</button>

Add a span with the ID person_output to display the person that is returned from the retrieve_person function:

    <div>Person: <span id="person_output"></span></div>
Check your code
<!DOCTYPE html>

<html lang="en">
  <head>
    <meta charset="utf-8" />

    <title>Hello GUI</title>
    <meta name="description" content="GUI for a Holochain app" />
    <link
      rel="stylesheet"
      href="https://cdn.jsdelivr.net/gh/kognise/water.css@latest/dist/dark.min.css"
    />
  </head>

  <body>
    <button onclick="hello()" type="button">Say Hello</button>
    <div>Response: <span id="output"></span></div>
    <h3>Create a person</h3>
    <input type="text" id="name" placeholder="Enter your name :)">
    <button onclick="create_person()" type="button">Submit Name</button>
    <div>Address: <span id="address_output"></span></div>
    <h3>Retrieve Person</h3>
    <input type="text" id="address_in" placeholder="Enter the entry address">
    <button onclick="retrieve_person()" type="button">Get Person</button>
    <div>Person: <span id="person_output"></span></div>
    <script
      type="text/javascript"
      src="hc-web-client/hc-web-client-0.5.1.browser.min.js"
    ></script>
    <script type="text/javascript" src="hello.js"></script>
  </body>
</html>

Go to your hello.js file

Add the retrieve_person function to call the zome function of the same name and show its response:

function retrieve_person() {

Get the value from the address_in text box:

  var address = document.getElementById('address_in').value;

Wait for the connection and then make a zome call:

  holochain_connection.then(({callZome, close}) => {

Call the retrieve_person public zome function, passing in the address. Then pass the result to show_person:

    callZome('test-instance', 'hello', 'retrieve_person')({
      address: address,
    }).then(result => show_person(result, 'person_output'));
  });
}

Add the show_person function. It is very similar to show_output except that you need to show the name.

function show_person(result) {
  var person = document.getElementById('person_output');
  var output = JSON.parse(result);
  person.textContent = ' ' + output.Ok.name;
}
Check your code
// Connect
var holochain_connection = holochainclient.connect({
  url: 'ws://localhost:3401',
});

// Render functions
function show_output(result, id) {
  var el = document.getElementById(id);
  var output = JSON.parse(result);
  if (output.Ok) {
    el.textContent = ' ' + output.Ok;
  } else {
    alert(output.Err.Internal);
  }
}

function show_person(result) {
  var person = document.getElementById('person_output');
  var output = JSON.parse(result);
  person.textContent = ' ' + output.Ok.name;
}

function show_posts(result) {
  var list = document.getElementById('posts_output');
  list.innerHTML = '';
  var output = JSON.parse(result);
  if (!output.Ok) {
    console.log(output);
  }
  var posts = output.Ok.sort((a, b) => a.timestamp - b.timestamp);
  for (post of posts) {
    var node = document.createElement('LI');
    var textnode = document.createTextNode(post.message);
    node.appendChild(textnode);
    list.appendChild(node);
  }
}

// Zome calls

function hello() {
  holochain_connection.then(({callZome, close}) => {
    callZome('test-instance', 'hello', 'hello_holo')({args: {}}).then(result =>
      show_output(result, 'output'),
    );
  });
}

function create_person() {
  const name = document.getElementById('name').value;
  holochain_connection.then(({callZome, close}) => {
    callZome('test-instance', 'hello', 'create_person')({
      person: {name: name},
    }).then(result => show_output(result, 'address_output'));
  });
}
function retrieve_person() {
  var address = document.getElementById('address_in').value;
  holochain_connection.then(({callZome, close}) => {
    callZome('test-instance', 'hello', 'retrieve_person')({
      address: address,
    }).then(result => show_person(result, 'person_output'));
  });
}
function show_person(result) {
  var person = document.getElementById('person_output');
  var output = JSON.parse(result);
  person.textContent = ' ' + output.Ok.name;
}

Enter the browser

Finally go and test this out at 0.0.0.0:8000.
You should see somehting like this: retrieving a person

Well done! You have stored and retrieved data from a private source chain all using a GUI.

Key takeaways

  • Entrys can be defined using Rust types.
  • Entry definitions tell holochain about the data it can hold and how to validate it.
  • Once an entry is commited this can never be undone and any other agent running the same DNA will always commit an entry that has passed validation. (This means validation must be deterministic)
  • The zome returns entries in the JSON format.

Learn more

Was this helpful?