ink! Framework Details
Understanding ink! for Rust contracts
ink! is Rust's smart contract framework built on Substrate contracts pallet.
What is ink!
- Rust eDSL - Uses macros, no new language
- WASM output - Small, efficient bytecode
- Substrate native - Works on Selendra and other chains
- Type safe - Rust compiler catches errors early
Core Attributes
#[ink::contract]
#[ink::contract]
mod my_contract {
// Contract code here
}
#[ink(storage)]
#[ink(storage)]
pub struct MyContract {
value: i32,
owner: AccountId,
data: Vec<u8>,
}
Supported types: bool, u8-u128, i32-i128, Balance, Vec<T>, Mapping<K, V>, Option<T>, custom structs with scale::Encode + scale::Decode
#[ink(constructor)]
#[ink(constructor)]
pub fn new(initial_value: i32) -> Self {
Self {
value: initial_value,
owner: Self::env().caller(),
data: Vec::new(),
}
}
Runs once during deployment.
#[ink(message)]
#[ink(message)]
pub fn get_value(&self) -> i32 {
self.value
}
#[ink(message)]
pub fn set_value(&mut self, new_value: i32) {
self.value = new_value;
}
State changes require &mut self. Read-only uses &self.
#[ink(payable)]
#[ink(message, payable)]
pub fn buy_item(&mut self, item_id: u32) {
let payment = self.env().transferred_value();
// Handle payment
}
#[ink(event)]
#[ink(event)]
pub struct ValueChanged {
#[ink(topic)]
old_value: i32,
#[ink(topic)]
new_value: i32,
}
impl MyContract {
#[ink(message)]
pub fn set_value(&mut self, new_value: i32) {
let old_value = self.value;
self.value = new_value;
self.env().emit_event(ValueChanged { old_value, new_value });
}
}
Up to 4 indexed topics for searchability.
Environment API
impl MyContract {
#[ink(message)]
pub fn get_info(&self) -> (AccountId, AccountId, u64, Balance) {
(
self.env().caller(), // Transaction sender
self.env().account_id(), // Contract address
self.env().block_number(), // Current block
self.env().balance(), // Contract balance
)
}
}
Other: block_timestamp(), transferred_value() (payable)
Error Handling
#[derive(Debug, PartialEq, Eq, scale::Encode, scale::Decode)]
#[cfg_attr(feature = "std", derive(scale_info::TypeInfo))]
pub enum Error {
InsufficientBalance,
Unauthorized,
InvalidAmount,
}
pub type Result<T> = core::result::Result<T, Error>;
impl MyContract {
#[ink(message)]
pub fn withdraw(&mut self, amount: Balance) -> Result<()> {
if self.env().caller() != self.owner {
return Err(Error::Unauthorized);
}
if self.env().balance() < amount {
return Err(Error::InsufficientBalance);
}
self.env().transfer(self.owner, amount)
.map_err(|_| Error::TransferFailed)?;
Ok(())
}
}
Cross-Contract Calls
use ink::env::call::{build_call, ExecutionInput, Selector};
impl MyContract {
#[ink(message)]
pub fn call_other(&self, target: AccountId) -> Result<u32> {
build_call::<Environment>()
.call(target)
.gas_limit(0)
.exec_input(ExecutionInput::new(Selector::new(ink::selector_bytes!("get_value"))))
.returns::<Result<u32>>()
.invoke()
}
}
Cargo.toml Setup
[dependencies]
ink = { version = "5.0.0-rc", default-features = false }
[features]
default = ["std"]
std = ["ink/std"]
Selendra uses ink! 5.0.0-rc. Check compatibility between versions.
Testing
#[cfg(test)]
mod tests {
use super::*;
#[ink::test]
fn constructor_works() {
let contract = MyContract::new(42);
assert_eq!(contract.get_value(), 42);
}
#[ink::test]
fn value_change() {
let mut contract = MyContract::new(0);
contract.set_value(100);
assert_eq!(contract.get_value(), 100);
}
}
Build & Deploy
cargo contract build --release
Outputs: target/ink/my_contract.wasm and target/ink/metadata.json
ink! vs Solidity
| Feature | ink! | Solidity |
|---|---|---|
| Memory safety | Compiler enforced | Manual |
| Performance | WASM optimized | EVM bytecode |
| Tooling | Rust ecosystem | Ethereum ecosystem |
| Best for | Complex logic, performance | Quick prototyping, familiarity |
Resources
- Docs: https://use.ink/
- GitHub: paritytech/ink
- Selendra: Telegram • X
Next Steps
Contribute
Found an issue or want to contribute?
Help us improve this documentation by editing this page on GitHub.
