diff --git a/creator-keys/tests/buy_key.rs b/creator-keys/tests/buy_key.rs index 0f0b417..7371fea 100644 --- a/creator-keys/tests/buy_key.rs +++ b/creator-keys/tests/buy_key.rs @@ -3,8 +3,9 @@ mod contract_test_env; use contract_test_env::{ - compute_expected_buy_price, register_creator_keys, register_test_creator, - set_key_price_for_tests, test_env_with_auths, + register_creator_keys, register_test_creator, set_key_price_for_tests, set_protocol_fee_bps, + test_env_with_auths, compute_expected_buy_price, + }; use creator_keys::ContractError; use soroban_sdk::{testutils::Address as _, Address}; @@ -13,14 +14,29 @@ use soroban_sdk::{testutils::Address as _, Address}; fn test_buy_key_unregistered_creator_fails() { let env = test_env_with_auths(); let (client, _) = register_creator_keys(&env); - let base_price = 100i128; - set_key_price_for_tests(&env, &client, base_price); + set_key_price_for_tests(&env, &client, 100i128); + set_protocol_fee_bps(&env, &client, 9000u32, 1000u32); let creator = Address::generate(&env); let buyer = Address::generate(&env); + let fee_view_before = client.get_protocol_fee_view(); + assert_eq!(client.get_total_key_supply(&creator), 0); + assert_eq!(client.get_key_balance(&creator, &buyer), 0); + + let result = client.try_buy_key(&creator, &buyer, &100i128); + + + let expected_price = compute_expected_buy_price(0, base_price); let result = client.try_buy_key(&creator, &buyer, &expected_price); assert_eq!(result, Err(Ok(ContractError::NotRegistered))); + assert_eq!(client.get_total_key_supply(&creator), 0); + assert_eq!(client.get_key_balance(&creator, &buyer), 0); + + let fee_view_after = client.get_protocol_fee_view(); + assert_eq!(fee_view_after.creator_bps, fee_view_before.creator_bps); + assert_eq!(fee_view_after.protocol_bps, fee_view_before.protocol_bps); + assert_eq!(fee_view_after.is_configured, fee_view_before.is_configured); } #[test] diff --git a/creator-keys/tests/contract_test_env/mod.rs b/creator-keys/tests/contract_test_env/mod.rs index ac82658..7f10ac6 100644 --- a/creator-keys/tests/contract_test_env/mod.rs +++ b/creator-keys/tests/contract_test_env/mod.rs @@ -15,6 +15,7 @@ use soroban_sdk::{ testutils::{Address as _, Ledger}, Address, Env, String, }; +use std::string::String as StdString; /// Stable timestamp used by integration tests unless a test needs to override it. pub const DEFAULT_TEST_TIMESTAMP: u64 = 1_700_000_000; @@ -43,6 +44,16 @@ pub fn test_env_with_auths() -> Env { env } +/// Return a deterministic test wallet address for a seed string. +pub fn test_wallet_address(env: &Env, seed: &str) -> Address { + Address::from_string(&String::from_str(env, &account_strkey_from_seed(seed))) +} + +/// Return a deterministic test wallet address for an index. +pub fn test_wallet_address_from_index(env: &Env, index: u32) -> Address { + test_wallet_address(env, &std::format!("wallet-{index}")) +} + /// Register [`CreatorKeysContract`] and return a client and the contract id. pub fn register_creator_keys<'a>(env: &'a Env) -> (CreatorKeysContractClient<'a>, Address) { let id = env.register(CreatorKeysContract, ()); @@ -132,6 +143,80 @@ pub fn set_stored_key_price(env: &Env, contract_id: &Address, price: i128) { }); } +fn account_strkey_from_seed(seed: &str) -> StdString { + let mut payload = [0u8; 32]; + let mut state = 0xcbf2_9ce4_8422_2325u64; + + for byte in seed.as_bytes() { + state ^= u64::from(*byte); + state = state.wrapping_mul(0x0000_0100_0000_01b3); + } + + for chunk in payload.chunks_mut(8) { + state ^= state >> 33; + state = state.wrapping_mul(0xff51_afd7_ed55_8ccd); + state ^= state >> 33; + state = state.wrapping_mul(0xc4ce_b9fe_1a85_ec53); + state ^= state >> 33; + chunk.copy_from_slice(&state.to_be_bytes()); + } + + let mut raw = [0u8; 35]; + raw[0] = 6 << 3; + raw[1..33].copy_from_slice(&payload); + let checksum = crc16_xmodem(&raw[..33]).to_le_bytes(); + raw[33..].copy_from_slice(&checksum); + + base32_encode(&raw) +} + +fn crc16_xmodem(bytes: &[u8]) -> u16 { + let mut crc = 0u16; + + for byte in bytes { + crc ^= u16::from(*byte) << 8; + for _ in 0..8 { + if crc & 0x8000 != 0 { + crc = (crc << 1) ^ 0x1021; + } else { + crc <<= 1; + } + } + } + + crc +} + +fn base32_encode(bytes: &[u8]) -> StdString { + const ALPHABET: &[u8; 32] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZ234567"; + + let mut encoded = StdString::new(); + let mut buffer = 0u16; + let mut bits = 0u8; + + for byte in bytes { + buffer = (buffer << 8) | u16::from(*byte); + bits += 8; + + while bits >= 5 { + bits -= 5; + let index = ((buffer >> bits) & 0x1f) as usize; + encoded.push(ALPHABET[index] as char); + } + + if bits > 0 { + buffer &= (1 << bits) - 1; + } else { + buffer = 0; + } + } + + if bits > 0 { + let index = ((buffer << (5 - bits)) & 0x1f) as usize; + encoded.push(ALPHABET[index] as char); + } + + encoded /// Computes the expected buy price for a given supply value. /// /// Current bonding curve formula: diff --git a/creator-keys/tests/key_supply.rs b/creator-keys/tests/key_supply.rs index 3bad7b0..69f7361 100644 --- a/creator-keys/tests/key_supply.rs +++ b/creator-keys/tests/key_supply.rs @@ -55,6 +55,30 @@ fn test_get_total_key_supply_increments_after_buy() { assert_eq!(client.get_total_key_supply(&creator), 2); } +#[test] +fn test_get_total_key_supply_increments_after_three_sequential_buys() { + let env = Env::default(); + env.mock_all_auths(); + let (client, _) = setup(&env); + + let creator = Address::generate(&env); + let buyer1 = Address::generate(&env); + let buyer2 = Address::generate(&env); + let buyer3 = Address::generate(&env); + client.register_creator(&creator, &String::from_str(&env, "alice")); + + assert_eq!(client.get_total_key_supply(&creator), 0); + + client.buy_key(&creator, &buyer1, &100_i128); + assert_eq!(client.get_total_key_supply(&creator), 1); + + client.buy_key(&creator, &buyer2, &100_i128); + assert_eq!(client.get_total_key_supply(&creator), 2); + + client.buy_key(&creator, &buyer3, &100_i128); + assert_eq!(client.get_total_key_supply(&creator), 3); +} + #[test] fn test_get_total_key_supply_is_read_only() { let env = Env::default(); diff --git a/creator-keys/tests/supply_invariants.rs b/creator-keys/tests/supply_invariants.rs index 75b48eb..ad8007f 100644 --- a/creator-keys/tests/supply_invariants.rs +++ b/creator-keys/tests/supply_invariants.rs @@ -10,6 +10,7 @@ mod contract_test_env; use contract_test_env::{ register_creator_keys, register_test_creator, set_key_price_for_tests, test_env_with_auths, + test_wallet_address, test_wallet_address_from_index, }; use creator_keys::CreatorKeysContractClient; use soroban_sdk::{testutils::Address as _, Address, Env}; @@ -23,6 +24,30 @@ fn setup<'a>(env: &'a Env, price: i128) -> (CreatorKeysContractClient<'a>, Addre // ── Supply conservation: buy then sell returns to prior state ───────────���─ +#[test] +fn test_wallet_address_returns_same_address_for_same_seed() { + let env = test_env_with_auths(); + + assert_eq!( + test_wallet_address(&env, "same-seed"), + test_wallet_address(&env, "same-seed") + ); +} + +#[test] +fn test_wallet_address_returns_distinct_addresses_for_distinct_inputs() { + let env = test_env_with_auths(); + + assert_ne!( + test_wallet_address_from_index(&env, 1), + test_wallet_address_from_index(&env, 2) + ); + assert_ne!( + test_wallet_address(&env, "seed-a"), + test_wallet_address(&env, "seed-b") + ); +} + #[test] fn test_supply_buy_then_sell_returns_to_zero() { let env = test_env_with_auths(); @@ -73,9 +98,9 @@ fn test_supply_three_buyers_sum_equals_total() { let env = test_env_with_auths(); let (client, creator) = setup(&env, 100); - let b1 = Address::generate(&env); - let b2 = Address::generate(&env); - let b3 = Address::generate(&env); + let b1 = test_wallet_address_from_index(&env, 1); + let b2 = test_wallet_address_from_index(&env, 2); + let b3 = test_wallet_address_from_index(&env, 3); client.buy_key(&creator, &b1, &100); client.buy_key(&creator, &b2, &100);