Published on

Calculator Tutorial, Your Fifth Seahorse Solana Program

Authors

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

This is the first tutorial where we won't be using Solana Playgrounds. Instead we've graduated over to using Github codespaces, which will allow us to see what Seahorse is really doing under the hood, gaining a deeper understanding of Solana development. First you will need to set up a Github codespace and install Rust, Solana, Anchor and Seahorse. This post here will walk you through the process step by step. Alternatively you could install everything on your local machine, it's up to you really.

We'll be building a simple calculator program in this tutorial. One that will be able to add, subtract, multiply and divide. Pretty straightforward so let's dig in!

Building our Seahorse project

Once you have cloned the Seahorse repo and set up your environment correctly (again see this post for instructions), it's now time to build our calculator app. Let's create a new directory for our projects from the command line with mkdir projects. Cd into the directory and then run the following command seahorse init calculator. This will create a new directory within 'projects' that will contain a standard Seahorse setup with the following file structure.

calculator tutorial 1

The directory that holds our main python file is 'programs_py'. Open that file up and delete everything in the file for now. Exactly as we did in previous projects on Solana Playgrounds the first thing we'll need to do is import Seahorse and declare a placeholder ID.

Calculator.py

from seahorse.prelude import *

declare_id('11111111111111111111111111111111')

Next we add our classes. This time there's 2. The first lists Eums for our 4 operations, addition, subtraction, multiplication and division. These are listed out and given values 0 through 3. The values are necessary due to the way Seahorse works, but they are arbitrary, meaning they don't correspond to anything later in the logic. The Calculator class takes an Account type and has two properties owner and display. Display being the number which the calculator is currently displaying. Owner being the public key of the user's Solana account.

Calculator.py

class Operation(Enum):
  ADD = 0
  SUB = 1
  MUL = 2
  DIV = 3

class Calculator(Account):
  owner: Pubkey
  display: i64

Our first instruction as always is to initialize things. So here init_calculator takes 2 arguments, the owner, which is the user's account and an Empty instance of the Calculator. Inside the function we initialize the calculator with calculator.init() passing in the owner as payer and the seeds to generate the PDA (program derived account). 2 seeds are necessary, the string 'Calculator' and the owner's address. Finally the user is set as the owner of the calculator.

Calculator.py

@instruction
def init_calculator(owner: Signer, calculator: Empty[Calculator]):
  calculator = calculator.init(
    payer = owner,
    seeds = ['Calculator', owner]
  )
  calculator.owner = owner.key()

Next up, a reset instruction for when we want to get our calculator's display value back to zero. This would be equivalent to pressing 'CE' on a real calculator. The function takes the owner and the calculator as arguments. We print to console what's happening. Run an assert to check if the user is the owner of the calculator. If not we exit with a short message. Lastly we set the calculator's display value to zero.

Calculator.py

@instruction
def reset_calculator(owner: Signer, calculator: Calculator):
  print(owner.key(), 'is resetting', calculator.key())

  # Verify owner
  assert owner.key() == calculator.owner, 'This is not your calculator!'

  calculator.display = 0

Now for the main show, the calculator's core logic. Our function here takes 4 arguments, the user's account, the calculator account and the operation to be completed with the number (e.g. ADD, 4). As before, we assert the user is the owner of the calculator. If not we exit with a message. Otherwise we move forward with our operation which is a set of if, else if, else statements. If the operation passed into the function is of Enum ADD then we add the value to the display, if it's SUB we subtract and so on.

Calculator.py

@instruction
def do_operation(owner: Signer, calculator: Calculator, op: Operation, num: i64):
  # Verify owner, like before
  assert owner.key() == calculator.owner, 'This is not your calculator!'

  if op == Operation.ADD:
    calculator.display += num
  elif op == Operation.SUB:
    calculator.display -= num
  elif op == Operation.MUL:
    calculator.display *= num
  elif op == Operation.DIV:
    calculator.display //= num

Final code

