Build a Calorie Tracker Dapp with Soroban and React.js
Tags: smartcontract, rust, react, soroban
Published: 2023-12-11 08:22:01

In this article, we will learn how to build a Dapp which tracks calories. We'll use Soroban, a smart contract platform built on Rust and running on the Stellar network. We'll use React.js for the frontend.

You can get a demo of what we are going to build here: https://calorie-tracker-iota.vercel.app/

## Getting started

Lets start with installing all our prerequisite dependencies:

### Rust
We are going to write our smart contract in Rust. Rust offers memory safety and amazing concurrency primitives, making it a very nice language to work with blockchains and smart contracts. You can install Rust by following the instructions here:
* Linux, macOS: https://www.rust-lang.org/tools/install
* Windows https://forge.rust-lang.org/infra/other-installation-methods.html#other-ways-to-install-rustup

If you don't know Rust, the Rust Programming Language book is an excellent way to learn the language. Check it out: https://doc.rust-lang.org/book/.

### Soroban
Now that we have Rust installed, we can install the Soroban CLI. The Soroban CLI lets us build, deploy and invoke our contracts along with a plethora of other things.
Since the contracts are compiled to WASM binaries and deployed as such, we first need to configure the Rust toolchain's target:

```bash
rustup target add wasm32-unknown-unknown
```

Now go ahead and install the CLI:
```bash
cargo install --locked --version 20.0.1 soroban-cli
```

## Building our contract

Our smart contract will provide three methods that can be invoked:
* add: To add calories for a particular date
* subtract: To subtract calories for a particular date
* get: To get the calories for a particular date range

Lets start a new project:

```
cargo new --lib calorie-tracker
```

Now

Now we'll do some project reorganization. Since we might want to add more contracts in the future, it'd make sense to organize the contracts in one directory:

```
mkdir -p contracts/calorie-tracker
mv Cargo.toml contracts/calorie-tracker
mv src contracts/calorie-tracker
```

Now modify `contracts/calorie-tracker/Cargo.toml` to look like this:
```toml
[package]
name = "calorie-tracker"
version = "0.1.0"
edition = "2021"

[dependencies]
soroban-sdk = "20.0.0"
chrono = "0.4.31"

[dev_dependencies]
soroban-sdk = { version = "20.0.0" }

[lib]
crate-type = ["cdylib"]
```

and `Cargo.toml` to look like:
```toml
[workspace]
resolver = "2"
members = [
  "contracts/*",
]

[workspace.dependencies]
soroban-sdk = "20.0.0"

[profile.release]
opt-level = "z"
overflow-checks = true
debug = 0
strip = "symbols"
debug-assertions = false
panic = "abort"
codegen-units = 1
lto = true

[profile.release-with-logs]
inherits = "release"
debug-assertions = true
```

With this out of the way, we can actually focus on writing some code!

First lets import some useful things from the `soroban_sdk` crate. This crate contains utilities and constructs that make it simple to create smart contracts:

```rust
#![no_std]
use soroban_sdk::{contract, contractimpl, contracttype, vec, Address, Env, Map, String, Vec};
```
Notice that we specify that we don't want to include the `std` inbuilt crate since this will be built into a WASM binary at the end.

Now lets define the enum that will contain the user's info:

```rust
#[contracttype]
pub enum DataKey {
    Counter(Address),
}
```

Address is a universal Soroban identifier that may represent a Stellar account, a contract or an 'account contract'. For more info see the docs: https://soroban.stellar.org/docs/basic-tutorials/auth

Next, we'll define an enum that specifies the kind of operation that the user wants to perform:

```rust
#[contracttype]
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum Op {
    Add,
    Subtract,
}
```

Since the user can either add or subtract calories, we have two variants, one for each operation.

Let's define our contract type now:

```rust
#[contract]
pub struct CalorieTracker;
```
The `#[contract]` attribute designates the `CalorieTracker` struct as the type to which contract functions are associated. This implies that the struct will have contract functions implemented for it.

Now lets implement those functions:

