Arbitrum Stylus logo

Stylus by Example

Using the CLI

In this example, we'll cover a typical workflow for deploying a Rust smart contract to a local Stylus dev node and how to manually test your smart contract with some handy CLI tools. This guide assumes you have already followed the instructions from Arbitrum docs to get your environment set up.

Requirements

Deploying and Testing the Counter Contract

We'll be using Cargo Stylus to set up and deploy our smart contract and Foundry's Cast CLI tool to call and send transactions to our deployed smart contract.

~/projects

1❯ cargo stylus new counter
2Cloning into 'counter'...
3remote: Enumerating objects: 236, done.
4remote: Counting objects: 100% (78/78), done.
5remote: Compressing objects: 100% (45/45), done.
6remote: Total 236 (delta 40), reused 48 (delta 27), pack-reused 158
7Receiving objects: 100% (236/236), 650.36 KiB | 3.89 MiB/s, done.
8Resolving deltas: 100% (118/118), done.
9Initialized Stylus project at: /Users/your_name/projects/counter
1❯ cargo stylus new counter
2Cloning into 'counter'...
3remote: Enumerating objects: 236, done.
4remote: Counting objects: 100% (78/78), done.
5remote: Compressing objects: 100% (45/45), done.
6remote: Total 236 (delta 40), reused 48 (delta 27), pack-reused 158
7Receiving objects: 100% (236/236), 650.36 KiB | 3.89 MiB/s, done.
8Resolving deltas: 100% (118/118), done.
9Initialized Stylus project at: /Users/your_name/projects/counter

Open the newly created counter folder in VS Code. Take a look at src/lib.rs, important focal points below:

src/lib.rs

1sol_storage! {
2    #[entrypoint]
3    pub struct Counter {
4        uint256 number;
5    }
6}
7
8/// Define an implementation of the generated Counter struct, defining a set_number
9/// and increment method using the features of the Stylus SDK.
10#[external]
11impl Counter {
12    /// Gets the number from storage.
13    pub fn number(&self) -> Result<U256, Vec<u8>> {
14        Ok(self.number.get())
15    }
16
17    /// Sets a number in storage to a user-specified value.
18    pub fn set_number(&mut self, new_number: U256) -> Result<(), Vec<u8>> {
19        self.number.set(new_number);
20        Ok(())
21    }
22
23    /// Increments number and updates it values in storage.
24    pub fn increment(&mut self) -> Result<(), Vec<u8>> {
25        let number = self.number.get();
26        self.set_number(number + U256::from(1))
27    }
28}
1sol_storage! {
2    #[entrypoint]
3    pub struct Counter {
4        uint256 number;
5    }
6}
7
8/// Define an implementation of the generated Counter struct, defining a set_number
9/// and increment method using the features of the Stylus SDK.
10#[external]
11impl Counter {
12    /// Gets the number from storage.
13    pub fn number(&self) -> Result<U256, Vec<u8>> {
14        Ok(self.number.get())
15    }
16
17    /// Sets a number in storage to a user-specified value.
18    pub fn set_number(&mut self, new_number: U256) -> Result<(), Vec<u8>> {
19        self.number.set(new_number);
20        Ok(())
21    }
22
23    /// Increments number and updates it values in storage.
24    pub fn increment(&mut self) -> Result<(), Vec<u8>> {
25        let number = self.number.get();
26        self.set_number(number + U256::from(1))
27    }
28}

It's not necessary to fully understand this code for this example. For now, just note that there are 3 external methods available on this smart contract: number, set_number, and increment. These functions form the public API for the contract. Their functionality is fairly self explanatory, they allow you to fetch the current count, set the counter to some arbitrary value, or increment the current value by one.

Let's go ahead and deploy the contract to our Local Stylus Dev Node. When you set up your local dev node, two addresses are funded with "local ETH". We'll use the local dev address 0x3f1Eae7D46d88F08fc2F8ed27FCb2AB183EB2d0E with the private key 0xb6b15c8cb491557369f3c7d2c287b053eb229daa9c22138887752191c9520659 for this example.

From the CLI, with current directory set to the counter folder:

~/projects/counter

1cargo stylus deploy -e http://localhost:8547 --private-key 0xb6b15c8cb491557369f3c7d2c287b053eb229daa9c22138887752191c9520659
1cargo stylus deploy -e http://localhost:8547 --private-key 0xb6b15c8cb491557369f3c7d2c287b053eb229daa9c22138887752191c9520659

After a minute or so, the counter project will be compiled into a single WASM file, then that file will be compressed before being deployed and then 'activated' onchain. Your terminal should display something like this:

~/projects/counter

