Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
107 changes: 106 additions & 1 deletion contracts/liquid_staking/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,23 @@ pub enum DataKey {
StakeLockTime(u64), // NFT ID -> Lock expiration timestamp
NftRewardPerTokenPaid(u64), // NFT ID -> Snapshot
NftRewards(u64), // NFT ID -> Accrued rewards
/// Branding / project metadata (description, icon_url, website)
ContractMeta,
}

/// On-chain branding metadata for the contract.
///
/// Stored independently of staking logic so it can be updated at any time
/// by the admin without touching the core contract state.
#[contracttype]
#[derive(Clone, Debug, PartialEq)]
pub struct ContractMetadata {
/// Human-readable description of the contract.
pub description: String,
/// URL pointing to the project icon / logo.
pub icon_url: String,
/// Project or protocol website URL.
pub website: String,
}

#[contracttype]
Expand Down Expand Up @@ -50,6 +67,13 @@ impl LiquidStaking {
env.storage().instance().set(&DataKey::NftContract, &nft_contract);
env.storage().instance().set(&DataKey::TotalStaked, &0_i128);
env.storage().instance().set(&DataKey::RewardPerTokenStored, &0_i128);

// Initialise branding metadata with empty strings.
env.storage().instance().set(&DataKey::ContractMeta, &ContractMetadata {
description: String::from_str(&env, ""),
icon_url: String::from_str(&env, ""),
website: String::from_str(&env, ""),
});
}

pub fn deposit_rewards(env: Env, from: Address, amount: i128) {
Expand Down Expand Up @@ -233,6 +257,48 @@ impl LiquidStaking {
}
}

// ── Contract Metadata ─────────────────────────────────────────────────────

/// Update the contract's branding metadata (admin only).
///
/// All three fields are replaced atomically. Pass the current value for
/// any field you do not want to change.
///
/// # Arguments
/// * `caller` – Must be the contract admin
/// * `description` – New human-readable description
/// * `icon_url` – New icon / logo URL
/// * `website` – New project website URL
pub fn update_contract_meta(
env: Env,
caller: Address,
description: String,
icon_url: String,
website: String,
) {
caller.require_auth();

let admin: Address = env
.storage()
.instance()
.get(&DataKey::Admin)
.expect("admin not found");
assert!(caller == admin, "only admin can update contract metadata");

let meta = ContractMetadata { description, icon_url, website };
env.storage().instance().set(&DataKey::ContractMeta, &meta);

env.events().publish((symbol_short!("meta_upd"),), meta);
}

/// Return the current contract branding metadata.
pub fn get_contract_meta(env: Env) -> ContractMetadata {
env.storage()
.instance()
.get(&DataKey::ContractMeta)
.expect("contract metadata not initialised")
}

fn _update_reward(env: &Env, token_id: u64) {
let rpt: i128 = env.storage().instance().get(&DataKey::RewardPerTokenStored).unwrap_or(0);
let nft_rpt: i128 = env.storage().persistent().get(&DataKey::NftRewardPerTokenPaid(token_id)).unwrap_or(0);
Expand Down Expand Up @@ -354,4 +420,43 @@ mod tests {
// Should panic because 3600 seconds haven't passed
client.unstake(&alice, &token_id);
}
}

#[test]
fn test_update_contract_meta() {
let (env, ls_id, _, admin, _, _, _) = setup();
let client = LiquidStakingClient::new(&env, &ls_id);

// Initial metadata should be empty strings.
let initial = client.get_contract_meta();
assert_eq!(initial.description, String::from_str(&env, ""));
assert_eq!(initial.icon_url, String::from_str(&env, ""));
assert_eq!(initial.website, String::from_str(&env, ""));

// Admin updates branding.
client.update_contract_meta(
&admin,
&String::from_str(&env, "Liquid staking protocol on Stellar"),
&String::from_str(&env, "https://example.com/icon.png"),
&String::from_str(&env, "https://example.com"),
);

let updated = client.get_contract_meta();
assert_eq!(updated.description, String::from_str(&env, "Liquid staking protocol on Stellar"));
assert_eq!(updated.icon_url, String::from_str(&env, "https://example.com/icon.png"));
assert_eq!(updated.website, String::from_str(&env, "https://example.com"));
}

