Skip to main content
IntermediateEst10 minsdk-rs^0.64.1Reference

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 the wasm32v1-none target installed (see "A version note" below for why not wasm32-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, no wasm/, and no output/ yet. A bare sc-meta new output 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(), not fn 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

greeter/src/greeter.rs
#![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:

  1. Add sc-config.toml: [[proxy]] / path = "src/greeter_proxy.rs".
  2. Write the contract logic WITHOUT the pub mod greeter_proxy; line yet.
  3. Run sc-meta all proxy, which compiles the contract (which does not yet reference the proxy module) and generates src/greeter_proxy.rs from the resulting ABI.
  4. 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

greeter/tests/greeter_blackbox_test.rs
// 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:

Sourcemultiversx-sc version
Commonly documented "current" version0.65.0
mx-sdk-rs GitHub master, contracts/examples/empty/Cargo.toml0.66.2
This machine's installed sc-meta (0.64.1), and what it actually generated0.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

Pitfall 1: declaring the proxy module before generating it breaks the build

See Step 2's ordering: generate first, declare the module second.

Pitfall 2: a bare sc-meta new output has no sc-config.toml

Project-anatomy diagrams often show one unconditionally; you only need it once you want proxy generation or a multi-contract build.

Pitfall 3: wasm-opt not being installed does not fail the 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.

Pitfall 4: scenario test function names surviving a rename are not a sign something went wrong

See Step 1's second bullet.

Pitfall 5: sc-meta new's bundled template version may lag the framework's documented current version

Check what actually got generated (cat Cargo.toml) rather than assuming it matches the newest release.

See also