More features

Storage variable with multiple values

A storage variable does not have to be a single field element, it can also be a tuple of several field elements. For example:

// A mapping from user to a pair (min, max).
@storage_var
func range(user: felt) -> (res: (felt, felt)) {
}

You can read and write this value as follows:

@external
func extend_range{
    syscall_ptr: felt*,
    pedersen_ptr: HashBuiltin*,
    range_check_ptr,
}(user: felt) {
    let (min_max) = range.read(user);
    range.write(user, (min_max[0] - 1, min_max[1] + 1));
    return ();
}

Note that in this case the range.read() returns one item that is a pair. Thus, let (min, max) = range.read(user); will not work.

Storage variable with struct arguments

An argument of a storage variable may also be a struct or a tuple, as long as they don’t contain pointers (such types, that don’t contain pointers, are called felts-only types). For example:

struct User {
    first_name: felt,
    last_name: felt,
}

// A mapping from a user to 1 if they voted and 0 otherwise.
@storage_var
func user_voted(user: User) -> (res: felt) {
}

@external
func vote{
    syscall_ptr: felt*,
    pedersen_ptr: HashBuiltin*,
    range_check_ptr,
}(user: User) {
    user_voted.write(user, 1);
    return ();
}

Array arguments in calldata

An external function may get an array of field elements as an argument. In order to define an array named a, pass two consecutive arguments: a_len of type felt and a of type felt* (the first argument must be named a_len if the second argument is named a). For example:

@external
func compare_arrays(
    a_len: felt, a: felt*, b_len: felt, b: felt*
) {
    assert a_len = b_len;
    if (a_len == 0) {
        return ();
    }
    assert a[0] = b[0];
    return compare_arrays(
        a_len=a_len - 1, a=&a[1], b_len=b_len - 1, b=&b[1]
    );
}

In order to call compare_arrays with the arrays [10, 20, 30, 40] and [50, 60], you should pass the following inputs to starknet invoke:

starknet invoke \
    --address ${CONTRACT_ADDRESS} \
    --abi contract_abi.json \
    --function compare_arrays \
    --inputs 4 10 20 30 40 2 50 60

The first value, 4, is the length of the first array, then its 4 entires. After that, we have the length of the second arrays (2) followed by its entries. Note that calling compare_arrays with the aforementioned arguments will fail as the arrays are different.

A StarkNet contract using array arguments in external functions must have the range_check builtin, which is used to validate that the array’s length is nonnegative.

Passing tuples and structs in calldata

Calldata arguments and return values may be of any type that does not contain pointers. E.g., structs with felt members, tuples of felts and tuples of tuples of felts. For example:

struct Point {
    x: felt,
    y: felt,
}

@view
func sum_points(points: (Point, Point)) -> (res: Point) {
    return (
        res=Point(
        x=points[0].x + points[1].x,
        y=points[0].y + points[1].y),
    );
}

In order to call sum_points with the points (1, 2), (10, 20), you should pass the following inputs to starknet call:

starknet call \
    --address ${CONTRACT_ADDRESS} \
    --abi contract_abi.json \
    --function sum_points \
    --inputs 1 2 10 20

Passing arrays of structs

In a similar way, passing arrays of structs is supported, as long as the structs do not contain pointers:

@external
func sum_points_arr(a_len: felt, a: Point*) -> (res: Point) {
    if (a_len == 0) {
        return (res=Point(0, 0));
    }
    let (res) = sum_points_arr(a_len=a_len - 1, a=&a[1]);
    return (res=Point(x=res.x + a[0].x, y=res.y + a[0].y));
}

In order to call sum_points_arr with the 3 points (1, 2), (10, 20), (100, 200), you should pass the following inputs to starknet call:

starknet call \
    --address ${CONTRACT_ADDRESS} \
    --abi contract_abi.json \
    --function sum_points_arr \
    --inputs 3 1 2 10 20 100 200

Retrieving the transaction information

You can retrieve the transaction information (which includes, for example, the signature and the transaction fee), by using the get_tx_info() library function:

from starkware.starknet.common.syscalls import get_tx_info

func get_tx_max_fee{syscall_ptr: felt*}() -> (max_fee: felt) {
    let (tx_info) = get_tx_info();

    return (max_fee=tx_info.max_fee);
}

The returned value is a pointer to a TxInfo struct, which is defined as follows:

struct TxInfo {
    // The version of the transaction. It is fixed (currently, 1) in the OS, and should be
    // signed by the account contract.
    // This field allows invalidating old transactions, whenever the meaning of the other
    // transaction fields is changed (in the OS).
    version: felt,

    // The account contract from which this transaction originates.
    account_contract_address: felt,

    // The max_fee field of the transaction.
    max_fee: felt,

    // The signature of the transaction.
    signature_len: felt,
    signature: felt*,

    // The hash of the transaction.
    transaction_hash: felt,

    // The identifier of the chain.
    // This field can be used to prevent replay of testnet transactions on mainnet.
    chain_id: felt,

    // The transaction's nonce.
    nonce: felt,
}

Block number and timestamp

You can get the current block number and timestamp (seconds since unix epoch) by using the get_block_number() and get_block_timestamp() library functions.

from starkware.starknet.common.syscalls import (
    get_block_number,
    get_block_timestamp,
)

// ...

let (block_number) = get_block_number();
let (block_timestamp) = get_block_timestamp();

Note that both of the above functions require the implicit argument syscall_ptr. Presently, the result of get_block_timestamp() is not enforced by the StarkNet OS or Core contract (i.e., the sequencer may choose an arbitrary timestamp). In the future, some restrictions on the new timestamp will be added. Also note that the block timestamp is the time at the beginning of the block creation, which can differ significantly from the time the block is accepted on L1.