#[test]
#[should_panic(expected = "only admin can update contract metadata")]
fn test_update_contract_meta_non_admin() {
let (env, ls_id, _, _, alice, _, _) = setup();
let client = LiquidStakingClient::new(&env, &ls_id);

// Non-admin should be rejected.
client.update_contract_meta(
&alice,
&String::from_str(&env, "Hacked"),
&String::from_str(&env, ""),
&String::from_str(&env, ""),
);
}
111 changes: 111 additions & 0 deletions contracts/nft_metadata/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,27 @@ pub enum DataKey {
TokenApproval(u64, Address),
/// Whether an operator is approved for all tokens of an owner
OperatorApproval(Address, Address),
/// Branding / project metadata (description, icon_url, website)
ContractMeta,
}

// ============================================================================
// Data Structures
// ============================================================================

/// On-chain branding metadata for the contract.
///
/// Stored independently of NFT logic so it can be updated at any time
/// by the admin without touching token state.
#[contracttype]
#[derive(Clone, Debug, PartialEq)]
pub struct ContractMetadata {
/// Human-readable description of the contract.
pub description: String,
/// URL pointing to the project icon / logo.
pub icon_url: String,
/// Project or protocol website URL.
pub website: String,
}

// ============================================================================
Expand Down Expand Up @@ -174,6 +195,13 @@ impl NftMetadataContract {
env.storage().instance().set(&DataKey::Admin, &admin);
env.storage().instance().set(&DataKey::CollectionMetadata, &collection);
env.storage().instance().set(&DataKey::TokenCounter, &0u64);

// Initialise branding metadata with empty strings.
env.storage().instance().set(&DataKey::ContractMeta, &ContractMetadata {
description: String::from_str(&env, ""),
icon_url: String::from_str(&env, ""),
website: String::from_str(&env, ""),
});
}

// ========================================================================
Expand Down Expand Up @@ -698,6 +726,50 @@ impl NftMetadataContract {
env.storage().instance().set(&DataKey::CollectionMetadata, &collection);
}

// ========================================================================
// Contract Metadata
// ========================================================================

/// Update the contract's branding metadata (admin only).
///
/// All three fields are replaced atomically. Pass the current value for
/// any field you do not want to change.
///
/// # Arguments
/// * `caller` – Must be the contract admin
/// * `description` – New human-readable description
/// * `icon_url` – New icon / logo URL
/// * `website` – New project website URL
pub fn update_contract_meta(
env: Env,
caller: Address,
description: String,
icon_url: String,
website: String,
) {
caller.require_auth();

let admin: Address = env
.storage()
.instance()
.get(&DataKey::Admin)
.expect("admin not found");
assert!(caller == admin, "only admin can update contract metadata");

let meta = ContractMetadata { description, icon_url, website };
env.storage().instance().set(&DataKey::ContractMeta, &meta);

env.events().publish((symbol_short!("meta_upd"),), meta);
}

/// Return the current contract branding metadata.
pub fn get_contract_meta(env: Env) -> ContractMetadata {
env.storage()
.instance()
.get(&DataKey::ContractMeta)
.expect("contract metadata not initialised")
}

/// Get total supply of tokens
///
/// # Returns
Expand Down Expand Up @@ -926,4 +998,43 @@ mod tests {

assert!(client.is_approved(&token_id, &spender));
}

#[test]
fn test_update_contract_meta() {
let (env, client, admin) = setup();

// Initial metadata should be empty strings.
let initial = client.get_contract_meta();
assert_eq!(initial.description, String::from_str(&env, ""));
assert_eq!(initial.icon_url, String::from_str(&env, ""));
assert_eq!(initial.website, String::from_str(&env, ""));

// Admin updates branding.
client.update_contract_meta(
&admin,
&String::from_str(&env, "NFT collection on Stellar"),
&String::from_str(&env, "https://example.com/icon.png"),
&String::from_str(&env, "https://example.com"),
);

let updated = client.get_contract_meta();
assert_eq!(updated.description, String::from_str(&env, "NFT collection on Stellar"));
assert_eq!(updated.icon_url, String::from_str(&env, "https://example.com/icon.png"));
assert_eq!(updated.website, String::from_str(&env, "https://example.com"));
}

