Storage maps

Suppose that instead of maintaining one global variable `balance`, we would like to have a balance for each user (users will be identified by their STARK public keys).

Our first task will be to change the `balance` storage variable to a map from public key (user) to balance (instead of a single value). This can be done by simply adding an argument:

``````// A map from user (represented by account contract address)
// to their balance.
@storage_var
func balance(user: felt) -> (res: felt) {
}``````

In fact, the `@storage_var` decorator allows you to add multiple arguments to create even more complicated maps. The functions `balance.read()` and `balance.write()` will now have the following signatures:

``````func read{
syscall_ptr: felt*,
range_check_ptr,
pedersen_ptr: HashBuiltin*,
}(user: felt) -> (res: felt) {
}

func write{
syscall_ptr: felt*,
range_check_ptr,
pedersen_ptr: HashBuiltin*,
}(user: felt, value: felt) {
}``````

Note that the default value of all the entries in the map is 0.

In order to obtain the address of the account contract (or any other contract, in the case that the function was invoked by a contract) that invoked our function, we can use the `get_caller_address()` library function:

``````from starkware.starknet.common.syscalls import get_caller_address

// ...

`get_caller_address()` returns the address of the source contract that called this contract. It can be the address of the account contract or the address of another contract (if the function was invoked by another contract). When the contract is called directly (rather than through a contract), the function returns 0.

Note that if you use `get_caller_address()` in a function `foo()` that was called by another function `bar()` within your contract, it will still return the address of the contract that invoked `bar()` (or 0 if it was invoked directly).

Modifying the contract’s functions

Change the code of `increase_balance()` to:

``````from starkware.cairo.common.math import assert_nn

// Increases the balance of the user by the given amount.
@external
func increase_balance{
syscall_ptr: felt*,
pedersen_ptr: HashBuiltin*,
range_check_ptr,
}(amount: felt) {
// Verify that the amount is positive.
with_attr error_message(
"Amount must be positive. Got: {amount}.") {
assert_nn(amount);
}

// Obtain the address of the account contract.

// Read and update its balance.
balance.write(user, res + amount);
return ();
}``````

Note that we added a constraint that the value of `amount` must be nonnegative, by calling `assert_nn`. In order to obtain an indicative message in case of an error, we wrapped the function call with the `with_attr error_message(...)` block. See Retrieving the revert reason for more details.

Similarly, change the code of `get_balance()`. Here we chose to allow the caller to query any user (since StarkNet’s storage is not private anyway):

``````// Returns the balance of the given user.
@view
func get_balance{
syscall_ptr: felt*,
pedersen_ptr: HashBuiltin*,
range_check_ptr,
}(user: felt) -> (res: felt) {
return (res=res);
}``````

Compile and deploy

Save the new contract file as `user_auth.cairo`. You can find the full Cairo file here.

Compile and declare the contract:

``````starknet-compile user_auth.cairo \
--output user_auth_compiled.json \
--abi user_auth_abi.json
starknet declare --contract user_auth_compiled.json``````

Deploy the contract:

``starknet deploy --class_hash ${USER_AUTH_CLASS_HASH}`` where `${USER_AUTH_CLASS_HASH}` is the value of class_hash. Don’t forget to set the `STARKNET_NETWORK` and `STARKNET_WALLET` environment variables and deploy an account contract before running `starknet deploy`.

Interacting with the contract

Let’s update the balance:

``````starknet invoke \
--address ${CONTRACT_ADDRESS} \ --abi user_auth_abi.json \ --function increase_balance \ --inputs 4321`````` You can query the transaction status: ``starknet tx_status --hash TX_HASH`` Finally, after the transaction is executed (status `ACCEPTED_ON_L2` or `ACCEPTED_ON_L1`) you may query the user’s balance: ``````starknet call \ --address${CONTRACT_ADDRESS} \
--abi user_auth_abi.json \
--function get_balance \
--inputs ${ACCOUNT_ADDRESS}`````` You should get: ``4321`` Note that if you want to use the get_storage_at CLI command to query the balance of a specific user, you can no longer compute the relevant key by only supplying the name of the storage variable. That is because the balance storage variable now requires an additional argument, namely, the user key. Hence, you will need to supply the additional arguments when acquiring the key used in `get_storage_at`. In our case, this translates to the following Python code: ``````from starkware.starknet.public.abi import get_storage_var_address user = ACCOUNT_ADDRESS user_balance_key = get_storage_var_address('balance', user) print(f'Storage key for user {user}:\n{user_balance_key}')`````` Retrieving the revert reason Let’s try to invoke `increase_balance` with a negative amount: ``````starknet invoke \ --address${CONTRACT_ADDRESS} \
--abi user_auth_abi.json \
--function increase_balance \
--inputs -1000``````

