index

Smart Contract Basics - Setup & Token Contract

· 6min

Sekarang kita akan mulai membuat smart contract pertama! 🚀

📦 Setup Development Environment

1. Install Rust

Rust adalah bahasa pemrograman yang akan kita gunakan untuk menulis smart contract.

macOS/Linux:

curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh

Windows:

Download dan install dari rustup.rs

Verifikasi instalasi:

rustc --version
cargo --version

Seharusnya muncul versi Rust yang terinstall (contoh: rustc 1.84.0).


2. Install Target WASM

Smart contract Stellar dikompilasi ke WebAssembly (WASM). Install target WASM:

rustup target add wasm32v1-none

Catatan: Untuk Rust versi < 1.85.0, gunakan:

rustup target add wasm32-unknown-unknown

3. Install Visual Studio Code

Download dan install VS Code dari code.visualstudio.com

Install Extension:

  1. Rust Analyzer - Autocomplete dan syntax highlighting Rust
  2. CodeLLDB - Debugging Rust code

Cara install:

  • Buka VS Code
  • Tekan Ctrl+Shift+X (Windows/Linux) atau Cmd+Shift+X (Mac)
  • Search “Rust Analyzer” dan “CodeLLDB”
  • Klik Install

4. Install Stellar CLI

Stellar CLI adalah tool untuk build, test, dan deploy smart contract.

macOS/Linux (menggunakan Homebrew):

brew install stellar-cli

Install dari source (semua platform):

cargo install --locked stellar-cli@23.1.4

Autocompletion:

echo "source <(stellar completion --shell bash)" >> ~/.bashrc

Verifikasi instalasi:

stellar --version

Seharusnya muncul versi stellar-cli (contoh: stellar 23.1.4).


5. Generate Wallet untuk Testnet

Kita perlu wallet untuk deploy contract ke Stellar Testnet.

Generate identity bernama alice:

stellar keys generate alice --network testnet --fund

Flag --fund akan otomatis request XLM dari Friendbot (faucet testnet).

Import address (create dari extension/apps):

stellar keys add <name> --seed-phrase

Lihat public key:

stellar keys address alice

Output: GXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

Lihat secret key:

stellar keys show alice

⚠️ PENTING: Jangan pernah share secret key! Ini seperti password wallet Anda.


🪙 Membuat Token Contract

Kita akan membuat contract sederhana untuk create token custom. Token ini bisa digunakan untuk:

  • 💰 Cryptocurrency (coin/token)
  • 🎟️ Voucher/Kupon digital
  • 🏆 Points/Reward system
  • 🎨 NFT (Non-Fungible Token)

Struktur Project

Initialize project baru:

stellar contract init my-token-project --name token
cd my-token-project

Struktur folder:

my-token-project/
├── Cargo.toml
├── contracts/
│   └── token/
│       ├── Cargo.toml
│       ├── src/
│       │   ├── lib.rs
│       │   └── test.rs

📝 Token Contract Code

Sekarang kita akan menulis code! Buka contracts/token/src/lib.rs dan replace dengan code berikut:

#![no_std]
use soroban_sdk::{contract, contractimpl, contracttype, Address, Env, String, symbol_short};

// Storage keys
const BALANCE: soroban_sdk::Symbol = symbol_short!("BALANCE");
const NAME: soroban_sdk::Symbol = symbol_short!("NAME");
const SYMBOL: soroban_sdk::Symbol = symbol_short!("SYMBOL");
const TOTAL: soroban_sdk::Symbol = symbol_short!("TOTAL");

// Contract struct
#[contract]
pub struct TokenContract;

// Token data structure
#[contracttype]
#[derive(Clone)]
pub struct TokenInfo {
    pub name: String,
    pub symbol: String,
    pub total_supply: i128,
}

#[contractimpl]
impl TokenContract {

    // Initialize token dengan nama, symbol, dan supply
    pub fn initialize(
        env: Env,
        admin: Address,
        name: String,
        symbol: String,
        total_supply: i128,
    ) {
        // Verify admin authorization
        admin.require_auth();

        // Validasi input
        if total_supply <= 0 {
            panic!("Total supply harus lebih dari 0");
        }

        // Simpan token info
        env.storage().instance().set(&NAME, &name);
        env.storage().instance().set(&SYMBOL, &symbol);
        env.storage().instance().set(&TOTAL, &total_supply);

        // Set balance admin = total supply
        env.storage().instance().set(&BALANCE, &total_supply);
    }