1Finished `release` profile [optimized] target(s) in 1.81s
2stripped custom section from user wasm to remove any sensitive data
3contract size: 7.3 KB
4wasm size: 21.1 KB
5File used for deployment hash: ./Cargo.lock
6File used for deployment hash: ./Cargo.toml
7File used for deployment hash: ./examples/counter.rs
8File used for deployment hash: ./rust-toolchain.toml
9File used for deployment hash: ./src/lib.rs
10File used for deployment hash: ./src/main.rs
11project metadata hash computed on deployment: "1127f72e245f7aefced1b75129da2d50e25f1911a703748299031df47408ed50"
12stripped custom section from user wasm to remove any sensitive data
13contract size: 7.3 KB
14wasm data fee: 0.000065 ETH
15deployed code at address: 0x677c7e0584b0202417762ce06e89dbc5935a7399
16deployment tx hash: 0x073042d1d3303dcf0a9f9478461764d39dfe34420e33ac1a3ba8c7dfc57f5ad9
17contract activated and ready onchain with tx hash: 0xd3db95a5d20b003ac34efd36e16da365a99a3d8debd531d97370b44f258b1526
1Finished `release` profile [optimized] target(s) in 1.81s
2stripped custom section from user wasm to remove any sensitive data
3contract size: 7.3 KB
4wasm size: 21.1 KB
5File used for deployment hash: ./Cargo.lock
6File used for deployment hash: ./Cargo.toml
7File used for deployment hash: ./examples/counter.rs
8File used for deployment hash: ./rust-toolchain.toml
9File used for deployment hash: ./src/lib.rs
10File used for deployment hash: ./src/main.rs
11project metadata hash computed on deployment: "1127f72e245f7aefced1b75129da2d50e25f1911a703748299031df47408ed50"
12stripped custom section from user wasm to remove any sensitive data
13contract size: 7.3 KB
14wasm data fee: 0.000065 ETH
15deployed code at address: 0x677c7e0584b0202417762ce06e89dbc5935a7399
16deployment tx hash: 0x073042d1d3303dcf0a9f9478461764d39dfe34420e33ac1a3ba8c7dfc57f5ad9
17contract activated and ready onchain with tx hash: 0xd3db95a5d20b003ac34efd36e16da365a99a3d8debd531d97370b44f258b1526

Note the contract activated and ready onchain with tx hash: 0xd3db95a5d20b003ac34efd...44f258b1526 statement. This is the address your contract has been deployed to, so take note of that address. Select it and copy it to your clipboard, we'll be using it in the next step to call it.

We'll now use cast, which was installed as part of our Foundry CLI suite, to call the contract. Later we'll send a transaction to the contract. The difference between call and send is that call costs no gas, so it can only be used to invoke read-only functions.

~/projects/counter

1❯ cast call --rpc-url 'http://localhost:8547' --private-key 0xb6b15c8cb491557369f3c7d2c287b053eb229daa9c22138887752191c9520659 0x677c7e0584b0202417762ce06e89dbc5935a7399 "number()(uint256)"
20
1❯ cast call --rpc-url 'http://localhost:8547' --private-key 0xb6b15c8cb491557369f3c7d2c287b053eb229daa9c22138887752191c9520659 0x677c7e0584b0202417762ce06e89dbc5935a7399 "number()(uint256)"
20

Let's break down the above call. We are passing two flags to it. --rpc-url corresponds to the RPC URL of the Stylus chain we deployed on. --private-key is the provided private key used for development purposes. It corresponds to the address 0x3f1eae7d46d88f08fc2f8ed27fcb2ab183eb2d0e.

Technically, we do not need to include a private key to call a contract, since there is no need for any gas to call read-only functions. However, it tends to be more convenient to leave it in there for convenient switching between call and send. It's usually quicker to press the up key on your terminal to recall your last command and then edit it by navigating to the word or words you need to change.

After the private key, we include the contract address, which on my machine was 0x677c7e0584b0202417762ce06e89dbc5935a7399 (but will likely differ on yours). So we are letting cast know we wish to call our newly deployed contract. We now need to tell cast how to interpret the API function that we're invoking. We do that with the function's Solidity-style signature. The "number()(uint256)" argument says that we wish to call the number external function, it takes no arguments and it returns a 256-bit integer as denoted in the second pair of parentheses. The uint256 syntax comes from the types listed in the Solidity docs.

The result was 0, which is what we expect a new counter to be initialized to. Let's try incrementing it! This time, we'll invoke the send command.

~/projects/counter

