DRAFT
We will be building a complete Holochain app that includes testing and a user interface. We'll start with a simple zome, add the smallest test we can write, and call the zome from a simple web client.
Once we have something simple that operates across the full stack, we'll add more complexity in future parts of this tutorial by adding more features to the app. This will allow us to learn different aspects of Holochain as we need the functionality.
As far as what we're building β¦ I haven't decided yet. We're just going to start. Perhaps we'll identify the path we're on once we start walking.
Install the Nix package manager
Create a folder to build this tutorial in. Optionally make it a git repo.
Ensure you have Node.js and npm installed correctly by running: node -v; npm -v
Create additional folders. Make them look like this:
ββββ. # tutorial
βββββββ service
ββββ βββ tests
ββββ β βββ src
ββββ βββ workdir
ββββ βββ zomes
ββββ βββ greeter
ββββ βββ src
If it's easier, feel free to use this:
mkdir -p service/tests/src service/workdir service/zomes/greeter/src
And just for completeness, let's drop a .gitignore
file in the service
folder with these contents:
ββββ.hc # holochain temp folder?
ββββ.cargo # by-product of using Rust from a Nix shell?
ββββdebug # Rust build artifact
ββββtarget # Rust build artifact
ββββ**/*.rs.bk # rustfmt backup files
Now that we have our files and folders in place, let's finish setting up our development environment.
In order to ensure a consistent environment, the Holochain dev environment uses Nix/NixOS. At the moment, we're using project-specific environment configurations and they exist in the form of a default.nix
file.
Create a default.nix
file with these contents in your tutorial folder.
let
holonixPath = builtins.fetchTarball {
url = "https://github.com/holochain/holonix/archive/90a19d5771c069dbcc9b27938009486b54b12fb7.tar.gz";
sha256 = "11wv7mwliqj38jh1gda3gd0ad0vqz1d42hxnhjmqdp037gcd2cjg";
};
holonix = import (holonixPath) {
includeHolochainBinaries = true;
holochainVersionId = "custom";
holochainVersion = {
rev = "d3a61446acaa64b1732bc0ead5880fbc5f8e3f31";
sha256 = "0k1fsxg60spx65hhxqa99nkiz34w3qw2q4wspzik1vwpkhk4pwqv";
cargoSha256 = "0fz7ymyk7g3jk4lv1zh6gbn00ad7wsyma5r7csa88myl5xd14y68";
bins = {
holochain = "holochain";
hc = "hc";
};
};
};
in holonix.main
Alternatively, you can download this one (they're the same).
In your terminal, navigate to your tutorial folder and open the Nix shell.
nix-shell .
This might take a while, so let's continue to prepare while we wait. It's fine to open another terminal window in the same folder, open your code editor, create files, etc. while we wait.
We're going to take real small baby steps to start off. Our first zome will contain a single function, named hello
, that has no input parameters and returns a static string.
Create service/zomes/greeter/src/lib.rs
with these contents:
ββββuse hdk::prelude::*;
ββββ#[hdk_extern]
ββββpub fn hello(_: ()) -> ExternResult<String> {
ββββOk(String::from("Hello Holo Dev"))
ββββ}
The use
statement on line #1 brings in the HDK.
The hdk_extern
attribute marks the function as available to be called by the Holochain conductor.
The ExternResult
type ensures we're returning a type that can be serialized back to the user interface.
Create service/zomes/greeter/Cargo.toml
with these contents:
ββββ[package]
ββββname = "greeter"
ββββversion = "0.0.1"
ββββauthors = [ "[your name]", "[your email address]" ]
ββββedition = "2018"
ββββ[lib]
ββββname = "greeter"
ββββcrate-type = [ "cdylib", "rlib" ]
ββββ[dependencies]
ββββhdk = "0.0.100"
ββββserde = "1"
This file defines the metadata for our greeter
zome.
Create another Cargo.toml
file with these contents:
ββββ[workspace]
ββββmembers = [
ββββ "zomes/greeter",
ββββ]
ββββ[profile.dev]
ββββopt-level = "z"
ββββ[profile.release]
ββββopt-level = "z"
This file defines all of the zomes in our project.
Hopefully our Nix shell has finished building. If not, you'll have to wait before completing this step. Let's build the zome into Web Assembly (WASM). From the service
folder, inside your nix-shell, run this:
ββββCARGO_TARGET_DIR=target cargo build --release --target wasm32-unknown-unknown
If this succeeded, you won't see any errors, but you will have an service/target/wasm32-unknown-unknown/release/greeter.wasm
file.
Now let's build the DNA file
ββββhc dna init workdir/dna
When prompted, enter greeter
as the name and leave the uuid with its default value by just hitting enter.
Add the zome to the zomes
array in the newly created DNA file service/workdir/dna/dna.yaml
so it looks like this:
ββββ---
ββββmanifest_version: "1"
ββββname: greeter
ββββuuid: 00000000-0000-0000-0000-000000000000
ββββproperties: ~
ββββzomes:
ββββ - name: greeter
ββββ bundled: ../../target/wasm32-unknown-unknown/release/greeter.wasm
Package the WASM into a DNA file.
ββββhc dna pack workdir/dna
This will create service/workdir/dna/greeter.dna
. Now we're ready to do zome testing (sorry, I couldn't resist).
We have the option of writing 2 different types of tests: unit tests written in Rust, and integration tests written in TypeScript. Writing a unit test doesn't have anything to do with Holochain - we just write them as we would any Rust code. However, our integration tests use the Tryorama tool to create a mock environment. We'll forego writing unit tests for the time being and setup our integration testing environment instead.
Inside your service/tests
folder, let's create some new files.
package.json
βββ{
ββββ "name": "hello-integration-tests",
ββββ "version": "0.0.1",
ββββ "description": "An integration test runner using Tryorama",
ββββ "main": "index.js",
ββββ "scripts": {
ββββ "test": "TRYORAMA_LOG_LEVEL=info RUST_LOG=error RUST_BACKTRACE=1 TRYORAMA_HOLOCHAIN_PATH=\"holochain\" ts-node src/index.ts"
ββββ },
ββββ "author": "Keen Holo Dev",
ββββ "license": "ISC",
ββββ "dependencies": {
ββββ "@holochain/tryorama": "0.4.1",
ββββ "@msgpack/msgpack": "^2.5.1",
ββββ "@types/lodash": "^4.14.168",
ββββ "@types/node": "^14.14.37",
ββββ "lodash": "^4.17.21",
ββββ "tape": "^5.2.2",
ββββ "ts-node": "^9.1.1",
ββββ "typescript": "^4.2.3"
ββββ }
ββββ}
tsconfig.json
ββββ{
ββββ "compilerOptions": {
ββββ "target": "es5",
ββββ "module": "commonjs",
ββββ "resolveJsonModule": true,
ββββ "strict": true,
ββββ "noImplicitAny": false,
ββββ "esModuleInterop": true,
ββββ "skipLibCheck": true,
ββββ "forceConsistentCasingInFileNames": true
ββββ }
ββββ}
.gitignore
ββββnode_modules/
ββββ*.log
In preparation for our test run, from the service/tests
folder, install our dependencies.
npm install
For our test file, let's create this index.ts
file in our src
folder.
ββββimport path from "path";
ββββimport { Orchestrator, Config, InstallAgentsHapps } from "@holochain/tryorama";
ββββ// Create a configuration for our conductor
ββββconst conductorConfig = Config.gen();
ββββ// Construct proper paths for your DNAs
ββββconst dnaPath = path.join(__dirname, "../../workdir/dna/greeter.dna");
ββββ// create an InstallAgentsHapps array with your DNAs to tell tryorama what
ββββ// to install into the conductor.
ββββconst installation: InstallAgentsHapps = [
ββββ // agent 0
ββββ [
ββββ // happ 0
ββββ [dnaPath],
ββββ ],
ββββ];
ββββconst orchestrator = new Orchestrator();
ββββorchestrator.registerScenario("holo says hello", async (s, t) => {
ββββ const [alice] = await s.players([conductorConfig]);
ββββ // install your happs into the coductors and destructuring the returned happ data using the same
ββββ // array structure as you created in your installation array.
ββββ const [[alice_common]] = await alice.installAgentsHapps(installation);
ββββ let result = await alice_common.cells[0].call("greeter", "hello", null);
ββββ t.equal(result, "Hello Holo Dev");
ββββ});
ββββorchestrator.run();
Now, from inside our service/tests
folder, we can run our test with: npm test
. Everything is passing/working if the end of our output is
ββββ# tests 1
ββββ# pass 1
ββββ# ok
Now is a good time to make sure you can break the test in an expected way. For example, on line #30, change "Hello Holo Dev"
to something else and re-run the test to watch it fail.
Holochain doesn't force you to use specific technologies for the user interface. You only need to use something that can send msgpack messages to the Websocket endpoint the conductor is listening to. To make this easier, we'll use JavaScript so we can use the conductor-api package.
The user interface technology, and how to use it, is not the focus of this tutorial. So we're going to do the most basic thing we can that keeps the focus on how to interact with the Holochain conductor. To that end, we're going to use Svelte and Snowpack.
Note: this is my first time using Svelte and Snowpack. So if you see something egregious, please let me know.
Let's start in our tutorial folder (not service
) and get a basic web app in place by running this in your terminal:
ββββnpx create-snowpack-app ui --template @snowpack/app-template-minimal
We don't need Git submodules, so delete the new repo and install some necessary dependencies.
ββββcd ui
ββββrm -rf .git
ββββnpm install svelte @snowpack/plugin-svelte @holochain/conductor-api
Note: we'll stay in the ui
folder for the remainder of the UI part of the tutorial.
Tell Snowpack about Svelte by adding its plugin to the config. Make this change to line 6 of snowpack.config.js
:
ββββmodule.exports = {
ββββ mount: {
ββββ /* ... */
ββββ },
ββββ plugins: [
ββββ '@snowpack/plugin-svelte'
ββββ ],
Add an App.svelte
file:
ββββ<script>
ββββ let greeting = ''
ββββ function handleClick () {
ββββ greeting = 'Hello from Svelte'
ββββ }
ββββ</script>
ββββ<div>
ββββ <button on:click={handleClick}>Say hello</button>
ββββ <p>Greeting: {greeting}</p>
ββββ</div>
Fix up our index.js
file so it looks like this:
ββββimport App from "./App.svelte"
ββββconst app = new App({
ββββ target: document.body
ββββ})
ββββexport default app
Ensure we're working up to this point.
ββββnpm start
This should open your browser on http://localhost:8080. You should be able to see the message display when you click this button without any errors in your console.
hello
zomeBefore we can call the zome, we need to build the hApp bundle and deploy it to the local Holochain.
Create the hApp bundle by running this in your nix-shell from the service
folder:
ββββhc app init workdir/happ
When prompted, enter tutorial
for the name and Tutorial hApp
for the description.
Update our service/workdir/happ/happ.yaml
to fix the bundle path of our DNA by making its dna
section look like this:
ββββ---
ββββmanifest_version: "1"
ββββname: tutorial
ββββdescription: Tutorial hApp
ββββslots:
ββββ - id: sample-slot
ββββ provisioning:
ββββ strategy: create
ββββ deferred: false
ββββ dna:
ββββ bundled: "../dna/greeter.dna"
ββββ properties: ~
ββββ uuid: ~
ββββ version: ~
ββββ clone_limit: 0
Package our DNA into a hApp bundle by running this in your nix-shell.
ββββhc app pack workdir/happ
This will create service/workdir/happ/greeter.happ
Deploy the hApp to a local sandbox Holochain by running this in your nix-shell:
ββββhc sandbox generate workdir/happ/ --run=8888 --app-id=greeter-app
Invoke the zome function from the web UI.
ββββ<script>
ββββ import Buffer from 'buffer'
ββββ import { AppWebsocket } from '@holochain/conductor-api'
ββββ // A temporary hack for @holochain/conductor-api
ββββ window.Buffer = Buffer.Buffer
ββββ let greeting = ''
ββββ function handleClick () {
ββββ getGreeting()
ββββ }
ββββ async function getGreeting () {
ββββ const appConnection = await AppWebsocket.connect('ws://localhost:8888')
ββββ const appInfo = await appConnection.appInfo({ installed_app_id: 'greeter-app' })
ββββ const cellId = appInfo.cell_data[0].cell_id
ββββ const message = await appConnection.callZome({
ββββ cap: null,
ββββ cell_id: cellId,
ββββ zome_name: 'greeter',
ββββ fn_name: 'hello',
ββββ provenance: cellId[1],
ββββ payload: null,
ββββ })
ββββ greeting = message
ββββ }
ββββ</script>