From b4cd6398f2a52170eae293a2e7633bcf75fb1f9d Mon Sep 17 00:00:00 2001 From: Noah Akerityo Date: Thu, 28 May 2026 23:47:33 +0100 Subject: [PATCH] feat: add second example template: token-allowlist --- templates/README.md | 7 ++ templates/examples/token-allowlist/Cargo.toml | 23 +++++ templates/examples/token-allowlist/README.md | 56 ++++++++++++ templates/examples/token-allowlist/src/lib.rs | 89 +++++++++++++++++++ tests/template_marketplace_test.rs | 47 ++++++---- 5 files changed, 203 insertions(+), 19 deletions(-) create mode 100644 templates/examples/token-allowlist/Cargo.toml create mode 100644 templates/examples/token-allowlist/README.md create mode 100644 templates/examples/token-allowlist/src/lib.rs diff --git a/templates/README.md b/templates/README.md index b84e8765..b72643c3 100644 --- a/templates/README.md +++ b/templates/README.md @@ -83,6 +83,13 @@ To be valid, a template must contain: - `src/` directory - Source code - `src/lib.rs` - Main contract file +## Example Templates + +Built-in example templates are provided under `templates/examples/`: + +- `simple-counter`: A basic smart contract demonstrating storage usage by incrementing, getting, and resetting a counter. +- `token-allowlist`: A smart contract for managing an allowlist of approved addresses, controlled by an administrator. + ## Template Placeholders Templates can use placeholders that will be replaced during scaffolding: diff --git a/templates/examples/token-allowlist/Cargo.toml b/templates/examples/token-allowlist/Cargo.toml new file mode 100644 index 00000000..57324939 --- /dev/null +++ b/templates/examples/token-allowlist/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "{{PROJECT_NAME}}" +version = "0.1.0" +edition = "2021" + +[lib] +crate-type = ["cdylib"] + +[dependencies] +soroban-sdk = "21.0.0" + +[dev-dependencies] +soroban-sdk = { version = "21.0.0", features = ["testutils"] } + +[profile.release] +opt-level = "z" +overflow-checks = true +debug = 0 +strip = "symbols" +debug-assertions = false +panic = "abort" +codegen-units = 1 +lto = true diff --git a/templates/examples/token-allowlist/README.md b/templates/examples/token-allowlist/README.md new file mode 100644 index 00000000..87b6ffe9 --- /dev/null +++ b/templates/examples/token-allowlist/README.md @@ -0,0 +1,56 @@ +# {{PROJECT_NAME}} + +A token allowlist smart contract for Soroban. It enables managing a list of approved addresses that are permitted to perform actions (like transfer/receive tokens, or participate in a DAO). + +## Features + +- Initialize contract with an admin +- Check if an address is allowlisted +- Add addresses to the allowlist (admin only) +- Remove addresses from the allowlist (admin only) +- Update admin address (admin only) + +## Build + +```bash +stellar contract build +``` + +## Test + +```bash +cargo test +``` + +## Deploy + +```bash +starforge deploy \ + --wasm target/wasm32-unknown-unknown/release/{{PROJECT_NAME_SNAKE}}.wasm \ + --network testnet +``` + +## Usage + +```bash +# Initialize the contract +stellar contract invoke \ + --id \ + --network testnet \ + -- initialize \ + --admin + +# Add user to allowlist +stellar contract invoke \ + --id \ + --network testnet \ + -- add \ + --address + +# Check allowlist status +stellar contract invoke \ + --id \ + --network testnet \ + -- is_allowed \ + --address +``` diff --git a/templates/examples/token-allowlist/src/lib.rs b/templates/examples/token-allowlist/src/lib.rs new file mode 100644 index 00000000..23a9ee1e --- /dev/null +++ b/templates/examples/token-allowlist/src/lib.rs @@ -0,0 +1,89 @@ +#![no_std] +use soroban_sdk::{contract, contractimpl, contracttype, Address, Env}; + +#[contracttype] +#[derive(Clone)] +pub enum DataKey { + Admin, + Allowed(Address), +} + +#[contract] +pub struct {{PROJECT_NAME_PASCAL}}; + +#[contractimpl] +impl {{PROJECT_NAME_PASCAL}} { + /// Initialize the contract with an admin address + pub fn initialize(env: Env, admin: Address) { + if env.storage().instance().has(&DataKey::Admin) { + panic!("already initialized"); + } + env.storage().instance().set(&DataKey::Admin, &admin); + } + + /// Check if an address is in the allowlist + pub fn is_allowed(env: Env, address: Address) -> bool { + env.storage().persistent().get(&DataKey::Allowed(address)).unwrap_or(false) + } + + /// Add an address to the allowlist (admin only) + pub fn add(env: Env, address: Address) { + let admin: Address = env.storage().instance().get(&DataKey::Admin).expect("not initialized"); + admin.require_auth(); + env.storage().persistent().set(&DataKey::Allowed(address), &true); + } + + /// Remove an address from the allowlist (admin only) + pub fn remove(env: Env, address: Address) { + let admin: Address = env.storage().instance().get(&DataKey::Admin).expect("not initialized"); + admin.require_auth(); + env.storage().persistent().set(&DataKey::Allowed(address), &false); + } + + /// Update the admin address (admin only) + pub fn set_admin(env: Env, new_admin: Address) { + let admin: Address = env.storage().instance().get(&DataKey::Admin).expect("not initialized"); + admin.require_auth(); + env.storage().instance().set(&DataKey::Admin, &new_admin); + } +} + +#[cfg(test)] +mod test { + use super::*; + use soroban_sdk::testutils::Address as _; + + #[test] + fn test_allowlist_flow() { + let env = Env::default(); + env.mock_all_auths(); + + let contract_id = env.register_contract(None, {{PROJECT_NAME_PASCAL}}); + let client = {{PROJECT_NAME_PASCAL}}Client::new(&env, &contract_id); + + let admin = Address::generate(&env); + let user1 = Address::generate(&env); + let user2 = Address::generate(&env); + + // Initialize contract + client.initialize(&admin); + + // Should not be allowed initially + assert!(!client.is_allowed(&user1)); + assert!(!client.is_allowed(&user2)); + + // Add user1 to allowlist + client.add(&user1); + assert!(client.is_allowed(&user1)); + assert!(!client.is_allowed(&user2)); + + // Add user2 to allowlist + client.add(&user2); + assert!(client.is_allowed(&user2)); + + // Remove user1 + client.remove(&user1); + assert!(!client.is_allowed(&user1)); + assert!(client.is_allowed(&user2)); + } +} diff --git a/tests/template_marketplace_test.rs b/tests/template_marketplace_test.rs index 10e16e1f..93fca858 100644 --- a/tests/template_marketplace_test.rs +++ b/tests/template_marketplace_test.rs @@ -27,33 +27,42 @@ mod template_tests { #[test] fn test_example_template_structure() { - // Verify the example template has required files - let template_path = PathBuf::from("templates/examples/simple-counter"); - - assert!(template_path.exists(), "Example template should exist"); - assert!(template_path.join("Cargo.toml").exists(), "Template should have Cargo.toml"); - assert!(template_path.join("src").exists(), "Template should have src directory"); - assert!(template_path.join("src/lib.rs").exists(), "Template should have src/lib.rs"); + // Verify the example templates have required files + let templates = ["simple-counter", "token-allowlist"]; + for template in templates { + let template_path = PathBuf::from(format!("templates/examples/{}", template)); + + assert!(template_path.exists(), "Example template '{}' should exist", template); + assert!(template_path.join("Cargo.toml").exists(), "Template '{}' should have Cargo.toml", template); + assert!(template_path.join("src").exists(), "Template '{}' should have src directory", template); + assert!(template_path.join("src/lib.rs").exists(), "Template '{}' should have src/lib.rs", template); + } } #[test] fn test_template_placeholders() { // Verify template files contain placeholders - let lib_rs = PathBuf::from("templates/examples/simple-counter/src/lib.rs"); - let content = fs::read_to_string(&lib_rs) - .expect("Should be able to read lib.rs"); - - assert!(content.contains("{{PROJECT_NAME_PASCAL}}"), - "Template should contain PROJECT_NAME_PASCAL placeholder"); + let templates = ["simple-counter", "token-allowlist"]; + for template in templates { + let lib_rs = PathBuf::from(format!("templates/examples/{}/src/lib.rs", template)); + let content = fs::read_to_string(&lib_rs) + .expect(&format!("Should be able to read lib.rs for '{}'", template)); + + assert!(content.contains("{{PROJECT_NAME_PASCAL}}"), + "Template '{}' should contain PROJECT_NAME_PASCAL placeholder", template); + } } #[test] fn test_cargo_toml_placeholders() { - let cargo_toml = PathBuf::from("templates/examples/simple-counter/Cargo.toml"); - let content = fs::read_to_string(&cargo_toml) - .expect("Should be able to read Cargo.toml"); - - assert!(content.contains("{{PROJECT_NAME}}"), - "Cargo.toml should contain PROJECT_NAME placeholder"); + let templates = ["simple-counter", "token-allowlist"]; + for template in templates { + let cargo_toml = PathBuf::from(format!("templates/examples/{}/Cargo.toml", template)); + let content = fs::read_to_string(&cargo_toml) + .expect(&format!("Should be able to read Cargo.toml for '{}'", template)); + + assert!(content.contains("{{PROJECT_NAME}}"), + "Cargo.toml for '{}' should contain PROJECT_NAME placeholder", template); + } } }