1❯ cast send --rpc-url 'http://localhost:8547' --private-key 0xb6b15c8cb491557369f3c7d2c287b053eb229daa9c22138887752191c9520659 0x677c7e0584b0202417762ce06e89dbc5935a7399 "increment()"
2
3blockHash               0x581f5140fe891f798f4829a7bc2826fbafceaaa2670f12fd09f1cbe3633a2b2d
4blockNumber             167
5contractAddress
6cumulativeGasUsed       45489
7effectiveGasPrice       100000000
8gasUsed                 45489
9logs                    []
10logsBloom               0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
11root
12status                  1
13transactionHash         0x6229739622a69d3c573e7fe2364263ae825ecd731205c6ab367b17ba326d03cb
14transactionIndex        1
15type                    2
1❯ cast send --rpc-url 'http://localhost:8547' --private-key 0xb6b15c8cb491557369f3c7d2c287b053eb229daa9c22138887752191c9520659 0x677c7e0584b0202417762ce06e89dbc5935a7399 "increment()"
2
3blockHash               0x581f5140fe891f798f4829a7bc2826fbafceaaa2670f12fd09f1cbe3633a2b2d
4blockNumber             167
5contractAddress
6cumulativeGasUsed       45489
7effectiveGasPrice       100000000
8gasUsed                 45489
9logs                    []
10logsBloom               0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
11root
12status                  1
13transactionHash         0x6229739622a69d3c573e7fe2364263ae825ecd731205c6ab367b17ba326d03cb
14transactionIndex        1
15type                    2

Nice! Our transaction went through successfully and we even received a transactionHash and detailed logs in the CLI.

Let's now check to see if our counter was properly incremented by calling the number() again method like we did in the last step.

~/projects/counter

1❯ cast call --rpc-url 'http://localhost:8547' --private-key 0xb6b15c8cb491557369f3c7d2c287b053eb229daa9c22138887752191c9520659 0x677c7e0584b0202417762ce06e89dbc5935a7399 "number()(uint256)"
21
1❯ cast call --rpc-url 'http://localhost:8547' --private-key 0xb6b15c8cb491557369f3c7d2c287b053eb229daa9c22138887752191c9520659 0x677c7e0584b0202417762ce06e89dbc5935a7399 "number()(uint256)"
21

Great! Our counter now displays a value of 1! We successfully changed our contract's state.

To demonstrate passing arguments to cast, let's try setting the counter to 5 by invoking the set_number function. Note, that instead of calling set_number we instead call setNumber, which is the Solidity-compatible camel casing for external functions (as opposed to Rust's snake casing standard). By using Solidity ABI standards for external methods, we can more easily maintain cross-contract compatiblity between Rust and Solidity smart contracts.

~/projects/counter

1❯ cast send --rpc-url 'http://localhost:8547' --private-key 0xb6b15c8cb491557369f3c7d2c287b053eb229daa9c22138887752191c9520659 0x677c7e0584b0202417762ce06e89dbc5935a7399 "setNumber(uint256)" 5
2
3blockHash               0x3100b0c4ea268081f9b9a2cf1daf0a66c33cb6d8f1c041de4e2a787293c33ab9
4blockNumber             169
5contractAddress
6cumulativeGasUsed       28073
7effectiveGasPrice       100000000
8gasUsed                 28073
9logs                    []
10logsBloom               0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
11root
12status                  1
13transactionHash         0x9129a971d919732930937654ee1b6790f2b4dcee5e8ad56aab45dee37784e0a5
14transactionIndex        1
15type                    2
1❯ cast send --rpc-url 'http://localhost:8547' --private-key 0xb6b15c8cb491557369f3c7d2c287b053eb229daa9c22138887752191c9520659 0x677c7e0584b0202417762ce06e89dbc5935a7399 "setNumber(uint256)" 5
2
3blockHash               0x3100b0c4ea268081f9b9a2cf1daf0a66c33cb6d8f1c041de4e2a787293c33ab9
4blockNumber             169
5contractAddress
6cumulativeGasUsed       28073
7effectiveGasPrice       100000000
8gasUsed                 28073
9logs                    []
10logsBloom               0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
11root
12status                  1
13transactionHash         0x9129a971d919732930937654ee1b6790f2b4dcee5e8ad56aab45dee37784e0a5
14transactionIndex        1
15type                    2

Note how we passed in the number 5 as the argument to setNumber(uint256). cast was expecting a single 256-bit integer to be passed in. Now, let's check our work:

~/projects/counter

1❯ cast call --rpc-url 'http://localhost:8547' --private-key 0xb6b15c8cb491557369f3c7d2c287b053eb229daa9c22138887752191c9520659 0x677c7e0584b0202417762ce06e89dbc5935a7399 "number()(uint256)"
25
1❯ cast call --rpc-url 'http://localhost:8547' --private-key 0xb6b15c8cb491557369f3c7d2c287b053eb229daa9c22138887752191c9520659 0x677c7e0584b0202417762ce06e89dbc5935a7399 "number()(uint256)"
25

It worked perfect! Our counter now has the value 5. If we increment() again, it will be increased to 6. Using cast can help you easily test the functionality of your contracts in your local environment. For more information, see Foundry's Cast documentation.