Skip to content

CodeWithJV/holochain-challenge-1

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

26 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Challenge 1 - Actions and Entries

In this challenge we are going to create a joke app, where each user (agent) can create jokes and store them on the DHT.

A large portion of the code has been scaffolded using hc scaffold and we have removed key sections to help you understand what the code generated by hc scaffold is actually doing.

Your mission is to update the code so an agent can create, retrieve, edit and delete jokes.

Installation

1. Fork this repo and clone it down

2. run nix develop.

3. run npm i to install packages

4. run npm start and open up the holochain playground

Create a joke

1. In one the agents windows, enter some text into the text field and click the create joke button.

You will notice the error popup: Attempted to call a zome function that doesn't exist: Zome: jokes Fn create_joke

Tip!

If you see the error sh: 1: hc: Permission denied or sh: 1: hc: not found in your terminal, it means you forgot to run nix develop!

2. Open up the browser console, navigate to where the error occured. You will see our frontend is trying to make a call to a backend zome function. We need to implement this function on the holochain app to resolve the error.

Hint

Press F12 or right click > inspect element to open up the dev tools. Select console you should then see the error.

The error inside the console should point us to our CreateJoke.svelte file

Have a look at the createJoke function in CreateJoke.svelte file. This uses the holochain javascript function callZome which is the main way our front end communicates with the holochain conductor.

client.callZome client.callZomeRequestGeneric

3. Navigate to dnas/jokes/zomes/coordinator/jokes/src/joke.rs and paste the following code at the top of the file, underneath the use statements

#[hdk_extern]
pub fn create_joke(joke: Joke) -> ExternResult<Record> {
    let joke_hash = create_entry(&EntryTypes::Joke(joke.clone()))?;
    let record = get(joke_hash.clone(), GetOptions::default())?.ok_or(wasm_error!(
        WasmErrorInner::Guest("Could not find the newly created Joke".to_string())
    ))?;
    Ok(record)
}

Useful api docs

4. Save the file, restart the holochain application and create another joke.

The error should have gone away.

Hint

Press Ctrl + C in your terminal to stop the holochain process, and npm start to start it again

Have a look through CreateJoke.svelte and see how the createJoke function is implemented.

Given we can get the author of an entry through the create action, why might we want to store the creator field int he entry as well?

5. Select each DHT cell inside the dht-cell panel.

You will notice that the source chain of each cell is different. The cell of the Agent who created the joke, contains an entry and its corresponding create action, and the other cell will not have this.

Get a Joke from another agent

To retrieve an agent's joke from the DHT, we are going to need the Action Hash of that joke.

Inside ui/src/App.svelte, we are going to create a text field for where we can input an action hash.

1. Start by declaring a variable that holds the state for the text field inside the <script> block

let jokeHash = ''
let retrieveJokeHash = ''
$: jokeHash

2. Next we can create our text field element. Paste this code just below where the CreateJoke component is implemented, inside the <main> block

<h3 style="margin-bottom: 16px; margin-top: 32px;">Retrieve A Joke!</h3>
<input
    type="text"
    placeholder="Enter the action hash of a joke..."
    value={jokeHash}
    on:input={(e) => {
    jokeHash = e.currentTarget.value
    }}
    required
    style="margin-bottom: 16px;"
/>

3. Save App.svelte, and head back into one of the agent windows. You should see the text field displayed

Notice how we didn't need to completely restart the app this time? You only need to restart your app when you change rust code, the front end will use hot reloading to stay up to date.

4. We are going to need to add a button which triggers the retrieval of a joke from the DHT, and then displays it for the user. To do this, we will use the UI component JokeDetail, as well as another piece of state to manage its visibility.

Place these lines of code inside the same script tag of App.svelte

import JokeDetail from './jokes/jokes/JokeDetail.svelte'
<!-- svelte-ignore a11y-click-events-have-key-events -->
<button
    on:click={() => {
      retrieveJokeHash = '' //force reload even if jokeHash is the same
      retrieveJokeHash = jokeHash
    }}
>
    Get Joke
