Introduction
A Scrypto blueprint is Rust compiled to WebAssembly, so it can use most of Rust's types, plus a set that Scrypto adds for money, ownership and the ledger. This guide covers the types a blueprint keeps in its state and passes across calls, the fixed-point arithmetic that replaces floating point, and the Runtime and Clock APIs through which a blueprint reads the epoch, the time, its own address and the transaction it is running in (Data Types).
The results quoted come from a blueprint run in the ledger simulator. Three of them correct the official docs: the docs say Scrypto's Hash is SHA-256, say Rust's integer types wrap silently on overflow, and tell you to quote fractional literals for dec!. None of the three holds in Scrypto 1.4.0, as the sections below show.
Which types can a blueprint use?
Anything whose type implements ScryptoSbor, the encoding used to store state and pass arguments (SBOR). That covers:
- Rust primitives:
bool,i8toi128,u8tou128,String.isizeandusizecompile to 32-bit integers. There is nof32orf64: floating-point results can differ between processors, and every validator must compute the same result. - Containers:
Option, arrays, tuples,Vec,HashMap,BTreeMap,IndexMapand the matching sets. - Your own structs and enums, with
#[derive(ScryptoSbor)]. The docs say generic structs and enums are not supported. - Wide integers that always panic on overflow:
I8toI512andU8toU512. - Ledger types:
ComponentAddress,ResourceAddress,PackageAddress;Global<T>, a typed reference to a global component;Owned<T>, a component owned by another;Vault,BucketandProofand their fungible and non-fungible forms;KeyValueStore<K, V>;NonFungibleLocalId;Hash.
A NonFungibleLocalId takes one of four forms, written in a manifest as #7# for an integer, <abc> for a string, [0a1b] for bytes, and {…} for a RUID, the random 32-byte ID the engine assigns when a blueprint calls mint_ruid_non_fungible.
The choice between a map and a KeyValueStore matters most. A HashMap in component state is loaded in full every time the component is called, and every byte read costs fees. A KeyValueStore is loaded one entry at a time. A collection that grows with users belongs in a KeyValueStore (Before You Release).
How do Decimal and PreciseDecimal work?
Decimal is a 192-bit signed integer read as a number with 18 decimal places; PreciseDecimal is 256-bit with 36 (decimal.rs). Resource amounts are always Decimal. Both are written with macros, dec!(10) and pdec!("0.5"). The macro parses a fractional literal such as dec!(1.001) from its digits at compile time, so it never becomes a float; the docs' instruction to put such literals in quotes is no longer needed (radix-common-derive).
pub fn thirds() -> (Decimal, PreciseDecimal, Decimal) {
let d = dec!(1) / dec!(3);
let p = pdec!(1) / pdec!(3);
let back = (p * pdec!(3)).checked_truncate(RoundingMode::ToZero).unwrap();
(d, p, back)
}
// (0.333333333333333333,
// 0.333333333333333333333333333333333333,
// 0.999999999999999999)
pub fn checked(a: Decimal, b: Decimal) -> (Option<Decimal>, Option<Decimal>) {
(a.checked_mul(b), a.checked_div(Decimal::ZERO))
}
// checked(Decimal::MAX, 2) returns (None, None)
pub fn round_half(x: Decimal) -> (Decimal, Decimal, Decimal) {
(
x.checked_round(0, RoundingMode::ToNearestMidpointToEven).unwrap(),
x.checked_round(0, RoundingMode::ToNearestMidpointAwayFromZero).unwrap(),
x.checked_round(0, RoundingMode::ToZero).unwrap(),
)
}
// round_half(2.5) returns (2, 3, 2)Dividing first and multiplying back loses the last digit even in PreciseDecimal. The checked methods return None on overflow and on division by zero; the +, -, * and / operators panic instead, which fails the transaction. Rounding takes an explicit mode: ToNearestMidpointToEven, banker's rounding, turned 2.5 into 2, and ToNearestMidpointAwayFromZero turned it into 3.
Do Rust integers overflow silently in Scrypto?
Not in a package created with scrypto new-package. The docs say built-in types such as i32 wrap silently while Scrypto's I32 panics. That is Rust's default for a release build, but the Cargo.toml that scrypto new-package generates sets overflow-checks = true in its release profile, so i32 panics too:
pub fn add_i32(a: i32, b: i32) -> i32 {
a + b
}
// add_i32(i32::MAX, 1) fails the transaction:
// PanicMessage("attempt to add with overflow @ src/numbers.rs:29:13")A package whose profile was edited, or that predates this template, may behave differently; the setting is worth checking before relying on either behaviour.
What can a blueprint ask the engine?
The Runtime and Clock structs are a blueprint's view of its surroundings (runtime.rs, clock.rs):
pub fn context(&self) -> (u64, i64, String, Hash, Hash) {
info!("context called");
debug!("debug line");
(
Runtime::current_epoch().number(),
Clock::current_time_rounded_to_seconds().seconds_since_unix_epoch,
Runtime::bech32_encode_address(Runtime::global_address()),
hash("radix"), // Blake2b-256
CryptoUtils::keccak256_hash("radix"), // Keccak-256
)
}- Time:
Runtime::current_epoch()returns the consensus epoch, about five minutes long on mainnet.Clock::current_time_rounded_to_seconds()and…_rounded_to_minutes()return the proposer's timestamp;Clock::current_time_is_strictly_before(instant, precision)and its relatives compare against a deadline without reading the exact time. In the simulator, after advancing the clock to 1,800,000,000 seconds, the method returned exactly that. - Identity:
Runtime::global_address(),package_address()andblueprint_name()describe the running component;Runtime::bech32_encode_address()turns an address into the network's text form, here acomponent_sim1…address on the simulator network. - Addresses for later:
Runtime::allocate_component_address()reserves a component's address before it exists (Badge Patterns). - The transaction:
Runtime::transaction_hash(),generate_ruid(),emit_event(),assert_access_rule(), and the fee figuresget_fee_balance(),get_usd_price()andget_tip_percentage().
hash() is Blake2b-256 (hash.rs); the docs' statement that it is SHA-256 is wrong. Ethereum-compatible Keccak-256 is CryptoUtils::keccak256_hash(), and CryptoUtils also verifies BLS12-381 signatures (Keccak256, BLS12-381).
Logging
Five macros write to the transaction receipt's log: error!, warn!, info!, debug! and trace!, each taking a format string like println! (Logging). Which of them are compiled in depends on a build setting: scrypto build includes levels up to INFO unless given --log-level DEBUG or TRACE, and the macros above the level compile to nothing. scrypto-test compiles packages at TRACE, so tests see every level; the simulator run above logged both the info! and the debug! line.
Functions, methods and visibility
A function has no self and is called on the blueprint, typically to instantiate a component. A method takes &self to read the component's state or &mut self to change it, and is called on a component. Every pub function and method is callable from any transaction unless an access rule restricts it; one without pub is private to the blueprint and absent from its interface (Functions and Methods). Restricting a public method is the job of enable_method_auth! (Authorization and Access Rules).
Next Steps
- Before You Release – the mistakes these types make possible, and how to avoid them
- SBOR – how these types are encoded on the ledger and in manifests
External Links
- Data Types – Radix documentation
- Runtime and Clock – docs.rs
- Decimal – docs.rs
- Logging – Radix documentation
