events.rs
Contains event emitting logic
use soroban_sdk::{Address, Env, Symbol};
pub(crate) fn increase_allowance(e: &Env, from: Address, to: Address, amount: i128) {
let topics = (Symbol::new(e, "increase_allowance"), from, to);
e.events().publish(topics, amount);
}
pub(crate) fn decrease_allowance(e: &Env, from: Address, to: Address, amount: i128) {
let topics = (Symbol::new(e, "decrease_allowance"), from, to);
e.events().publish(topics, amount);
}
pub(crate) fn transfer(e: &Env, from: Address, to: Address, amount: i128) {
let topics = (Symbol::short("transfer"), from, to);
e.events().publish(topics, amount);
}
pub(crate) fn mint(e: &Env, admin: Address, to: Address, amount: i128) {
let topics = (Symbol::short("mint"), admin, to);
e.events().publish(topics, amount);
}
pub(crate) fn clawback(e: &Env, admin: Address, from: Address, amount: i128) {
let topics = (Symbol::short("clawback"), admin, from);
e.events().publish(topics, amount);
}
pub(crate) fn set_authorized(e: &Env, admin: Address, id: Address, authorize: bool) {
let topics = (Symbol::new(e, "set_authorized"), admin, id);
e.events().publish(topics, authorize);
}
pub(crate) fn set_admin(e: &Env, admin: Address, new_admin: Address) {
let topics = (Symbol::short("set_admin"), admin);
e.events().publish(topics, new_admin);
}
pub(crate) fn burn(e: &Env, from: Address, amount: i128) {
let topics = (Symbol::short("burn"), from);
e.events().publish(topics, amount);
}
This module contains implementations of functions that emit events from the token contract. Events allow light clients to track state changes on the blockchain without needing to fully verify all transactions.
The functions are:
increase_allowance
- Emits an event when an allowance is increased
decrease_allowance
- Emits an event when an allowance is decreased
transfer
- Emits an event when tokens are transferred
mint
- Emits an event when tokens are minted
clawback
- Emits an event when tokens are clawed back
set_authorized
- Emits an event when an account's authorization is changed
set_admin
- Emits an event when the admin is changed
burn
- Emits an event when tokens are burned
Each function requires various arguments (Env, addresses, amounts, authorize flag) and publishes an event using the Env e argument, specifying topics ( identifiers for the event type and relevant addresses/admin) and data (the amount or authorize flag).
lib.rs
This is our main crate file
#![no_std]
mod admin;
mod allowance;
mod balance;
mod contract;
mod event;
mod metadata;
mod storage_types;
mod test;
pub use crate::contract::TokenClient;
This file imports and exports the key modules (functionality groupings) of the token crate, making the main TokenClient
type available for use by other crates.
metadata.rs
Contains metadata logic (reading/writing name, symbol, decimals)
use crate::storage_types::DataKey;
use soroban_sdk::{Bytes, Env};
pub fn read_decimal(e: &Env) -> u32 {
let key = DataKey::Decimals;
e.storage().get_unchecked(&key).unwrap()
}
pub fn write_decimal(e: &Env, d: u8) {
let key = DataKey::Decimals;
e.storage().set(&key, &u32::from(d))
}
pub fn read_name(e: &Env) -> Bytes {
let key = DataKey::Name;
e.storage().get_unchecked(&key).unwrap()
}
pub fn write_name(e: &Env, d: Bytes) {
let key = DataKey::Name;
e.storage().set(&key, &d)
}
pub fn read_symbol(e: &Env) -> Bytes {
let key = DataKey::Symbol;
e.storage().get_unchecked(&key).unwrap()
}
pub fn write_symbol(e: &Env, d: Bytes) {
let key = DataKey::Symbol;
e.storage().set(&key, &d)
}
The metadata.rs
file contains functions for reading and writing metadata from storage.
It imports:
DataKey
from storage_types
(to access keys for storage)
Env
, Bytes
from soroban_sdk
(to access the environment and work with byte vectors)
It then defines functions for:
Reading/writing the decimal places
Reading/writing the token name
Reading/writing the token symbol
Each function takes an Env
reference and either reads a value from or writes a value to storage, using the appropriate DataKey
and calling getter/setter functions on the Env's storage.
So in summary, this file provides an interface for managing the metadata (decimals, name, symbol) stored for the token contract.
-
storage_types.rs
Types for contract storage
use soroban_sdk::{contracttype, Address};
#[derive(Clone)]
#[contracttype]
pub struct AllowanceDataKey {
pub from: Address,
pub spender: Address,
}
#[derive(Clone)]
#[contracttype]
pub enum DataKey {
Allowance(AllowanceDataKey),
Balance(Address),
Nonce(Address),
State(Address),
Admin,
Decimals,
Name,
Symbol,
}
The storage_types.rs
file defines types for keys used to access storage.
It imports Address
from soroban_sdk
It defines:
AllowanceDataKey
- A key for allowance data, containing from and spender addresses
DataKey
- An enum containing keys for all the main storage items:
-
Allowance
(using AllowanceDataKey) -
Balance
(using an address) -
Nonce
(using an address) -
State
(using an address) Admin
Decimals
Name
Symbol
The #[contracttype]
attribute indicates these are types used for a Soroban contract.
So in summary, this file defines typed keys to use to access the key storage items for the token contract, allowing strong typing and safety when accessing storage.
Running Contract Test
To ensure that the contract functions as intended, you can run the contract tests using the following command:
cargo test
If the tests are successful, you should see an output similar to:
running 8 tests
test test::decimal_is_over_max - should panic ... ok
test test::initialize_already_initialized - should panic ... ok
test test::xfer_insufficient_balance - should panic ... ok
test test::test_burn ... ok
test test::xfer_from_insufficient_allowance - should panic ... ok
test test::xfer_spend_deauthorized - should panic ... ok
test test::xfer_receive_deauthorized - should panic ... ok
test test::test ... ok
test result: ok. 8 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.37s
Building The Contract
To build the contract, use the following command:
cargo build --target wasm32-unknown-unknown --release
This should output a .wasm file in the ../target directory:
../target/wasm32-unknown-unknown/release/soroban_token_contract.wasm
Deploying The Contract
To deploy the contract, use the following command:
soroban contract deploy --wasm /target/wasm32-unknown-unknown/release/soroban_token_contract.wasm
Invoking The Contract
This token contract contains many functions, but in this post I will only demonstrate initializing the token and minting tokens to an address. You can explore the other functions yourself. To initialize the token i'm using this command :
$ soroban contract invoke --id 31e2cf6483b1d6e4149780c7b97a455f3d17d6b43ef9050549fbd987d988039e --secret-key SCTDJOSFJOD3XGDAPDW73TSEVKRHSYVBQ4CK3QEHIAI4NH6SWDG53EBG -- initialize --admin GAREW2RCA4GELVLKPAIZNMNETTN57PAIVGOMPNMXHV3KMHLMMTPT2BAD --decimal 100 --name "4d79546f6b656e" --symbol "4d544b"
success
-
31e2cf6483b1d6e4149780c7b97a455f3d17d6b43ef9050549fbd987d988039e
- The ContractID of the deployed contract before
-
SCTDJOSFJOD3XGDAPDW73TSEVKRHSYVBQ4CK3QEHIAI4NH6SWDG53EBG
- Secret Key from the account used to invoke the function -
initialize
- invoked function -
GAREW2RCA4GELVLKPAIZNMNETTN57PAIVGOMPNMXHV3KMHLMMTPT2BAD
- - The admin address -
100
- value fordecimal
-
4d79546f6b656e
- The token name (in Hex bytes - "MyToken" in string) -
4d544b
- The Token Symbol (in Hex bytes - "MTK" in string)
Token Contract already initialized, then i will mint that token into an address, using this command :
$ soroban contract invoke --id 31e2cf6483b1d6e4149780c7b97a455f3d17d6b43ef9050549fbd987d988039e --secret-key SCTDJOSFJOD3XGDAPDW73TSEVKRHSYVBQ4CK3QEHIAI4NH6SWDG53EBG -- mint --admin GAREW2RCA4GELVLKPAIZNMNETTN57PAIVGOMPNMXHV3KMHLMMTPT2BAD --to GDB3PKTGIMFAE4D36RAU5CKV3BHS75G5PMTJECIWAGELUSVBEMUBOUA3 --amount 100
success
-
31e2cf6483b1d6e4149780c7b97a455f3d17d6b43ef9050549fbd987d988039e
- The Contract ID -
SCTDJOSFJOD3XGDAPDW73TSEVKRHSYVBQ4CK3QEHIAI4NH6SWDG53EBG
- The Secret Key from the account used to invoke the function -
mint
- The Invoked Function -
GAREW2RCA4GELVLKPAIZNMNETTN57PAIVGOMPNMXHV3KMHLMMTPT2BAD
- The Admin Address of The Token Contract -
GDB3PKTGIMFAE4D36RAU5CKV3BHS75G5PMTJECIWAGELUSVBEMUBOUA3
- The recipient address -
100
- Token minted amount
To make sure the token already minted we will check it using this command :
$ soroban contract invoke --id 31e2cf6483b1d6e4149780c7b97a455f3d17d6b43ef9050549fbd987d988039e --secret-key SCTDJOSFJOD3XGDAPDW73TSEVKRHSYVBQ4CK3QEHIAI4NH6SWDG53EBG -- balance --id GDB3PKTGIMFAE4D36RAU5CKV3BHS75G5PMTJECIWAGELUSVBEMUBOUA3
success
"100"
Conclusion
That's it! We explored all modules code and the key parts of a token contract on Soroban. We saw how the contract is initialized and token minted. By piecing together the admin, allowance, balance, event, metadata, and storage_types modules, and using the TokenClient interface, a custom token contract can be easily developed and managed on Soroban. Stay tuned for more post in this "Soroban Contracts 101" Series where we will dive deeper into Soroban Contracts and their functionalities.
Top comments (1)
What about non-financial assets? Do you have some insights on working with them? Like, for instance, storing some chunks of structural data as an asset? (sorry if it's a dumb question 😅)