Skip to content

Commit 0197073

Browse files
authored
feat: add full user journey example (OpenZeppelin#148)
Resolves OpenZeppelin#140, OpenZeppelin#149 This also fixes a long-standing issue we had where we couldn't compile contracts without some hacky feature flags because of multiple panic handler implementations. It turns out the fix was `target_arch = "wasm32"` instead of `target_arch = "wasm32-unknown-unknown"`.
1 parent f660b75 commit 0197073

File tree

18 files changed

+273
-27
lines changed

18 files changed

+273
-27
lines changed

.github/scripts/check-wasm.sh

+1-1
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ check_wasm () {
1919
get_example_crate_names () {
2020
# shellcheck disable=SC2038
2121
# NOTE: optimistically relying on the 'name = ' string at Cargo.toml file
22-
find ./examples -type f -name "Cargo.toml" | xargs grep 'name = ' | grep -oE '".*"' | tr -d "'\""
22+
find ./examples -maxdepth 2 -type f -name "Cargo.toml" | xargs grep 'name = ' | grep -oE '".*"' | tr -d "'\""
2323
}
2424

2525
NIGHTLY_TOOLCHAIN=${NIGHTLY_TOOLCHAIN:-nightly}

.github/workflows/check.yml

+7-3
Original file line numberDiff line numberDiff line change
@@ -93,12 +93,16 @@ jobs:
9393
submodules: true
9494
- name: Install stable
9595
uses: dtolnay/rust-toolchain@stable
96+
with:
97+
target: wasm32-unknown-unknown
9698
- name: cargo install cargo-hack
9799
uses: taiki-e/install-action@cargo-hack
98-
# Intentionally no target specifier; see https://github.com/jonhoo/rust-ci-conf/pull/4
99-
# `--feature-powerset` runs for every combination of features.
100+
# Intentionally no target specifier; see https://github.com/jonhoo/rust-ci-conf/pull/4
101+
# `--feature-powerset` runs for every combination of features. Note that
102+
# target in this context means one of `--lib`, `--bin`, etc, and not the
103+
# target triple.
100104
- name: cargo hack
101-
run: cargo hack --feature-powerset check --depth 2 --release
105+
run: cargo hack check --feature-powerset --depth 2 --release --target wasm32-unknown-unknown --skip std --workspace --exclude e2e --exclude basic-example-script
102106
typos:
103107
runs-on: ubuntu-latest
104108
name: ubuntu / stable / typos

.github/workflows/nostd.yml

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1-
name: no-std
21
# This workflow checks whether the library is able to run without the std
32
# library. See `check.yml` for information about how the concurrency
43
# cancellation and workflow triggering works.
4+
name: no-std
55
permissions:
66
contents: read
77
on:
@@ -29,4 +29,4 @@ jobs:
2929
- name: rustup target add ${{ matrix.target }}
3030
run: rustup target add ${{ matrix.target }}
3131
- name: cargo check
32-
run: cargo check --release --target ${{ matrix.target }} --no-default-features --all-features
32+
run: cargo check --release --target ${{ matrix.target }} --no-default-features

Cargo.lock

+24-2
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

+6-3
Original file line numberDiff line numberDiff line change
@@ -4,25 +4,28 @@ members = [
44
"lib/crypto",
55
"lib/motsu",
66
"lib/motsu-proc",
7+
"lib/e2e",
8+
"lib/e2e-proc",
79
"examples/erc20",
810
"examples/erc721",
911
"examples/merkle-proofs",
1012
"examples/ownable",
1113
"examples/access-control",
12-
"lib/e2e",
13-
"lib/e2e-proc",
14+
"examples/basic/token",
15+
"examples/basic/script",
1416
]
1517
default-members = [
1618
"contracts",
1719
"lib/crypto",
1820
"lib/motsu",
1921
"lib/motsu-proc",
22+
"lib/e2e-proc",
2023
"examples/erc20",
2124
"examples/erc721",
2225
"examples/merkle-proofs",
2326
"examples/ownable",
2427
"examples/access-control",
25-
"lib/e2e-proc",
28+
"examples/basic/token",
2629
]
2730

2831
# Explicitly set the resolver to version 2, which is the default for packages

README.md

+5
Original file line numberDiff line numberDiff line change
@@ -57,10 +57,15 @@ impl Erc20Example { }
5757
For a more complex display of what this library offers, refer to our
5858
[examples](./examples).
5959

60+
For a full example that includes deploying and querying a contract, see the
61+
[basic] example.
62+
6063
For more information on what this library will include in the future, see our
6164
[roadmap].
6265

6366
[specify a git dependency]: https://doc.rust-lang.org/cargo/reference/specifying-dependencies.html#specifying-dependencies-from-git-repositories
67+
[examples]: ./examples
68+
[basic]: ./examples/basic
6469
[roadmap]: https://github.com/OpenZeppelin/rust-contracts-stylus/milestone/1
6570

6671
## Contribute

contracts/src/access/mod.rs

+1-3
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
11
//! Contracts implementing access control mechanisms.
2-
3-
pub mod ownable;
4-
52
pub mod control;
3+
pub mod ownable;

contracts/src/lib.rs

+1-3
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,10 @@ extern crate alloc;
77
static ALLOC: mini_alloc::MiniAlloc = mini_alloc::MiniAlloc::INIT;
88

99
pub mod access;
10-
1110
pub mod token;
12-
1311
pub mod utils;
1412

15-
#[cfg(not(any(feature = "std", target_arch = "wasm32-unknown-unknown")))]
13+
#[cfg(target_arch = "wasm32")]
1614
#[panic_handler]
1715
fn panic(_info: &core::panic::PanicInfo) -> ! {
1816
loop {}

examples/basic/README.md

+47
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
# Basic Token Example
2+
3+
This example showcases an end-to-end user journey of how to write, deploy and
4+
query a smart contract written using our library.
5+
6+
## Running the example
7+
8+
There are two crates in the example: a library crate for our contract
9+
implementation, which is a simple `ERC-20` token extended with `Metadata`, and
10+
a binary crate, which holds the deployment script.
11+
12+
The deployment script uses [`koba`] to deploy the token with name `Test Token`
13+
and symbol `TTK`. This means we should have a `solc` installation with a version
14+
previous to `0.8.25` (see [limitations]).
15+
16+
Before running the example, set the `PRIVATE_KEY` const variable and compile
17+
your contract with:
18+
19+
```bash
20+
cargo build --release --target wasm32-unknown-unknown
21+
```
22+
23+
You should now be able to run your contract with:
24+
25+
```bash
26+
$ cargo run -p basic-example-script
27+
wasm data fee: Ξ0.000097
28+
init code size: 17.0 KB
29+
deploying to RPC: https://sepolia-rollup.arbitrum.io/rpc
30+
deployed code: 0x7E57f52Bb61174DCE87deB10c4B683a550b39e8F
31+
deployment tx hash: 0xc627970c65caa65b3b87703ecf2d26d83520c3ac570d76c72f2d0a04b9895d91
32+
activating contract: 0x7E57f52Bb61174DCE87deB10c4B683a550b39e8F
33+
activated with 2651090 gas
34+
activation tx hash: 0x88c992c4c6e36fd2f49f2b30ea12412a2d5436bbfe80df8d10606abdb1f3f39d
35+
```
36+
37+
Note that the script asserts that the deployed contract has the correct name and
38+
symbol.
39+
40+
[`koba`]: https://github.com/OpenZeppelin/koba
41+
[limitations]: https://github.com/OpenZeppelin/koba#limitations
42+
43+
## Why two crates?
44+
45+
This split is necessary because we need to compile the contract to `wasm`,
46+
however, the script depends on `alloy`, which in turn depends on `getrandom`
47+
which is not compatible with wasm targets.

examples/basic/script/Cargo.toml

+14
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
[package]
2+
name = "basic-example-script"
3+
edition.workspace = true
4+
license.workspace = true
5+
repository.workspace = true
6+
publish = false
7+
version = "0.0.0"
8+
9+
[dependencies]
10+
basic-example = { path = "../token" }
11+
alloy-primitives.workspace = true
12+
alloy.workspace = true
13+
koba.workspace = true
14+
tokio.workspace = true

examples/basic/script/src/main.rs

+94
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
use alloy::{
2+
network::EthereumWallet, primitives::Address, providers::ProviderBuilder,
3+
signers::local::PrivateKeySigner, sol, sol_types::SolConstructor,
4+
};
5+
use koba::config::Deploy;
6+
7+
sol!(
8+
#[sol(rpc)]
9+
contract BasicToken {
10+
constructor(string memory name_, string memory symbol_);
11+
12+
function name() external view returns (string name);
13+
function symbol() external view returns (string symbol);
14+
}
15+
);
16+
17+
const RPC_URL: &str = "https://sepolia-rollup.arbitrum.io/rpc";
18+
const PRIVATE_KEY: &str = "your private key";
19+
const TOKEN_NAME: &str = "Test Token";
20+
const TOKEN_SYMBOL: &str = "TTK";
21+
22+
#[tokio::main]
23+
async fn main() {
24+
let contract_address = deploy().await;
25+
26+
// WARNING: Please use a more secure method for storing your privaket key
27+
// than a string at the top of this file. The following code is for testing
28+
// purposes only.
29+
let signer = PRIVATE_KEY
30+
.parse::<PrivateKeySigner>()
31+
.expect("should parse the private key");
32+
let wallet = EthereumWallet::from(signer);
33+
34+
let rpc_url = RPC_URL.parse().expect("should parse rpc url");
35+
let provider = ProviderBuilder::new()
36+
.with_recommended_fillers()
37+
.wallet(wallet)
38+
.on_http(rpc_url);
39+
40+
let contract = BasicToken::new(contract_address, &provider);
41+
42+
let call_result = contract.name().call().await.unwrap();
43+
assert_eq!(call_result.name, TOKEN_NAME.to_owned());
44+
45+
let call_result = contract.symbol().call().await.unwrap();
46+
assert_eq!(call_result.symbol, TOKEN_SYMBOL.to_owned());
47+
}
48+
49+
/// Deploy a `BasicToken` contract to `RPC_URL` using `koba`.
50+
async fn deploy() -> Address {
51+
let args = BasicToken::constructorCall {
52+
name_: TOKEN_NAME.to_owned(),
53+
symbol_: TOKEN_SYMBOL.to_owned(),
54+
};
55+
let args = alloy::hex::encode(args.abi_encode());
56+
57+
let manifest_dir =
58+
std::env::current_dir().expect("should get current dir from env");
59+
60+
// NOTE: It's expected that you compiled your contract beforehand.
61+
//
62+
// You should run `cargo build --release --target wasm32-unknown-unknown` to
63+
// get a wasm binary at `target/wasm32-unknown-unknown/release/{name}.wasm`.
64+
let wasm_path = manifest_dir
65+
.join("target")
66+
.join("wasm32-unknown-unknown")
67+
.join("release")
68+
.join("basic_example.wasm");
69+
let sol_path = manifest_dir
70+
.join("examples")
71+
.join("basic")
72+
.join("token")
73+
.join("src")
74+
.join("constructor.sol");
75+
76+
let config = Deploy {
77+
generate_config: koba::config::Generate {
78+
wasm: wasm_path.clone(),
79+
sol: sol_path,
80+
args: Some(args),
81+
legacy: false,
82+
},
83+
auth: koba::config::PrivateKey {
84+
private_key_path: None,
85+
private_key: Some(PRIVATE_KEY.to_owned()),
86+
keystore_path: None,
87+
keystore_password_path: None,
88+
},
89+
endpoint: RPC_URL.to_owned(),
90+
deploy_only: false,
91+
};
92+
93+
koba::deploy(&config).await.expect("should deploy contract")
94+
}

examples/basic/token/Cargo.toml

+20
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
[package]
2+
name = "basic-example"
3+
edition.workspace = true
4+
license.workspace = true
5+
repository.workspace = true
6+
publish = false
7+
version = "0.0.0"
8+
9+
[dependencies]
10+
openzeppelin-stylus = { path = "../../../contracts" }
11+
alloy-primitives.workspace = true
12+
stylus-sdk.workspace = true
13+
stylus-proc.workspace = true
14+
mini-alloc.workspace = true
15+
16+
[features]
17+
std = []
18+
19+
[lib]
20+
crate-type = ["lib", "cdylib"]
+16
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
// SPDX-License-Identifier: MIT
2+
pragma solidity ^0.8.21;
3+
4+
contract BasicToken {
5+
mapping(address account => uint256) private _balances;
6+
mapping(address account => mapping(address spender => uint256))
7+
private _allowances;
8+
uint256 private _totalSupply;
9+
string private _name;
10+
string private _symbol;
11+
12+
constructor(string memory name_, string memory symbol_) {
13+
_name = name_;
14+
_symbol = symbol_;
15+
}
16+
}

examples/basic/token/src/lib.rs

+31
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
#![cfg_attr(not(test), no_main, no_std)]
2+
extern crate alloc;
3+
4+
use alloc::vec::Vec;
5+
6+
use alloy_primitives::{Address, U256};
7+
use openzeppelin_stylus::token::erc20::{extensions::Erc20Metadata, Erc20};
8+
use stylus_sdk::prelude::{entrypoint, external, sol_storage};
9+
10+
sol_storage! {
11+
#[entrypoint]
12+
struct Erc20Example {
13+
#[borrow]
14+
Erc20 erc20;
15+
#[borrow]
16+
Erc20Metadata metadata;
17+
}
18+
}
19+
20+
#[external]
21+
#[inherit(Erc20, Erc20Metadata)]
22+
impl Erc20Example {
23+
pub fn mint(
24+
&mut self,
25+
account: Address,
26+
value: U256,
27+
) -> Result<(), Vec<u8>> {
28+
self.erc20._mint(account, value)?;
29+
Ok(())
30+
}
31+
}

examples/merkle-proofs/src/lib.rs

+1-5
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,7 @@ use stylus_sdk::{
1717
#[global_allocator]
1818
static ALLOC: mini_alloc::MiniAlloc = mini_alloc::MiniAlloc::INIT;
1919

20-
#[cfg(not(any(
21-
test,
22-
feature = "std",
23-
target_arch = "wasm32-unknown-unknown"
24-
)))]
20+
#[cfg(target_arch = "wasm32")]
2521
#[panic_handler]
2622
fn panic(_info: &core::panic::PanicInfo) -> ! {
2723
loop {}

0 commit comments

Comments
 (0)