New contract from sc-meta new --template empty
This walkthrough records the output of sc-meta new --template empty, then adds
the smallest customization on top (one storage mapper, one endpoint, one view).
The commands and output were manually verified at authoring time with
rustc 1.93.0, cargo 1.93.0, and multiversx-sc-meta 0.64.1; this reference
page is not compiled or regenerated continuously by the cookbook CI.
Prerequisites
- Rust via
rustup, with thewasm32v1-nonetarget installed (see "A version note" below for why notwasm32-unknown-unknown). sc-meta(cargo install multiversx-sc-meta).- Optional:
wasm-opt, for size-optimized release builds (this recipe was verified without it).
Step 1: the bare scaffold
sc-meta new --template empty --name greeter
This does more than copy files: it renames the trait, the crate names, the module paths, and the scenario references throughout, all in one pass. Two things stood out during that authoring-time run:
- There is no
sc-config.toml, nowasm/, and nooutput/yet. A baresc-meta newoutput does not have these; project-anatomy overviews often show them as if every contract project always does. - The scenario test files' function names do not get renamed. The file is
renamed and the scenario path inside is updated, but the test function itself is
still
fn empty_go(), notfn greeter_go(). Not a bug, just something to expect when you diff a freshly generated project.
Step 2: add a storage mapper, an endpoint, and a proxy
#![no_std]
use multiversx_sc::imports::*;
pub mod greeter_proxy;
/// A minimal greeter contract. Starts from `sc-meta new --template empty
/// --name greeter` (the exact, unmodified output of that command) with one
/// endpoint and one storage mapper added on top — the smallest real next step
/// after scaffolding, and the natural bridge into the
/// storage-mapper-decision-table recipe.
#[multiversx_sc::contract]
pub trait Greeter {
#[init]
fn init(&self) {}
#[upgrade]
fn upgrade(&self) {}
/// Stores a greeting message for the calling address. Overwrites any
/// previous greeting for the same caller.
#[endpoint(setGreeting)]
fn set_greeting(&self, message: ManagedBuffer) {
let caller = self.blockchain().get_caller();
self.greeting(&caller).set(message);
}
/// Reads back the greeting message stored for `address`. Returns an
/// empty ManagedBuffer if that address never called setGreeting.
#[view(getGreeting)]
#[storage_mapper("greeting")]
fn greeting(&self, address: &ManagedAddress) -> SingleValueMapper<ManagedBuffer>;
}
Generating the proxy has a real chicken-and-egg order to it. Adding
pub mod greeter_proxy; before the file exists fails the build
(error[E0583]: file not found for module). The working order:
- Add
sc-config.toml:[[proxy]]/path = "src/greeter_proxy.rs". - Write the contract logic WITHOUT the
pub mod greeter_proxy;line yet. - Run
sc-meta all proxy, which compiles the contract (which does not yet reference the proxy module) and generatessrc/greeter_proxy.rsfrom the resulting ABI. - Add
pub mod greeter_proxy;now that the file exists.
Step 3: build
sc-meta all build
Output captured during the authoring-time manual build (not regenerated by CI):
Building greeter.wasm in .../greeter/wasm ...
RUSTFLAGS="-C link-arg=-s -C link-arg=-zstack-size=131072" cargo +1.93-aarch64-apple-darwin build --target=wasm32v1-none --release ...
Finished `release` profile [optimized] target(s) in 6.78s
Copying .../target/wasm32v1-none/release/greeter_wasm.wasm to ../output/greeter.wasm ...
Warning: wasm-opt not installed.
Packing ../output/greeter.mxsc.json ...
Contract size: 996 bytes.
During that manual verification, sc-meta all build also regenerated
wasm/src/lib.rs's endpoint list after setGreeting / getGreeting were added;
both appeared automatically with no manual edit.
Step 4: test
// tests/greeter_blackbox_test.rs — a hand-written blackbox test for the
// endpoint added on top of the bare `empty` scaffold. The two
// `greeter_scenario_*_test.rs` files (unmodified from `sc-meta new
// --template empty`) only exercise deploy; this test exercises the actual
// custom logic, following the recommended blackbox-test pattern.
use multiversx_sc_scenario::imports::*;
use greeter::greeter_proxy;
const OWNER: TestAddress = TestAddress::new("owner");
const CONTRACT: TestSCAddress = TestSCAddress::new("greeter-contract");
const CODE_PATH: MxscPath = MxscPath::new("output/greeter.mxsc.json");
fn world() -> ScenarioWorld {
let mut blockchain = ScenarioWorld::new();
blockchain.register_contract(CODE_PATH, greeter::ContractBuilder);
blockchain
}
#[test]
fn set_and_get_greeting() {
let mut world = world();
world.account(OWNER).nonce(1);
// Deploy.
world
.tx()
.from(OWNER)
.typed(greeter_proxy::GreeterProxy)
.init()
.code(CODE_PATH)
.new_address(CONTRACT)
.run();
// Before any call, the greeting for OWNER is empty.
world
.query()
.to(CONTRACT)
.typed(greeter_proxy::GreeterProxy)
.greeting(OWNER.to_address())
.returns(ExpectValue(ManagedBuffer::<StaticApi>::new()))
.run();
// Call setGreeting as OWNER.
world
.tx()
.from(OWNER)
.to(CONTRACT)
.typed(greeter_proxy::GreeterProxy)
.set_greeting(ManagedBuffer::<StaticApi>::from(b"hello devnet"))
.run();
// getGreeting now returns what we stored, keyed by the caller address.
world
.query()
.to(CONTRACT)
.typed(greeter_proxy::GreeterProxy)
.greeting(OWNER.to_address())
.returns(ExpectValue(ManagedBuffer::<StaticApi>::from(b"hello devnet")))
.run();
}
#[test]
fn greeting_is_keyed_per_caller() {
let mut world = world();
world.account(OWNER).nonce(1);
let other: TestAddress = TestAddress::new("other-caller");
world.account(other).nonce(1);
world
.tx()
.from(OWNER)
.typed(greeter_proxy::GreeterProxy)
.init()
.code(CODE_PATH)
.new_address(CONTRACT)
.run();
world
.tx()
.from(OWNER)
.to(CONTRACT)
.typed(greeter_proxy::GreeterProxy)
.set_greeting(ManagedBuffer::<StaticApi>::from(b"from owner"))
.run();
// A different caller who never called setGreeting still reads back
// empty — this is what a parameterized storage mapper gives you: the key
// includes the parameter automatically, so each address gets its own
// storage slot, not a shared one.
world
.query()
.to(CONTRACT)
.typed(greeter_proxy::GreeterProxy)
.greeting(other.to_address())
.returns(ExpectValue(ManagedBuffer::<StaticApi>::new()))
.run();
world
.query()
.to(CONTRACT)
.typed(greeter_proxy::GreeterProxy)
.greeting(OWNER.to_address())
.returns(ExpectValue(ManagedBuffer::<StaticApi>::from(b"from owner")))
.run();
}
cargo test
running 2 tests
test greeting_is_keyed_per_caller ... ok
test set_and_get_greeting ... ok
test result: ok. 2 passed; 0 failed; ...
running 1 test
test empty_go ... ok
running 1 test
test empty_rs ... ok
All 4 tests pass: the 2 new blackbox tests plus the 2 scaffold-provided scenario tests.
A version note
Three different version numbers showed up while working through this recipe, and they disagree:
| Source | multiversx-sc version |
|---|---|
| Commonly documented "current" version | 0.65.0 |
mx-sdk-rs GitHub master, contracts/examples/empty/Cargo.toml | 0.66.2 |
This machine's installed sc-meta (0.64.1), and what it actually generated | 0.64.1 |
sc-meta new bundles its own template, versioned to match whatever
multiversx-sc-meta release you have installed; it does not fetch the latest
framework version from anywhere. Run sc-meta upgrade after scaffolding for the
latest framework version, or pass sc-meta new --tag <version> to pin one
explicitly.
Also: the actual build command targets wasm32v1-none, not
wasm32-unknown-unknown as some older references list, confirmed directly from
the real sc-meta all build invocation logged above.
Pitfalls
See Step 2's ordering: generate first, declare the module second.
Project-anatomy diagrams often show one unconditionally; you only need it once you want proxy generation or a multi-contract build.
It is a warning, and sc-meta all build still produces a valid, deployable
.wasm / .mxsc.json, just without size optimization. Install it before
shipping to mainnet if contract size matters.
See Step 1's second bullet.
Check what actually got generated (cat Cargo.toml) rather than assuming it
matches the newest release.
See also
- Storage mappers: which to pick, when is the natural next recipe: this one introduces exactly one mapper; that one covers the rest.
- Sign and send a transaction
is the dApp side that would eventually call an endpoint like
setGreeting.