Skip to content
Closed
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
24 changes: 20 additions & 4 deletions creator-keys/tests/buy_key.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand All @@ -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]
Expand Down
85 changes: 85 additions & 0 deletions creator-keys/tests/contract_test_env/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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, ());
Expand Down Expand Up @@ -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:
Expand Down
24 changes: 24 additions & 0 deletions creator-keys/tests/key_supply.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
31 changes: 28 additions & 3 deletions creator-keys/tests/supply_invariants.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand All @@ -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();
Expand Down Expand Up @@ -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);
Expand Down
Loading