    // Get nama token
    pub fn get_name(env: Env) -> String {
        env.storage().instance().get(&NAME).unwrap()
    }

    // Get symbol token
    pub fn get_symbol(env: Env) -> String {
        env.storage().instance().get(&SYMBOL).unwrap()
    }

    // Get total supply
    pub fn get_total_supply(env: Env) -> i128 {
        env.storage().instance().get(&TOTAL).unwrap()
    }

    // Get balance
    pub fn get_balance(env: Env) -> i128 {
        env.storage().instance().get(&BALANCE).unwrap_or(0)
    }

    // Transfer token (simplified - real token contract lebih kompleks)
    pub fn transfer(env: Env, from: Address, to: Address, amount: i128) {
        // Verify authorization
        from.require_auth();

        // Validasi amount
        if amount <= 0 {
            panic!("Amount harus lebih dari 0");
        }

        // Get current balances (simplified)
        let balance: i128 = env.storage().instance().get(&BALANCE).unwrap_or(0);

        // Check sufficient balance
        if balance < amount {
            panic!("Balance tidak cukup");
        }

        // Update balance (simplified version)
        let new_balance = balance - amount;
        env.storage().instance().set(&BALANCE, &new_balance);
    }
}

mod test;

🧪 Test Code

Sekarang kita akan menulis test! Buka contracts/token/src/test.rs:

#![cfg(test)]

use super::*;
use soroban_sdk::{testutils::Address as _, Address, Env, String};

#[test]
fn test_initialize_token() {
    // Setup environment
    let env = Env::default();
    let contract_id = env.register(TokenContract, ());
    let client = TokenContractClient::new(&env, &contract_id);

    // Create admin address
    let admin = Address::generate(&env);

    // Mock authorization
    env.mock_all_auths();

    // Initialize token
    let name = String::from_str(&env, "Indonesian Rupiah");
    let symbol = String::from_str(&env, "IDR");
    let supply = 1_000_000_000i128; // 1 miliar

    client.initialize(&admin, &name, &symbol, &supply);

    // Verify token info
    assert_eq!(client.get_name(), name);
    assert_eq!(client.get_symbol(), symbol);
    assert_eq!(client.get_total_supply(), supply);
    assert_eq!(client.get_balance(), supply);
}

#[test]
fn test_get_token_info() {
    let env = Env::default();
    let contract_id = env.register(TokenContract, ());
    let client = TokenContractClient::new(&env, &contract_id);

    let admin = Address::generate(&env);
    env.mock_all_auths();

    let name = String::from_str(&env, "Workshop Token");
    let symbol = String::from_str(&env, "WST");
    let supply = 5_000_000i128;

    client.initialize(&admin, &name, &symbol, &supply);

    // Test getter functions
    assert_eq!(client.get_name(), name);
    assert_eq!(client.get_symbol(), symbol);
    assert_eq!(client.get_total_supply(), supply);
}

#[test]
#[should_panic(expected = "Total supply harus lebih dari 0")]
fn test_initialize_invalid_supply() {
    let env = Env::default();
    let contract_id = env.register(TokenContract, ());
    let client = TokenContractClient::new(&env, &contract_id);

    let admin = Address::generate(&env);
    env.mock_all_auths();

    let name = String::from_str(&env, "Bad Token");
    let symbol = String::from_str(&env, "BAD");
    let supply = 0i128; // Invalid!

    // Should panic
    client.initialize(&admin, &name, &symbol, &supply);
}

#[test]
fn test_transfer() {
    let env = Env::default();
    let contract_id = env.register(TokenContract, ());
    let client = TokenContractClient::new(&env, &contract_id);

    let admin = Address::generate(&env);
    let user = Address::generate(&env);

    env.mock_all_auths();

    let name = String::from_str(&env, "Test Token");
    let symbol = String::from_str(&env, "TST");
    let supply = 1_000_000i128;

    client.initialize(&admin, &name, &symbol, &supply);

    // Transfer tokens
    let transfer_amount = 100_000i128;
    client.transfer(&admin, &user, &transfer_amount);

    // Balance should be reduced
    assert_eq!(client.get_balance(), supply - transfer_amount);
}