Your final code should look something like this.

Calculator.py

from seahorse.prelude import *

declare_id('11111111111111111111111111111111')

class Operation(Enum):
  ADD = 0
  SUB = 1
  MUL = 2
  DIV = 3

class Calculator(Account):
  owner: Pubkey
  display: i64

@instruction
def init_calculator(owner: Signer, calculator: Empty[Calculator]):
  calculator = calculator.init(
    payer = owner,
    seeds = ['Calculator', owner]
  )
  calculator.owner = owner.key()

@instruction
def reset_calculator(owner: Signer, calculator: Calculator):
  print(owner.key(), 'is resetting', calculator.key())

  # Verify owner
  assert owner.key() == calculator.owner, 'This is not your calculator!'

  calculator.display = 0

@instruction
def do_operation(owner: Signer, calculator: Calculator, op: Operation, num: i64):
  # Verify owner, like before
  assert owner.key() == calculator.owner, 'This is not your calculator!'

  if op == Operation.ADD:
    calculator.display += num
  elif op == Operation.SUB:
    calculator.display -= num
  elif op == Operation.MUL:
    calculator.display *= num
  elif op == Operation.DIV:
    calculator.display //= num

Make sure you've saved the current version of the file, then in the terminal run build.

seahorse build

It's normal for this to take a while. When it's finished Seahorse should have taken your Python code and compiled it into Anchor code. If you open the 'scr > dot' folder you will see the file 'program.rs'. This is your compiled code. Open the files side by side and compare the code. You'll notice the file is approximately 3x longer in Rust compared with the original Python, that's the power of Seahorse.

calculator tutorial 2

Deploying to Devnet

Before we deploy to devnet, we'll need to change some settings in the 'Anchor.toml' file. Except for the calculator variable which is unique to your program, everything should be set to the same as below. Make sure the calculator variable is set to the latest as this address may have changed after you ran seahorse build.

[features]
seeds = true
skip-lint = false
[programs.devnet]
calculator = "Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS" // CHANGE THIS TO YOUR ADDRESS

[registry]
url = "https://anchor.projectserum.com"

[provider]
cluster = "devnet"
wallet = "/home/codespace/.config/solana/id.json"

[scripts]
test = "yarn run ts-mocha -p ./tsconfig.json -t 1000000 tests/**/*.ts"

Check you have a local Solana wallet set up with funds.

solana airdrop 2
solana balance

Now we should be able to deploy with the following command.

anchor deploy

If this is running correctly, you should see a message something like this:

Deploying workspace: https://api.devnet.solana.com
Upgrade authority: /home/codespace/.config/solana/id.json
Deploying program "calculator"...
Program path: /workspaces/seahorse/projects/calculator/target/deploy/calculator.so...
Program Id: 8JN7MhbuVbRjoYEbZKK6yqojL8GvUZv417WFXjn3mVv1

Deploy success

Take the 'Program Id' and copy and paste it into a Solana block explorer to check if the program exists and has actually been deployed to devnet.

Generate an IDL

Make sure you are in the base directory for your project. So if you named your project 'calculator' as we recommended at the start of this tutorial, you should cd into the calculator directory. Run the command below replace 8JN7MhbuVbRjoYEbZKK6yqojL8GvUZv417WFXjn3mVv1 with the real ID of your deployed program.

anchor idl init -f ./target/idl/calculator.json 8JN7MhbuVbRjoYEbZKK6yqojL8GvUZv417WFXjn3mVv1 --provider.cluster devnet

If this runs correctly you should see a message similar to this:

Idl account created: C79ZP6uUQDKQi3gUFHvHwTGMtAm9DRETXgbxg56DgLPi

Refresh the page on the Solana block explorer for your deployed calculator app. Scroll around and you should be able to now find a tab with the Anchor IDL JSON. Not all block explorers display this information, as of writing Solana Explorer does.

calculator tutorial 1

In the next tutorial we'll look at how to test our newly deployed program.