Hello World

WIP

This article is currently a work in progress and subject to frequent change.
See changelog for details.

Time & Level

Time: ~2 hours | Level: Beginner

Hello and welcome to the hello world tutorial. It's a little strange to do a hello world tutorial as the 5th tutorial, however that's because we really want to show the agent perspective of a Holochain app. This is the first time the agent will be interacting with the world.

So far all the previous tutorials have had a local perspective of a single agent. However, the real power of Holochain comes from interacting with other agents.

What will you learn

You will learn how to share data between two agents. To achieve this you will run two conductors, Alice and Bob.
Then add an entry to Alice's local chain.
Finally, retrieve that same entry from Bob's instance.

Why it matters

Holochain applications are all about creating cooperation between multiple agents. By sharing data amoung the agents you can validate each others entries.

Make your entry public

So far the only entry you have had has been private. You want your users to be able to share data then you can set the entry to public in the definition.

Open up your zomes/hello/code/src/lib.rs file.

Change the entry sharing to Sharing::Public:

    fn person_entry_def() -> ValidatingEntryType {
        entry!(
            name: "person",
            description: "Person to say hello to",
-            sharing: Sharing::Private,
+            sharing: Sharing::Public,
            validation_package: || {
                hdk::ValidationPackageDefinition::Entry
            },
            validation: | _validation_data: hdk::EntryValidationData<Person>| {
                Ok(())
            }
        )
    }

Add Bob to the test

Previously you wrote a test where Alice made a few zome calls and verified the results. Now, to test that the entries can be shared between agents running the same DNA, you can use Bob in your tests to interact with Alice.

The aim here is for Alice to create a person and then Bob retrieve that same person.

Open up your test/index.js file.

Before you get Bob to retrieve Alice's person you need Bob to be able to see the person that Alice committed. This goes back to to an idea that will come up a lot in Holochain, eventual consistency. In a nutshell an Agent that is connected to the same network as another agent will eventually come to agreement on what data exists.

To make sure this has happened add this line to the end of the scenario:

  await s.consistency();

This one line says a lot about the nature of a Holocahin application. The word await shows that we are in an asynchronous world and want to wait for consistency to be achieved. What kind of situation would lead to this line never completing? Hint: Think about networks that might not be perfect.

Get Bob to retrieve Alice's person using the same address she got when she created the entry:

  const bob_retrieve_result = await bob.call('cc_tuts', 'hello', 'retrieve_person', {'address': alice_person_address });

The result is checked and stored:

  t.ok(bob_retrieve_result.Ok);
  const bobs_person = bob_retrieve_result.Ok;

Finally a deeper check makes sure the contents of the two persons match:

  t.deepEqual(bobs_person, { "name": "Alice"});
Your test should 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, bob } = await s.players({alice: config, bob: config}, true)
  // Make a call to the `hello_holo` Zome function
  // passing no arguments.
  const result = await alice.call('cc_tuts', "hello", "hello_holo", {});
  // Make sure the result is ok.
  t.ok(result.Ok);

  // Check that the result matches what you expected.
  t.deepEqual(result, { Ok: 'Hello Holo' })
  await s.consistency()
  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"} })
  await s.consistency();
  const bob_retrieve_result = await bob.call('cc_tuts', 'hello', 'retrieve_person', {'address': alice_person_address });
  t.ok(bob_retrieve_result.Ok);
  const bobs_person = bob_retrieve_result.Ok;
  t.deepEqual(bobs_person, { "name": "Alice"});
})
orchestrator.run()

Run the test

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

nix-shell https://holochain.love

Now run the test and make sure it passes:

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

hc test

If everything went okay, then right at the end you will see:

# tests 7
# pass  7

# ok

Switch to the Holochain conductor

Now it would be cool to see this happen for real, outside of a test. Up till now you have only used hc run to run a single conductor. However, in order to have two separate conductors communicate on one machine, we need to use the holochain cli tool.
This takes a bit of setting up.

hc run vs holochain

hc and holochain are both conductors that host your apps on your users' machines. hc run is for testing and development, and holochain is for end-users. It can host multiple instances of multiple DNAs for multiple users. Normally Alice and Bob would be running instances of your app in their own conductors on their own machines. But for the purposes of this tutorial, it'll be a lot more convenient to try this on one machine, so you don't have to worry about network setup. Although you could use multiple machines if you'd like.

Before you can create the config file, you will need to generate some keys for your agents.

Use hc keygen in your nix-shell to generate a key for each agent:

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

hc keygen -n -p alice.key

This will output something similar to the following:

Generating keystore (this will take a few moments)...

Succesfully created new agent keystore.

Public address: HcScjdwyq86W3w5y3935jKTcs4x9H9Pev898Ui5J36Sr7TUzoRjMhoNb9fikqez
Keystore written to: alice.key 

You can set this file in a conductor config as keystore_file for an agent.

Take note of the Public address; you will need it later.

Now run hc keygen again but copy the key store to bob.key:

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

hc keygen -n -p bob.key

Create the conductor config file

