Skip to content
Merged
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
96 changes: 96 additions & 0 deletions creator-keys/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ pub enum ContractError {
NotPositiveAmount = 6,
FeeConfigNotSet = 7,
InvalidFeeConfig = 8,
InsufficientBalance = 9,
}

pub mod fee {
Expand Down Expand Up @@ -117,6 +118,18 @@ pub struct HolderKeyCountView {
pub creator_exists: bool,
}

/// Stable, non-optional view of a buy or sell quote.
///
/// Returned by [`CreatorKeysContract::get_buy_quote`] and [`CreatorKeysContract::get_sell_quote`].
#[derive(Clone, Debug, PartialEq)]
#[contracttype]
pub struct QuoteResponse {
pub price: i128,
pub creator_fee: i128,
pub protocol_fee: i128,
pub total_amount: i128,
}

/// Stable protocol state version for read-only consumers.
///
/// Bump this value only when externally consumed protocol state semantics change.
Expand Down Expand Up @@ -161,6 +174,29 @@ pub fn read_key_balance(env: &Env, creator: &Address) -> u32 {
.unwrap_or(0)
}

/// Formats a quote response with consistent total amount calculation.
///
/// For buys, total_amount = price + fees.
/// For sells, total_amount = price - fees.
fn format_quote_response(
price: i128,
creator_fee: i128,
protocol_fee: i128,
is_buy: bool,
) -> QuoteResponse {
let total_amount = if is_buy {
price + creator_fee + protocol_fee
} else {
price - creator_fee - protocol_fee
};
QuoteResponse {
price,
creator_fee,
protocol_fee,
total_amount,
}
}

#[contract]
pub struct CreatorKeysContract;

Expand Down Expand Up @@ -447,6 +483,66 @@ impl CreatorKeysContract {
},
}
}

/// Read-only view: returns a quote for buying a key.
///
/// Returns a [`QuoteResponse`] containing the current price and fee breakdown.
/// Fees are calculated based on the fixed key price.
pub fn get_buy_quote(env: Env, creator: Address) -> Result<QuoteResponse, ContractError> {
let price: i128 = env
.storage()
.persistent()
.get(&DataKey::KeyPrice)
.ok_or(ContractError::KeyPriceNotSet)?;

if read_creator_profile(&env, &creator).is_none() {
return Err(ContractError::NotRegistered);
}

let (creator_fee, protocol_fee) = Self::compute_fees_for_payment(env.clone(), price)?;

Ok(format_quote_response(
price,
creator_fee,
protocol_fee,
true,
))
}

/// Read-only view: returns a quote for selling a key.
///
/// Returns a [`QuoteResponse`] containing the current price and fee breakdown.
/// Fees are calculated based on the fixed key price.
/// Rejects with [`ContractError::InsufficientBalance`] if the holder has no keys.
pub fn get_sell_quote(
env: Env,
creator: Address,
holder: Address,
) -> Result<QuoteResponse, ContractError> {
let price: i128 = env
.storage()
.persistent()
.get(&DataKey::KeyPrice)
.ok_or(ContractError::KeyPriceNotSet)?;

if read_creator_profile(&env, &creator).is_none() {
return Err(ContractError::NotRegistered);
}

let balance = Self::get_key_balance(env.clone(), creator, holder);
if balance == 0 {
return Err(ContractError::InsufficientBalance);
}

let (creator_fee, protocol_fee) = Self::compute_fees_for_payment(env.clone(), price)?;

Ok(format_quote_response(
price,
creator_fee,
protocol_fee,
false,
))
}
}

#[cfg(test)]
Expand Down
101 changes: 101 additions & 0 deletions creator-keys/src/test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -232,3 +232,104 @@ fn test_get_treasury_address_persists_across_reads() {
assert_eq!(first_read, second_read);
assert_eq!(first_read, Some(treasury));
}

#[test]
fn test_get_buy_quote_success() {
let env = Env::default();
env.mock_all_auths();
let contract_id = env.register(CreatorKeysContract, ());
let client = CreatorKeysContractClient::new(&env, &contract_id);

let admin = Address::generate(&env);
client.set_key_price(&admin, &1000);
client.set_fee_config(&admin, &9000, &1000); // 90/10 split

let creator = Address::generate(&env);
let handle = String::from_str(&env, "alice");
client.register_creator(&creator, &handle);

let quote = client.get_buy_quote(&creator);
assert_eq!(quote.price, 1000);
assert_eq!(quote.creator_fee, 900);
assert_eq!(quote.protocol_fee, 100);
assert_eq!(quote.total_amount, 2000); // 1000 + 900 + 100
}

#[test]
fn test_get_sell_quote_success() {
let env = Env::default();
env.mock_all_auths();
let contract_id = env.register(CreatorKeysContract, ());
let client = CreatorKeysContractClient::new(&env, &contract_id);

let admin = Address::generate(&env);
client.set_key_price(&admin, &1000);
client.set_fee_config(&admin, &9000, &1000); // 90/10 split

let creator = Address::generate(&env);
let handle = String::from_str(&env, "alice");
client.register_creator(&creator, &handle);

let buyer = Address::generate(&env);
client.buy_key(&creator, &buyer, &1000);

let quote = client.get_sell_quote(&creator, &buyer);
assert_eq!(quote.price, 1000);
assert_eq!(quote.creator_fee, 900);
assert_eq!(quote.protocol_fee, 100);
assert_eq!(quote.total_amount, 0); // 1000 - 900 - 100
}

#[test]
fn test_get_sell_quote_fails_if_insufficient_balance() {
let env = Env::default();
env.mock_all_auths();
let contract_id = env.register(CreatorKeysContract, ());
let client = CreatorKeysContractClient::new(&env, &contract_id);

let admin = Address::generate(&env);
client.set_key_price(&admin, &1000);
client.set_fee_config(&admin, &9000, &1000);

let creator = Address::generate(&env);
let handle = String::from_str(&env, "alice");
client.register_creator(&creator, &handle);

let holder = Address::generate(&env); // Zero balance
let result = client.try_get_sell_quote(&creator, &holder);
assert_eq!(result, Err(Ok(ContractError::InsufficientBalance)));
}

#[test]
fn test_get_quote_fails_if_not_registered() {
let env = Env::default();
env.mock_all_auths();
let contract_id = env.register(CreatorKeysContract, ());
let client = CreatorKeysContractClient::new(&env, &contract_id);

let admin = Address::generate(&env);
client.set_key_price(&admin, &1000);

let creator = Address::generate(&env); // Not registered
let result = client.try_get_buy_quote(&creator);
assert_eq!(result, Err(Ok(ContractError::NotRegistered)));
}

#[test]
fn test_get_quote_fails_if_fee_not_set() {
let env = Env::default();
env.mock_all_auths();
let contract_id = env.register(CreatorKeysContract, ());
let client = CreatorKeysContractClient::new(&env, &contract_id);

let admin = Address::generate(&env);
client.set_key_price(&admin, &1000);
// Fee config NOT set

let creator = Address::generate(&env);
let handle = String::from_str(&env, "alice");
client.register_creator(&creator, &handle);

let result = client.try_get_buy_quote(&creator);
assert_eq!(result, Err(Ok(ContractError::FeeConfigNotSet)));
}
Loading
Loading