Because this transaction is invalid (as the amount is negative), you will get an error from the StarkNet gateway that contains the following:

``{"code": "StarknetErrorCode.TRANSACTION_FAILED", "message": "Error at pc=0:38:\nGot an exception while executing a hint.\nCairo traceback (most recent call last):\nUnknown location (pc=0:522)\nUnknown location (pc=0:484)\nUnknown location (pc=0:590)\n\nError in the called contract (0x548245153813267af2d2793c6e5d60c40cb95f34d7404f2ce75550fafabede0):\nError at pc=0:6:\nGot an exception while executing a hint.\nCairo traceback (most recent call last):\nUnknown location (pc=0:155)\nError message: Amount must be positive. Got: -1000.\nUnknown location (pc=0:129)\n\nTraceback (most recent call last):\n  File \"<hint0>\", line 3, in <module>\nAssertionError: a = 3618502788666131213697322783095070105623107215331596699973092056135872019481 is out of range."}``

This indicates that the CLI could not estimate the transaction fee, because the transaction has failed. For the sake of demonstrating retrieving the revert reason, we will force the transaction to skip the fee estimation mechanism. To do so, add `--max_fee 100000000000000000` to the former invoke transaction, as follows:

``````starknet invoke \
--address ${CONTRACT_ADDRESS} \ --abi user_auth_abi.json \ --function increase_balance \ --inputs -1000 \ --max_fee 100000000000000000`````` After this, when querying the transaction status, you should get: ``````{ "tx_failure_reason": { "code": "TRANSACTION_FAILED", "error_message": "Error at pc=0:32:\nGot an exception while executing a hint.\nCairo traceback (most recent call last):\nUnknown location (pc=0:494)\nUnknown location (pc=0:453)\nUnknown location (pc=0:510)\n\nError in the called contract (0x3632c8d1265888e0eadb518cbf4a83d071d00cd8f946ec72fd661e69eea1963):\nError at pc=0:6:\nGot an exception while executing a hint.\nCairo traceback (most recent call last):\nUnknown location (pc=0:155)\nError message: Amount must be positive. Got: -1000.\nUnknown location (pc=0:129)\n\nTraceback (most recent call last):\n File \"<hint0>\", line 3, in <module>\nAssertionError: a = 3618502788666131213697322783095070105623107215331596699973092056135872019481 is out of range." }, "tx_status": "REJECTED" }`````` Notice that the error message entry states that the error location is unknown. This is because the StarkNet network is not aware of the source code and debug information of a contract. To retrieve the error location and reconstruct the traceback, add the path to the relevant compiled contract in the transaction status query, using the `--contracts` argument. To better display the error (and only it), add the `--error_message` flag as well: ``````starknet tx_status \ --hash TX_HASH \ --contracts${CONTRACT_ADDRESS}:user_auth_compiled.json \
--error_message``````

The output should look like:

``````Error at pc=0:28:
Got an exception while executing a hint.
Cairo traceback (most recent call last):
Unknown location (pc=0:494)
Unknown location (pc=0:453)
Unknown location (pc=0:510)

Error in the called contract (0x29cd5db92729052b3268471cf1b2327b61523565adeaa1d659236e806bd4b97):
math.cairo:47:5: Error at pc=0:6:
a = [range_check_ptr];
^*******************^
Got an exception while executing a hint.
Cairo traceback (most recent call last):
user_auth.cairo:15:6
func increase_balance{syscall_ptr: felt*, pedersen_ptr: HashBuiltin*, range_check_ptr}(
^**************^
Error message: Amount must be positive. Got: -1000.
user_auth.cairo:20:9
assert_nn(amount);
^***************^

Traceback (most recent call last):
File "<hint0>", line 3, in <module>
AssertionError: a = 3618502788666131213697322783095070105623107215331596699973092056135872019481 is out of range.``````

You should ignore the first part (before `Error in the called contract`) – it is caused by the account contract.