Published on

Todo App Tutorial, Your Forth Seahorse Solana Program

Authors

Welcome back. Let's write some more Seahorse! This tutorial is part 4 of a series and follows on from the previous fizz buzz example. Start here if you're an absolute beginner.

Today we'll be creating a simple todo app. Building a todo app is a rite of passage when learning a new language and for good reasons. It's a very efficient way to understand the basic CRUD app operations (create, read, update, delete). The only one we won't be covering here is delete, which we'll leave for another lesson.

Code

Firstly create a new project in Solana Playgrounds, delete the boilerplate code and rename the .py file to 'todo_app.py' import Seahorse and declare a placeholder ID.

todo_app.py

from seahorse.prelude import *

declare_id('11111111111111111111111111111111')

Next, let's set up our first class, the user profile. This will be used to create a PDA that holds 3 things. The owner: Pubkey, the last_todo:u8 and the todo_count:u8. u8 as you might have guessed by now u8 stands for an unsigned integer 8 bits in size (2^8).

todo_app.py

class UserProfile(Account):
  owner: Pubkey
  last_todo:u8
  todo_count:u8

Our second class will be for the individual todos. The actual todo will be stored as a string todo: str, it's completion status will be stored as a boolean done:bool, we'll assign each todo an index idx:u8 and of course it will need an owner owner: Pubkey.

todo_app.py

class TodoAccount(Account):
  owner: Pubkey
  idx:u8
  todo:str
  done:bool

That's it for classes, let's nail down our instructions. Firstly we'll need to initialize a user profile. The function will take 2 arguments, an owner and an empty instance of the UserProfile class. We initialize the UserProfile instance with a seed of the owner's public key and the string user_profile. The owner of the user profile will be the account calling the instruction. We initialize the todo count and last todo both to 0.

todo_app.py

@instruction
def init_userprofile(owner:Signer, user_profile:Empty[UserProfile]):
  user_profile=user_profile.init(
    payer=owner,
    seeds=['user_profile',owner])
  user_profile.owner=owner.key()
  user_profile.last_todo=0
  user_profile.todo_count=0

Now let's build the instruction for adding a task to our todo list. The arguments for this function will be an owner, a user profile, an empty instance of the TodoAccount and a string with our todo. We initialize the todo account with a seed of the owner's public key, the string 'todo_account' and the user_profile's last todo, which will be in u8 format. We not only add the necessary information to the todo_account but also increment the last_todo and todo_count on the user profile.

todo_app.py

@instruction
def add_task(owner:Signer, user_profile:UserProfile, todo_account:Empty[TodoAccount], todo:str):
  todo_account=todo_account.init(
    payer=owner,
    seeds=['todo_account',owner,user_profile.last_todo]
  )
  todo_account.todo=todo
  todo_account.id=user_profile.last_todo
  todo_account.owner=owner.key()
  user_profile.last_todo+=1
  user_profile.todo_count+=1

Finally we add an instruction for marking a todo as done. The function takes 4 arguments, an owner, the index, a user profile account and a todo account. The done flag is set to true and printed to the console.

todo_app.py

@instruction
def mark_task(owner:Signer, todo_index:u8, user_profile:UserProfile, todo_account:TodoAccount):
  todo_account.done=True
  print(todo_account.done)

Final code

Your final code should look something like this.

todo_app.py

from seahorse.prelude import *

declare_id('11111111111111111111111111111111')

class UserProfile(Account):
  owner: Pubkey
  last_todo:u8
  todo_count:u8

class TodoAccount(Account):
  owner: Pubkey
  idx:u8
  todo:str
  done:bool

@instruction
def init_userprofile(owner:Signer, user_profile:Empty[UserProfile]):
  user_profile=user_profile.init(
    payer=owner,
    seeds=['user_profile',owner])
  user_profile.owner=owner.key()
  user_profile.last_todo=0
  user_profile.todo_count=0
  
@instruction
def add_task(owner:Signer,user_profile:UserProfile, todo_account:Empty[TodoAccount], todo:str):
  todo_account=todo_account.init(
    payer=owner,
    seeds=['todo_account',owner,user_profile.last_todo]
  )
  todo_account.todo=todo
  todo_account.idx=user_profile.last_todo
  todo_account.owner=owner.key()
  user_profile.last_todo+=1
  user_profile.todo_count+=1

@instruction
def mark_task(owner:Signer, todo_index:u8, user_profile:UserProfile, todo_account:TodoAccount):
  todo_account.done=True
  print(todo_account.done)

Let's build and deploy it as before. Make sure you have enough SOL in your wallet to deploy. Don't forget to initialize and export the IDL!

Testing

First thing we want to test is initializing a user profile account. Go to Instructions > initUserprofile. In the owner input field select 'my address', for the userProfile field select 'from seed'. In the first field input user_profile without quotation marks. Tap the add icon and choose Pubkey from the drop down menu. Copy and paste the owner's address into the field and select 'generate' and then select 'test'.

todo app tutorial 1

Check the account has been created correctly Accounts > UserProfile > Fetch All. You should see some JSON similar to below.

[
  {
    "publicKey": "24JaWTxLNPxwAtSonVxB1KT7ayzoPsDZCzK3iAfVUdrh",
    "account": {
      "owner": "Azj5L3g2gCPmi35kZAGogf78xEgpS9nSbL1tp6Jn6DyR",
      "lastTodo": 0,
      "todoCount": 0
    }
  }
]

Next we can do a todo task. Go to Instructions > addTask. In the todo:string field enter a task. The string can't have spaces, so use kebab case, snake case or whatever suits you. Input the owner's public address into the owner field. Input the userProfile address from the previous step into the second field. You can copy this from the JSON. The final field needs to be generated 'from seed'. The first seed is the string 'todo_account' without quotation marks. Add the second seed which is the owner account. The final seed is 0 because zero is the current value for user_profile.last_todo, the type is u8.

todo app tutorial 2

Open the block explorer link for this test transaction. Find the newly created account for the todo task. Scroll down to see it's data stored and you should be able to see something like this.

todo app tutorial 3

Our todo task has been stored on the blockchain! Now let's mark it as done, Instructions > markTask. Fill in the addresses for the owner, userProfile and todoAccount. You can copy and paste these from the JSON fetch all options below. Summit the test and check the block explorer link. You should be able to see a log indicating that that done is now marked as true.

todo app tutorial 4

That's it! You've created a very simple todo app on the Solana blockchain. It holds todo tasks that can be marked as completed. Try adding a second task.