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.
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.
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:
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:
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:
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.
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 tocall
read-only functions. However, it tends to be more convenient to leave it in there for convenient switching betweencall
andsend
. 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.
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.
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.
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:
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.