```rust
#[contractimpl]
impl CalorieTracker {
    // Adds the provided calories to the user's calorie count.
    pub fn add(env: Env, user: Address, calories: u32, date: String) -> i32 {
        user.require_auth();

        let key = DataKey::Counter(user.clone());
        let val: Option<Map<String, Vec<(u32, Op)>>> = env.storage().persistent().get(&key);
        let mut total_calories: i32 = 0;
        if let Some(mut calorie_records) = val {
            let res: Option<Vec<(u32, Op)>> = calorie_records.get(date.clone());
            if let Some(mut calorie_vals) = res {
                calorie_vals.push_back((calories, Op::Add));
                calorie_vals.iter().for_each(|val| {
                    if val.1 == Op::Add {
                        total_calories += val.0 as i32;
                    } else {
                        total_calories -= val.0 as i32;
                    }
                });
                calorie_records.set(date, calorie_vals);
                env.storage().persistent().set(&key, &calorie_records);
            } else {
                let calorie_vals = vec![&env, (calories, Op::Add)];
                calorie_records.set(date, calorie_vals);
                env.storage().persistent().set(&key, &calorie_records);
                total_calories = calories as i32;
            }
        } else {
            let mut calorie_records = Map::new(&env);
            let calorie_vals = vec![&env, (calories, Op::Add)];
            calorie_records.set(date, calorie_vals);
            env.storage().persistent().set(&key, &calorie_records);
            total_calories = calories as i32;
        }
        total_calories
    }
}
```

Lets unpack the above method. We accept an `env` variable that acts like a service discovery mechanism containing all facilities available to the contract. You can read more about it here: https://soroban.stellar.org/docs/fundamentals-and-concepts/environment-concepts

The second argument is the `user`, which contains the information about the user. `user.require_auth()` tells the contract that the user needs to be authenticated for this method to execute.
The third and fourth argument pertain to our actual business logic, `calories` and `date`.

Since we need to keep track of each user's calorie information, we need some sort of persistent storage.
We can use the storage provided to the contract by `env.storage().persistent()`. Each user's information is sorted in separate maps of type: `Map<String, Vec<u32, Op>>`. The key contains the date for which the calories are being added for. The value is a vector where each element contains two things:
* The calorie count
* The operation.
Since we're implementing the `add` method, the operation here is `Op::Add`.

We first get the user's map, then insert the specified calorie count along with the operation for the provided date. If the user's map is not found we create one and make sure to store it along with the provided date and calorie count.
The method returns the resultant total calorie count for the provided date. To figure that out, we loop through the array containing the calorie count along with the operation for that date and then simply add or subtract each calorie count according to the related operation.

Voila! We have ourseleves a contract!

The `subtract` function looks very similar to this, so I'll leave that to you as an exercise.
Here is the definition to help you get started:
```rust
pub fn subtract(env: Env, user: Address, calories: u32, date: String) -> i32
```

Now lets implement our `get` method:

```rust
pub fn get(env: Env, user: Address, dates: Vec<String>) -> Map<String, i32> {
    user.require_auth();
    let key = DataKey::Counter(user.clone());

    let mut total_calories: Map<String, i32> = Map::new(&env);
    let val: Option<Map<String, Vec<(u32, Op)>>> = env.storage().persistent().get(&key);
    if let Some(calorie_records) = val {
        for date in dates {
            let mut calories: i32 = 0;
            let res: Option<Vec<(u32, Op)>> = calorie_records.get(date.clone());
            if let Some(calorie_vals) = res {
                calorie_vals.iter().for_each(|val| {
                    if val.1 == Op::Add {
                        calories += val.0 as i32;
                    } else {
                        calories -= val.0 as i32;
                    }
                });
            }
            total_calories.set(date.clone(), calories);
        }
    }
    total_calories
}
```

As you can see the major difference here is that we are accepting a list of dates. Then we get the user's calorie activity for each date, go through them adding or subtracting them to a total and then returning a map with the date as the key and the total calorie count as the value.

You can check out the entire contract here: https://github.com/aryan9600/calorie-tracker/tree/main/contracts/calorie-tracker/lib.rs

Now that we have our code ready, lets build and deploy it:

```bash
# Add details about the network that this contract will be deployed on
soroban config network add --global futurenet \
  --rpc-url https://rpc-futurenet.stellar.org \
  --network-passphrase "Test SDF Future Network ; October 2022"
 
# Create a identity to use to deploy the contract on the network
soroban config identity generate --global <username>
address=$(soroban config identity address alice)

# Fund the account with some test tokens
soroban config identity fund $address --network futurenet

# Build the contract; generates a wasm file
soroban contract build

# Install the wasm file on the ledger
wasm_hash=$(soroban contract install \
  --network futurenet \
  --source <username> \
  --wasm target/wasm32-unknown-unknown/release/calorie-tracker.wasm)

# Deploy the contract on the network
soroban contract deploy \
  --wasm-hash $wasm_hash \
  --source <username> \
  --network futurenet \
  > .soroban/calorie-tracker-id 
```