</button>
{#if retrieveJokeHash}
    <JokeDetail jokeHash={decodeHashFromBase64(retrieveJokeHash)} />
{/if}

5. Navigate back to dnas/jokes/zomes/coordinator/jokes/src/joke.rs and paste the following code underneath our create_joke function

#[hdk_extern]
pub fn get_joke_by_hash(original_joke_hash: ActionHash) -> ExternResult<Option<Record>> {
    let Some(details) = get_details(original_joke_hash, GetOptions::default())? else {
        return Ok(None);
    };
    match details {
        Details::Record(details) => Ok(Some(details.record)),
        _ => {
            Err(wasm_error!(WasmErrorInner::Guest(String::from("Malformed get details response"))))
        }
    }
}

This zome function is called by the JokeDetail component when it mounts. It takes in the action hash for the joke as an argument, and then returns the record corresponding to it.

hdk::entry::get_details

6. Save the file, restart the holochain app and create a new joke inside an Agent's window.

In the holochain playground, find the action for the joke just created by clicking on the Create action at the top of the agent's source tree. You should see the Action contents show up in the Entry Contents window. Click on the circle next to Action Contents, with hash to copy the hash of the action.

We've also put a console.log of the action hash, so you can easily grab it from the browser console.

Paste the action hash into the other Agents Get Joke text field, and press the Get Joke button.

You should see your newly created joke render on the UI!

Edit a joke

You may have noticed that when we retrieve a joke, our JokeDetail component displays the joke text, as well as an option to edit and delete the joke.

1. Create a joke, retrieve it, and then click on the edit button next it

2. Change the joke to text, and press Save

You will notice nothing will happen. Once again, we will need to implement some code to get this working.

3. Navigate to ui/src/jokes/jokes/EditJoke.svelte

This EditJoke component holds the code for the UI where we can edit jokes. It is already included inside the JokeDetail component.

4. Find the updateJoke function and paste the following code inside of it.

const joke: Joke = {
  text: text!,
  creator: currentJoke.creator,
};

try {
  const updateRecord: Record = await client.callZome({
    cap_secret: null,
    role_name: "jokes",
    zome_name: "jokes",
    fn_name: "update_joke",
    payload: {
      original_joke_hash: originalJokeHash,
      previous_joke_hash: currentRecord.signed_action.hashed.hash,
      updated_joke: joke,
    },
  });

  console.log(
    `NEW ACTION HASH: ${encodeHashToBase64(updateRecord.signed_action.hashed.hash)}`
  )

  dispatch("joke-updated", { actionHash: updateRecord.signed_action.hashed.hash });
} catch (e) {
  alert((e as HolochainError).message);
}

When the save button is clicked in the UI this block of code will make a call to the backend Zome function update_joke.

5. Save the file, navigate back to dnas/jokes/zomes/coordinator/jokes/src/joke.rs and paste the following code underneath our get_joke_by_hash function

#[derive(Serialize, Deserialize, Debug)]
pub struct UpdateJokeInput {
    pub original_joke_hash: ActionHash,
    pub previous_joke_hash: ActionHash,
    pub updated_joke: Joke,
}

#[hdk_extern]
pub fn update_joke(input: UpdateJokeInput) -> ExternResult<Record> {
    let updated_joke_hash = update_entry(input.previous_joke_hash.clone(), &input.updated_joke)?;
    let record = get(updated_joke_hash.clone(), GetOptions::default())?.ok_or(wasm_error!(
        WasmErrorInner::Guest("Could not find the newly updated Joke".to_string())
    ))?;
    Ok(record)
}

Notice how this block of code contains a struct as well as the Zome function. For this update function, we want to send multiple pieces of data from the client, but Zome functions can only a take a single parameter.

hdk::entry_update_entry

6. Save the file, and restart the app.

7. Create a joke, retrieve it, edit its contents, and press save.

8. Look at the source chain for the cell we just edited a joke for. You will see another action has been added.

It's important to understand how updates in Holochain work. When you commit an update action, it will not update the contents of the entry. Instead it will add new entry to to the DHT, and link it to the previous entry via the update action.

This applies to delete actions as well, and it means that any entries once added to the DHT will remain on it forever.

Try putting a console.log in the fetchJoke function of EditDetail.svelte to see what the record looks like when you retrieve multiple action hashes.

What do you think will happen if you edit two separate entries to have the same content?

Delete a joke

1. Navigate to the deleteJoke function inside JokeDetail.svelte, and write code to create a zome call to delete_joke.

  • The payload should be the jokeHash

2. Save the file, navigate to dnas/jokes/zomes/coordinator/jokes/src/joke.rs and write a zome function to delete a joke

Try figure it out yourself using the docs for delete_entry

Hint!
#[hdk_extern]
pub fn delete_joke(original_joke_hash: ActionHash) -> ExternResult<ActionHash> {
    delete_entry(original_joke_hash)
}

hdk::entry::delete_entry

3. Save the file and restart the app

4. Create a joke, retrieve it, and then delete it using the delete button.

Just like with editing and creating a joke, deleting a joke should add another action the the Agents source chain, however this action won't be associated with an entry.

Also like update actions, delete actions don't achually remove the previous actions/entries of this piece of data from the DHT. They just change the entry_dht_status to Dead. You can still access the initial data.

What does the delete_joke function return?

5. Update your get_joke_by_hash function to work for both action and entry hashes

pub fn get_joke_by_hash(original_joke_hash: AnyDhtHash) -> ExternResult<Option<Details>> {
    let Some(details) = get_details(original_joke_hash, GetOptions::default())? else {
        return Ok(None);
    };

    match details {
        Details::Record(details) => Ok(Some(Details::Record(details))),
        Details::Entry(details) => Ok(Some(Details::Entry(details))),
        _ => {
            Err(wasm_error!(WasmErrorInner::Guest(String::from("Malformed get details response"))))
        }
    }
}

Have a look at the differences between the two versions of the function and make a note of anything you want to explore further.

6. Update the fetchJoke function to get the full entry details

Update the fetchJoke function in JokeDetails.svelte

  async function fetchJoke() {
    loading = true

    try {
      let details = await client.callZome({
        cap_secret: null,
        role_name: 'jokes',
        zome_name: 'jokes',
        fn_name: 'get_joke_by_hash',
        payload: jokeHash,
      })
      if (details) {
        if (details.type === 'Record') {
          record = details.content.record
          let entry_hash = record.signed_action.hashed.content.entry_hash
          let entry_details = await client.callZome({
            cap_secret: null,
            role_name: 'jokes',
            zome_name: 'jokes',
            fn_name: 'get_joke_by_hash',
            payload: entry_hash,
          })
          console.log('ACTION HASH:', encodeHashToBase64(jokeHash))
          console.log('ENTRY HASH: ', encodeHashToBase64(entry_hash))
          console.log('LIVENESS:', entry_details.content.entry_dht_status)
          joke = decode((record.entry as any).Present.entry) as Joke
        } else {
          joke = undefined
          console.log('entry found')
          console.log(details)
        }
      }
    } catch (e) {
      error = e
    }

    loading = false
  }

Try creating, updating and deleting various jokes and seeing how the liveness changes

Well done! You made it to the end.

About

Actions and Entries

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Contributors 2

  •  
  •