🧪 Running Tests

Jalankan test untuk memastikan contract bekerja:

cargo test

Output yang diharapkan:

running 4 tests
test test::test_initialize_token ... ok
test test::test_get_token_info ... ok
test test::test_initialize_invalid_supply ... ok
test test::test_transfer ... ok

test result: ok. 4 passed; 0 failed

Tips:

  • Jika ada error, baca pesan error dengan teliti
  • Error messages Rust sangat helpful!
  • Jangan panik jika test gagal, ini normal saat belajar

🏗️ Build Contract

Setelah test pass, build contract menjadi WASM:

stellar contract build

Check file WASM yang dihasilkan:

ls target/wasm32v1-none/release/*.wasm

Seharusnya muncul file token.wasm.


🚀 Deploy ke Testnet

Sekarang waktunya deploy contract ke Stellar Testnet!

Step 1: Deploy Contract

stellar contract deploy \
  --wasm target/wasm32v1-none/release/token.wasm \
  --source-account alice \
  --network testnet \
  --alias my_token

Output: Contract ID (contoh: CACDYF3CYMJEJTIVFESQYZTN67GO2R5D5IUABTCUG3HXQSRXCSOROBAN)

Simpan contract ID ini!


Step 2: Initialize Token

Sekarang kita initialize token dengan nama, symbol, dan supply:

stellar contract invoke \
  --id <CONTRACT_ID> \
  --source-account alice \
  --network testnet \
  -- \
  initialize \
  --admin <ALICE_PUBLIC_KEY> \
  --name "Indonesian Rupiah Token" \
  --symbol "IDRT" \
  --total_supply "1000000000"

Ganti <CONTRACT_ID> dengan contract ID dari step sebelumnya. Ganti <ALICE_PUBLIC_KEY> dengan public key alice (dari stellar keys address alice).


Step 3: Check Token Info

Get nama token:

stellar contract invoke \
  --id <CONTRACT_ID> \
  --source-account alice \
  --network testnet \
  -- \
  get_name

Output: "Indonesian Rupiah Token"

Get symbol:

stellar contract invoke \
  --id <CONTRACT_ID> \
  --source-account alice \
  --network testnet \
  -- \
  get_symbol

Output: "IDRT"

Get total supply:

stellar contract invoke \
  --id <CONTRACT_ID> \
  --source-account alice \
  --network testnet \
  -- \
  get_total_supply

Output: 1000000000


🎉 Congratulations!

Anda sudah berhasil:

  • ✅ Setup development environment
  • ✅ Membuat token contract dari scratch
  • ✅ Menulis dan menjalankan test
  • ✅ Build contract ke WASM
  • ✅ Deploy ke Stellar Testnet
  • ✅ Interact dengan deployed contract

💡 Penjelasan Code

Kenapa #![no_std]?

Soroban contracts tidak menggunakan Rust standard library karena:

  • 🎯 Size - Standard library terlalu besar (contract max 64KB)
  • Performance - Optimized untuk blockchain
  • 🔒 Security - Controlled environment

Kenapa i128 untuk amount?

  • ✅ Stellar ecosystem standard
  • ✅ Compatible dengan native token (XLM)
  • ✅ Allows safe error checking
  • ✅ Prevent overflow issues

Storage Keys

const BALANCE: Symbol = symbol_short!("BALANCE");
  • Symbol = short string (max 32 chars)
  • symbol_short!() = compile-time optimization (max 9 chars)
  • Digunakan sebagai key di blockchain storage

🎓 Exercise

Coba modifikasi contract:

  1. Add mint function - Create token baru
  2. Add burn function - Destroy token
  3. Add multiple balances - Track balance per address (hint: use Map<Address, i128>)
  4. Add allowance - Izinkan address lain transfer token kamu

📚 Resources


Next: Mari kita buat contract yang lebih kompleks - Crowdfunding Contract! 🚀