Soroban Quest - Auth Store
Tags: soroban, stellar, tutorial
Published: 2022-11-25 11:29:54

After completing the Hello World quest on the [last post](https://dev.to/altug/soroban-quest-hello-world-1n3l), you should feel more comfortable about solving a more advanced quest.

## Understanding `README.md`

As before, let's open our `quests` folder, then, open the `2-auth-store` folder to start tackling the second quest.

![Folder View](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/isqyymbduwa9baqi2mrz.png)

On the `README.md` file, we can see that we are tasked with using the `put` function of the Soroban contract to save some data onto the Stellar ledger. Then, we should use `get` or `get_self` functions to retrieve the data we saved.

## Setting Up Our Stellar Quest Account

As usual, use `sq login` to login to your Stellar Quest account if you haven't, and use `sq play 2` to start solving the second quest. Don't forget to fund your account with `sq fund` to succesfully transact on the Futurenet.

## Examining the Contract

As usual, open up the `src/lib.rs` file to view the contract's source code. In addition to `test.rs`, you will also see `error.rs` for this quest. Let's start by checking out the functions of the `DataStoreContract`.

### `put` Function

The `put` function allows us to store arbitrary data mapped to our account address.

```rust
pub fn put(env: Env, value: Bytes) -> Result<(), ContractError> {
        let key = match env.invoker() {
            Address::Account(account_id) => account_id,
            Address::Contract(_) => {
                panic_with_error!(&env, ContractError::CrossContractCallProhibited)
            }
        };

        if value.len() <= 10 {
            panic_with_error!(&env, ContractError::InputValueTooShort)
        }

        env.data().set(key, value);

        Ok(())
}
```
The `put` function has two arguments, one of them being the environment variable `env`, and the other one is the `value` argument of type `Bytes`. The `Bytes` type can be thought as a `string`, as each character of a `string` consists of one `byte`. The difference between the `Symbol` and the `Bytes` type is that the `Symbol` type can only have a maximum of 10 characters. The function returns either the `Ok` value or a `ContractError`.

> #### Understanding Contract Errors
> 
> Custom errors are `enum`s, which is a number type that is mapped to labels. The use of numbers are efficient for computers, and labels are helpful for programmers to understand what they mean.
>
> Using custom contract errors is useful when negative testing the contract. Negative tests make sure that the implementations support unexpected input, while positive tests ensures that the implementations are working as expected. Since our Soroban contracts will be immutable, testing against harmful behavior helps with preventing some attack patterns.
>
> Using custom contract errors are also useful for User Experience, as the users usually get these errors before calling a contract function. Giving helpful names to errors help users fix their input arguments easily.

First, the `put` function finds the `key` of the contract invoker. The invoker can be one of two things: an Externally Owned Account (EOA), which will be us, an user with a private and public key pair, or another contract using cross-contract calls. This implementation blocks the use of cross-contract calls, which we can see by inspecting the `match` statement. if `env.invoker()` matches a contract address, the program `panic`s and exits with the custom contract error, `CrossContractCallProhibited`. The implementation of these errors can be found in the `error.rs` file.

Next, the `put` function checks if the length of the supplied `value` is shorter or equal to 10 `byte`s, and `panic`s with the custom `InputValueTooShort` error if it is the case. This is to highlight the difference between a `Symbol` variable and a `Bytes` variable, and is not required when implementing a function.

If all the checks are correct, the contract stores the supplied `value` variable mapped to the invoker's address to *persist* between function invocations.

> #### Persisting Data vs Temporary Data
>
> If you are familiar with any programming language, you would remember that there are *global* variables, which are accessible anywhere from the code, and *local* variables, that are scoped to their blocks.
>
> Variables declared in the functions are lost after the execution has ended, and not stored permanently onto the chain. By storing the variables into the environment however, we make permanent records on the Soroban network. Due to this, storage is usually very expensive compared to the other methods on smart contract chains.

### `get` Function

The `get` function allows us to retrieve data set by a given account.

```rust
pub fn get(env: Env, owner: AccountId) -> Bytes {
        env.data()
            .get(owner)
            .unwrap_or_else(|| Ok(bytes!(&env)))
            .unwrap()
}
```

The `get` function looks pretty simple, but has some hidden functionality behind it. Excluding the `env` argument, the `get` function has the `owner` parameter of type `AccountId`, which can be tricky to supply from the `soroban` CLI as an argument. First, the function uses `env.data.get(owner)` to get the value set by the given account. If this value does not exist, the function returns `Ok` with env converted to `bytes` using the `bytes!` macro. If this value *does* exist, it returns the value set by the account.

Note that the cross-contract calls are allowed for this function, meaning that we can use another contract to call this function to get any user's set data from our contract. Pretty handy!

### `get_self` Function

If you check out the `get_self` function at first, it looks a little off.

```rust
pub fn get_self(/* env: Env */) -> Result<Bytes, ContractError> {
        unimplemented!("not implemented")
        // let key = match env.invoker() {
        //     Address::Account(account_id) => account_id,
        //     Address::Contract(_) => {
        //         panic_with_error!(&env, ContractError::CrossContractCallProhibited)
        //     }
        // };
        // Ok(env
        //     .data()
        //     .get(key)
        //     .unwrap_or_else(|| Ok(bytes!(&env)))
        //     .unwrap())
    }
```

The implementation of the contract is *commented out*, meaning the compiler ignores those lines. Comments are shown with the double forward slashes, `//`, or can be done inline using `/* */` in Rust. To "unlock" the functionality of the `get_self` function, remove or comment the line starting with the `unimplemented!` macro, and uncomment the other lines, including the input parameter, `env`.

```rust
pub fn get_self(env: Env) -> Result<Bytes, ContractError> {
        // unimplemented!("not implemented")
        let key = match env.invoker() {
            Address::Account(account_id) => account_id,
            Address::Contract(_) => {
                panic_with_error!(&env, ContractError::CrossContractCallProhibited)
            }
        };
        Ok(env
            .data()
            .get(key)
            .unwrap_or_else(|| Ok(bytes!(&env)))
            .unwrap())
    }
```

> #### Comments on Codes
>
> On any code file, you will usually see some comments, explaining the code, disabling some lines, or drawing some awesome [ASCII art](https://www.asciiart.eu/animals/cats). The comments are an important part of programming, and they are not even for computers (mostly)!
>
> The comments are a helpful way for programmers, who generally work on a codebase on a different time than each other, to give out details, explain the cryptic parts, and even leave themselves some reminders. The Doom inverse square root code is a great example on comments:
>
> ![Doom Inverse Square Root](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/kcd6xkegnqhdfdbrkgsp.png)
> Well maybe not. The code is still cryptic, but the programmer also thinks that it is, which we can understand from the comment that they left for us. They also commented out the 2<sup>nd</sup> iteration as they think that it is not required. Be kind to other programmers and try to explain your code if you think commenting is required!

The `get_self` function allows us to retrieve data that *we* set ourselves. It first checks if the caller is an EOA and not an external contract as before, and returns the data set by the caller, as before. This function is easier to use for our quest, as we don't need to supply an account to retrieve a data. We will use both solutions to solve this quest.

## Examining the Tests

The `test.rs` file containing the tests is pretty hefty! Usually, tests take more time to write than the contract to ensure the safety of the contract. Since the tests are extremely well commented, I encourage you to understand them yourselves! I will explain some key features of these tests that do not exists on the `HelloContract` tests.

### `#[ignore]` Modifier

The `#[ignore]` modifier is used on tests which should be, well, ignored. The main reason for this can be unimplemented source code, or unfinished tests. Since we have removed the comments on the `get_self` file, we can remove the `#[ignore]` modifier for its test.

```rust
#[test]
#[should_panic(expected = "Status(ContractError(1))")]
fn test_contract_get_self() {
    /* the test code as usual */
}
```

Now, we can also test the functionality of the `get_self` function when we use `cargo test` to test our contract.

### `should_panic` Keyword

```rust
#[test]
#[should_panic(expected = "Status(ContractError(2))")]
fn test_store_value_too_short() {
    let env = Env::default();
    let contract_id = env.register_contract(None, DataStoreContract);
    let client = DataStoreContractClient::new(&env, &contract_id);

    let u1 = env.accounts().generate();
        .with_source_account(&u1)
        .put(&bytes![&env, 0x007]);
}
```

The `should_panic` keyword, supplied with a custom contract error makes sure that the contract returns the expected error when supplied with an invalid input argument. On this test, we are calling the `put` function with a value that is two `byte`s long, and expecting it to fail with the `InputValueTooShort` error, which has the value 2.

### Contract Implementation in `test.rs`

We can implement contracts just for testing on the `test.rs` file. this is helpful to test the cross contract calls, and many other methods that require another contract.

````rust
pub struct CallerContract;

#[contractimpl]
impl CallerContract {
    pub fn try_put(env: Env, contract_id: BytesN<32>, data: Bytes) {
        let cli = DataStoreContractClient::new(&env, contract_id);
        cli.put(&data);
    }

    pub fn try_get_s(env: Env, contract_id: BytesN<32>) -> Bytes {
        let cli = DataStoreContractClient::new(&env, contract_id);
        cli.get_self()
    }

    pub fn try_get(env: Env, contract_id: BytesN<32>, owner: AccountId) -> Bytes {
        let cli = DataStoreContractClient::new(&env, contract_id);
        cli.get(&owner)
    }
}
```

The `CallerContract` implements three functions. The first one is the `try_put` function, which tries to use the `put` function on the `DataStoreContract` using a cross-contract call. This invocation is expected to fail, but it is important to test the failing cases too! Next, it has the `try_get_s` function, which is used for trying to invoke the `get_self` function via a cross-contract call. This is also expected to fail with the custom `CrossContractCallProhibited` error. Lastly, it has the `try_get` function, which should successfully call the `get` function of the `DataStoreContract`.

> #### Why are Function Names Too Short?
>
> The functions are implemented as `Symbol`s, which are at most 10 characters long. Storing function names as `Symbol`s is helpful due to execution efficiency and storage cost. Since the efficiency is important on blockchains, and this issue does not really cause a bad user experience, this implementation is chosen.

### Generating a Test Account and Using Test Contracts

We can generate accounts for testing our contracts using `env.accounts().generate()`. This is helpful for testing the implementation using multiple accounts.

```rust
fn test_contract_get() {
    let env = Env::default();
    let contract_id_data_store = env.register_contract(None, DataStoreContract);
    let client_data_store = DataStoreContractClient::new(&env, &contract_id_data_store);

    let contract_id_caller = env.register_contract(None, CallerContract);
    let caller_client = CallerContractClient::new(&env, contract_id_caller);

    let u1 = env.accounts().generate();
    client_data_store
        .with_source_account(&u1)
        .put(&bytes!(&env, 0x48656c6c6f20536f726f62616e21));

    let value = caller_client.try_get(&contract_id_data_store, &u1);
    assert_eq!(value, bytes!(&env, 0x48656c6c6f20536f726f62616e21));
}
```

This test uses a test account to invoke the `put` function on the `DataStoreContract`, in order to test the `get` function using a cross-contract call, as it is not possible to put data to the contract environment using a contract.

We can use `let u1 = env.accounts().generate()` to create an EOA, then, use `client_data_store.with_source_account(&u1).put(&bytes!(&env, 0x48656c6c6f20536f726f62616e21));` to test invoking the `put` function using the generated account. `with_source_account` method is what accomplishes our task.

We can use `let contract_id_caller = env.register_contract(None, CallerContract);` to register the contract implementation into our testing environment, and use `let caller_client = CallerContractClient::new(&env, contract_id_caller);` to create the client used for testing cross-contract calls.

## Solving the Quest

Now that we fully understand the Quest, we can attempt solving it. We will use both `get` and `get_self` to solve this Quest.

### Deploying the Soroban Contract

As usual, we will use `cargo build --target wasm32-unknown-unknown --release` to build the contract, and `cargo test` to see if everything works before publishing our contract. `cargo build` will create the `target/wasm32-unknown-unknown/release/soroban_auth_store_contract.wasm` file, which we will deploy to the Soroban network.

We will use

```bash
soroban deploy --wasm target/wasm32-unknown-unknown/release/soroban_auth_store_contract.wasm
```

to deploy our contract. If it succeeds, we will see a success message, followed by the contract id of our contract.

![Contract Deployment](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/kg0k1ix7qge907oxadih.png)

If your local RPC does not work, use `sq rpc -c` to change your RPC endpoint to an official one.

> Why Use My Own RPC Endpoint?
>
> The Remote Procedural Call (RPC) endpoint is a server that can execute custom RPC commands on the server. The Soroban blockchain is interacted using the RPC procedure to deploy contracts, invoke functions, and check data on-chain. The `soroban` CLI acts as the middleware which uses the RPC calls underneath to interact with the Soroban network.
>
> Using your own RPC endpoint helps with privacy, as other RPC servers might collect your IP address, log your calls, and so on to identify who you are. Running a local Soroban node helps with hiding this information from third-party actors, and helps with the decentralization of the Soroban network.

### `put`ting Data to Our Contract

After deploying the contract, if everything works well, we can use

```bash
soroban invoke --id [your contract id] --fn put --arg [value]
```

to save data to your contract environment. If we recall that the type of `value` was `Bytes`, we know that we need to supply the hexadecimal value of the "string" we are looking to save. We can convert the string value we need to hexadecimal without even leaving our terminal! Type

```bash
echo "Hello Soroban!" | xxd -p
```

to convert the `string` "Hello Soroban!" to its hexadecimal representation. Note that you should select a value that is longer than 10 characters.

![Hexadecimal Representation of "Hello Soroban!"](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/cg0wc6jwbaythxxr354m.png)

> #### What Does `xxd` Do?
>
> The `xxd` program creates a hexdump of the input given. A hexdump is the hexadecimal representation of a given file, string, or anything digital, which is actually how the computer stores given data. The `x` character represents the heXadecimal word, and the `d` character represents the dumping process.
>
> The `-p` option prints the output plainly into the standard output, which is without grouping and without the `string` representation. Remove the `-p` flag to test it!
>
> As with almost all Linux commands, you can use `man xxd` to learn more about the `xxd` command by reading the **man**ual page. Getting to know more about Linux commands makes you a more efficient programmer!

After we obtain the hexadecimal value of our message, let's `put` the value into our contract data.

![Put Call](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/6lxf77tz4njlxmt8hqr1.png)

### Using `get_self` to Retrieve Data

Using `get_self` to retrieve data is simple, as we don't need to supply any arguments. Simply call

```bash
soroban invoke --id [your contract id] --fn get_self
```
to retrieve the data you saved using the `put` function. You should receive a success message, followed by an array of bytes in decimal representation.

![Get Self Call](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/cd3vmzubcpuwq6gx4hur.png)

> #### How to Check if the Returned Value is Correct
>
> The returned value consists of some decimal values, which makes it hard to check if the received value is the same with the supplied value. Luckily, we can use Python to easily check if that is the case.
>
> ```python
> import re
>
> for i in re.findall("[0-9]+", "[72,101,108,108,111,32,83,111,114,111,98,97,110,33]"):
>     print(chr(int(i)), end="")
> ```
> 
> That is one cryptic code, but what it does is replacing all the decimals in the returned array, that is `[72,101,108,108,111,32,83,111,114,111,98,97,110,33]`, with their ASCII representations. We should see the original `string` we converted into its hexadecimal representation.
>
> ![Python Result](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/zzd6fh63wpz5malzxgps.png)
>
> Success!

### Using `get` to Retrieve Data

Using `get` to retrieve the data we supplied with our account is harder than the first method, but nothing impossible. We will use

```bash
soroban invoke --id [your contract id] --fn get --arg '{"object": { "accountId": { "publicKeyTypeEd25519": "[your raw public key in hexadecimal format]" } } }'
```

As you see, the hard part is constructing the `AccountId` type from the CLI, and finding the hexadecimal format of our public key. Since Soroban uses the JSON-RPC spec to communicate with clients, the complex types derive from the `object` class. Please check out the [wonderful guide from Smephite](https://gist.github.com/Smephite/09b40e842ef454effe4693e0d18246d7) for more information about Soroban types.

> #### JSON-RPC Types
>
> JSON stands for JavaScript Object Notation, which is a data interchange format which is derived from the JavaScript types. The reason complex types derive from the "object" class is due to JSON supporting only a handful types, which are `string`s, `number`s, `boolean`s, `null`, `object`, and `array`. All other classes are composed using these 6 classes.
>
> Soroban has 8 default JSON-RPC classes, where `u63` and `u32` are unsigned integer types, `i32` is the signed integer type, `static` is an enum type that can take the values `void`, `true`, `false`, and `ledgerKeyContractCode`, `object` is the same `object` type from JSON, `symbol` is a value that is limited by 10 bytes of length, `bitset` is a type that consists of at most `60` bits, and `status` is also an `enum` type that denotes errors.

Since we have access to Python on our workspace, we can use the `stellar_sdk` Python package, which has some great utility tools we can use. Since it is not preinstalled, we can use

```bash
pip install stellar_sdk
```

to install it to our workspace. Note that the package will cease to exist after your workspace becomes inactive. After installing `stellar_sdk`, we can enter Python and find our public key's hexadecimal representation using

```python
from stellar_sdk import Keypair

print(Keypair.from_public_key("[your public key]").raw_public_key().hex())
```

![Raw Public Key](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/up00nwhhmugixyzb75i0.png)

Great! After calling the `get` function, we should get a success message, followed by the data we saved using the `put` function into the contract.

![Get Invocation](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/6vg8fem2su2kcz1qfn69.png)

Success! Use `sq check 2` to claim your reward 🎉!