Lets put it into action!
```bash
soroban contract invoke --id $(cat .soroban/calorie-tracker-id) \
  --source <username> \
  --network futurenet \
  -- \
  add --user $address --calories 10 --date "2023-12-08"

soroban contract invoke --id $(cat .soroban/calorie-tracker-id) \
  --source <username> \
  --network futurenet \
  -- \
  subtract --user $address --calories 5 --date "2023-12-08"
```

## Building the client app

Now that our contract is ready and deployed, lets build a decentralized application so that other users can invoke it. Lets initialize a new React.js project:

```bash
npx create-react-app calorie-tracker-client
```

Just like before we have to do some folder restructuring:

```bash
mv calorie-tracker-client/* .
rmdir calorie-tracker-client
```

Next we'll need to generate the Typescript bindings for the contract so that we can call our contract functions from the frontend:

```bash
soroban contract bindings typescript \
  --network futurenet \
  --contract-id $(cat .soroban/calorie-tracker-id) \
  --output-dir node_modules/calorie-tracker-client
```

Add the following command also as a `postinstall` script in your `package.json`:

```json
"postinstall" "soroban contract bindings typescript --network testnet --contract-id $(cat .soroban/hello-id) --output-dir node_modules/hello-soroban-client"
```

We also need to add a wallet so that we can sign the transaction. The best one right now is [Freigher](https://www.freighter.app/). Install its extension in your browser and then add its API package as a dependency:

```bash
npm install @stellar/freighter-api
npm run postinstall
```

Now open `src/App.tsx` and add the following code:

```typescript
const tracker = new Contract({
  contractId: networks.futurenet.contractId,
  networkPassphrase: networks.futurenet.networkPassphrase,
  rpcUrl: "https://rpc-futurenet.stellar.org/",
});
```

This creates a client through which we can call our contract functions. Next we'll create a function that calls Freigher's API to get the user's public key:

```typescript
let addressLookup = (async () => {
  if (await isConnected()) return getPublicKey()
})();

let address: string;

const addressObject = {
  address: '',
  displayName: '',
};

const addressToHistoricObject = (address: string) => {
  addressObject.address = address;
  addressObject.displayName = `${address.slice(0, 4)}...${address.slice(-4)}`;
  return addressObject
};

export function useAccount(): typeof addressObject | null {
  const [, setLoading] = useState(address === undefined);

  useEffect(() => {
    if (address !== undefined) return;

    addressLookup
      .then(user => { if (user) address = user })
      .finally(() => { setLoading(false) });
  }, []);

  if (address) return addressToHistoricObject(address);

  return null;
};
```

Now lets build the main React component that will call the method:

```typescript
return (
<>
  <div className="container">
    <h2> Select date </h2>
    <DatePicker
      showIcon
      selected={inputDate}
      onChange={(date) => setInputDate(date)}
      wrapperClassName={"customDatePickerWidth"}
      className={"customDatePickerWidth"}
    />

    <h2> Add calories </h2>
    <input
      type="number"
      name="add"
      min="0"
      value={caloriesToAdd}
      onChange={(e) => setCaloriesToAdd(Number(e.target.value))}
    />
    {isAddLoading ? (
      <div className="spinner"></div> // Spinner element
    ) : (
      <button
        onClick={() => {
          setIsAddLoading(true);
          tracker.add({
            user: account.address,
            calories: Number(caloriesToAdd),
            date: inputDate.toISOString().slice(0, 10)
          }).then(tx => {
            tx.signAndSend().then(val => {
              setDailyCalories(val.result);
              setModalMsg('Calories added!');
              setModalOpen(true);
              setIsAddLoading(false);
            }).catch(error => {
              console.error("error sending tx: ", error);
              setIsAddLoading(false);
            })
          }).catch(error => {
            console.error("Error updating calories:", error);
            setIsAddLoading(false);
          })
        }}
        disabled={isAddLoading}
      >
        Submit
      </button>
    )}
  </div>
</>
```

We have a date picker that lets the user selects the date for which they want to add the calories. Then we have a number input field which will contain the calorie count. Upon clicking on the Submit button, the contract is called with the user's public key, date and the calorie count. We let the user know that their calorie record has been stored and show the total number of calories for that date in a modal.

For the full client code, check: https://github.com/aryan9600/calorie-tracker/tree/main/src

## Conclusion
Now that we've seen how we can write Soroban smart contracts in Rust and then integrate them with React, you know the basics of how to build a dapp. There are several other resources, you can refer to build dapps and smart contracts:
* https://soroban.stellar.org/dapps
* https://dev.to/hastodev/cowchain-farm-a-dapp-built-with-soroban-and-flutter-2me0

Good luck on your dapp building journey! ❤️