#[test]
#[should_panic(expected = "only admin can update contract metadata")]
fn test_update_contract_meta_non_admin() {
let (env, client, _admin) = setup();
let non_admin = Address::generate(&env);

// Non-admin should be rejected.
client.update_contract_meta(
&non_admin,
&String::from_str(&env, "Hacked"),
&String::from_str(&env, ""),
&String::from_str(&env, ""),
);
}
}
99 changes: 99 additions & 0 deletions src/utils/contract_metadata.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
//! Reusable contract metadata module.
//!
//! Provides a `ContractMetadata` struct (description, icon_url, website) and
//! helper functions to read/write it from any Soroban contract's instance
//! storage. Metadata is stored under a caller-supplied key so it never
//! conflicts with the host contract's own storage layout.
//!
//! # Usage
//! ```ignore
//! // In your DataKey enum:
//! ContractMeta,
//!
//! // In initialize():
//! contract_metadata::init(&env, &DataKey::ContractMeta);
//!
//! // Admin-only update:
//! contract_metadata::update(&env, &DataKey::ContractMeta, &admin, description, icon_url, website);
//!
//! // Public read:
//! let meta = contract_metadata::get(&env, &DataKey::ContractMeta);
//! ```

use soroban_sdk::{symbol_short, Address, Env, IntoVal, String, TryFromVal, Val};

/// On-chain branding / project metadata for a contract.
///
/// All fields are optional in the sense that they may be empty strings;
/// the struct is always present after `init` is called.
#[soroban_sdk::contracttype]
#[derive(Clone, Debug, PartialEq)]
pub struct ContractMetadata {
/// Human-readable description of the contract's purpose.
pub description: String,
/// URL pointing to the project's icon / logo image.
pub icon_url: String,
/// Project or protocol website URL.
pub website: String,
}

/// Initialise metadata with empty strings.
///
/// Must be called once during the contract's `initialize` function.
/// Subsequent calls are no-ops (idempotent).
pub fn init<K>(env: &Env, key: &K)
where
K: IntoVal<Env, Val> + TryFromVal<Env, Val>,
{
if !env.storage().instance().has(key) {
env.storage().instance().set(key, &ContractMetadata {
description: String::from_str(env, ""),
icon_url: String::from_str(env, ""),
website: String::from_str(env, ""),
});
}
}

/// Return the current contract metadata.
///
/// # Panics
/// Panics if `init` was never called (metadata not found).
pub fn get<K>(env: &Env, key: &K) -> ContractMetadata
where
K: IntoVal<Env, Val> + TryFromVal<Env, Val>,
{
env.storage()
.instance()
.get(key)
.expect("contract metadata not initialised")
}

/// Replace the contract metadata (admin-only).
///
/// The caller must be the contract admin; `admin.require_auth()` is called
/// internally so the transaction will fail if the signature is absent.
///
/// # Arguments
/// * `env` – Soroban environment
/// * `key` – Storage key used by the host contract for metadata
/// * `admin` – Admin address (must sign the transaction)
/// * `description` – New description (pass current value to leave unchanged)
/// * `icon_url` – New icon URL
/// * `website` – New website URL
pub fn update<K>(
env: &Env,
key: &K,
admin: &Address,
description: String,
icon_url: String,
website: String,
) where
K: IntoVal<Env, Val> + TryFromVal<Env, Val>,
{
admin.require_auth();

let meta = ContractMetadata { description, icon_url, website };
env.storage().instance().set(key, &meta);

env.events().publish((symbol_short!("meta_upd"),), meta);
}
1 change: 1 addition & 0 deletions src/utils/lib.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
#![no_std]
pub mod contract_metadata;
pub mod events;
pub mod fees;