Create a new file in the root directory of your project called conductor-config-alice.toml.

Add an agent with ID alice and name it Alice:

[[agents]]
id = 'alice'
name = 'Alice'
Now point the keystore_file at alice.key and the public_address is set to the Public address you generated before:
keystore_file = 'alice.key'
public_address = 'HcScjdwyq86W3w5y3935jKTcs4x9H9Pev898Ui5J36Sr7TUzoRjMhoNb9fikqez'

Your public address will be different to this one.

Set your agent to a test agent. This makes it load faster:

test_agent = true
Next you need your DNA's hash:

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

hc hash 

You will see something similar to this:

DNA hash: QmPMMqNsbNqf3Hbizwwi6gDKw2nnSvpJQyHLG2SMYCCU8R

Add the DNA to your config file with the hash you got above:

[[dnas]]
file = 'dist/cc_tuts.dna.json'
hash = 'QmPMMqNsbNqf3Hbizwwi6gDKw2nnSvpJQyHLG2SMYCCU8R'
id = 'hc-run-dna'

Create the test instance with the alice agent:

[[instances]]
agent = 'alice'
dna = 'hc-run-dna'
id = 'test-instance'

[instances.storage]
type = "memory"

Setup the WebSocket interface on socket 3401:

[[interfaces]]
admin = true
id = 'websocket-interface'

[[interfaces.instances]]
id = 'test-instance'

[interfaces.driver]
port = 3401
type = 'websocket'

Finally add the sim2h network connection:

[network]
type = 'sim2h'
sim2h_url = 'wss://sim2h.holochain.org:9000'

Public server

The above sim2h server is a public server. It has not been secured and should never be used in production. You can also run your own local server.

The easiest thing to do now is copy this config file and change a few lines:

cp conductor-config-alice.toml conductor-config-bob.toml

Change the names to Bob.

[[agents]]
-id = 'alice'
+ id = 'bob'
- name = 'Alice'
+ name = 'Bob'
Point to Bob's key file and use his public address from before:
- keystore_file = 'alice.key'
+ keystore_file = 'bob.key'
- public_address = 'HcScjdwyq86W3w5y3935jKTcs4x9H9Pev898Ui5J36Sr7TUzoRjMhoNb9fikqez'
+ public_address = 'HcSCj4uMm999rT4B6kfgSYx6ONfg3misvoV76JI9J57KM89ejVf4uwhm7Mm6f7i'

[[instances]]
- agent = 'alice'
+ agent = 'bob'
dna = 'hc-run-dna'
id = 'test-instance'
Change the UI websocket port to 3402:
[interfaces.driver]
- port = 3401
+ port = 3402
type = 'websocket'

Allow the UI to choose the conductor

To use two agents from the gui, you need a way to specify which conductor the user wants to use. You can do this by setting the port for the websocket connection.

Open up gui/index.html.

Add a text box and button in the UI to set the port:

    <input type="text" id="port" placeholder="Set websocket port" />
    <button onclick="update_port()" type="button">update port</button>
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>
    <input type="text" id="port" placeholder="Set websocket port" />
    <button onclick="update_port()" type="button">update port</button>
    <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>

Now open gui/hello.js.

Add a update_port function that resets the connection to the new port:

function update_port() {
  const port = document.getElementById('port').value;
  holochain_connection = holochainclient.connect({
    url: 'ws://localhost:' + port,
  });
}

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;
}
function update_port() {
  const port = document.getElementById('port').value;
  holochain_connection = holochainclient.connect({
    url: 'ws://localhost:' + port,
  });
}

Run the app and two UIs

Now the fun part, where you get to play with what you just wrote.
You going to need a few terminals to do this.

Terminal one

Only for local:

Only do this if you are running a local copy of sim2h server.
Otherwise skip this step.

Run the sim2h server

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

sim2h_server -p 9001

Terminal two

Start by running the conductor. It's a bit different this time; instead of hc run you will use holochain directly:

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

holochain -c conductor-config-alice.toml

Terminal three

Start the second conductor:

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

holochain -c conductor-config-bob.toml

Terminal four

Go to the root folder of your GUI:

Run the first UI on port 8001:

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

python -m SimpleHTTPServer 8001

Terminal five

Still in the root folder of your GUI:

Run the second UI on port 8002:

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

python -m SimpleHTTPServer 8002

Open up the browser

Open two tabs.

Tab Alice

Go to 0.0.0.0:8001.

Tab Bob

Go to 0.0.0.0:8002.
Enter 3402 into the port text box and click update port.

Update the port to 3401

Tab Alice

Create a person entry with your name:

Enter your name into create person

Tab Bob

Copy the address from the Alice tab and retrieve the person entry:

Retrieve Alice's person from Bob's conductor

Hooray! Alice and Bob are now able to share data on the DHT

Key takeaways

  • Entries need to be explicitly marked public or they will only be commited to an agents local chain.
  • A public entry will be passed to other agents via gossip, validated and held.
  • Another agent can retieve your public entries.

Learn more

Was this helpful?