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

1❯ cargo stylus deploy -e http://localhost:8547 --private-key 0xb6b15c8cb491557369f3c7d2c287b053eb229daa9c22138887752191c9520659
1❯ cargo 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 [optimized] target(s) in 0.28s
2Reading WASM file at /Users/your_name/projects/counter/target/wasm32-unknown-unknown/release/stylus_hello_world.wasm
3Uncompressed WASM size: 32.3 KB
4Compressed WASM size to be deployed onchain: 11.5 KB
5Connecting to Stylus RPC endpoint: http://localhost:8547
6Program succeeded Stylus onchain activation checks with Stylus version: 1
7Deployer address: 0x3f1eae7d46d88f08fc2f8ed27fcb2ab183eb2d0e
8
9====DEPLOYMENT====
10Deploying program to address 0x677c7e0584b0202417762ce06e89dbc5935a7399
11Base fee: 0.100000000 gwei
12Estimated gas for deployment: 2539640 gas units
13Submitting deployment tx...
14Confirmed deployment tx 0xd07276221864ce0d7d1d18ba2602b58144b2fdd37bb9e1087343804732fd6e4b
15Gas units used 2539393, effective gas price 0.100000000 gwei
16Transaction fee: 0.000253939300000000 ETH
17
18====ACTIVATION====
19Activating program at address 0x677c7e0584b0202417762ce06e89dbc5935a7399
20Base fee: 0.100000000 gwei
21Estimated gas for activation: 14044675 gas units
22Submitting activation tx...
23Confirmed activation tx 0xc839dd989d1b0383a1e915d40ea355bc96a44cde74d3b0e81a8ea0ebcdbcabd4
24Gas units used 14044666, effective gas price 0.100000000 gwei
25Transaction fee: 0.001404466600000000 ETH
1Finished release [optimized] target(s) in 0.28s
2Reading WASM file at /Users/your_name/projects/counter/target/wasm32-unknown-unknown/release/stylus_hello_world.wasm
3Uncompressed WASM size: 32.3 KB
4Compressed WASM size to be deployed onchain: 11.5 KB
5Connecting to Stylus RPC endpoint: http://localhost:8547
6Program succeeded Stylus onchain activation checks with Stylus version: 1
7Deployer address: 0x3f1eae7d46d88f08fc2f8ed27fcb2ab183eb2d0e
8
9====DEPLOYMENT====
10Deploying program to address 0x677c7e0584b0202417762ce06e89dbc5935a7399
11Base fee: 0.100000000 gwei
12Estimated gas for deployment: 2539640 gas units
13Submitting deployment tx...
14Confirmed deployment tx 0xd07276221864ce0d7d1d18ba2602b58144b2fdd37bb9e1087343804732fd6e4b
15Gas units used 2539393, effective gas price 0.100000000 gwei
16Transaction fee: 0.000253939300000000 ETH
17
18====ACTIVATION====
19Activating program at address 0x677c7e0584b0202417762ce06e89dbc5935a7399
20Base fee: 0.100000000 gwei
21Estimated gas for activation: 14044675 gas units
22Submitting activation tx...
23Confirmed activation tx 0xc839dd989d1b0383a1e915d40ea355bc96a44cde74d3b0e81a8ea0ebcdbcabd4
24Gas units used 14044666, effective gas price 0.100000000 gwei
25Transaction fee: 0.001404466600000000 ETH

Note the Activating program at address 0x677c..7399 statement. The address your contract gets deployed to will likely differ, 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

1cast call --rpc-url 'http://localhost:8547' --private-key 0xb6b15c8cb491557369f3c7d2c287b053eb229daa9c22138887752191c9520659 0x677c7e0584b0202417762ce06e89dbc5935a7399 "number()(uint256)"
25
1cast 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.