instantiate/execute/query entry points and every handler behind them. This is where the rules actually run.
Live source from
contracts/names/src/contract.rs (1046 lines). Generated from the deployed contract code.use cosmwasm_schema::cw_serde;
use cosmwasm_std::{
entry_point, to_binary, Addr, BankMsg, Binary, Coin, CosmosMsg, Deps, DepsMut, Env,
MessageInfo, Order, Response, StdResult, Uint128,
};
use cw2::set_contract_version;
use cw_storage_plus::Bound;
use crate::error::ContractError;
use crate::msg::{
ExecuteMsg, InstantiateMsg, MigrateMsg, OwnedByResponse, PricesUpdate, PricingResponse,
QueryMsg, ResolveResponse, ReverseResponse,
};
use crate::state::{Config, Record, CONFIG, NAMES, OWNED, PRIMARY};
const CONTRACT_NAME: &str = "crates.io:bwick-names";
const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION");
const DEFAULT_MIN_LENGTH: u8 = 3;
const DEFAULT_MAX_LENGTH: u8 = 32;
const DEFAULT_TLD: &str = ".bwick";
const DEFAULT_PRICE_3: u128 = 100_000_000; // $100
const DEFAULT_PRICE_4: u128 = 25_000_000; // $25
const DEFAULT_PRICE_5_8: u128 = 10_000_000; // $10
const DEFAULT_PRICE_9_PLUS: u128 = 1_000_000; // $1
const BWICK_DENOM: &str = "ubwick";
const DEFAULT_LIMIT: u32 = 30;
const MAX_LIMIT: u32 = 100;
// ── Oracle integration ────────────────────────────────────────────────────────
//
// Mirror of the bwick-oracle contract's QueryMsg::Price response.
#[cw_serde]
struct OraclePriceQueryMsg {
price: OraclePriceArgs,
}
#[cw_serde]
struct OraclePriceArgs {
require_fresh: Option<bool>,
}
#[cw_serde]
struct OraclePriceResponse {
bwick_usd_price: Uint128,
sol_usd_price: Uint128,
last_update_height: u64,
last_update_timestamp: u64,
source: String,
fresh: bool,
}
/// Returns the current BWICK/USD price in micro-USD (6 decimals) from the oracle.
/// Returns 0 if no source provides a price.
fn get_bwick_usd_price(deps: Deps, config: &Config) -> u128 {
if config.oracle_contract.as_str().is_empty() {
return 0;
}
let req = OraclePriceQueryMsg {
price: OraclePriceArgs { require_fresh: None },
};
deps.querier
.query_wasm_smart::<OraclePriceResponse>(config.oracle_contract.as_str(), &req)
.map(|r| r.bwick_usd_price.u128())
.unwrap_or(0)
}
// ── Name validation ───────────────────────────────────────────────────────────
fn validate_name(name: &str, min_length: u8, max_length: u8) -> Result<(), ContractError> {
let len = name.len();
if len < min_length as usize {
return Err(ContractError::InvalidName {
reason: format!("name too short ({} < {})", len, min_length),
});
}
if len > max_length as usize {
return Err(ContractError::InvalidName {
reason: format!("name too long ({} > {})", len, max_length),
});
}
let bytes = name.as_bytes();
if bytes[0] == b'-' || bytes[bytes.len() - 1] == b'-' {
return Err(ContractError::InvalidName {
reason: "leading or trailing hyphen not allowed".into(),
});
}
let mut prev_hyphen = false;
for &b in bytes {
let ok = matches!(b, b'a'..=b'z' | b'0'..=b'9' | b'-');
if !ok {
return Err(ContractError::InvalidName {
reason: "only lowercase a-z, 0-9, and hyphen are allowed".into(),
});
}
if b == b'-' {
if prev_hyphen {
return Err(ContractError::InvalidName {
reason: "double hyphen not allowed".into(),
});
}
prev_hyphen = true;
} else {
prev_hyphen = false;
}
}
Ok(())
}
/// Strip the configured TLD if present, otherwise return the name as-is.
fn canonicalize<'a>(raw: &'a str, tld: &str) -> &'a str {
if !tld.is_empty() && raw.ends_with(tld) {
&raw[..raw.len() - tld.len()]
} else {
raw
}
}
fn is_free_period(env: &Env, config: &Config) -> bool {
env.block.time.seconds() <= config.free_until_timestamp
}
fn tier_usd_for(name: &str, config: &Config) -> u128 {
match name.len() {
3 => config.price_3,
4 => config.price_4,
5..=8 => config.price_5_8,
_ => config.price_9_plus,
}
}
/// Compute the BWICK fee (in ubwick) for a name given the current oracle price
/// and price tier. Returns (fee_ubwick, tier_usd).
///
/// fee_ubwick = ceil(tier_usd * 10^6 / bwick_usd_price)
///
/// Both `tier_usd` and `bwick_usd_price` are in micro-USD, so:
/// tier_usd / bwick_usd_price = whole BWICK
/// * 10^6 = ubwick
fn compute_bwick_fee(tier_usd: u128, bwick_usd_price: u128) -> Result<u128, ContractError> {
if bwick_usd_price == 0 {
return Err(ContractError::OracleUnavailable {});
}
let numerator = tier_usd
.checked_mul(1_000_000)
.ok_or_else(|| ContractError::Std(cosmwasm_std::StdError::generic_err("overflow")))?;
// Round up so we never charge less than the listed tier.
let fee = numerator.div_ceil(bwick_usd_price);
Ok(fee)
}
fn extract_bwick_from_funds(funds: &[Coin]) -> u128 {
funds
.iter()
.filter(|c| c.denom == BWICK_DENOM)
.map(|c| c.amount.u128())
.sum()
}
// ── Entry points ──────────────────────────────────────────────────────────────
#[cfg_attr(not(feature = "library"), entry_point)]
pub fn instantiate(
deps: DepsMut,
_env: Env,
info: MessageInfo,
msg: InstantiateMsg,
) -> Result<Response, ContractError> {
let admin = match msg.admin {
Some(a) => deps.api.addr_validate(&a)?,
None => info.sender.clone(),
};
let treasury = deps.api.addr_validate(&msg.treasury)?;
let oracle_contract = if msg.oracle_contract.is_empty() {
Addr::unchecked("")
} else {
deps.api.addr_validate(&msg.oracle_contract)?
};
let min_length = msg.min_length.unwrap_or(DEFAULT_MIN_LENGTH);
let max_length = msg.max_length.unwrap_or(DEFAULT_MAX_LENGTH);
if min_length == 0 || min_length > max_length {
return Err(ContractError::InvalidConfig {
reason: format!("bad length bounds {}..={}", min_length, max_length),
});
}
let config = Config {
admin,
treasury,
oracle_contract,
free_until_timestamp: msg.free_until_timestamp,
min_length,
max_length,
tld: msg.tld.unwrap_or_else(|| DEFAULT_TLD.to_string()),
price_3: msg.price_3.map(|x| x.u128()).unwrap_or(DEFAULT_PRICE_3),
price_4: msg.price_4.map(|x| x.u128()).unwrap_or(DEFAULT_PRICE_4),
price_5_8: msg
.price_5_8
.map(|x| x.u128())
.unwrap_or(DEFAULT_PRICE_5_8),
price_9_plus: msg
.price_9_plus
.map(|x| x.u128())
.unwrap_or(DEFAULT_PRICE_9_PLUS),
};
CONFIG.save(deps.storage, &config)?;
set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?;
Ok(Response::new()
.add_attribute("action", "instantiate")
.add_attribute("admin", config.admin.as_str())
.add_attribute("treasury", config.treasury.as_str())
.add_attribute("oracle_contract", config.oracle_contract.as_str())
.add_attribute(
"free_until_timestamp",
config.free_until_timestamp.to_string(),
))
}
#[cfg_attr(not(feature = "library"), entry_point)]
pub fn execute(
deps: DepsMut,
env: Env,
info: MessageInfo,
msg: ExecuteMsg,
) -> Result<Response, ContractError> {
match msg {
ExecuteMsg::RegisterName { name, owner } => {
execute_register(deps, env, info, name, owner)
}
ExecuteMsg::TransferName { name, recipient } => {
execute_transfer(deps, env, info, name, recipient)
}
ExecuteMsg::ReleaseName { name } => execute_release(deps, env, info, name),
ExecuteMsg::SetPrimaryName { name } => execute_set_primary(deps, env, info, name),
ExecuteMsg::UpdateConfig {
admin,
treasury,
oracle_contract,
free_until_timestamp,
prices,
} => execute_update_config(
deps,
info,
admin,
treasury,
oracle_contract,
free_until_timestamp,
prices,
),
}
}
fn execute_register(
deps: DepsMut,
env: Env,
info: MessageInfo,
raw_name: String,
owner: String,
) -> Result<Response, ContractError> {
let config = CONFIG.load(deps.storage)?;
let name_lower = raw_name.to_lowercase();
let name = canonicalize(&name_lower, &config.tld).to_string();
validate_name(&name, config.min_length, config.max_length)?;
if NAMES.may_load(deps.storage, name.as_str())?.is_some() {
return Err(ContractError::NameTaken { name });
}
let owner_addr = deps.api.addr_validate(&owner)?;
let now = env.block.time.seconds();
let free = is_free_period(&env, &config);
let mut messages: Vec<CosmosMsg> = Vec::new();
let mut tier_usd: u128 = 0;
let mut fee_ubwick: u128 = 0;
if !free {
tier_usd = tier_usd_for(&name, &config);
let price = get_bwick_usd_price(deps.as_ref(), &config);
fee_ubwick = compute_bwick_fee(tier_usd, price)?;
let sent = extract_bwick_from_funds(&info.funds);
if sent < fee_ubwick {
return Err(ContractError::InsufficientFunds {
required: fee_ubwick,
sent,
});
}
// Forward fee to treasury.
messages.push(CosmosMsg::Bank(BankMsg::Send {
to_address: config.treasury.to_string(),
amount: vec![Coin {
denom: BWICK_DENOM.into(),
amount: Uint128::from(fee_ubwick),
}],
}));
// Refund excess to msg.sender.
let refund = sent - fee_ubwick;
if refund > 0 {
messages.push(CosmosMsg::Bank(BankMsg::Send {
to_address: info.sender.to_string(),
amount: vec![Coin {
denom: BWICK_DENOM.into(),
amount: Uint128::from(refund),
}],
}));
}
} else {
// Free period: refund any funds attached so callers don't lose them.
let sent = extract_bwick_from_funds(&info.funds);
if sent > 0 {
messages.push(CosmosMsg::Bank(BankMsg::Send {
to_address: info.sender.to_string(),
amount: vec![Coin {
denom: BWICK_DENOM.into(),
amount: Uint128::from(sent),
}],
}));
}
}
let record = Record {
owner: owner_addr.clone(),
registered_at: now,
};
NAMES.save(deps.storage, name.as_str(), &record)?;
OWNED.save(deps.storage, (&owner_addr, name.as_str()), &())?;
// Auto-set primary if the owner has none yet.
if PRIMARY.may_load(deps.storage, &owner_addr)?.is_none() {
PRIMARY.save(deps.storage, &owner_addr, &name)?;
}
Ok(Response::new()
.add_messages(messages)
.add_attribute("action", "register_name")
.add_attribute("name", &name)
.add_attribute("owner", owner_addr.as_str())
.add_attribute("free", free.to_string())
.add_attribute("fee_ubwick", fee_ubwick.to_string())
.add_attribute("tier_usd", tier_usd.to_string()))
}
fn execute_transfer(
deps: DepsMut,
_env: Env,
info: MessageInfo,
raw_name: String,
recipient: String,
) -> Result<Response, ContractError> {
let config = CONFIG.load(deps.storage)?;
let name_lower = raw_name.to_lowercase();
let name = canonicalize(&name_lower, &config.tld).to_string();
let mut record = NAMES
.may_load(deps.storage, name.as_str())?
.ok_or_else(|| ContractError::NameNotFound { name: name.clone() })?;
if record.owner != info.sender {
return Err(ContractError::Unauthorized {});
}
let new_owner = deps.api.addr_validate(&recipient)?;
// Update primary mapping for the previous owner if this was their primary.
if let Some(prim) = PRIMARY.may_load(deps.storage, &info.sender)? {
if prim == name {
PRIMARY.remove(deps.storage, &info.sender);
}
}
OWNED.remove(deps.storage, (&info.sender, name.as_str()));
OWNED.save(deps.storage, (&new_owner, name.as_str()), &())?;
record.owner = new_owner.clone();
NAMES.save(deps.storage, name.as_str(), &record)?;
Ok(Response::new()
.add_attribute("action", "transfer_name")
.add_attribute("name", &name)
.add_attribute("from", info.sender.as_str())
.add_attribute("to", new_owner.as_str()))
}
fn execute_release(
deps: DepsMut,
_env: Env,
info: MessageInfo,
raw_name: String,
) -> Result<Response, ContractError> {
let config = CONFIG.load(deps.storage)?;
let name_lower = raw_name.to_lowercase();
let name = canonicalize(&name_lower, &config.tld).to_string();
let record = NAMES
.may_load(deps.storage, name.as_str())?
.ok_or_else(|| ContractError::NameNotFound { name: name.clone() })?;
if record.owner != info.sender {
return Err(ContractError::Unauthorized {});
}
NAMES.remove(deps.storage, name.as_str());
OWNED.remove(deps.storage, (&info.sender, name.as_str()));
if let Some(prim) = PRIMARY.may_load(deps.storage, &info.sender)? {
if prim == name {
PRIMARY.remove(deps.storage, &info.sender);
}
}
Ok(Response::new()
.add_attribute("action", "release_name")
.add_attribute("name", &name)
.add_attribute("owner", info.sender.as_str()))
}
fn execute_set_primary(
deps: DepsMut,
_env: Env,
info: MessageInfo,
raw_name: String,
) -> Result<Response, ContractError> {
let config = CONFIG.load(deps.storage)?;
let name_lower = raw_name.to_lowercase();
let name = canonicalize(&name_lower, &config.tld).to_string();
let record = NAMES
.may_load(deps.storage, name.as_str())?
.ok_or_else(|| ContractError::NameNotFound { name: name.clone() })?;
if record.owner != info.sender {
return Err(ContractError::Unauthorized {});
}
PRIMARY.save(deps.storage, &info.sender, &name)?;
Ok(Response::new()
.add_attribute("action", "set_primary_name")
.add_attribute("name", &name)
.add_attribute("owner", info.sender.as_str()))
}
#[allow(clippy::too_many_arguments)]
fn execute_update_config(
deps: DepsMut,
info: MessageInfo,
admin: Option<String>,
treasury: Option<String>,
oracle_contract: Option<String>,
free_until_timestamp: Option<u64>,
prices: Option<PricesUpdate>,
) -> Result<Response, ContractError> {
let mut config = CONFIG.load(deps.storage)?;
if info.sender != config.admin {
return Err(ContractError::Unauthorized {});
}
if let Some(a) = admin {
config.admin = deps.api.addr_validate(&a)?;
}
if let Some(t) = treasury {
config.treasury = deps.api.addr_validate(&t)?;
}
if let Some(o) = oracle_contract {
config.oracle_contract = if o.is_empty() {
Addr::unchecked("")
} else {
deps.api.addr_validate(&o)?
};
}
if let Some(ts) = free_until_timestamp {
config.free_until_timestamp = ts;
}
if let Some(p) = prices {
if let Some(v) = p.price_3 {
config.price_3 = v.u128();
}
if let Some(v) = p.price_4 {
config.price_4 = v.u128();
}
if let Some(v) = p.price_5_8 {
config.price_5_8 = v.u128();
}
if let Some(v) = p.price_9_plus {
config.price_9_plus = v.u128();
}
}
CONFIG.save(deps.storage, &config)?;
Ok(Response::new().add_attribute("action", "update_config"))
}
#[cfg_attr(not(feature = "library"), entry_point)]
pub fn migrate(
deps: DepsMut,
_env: Env,
msg: MigrateMsg,
) -> Result<Response, ContractError> {
let mut config = CONFIG.load(deps.storage)?;
if let Some(a) = msg.admin {
config.admin = deps.api.addr_validate(&a)?;
}
if let Some(t) = msg.treasury {
config.treasury = deps.api.addr_validate(&t)?;
}
if let Some(o) = msg.oracle_contract {
config.oracle_contract = if o.is_empty() {
Addr::unchecked("")
} else {
deps.api.addr_validate(&o)?
};
}
if let Some(ts) = msg.free_until_timestamp {
config.free_until_timestamp = ts;
}
CONFIG.save(deps.storage, &config)?;
set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?;
Ok(Response::new().add_attribute("action", "migrate"))
}
#[cfg_attr(not(feature = "library"), entry_point)]
pub fn query(deps: Deps, env: Env, msg: QueryMsg) -> StdResult<Binary> {
match msg {
QueryMsg::Resolve { name } => to_binary(&query_resolve(deps, name)?),
QueryMsg::Reverse { address } => to_binary(&query_reverse(deps, address)?),
QueryMsg::OwnedBy {
address,
start_after,
limit,
} => to_binary(&query_owned_by(deps, address, start_after, limit)?),
QueryMsg::Pricing { name } => to_binary(&query_pricing(deps, env, name)?),
QueryMsg::Config {} => to_binary(&CONFIG.load(deps.storage)?),
}
}
fn query_resolve(deps: Deps, raw_name: String) -> StdResult<ResolveResponse> {
let config = CONFIG.load(deps.storage)?;
let name_lower = raw_name.to_lowercase();
let name = canonicalize(&name_lower, &config.tld);
match NAMES.may_load(deps.storage, name)? {
Some(rec) => Ok(ResolveResponse {
owner: Some(rec.owner),
registered_at: Some(rec.registered_at),
}),
None => Ok(ResolveResponse {
owner: None,
registered_at: None,
}),
}
}
fn query_reverse(deps: Deps, address: String) -> StdResult<ReverseResponse> {
let addr = deps.api.addr_validate(&address)?;
Ok(ReverseResponse {
primary: PRIMARY.may_load(deps.storage, &addr)?,
})
}
fn query_owned_by(
deps: Deps,
address: String,
start_after: Option<String>,
limit: Option<u32>,
) -> StdResult<OwnedByResponse> {
let addr = deps.api.addr_validate(&address)?;
let limit = limit.unwrap_or(DEFAULT_LIMIT).min(MAX_LIMIT) as usize;
let start = start_after.as_deref().map(Bound::exclusive);
let names: Vec<String> = OWNED
.prefix(&addr)
.range(deps.storage, start, None, Order::Ascending)
.take(limit)
.filter_map(|item| item.ok().map(|(k, _)| k))
.collect();
Ok(OwnedByResponse { names })
}
fn query_pricing(deps: Deps, env: Env, raw_name: String) -> StdResult<PricingResponse> {
let config = CONFIG.load(deps.storage)?;
let name_lower = raw_name.to_lowercase();
let name = canonicalize(&name_lower, &config.tld);
if validate_name(name, config.min_length, config.max_length).is_err() {
// Not a valid name — return zero/free without erroring so the UI can still
// show why a name is unsupported separately via the validation rules.
return Ok(PricingResponse {
fee_ubwick: Uint128::zero(),
free: is_free_period(&env, &config),
tier_usd: 0,
});
}
if is_free_period(&env, &config) {
return Ok(PricingResponse {
fee_ubwick: Uint128::zero(),
free: true,
tier_usd: 0,
});
}
let tier_usd = tier_usd_for(name, &config);
let price = get_bwick_usd_price(deps, &config);
let fee = if price == 0 {
// Oracle unavailable; surface zero (UI should treat this as "price unknown").
0
} else {
compute_bwick_fee(tier_usd, price).unwrap_or(0)
};
Ok(PricingResponse {
fee_ubwick: Uint128::from(fee),
free: false,
tier_usd,
})
}
// ── Tests ─────────────────────────────────────────────────────────────────────
#[cfg(test)]
mod tests {
use super::*;
use cosmwasm_std::testing::{mock_dependencies, mock_env, mock_info};
use cosmwasm_std::{from_binary, ContractResult, OwnedDeps, SystemResult, WasmQuery};
fn default_config_msg(now: u64, free_until_offset: i64) -> InstantiateMsg {
let free_until = if free_until_offset >= 0 {
now + free_until_offset as u64
} else {
now.saturating_sub((-free_until_offset) as u64)
};
InstantiateMsg {
admin: None,
treasury: "treasury0".into(),
oracle_contract: "oracle0".into(),
free_until_timestamp: free_until,
min_length: None,
max_length: None,
tld: None,
price_3: None,
price_4: None,
price_5_8: None,
price_9_plus: None,
}
}
fn setup(
free_offset: i64,
) -> (
OwnedDeps<
cosmwasm_std::MemoryStorage,
cosmwasm_std::testing::MockApi,
cosmwasm_std::testing::MockQuerier,
>,
Env,
) {
let mut deps = mock_dependencies();
let env = mock_env();
let info = mock_info("admin0", &[]);
let msg = default_config_msg(env.block.time.seconds(), free_offset);
instantiate(deps.as_mut(), env.clone(), info, msg).unwrap();
(deps, env)
}
/// Mocks an oracle that returns a fixed BWICK/USD price in micro-USD.
fn install_oracle_price(
deps: &mut OwnedDeps<
cosmwasm_std::MemoryStorage,
cosmwasm_std::testing::MockApi,
cosmwasm_std::testing::MockQuerier,
>,
bwick_usd_price: u128,
) {
deps.querier.update_wasm(move |q| match q {
WasmQuery::Smart { .. } => {
let resp = OraclePriceResponse {
bwick_usd_price: Uint128::from(bwick_usd_price),
sol_usd_price: Uint128::zero(),
last_update_height: 1,
last_update_timestamp: 0,
source: "mock".into(),
fresh: true,
};
SystemResult::Ok(ContractResult::Ok(to_binary(&resp).unwrap()))
}
_ => SystemResult::Ok(ContractResult::Err("unimpl".into())),
});
}
#[test]
fn test_validate_name_rules() {
// ok cases
assert!(validate_name("abc", 3, 32).is_ok());
assert!(validate_name("a-b-c", 3, 32).is_ok());
assert!(validate_name("a1b2c3", 3, 32).is_ok());
// boundary length
assert!(validate_name("ab", 3, 32).is_err());
assert!(validate_name(&"a".repeat(33), 3, 32).is_err());
assert!(validate_name(&"a".repeat(32), 3, 32).is_ok());
// forbidden chars
assert!(validate_name("ABC", 3, 32).is_err());
assert!(validate_name("ab.c", 3, 32).is_err());
assert!(validate_name("ab_c", 3, 32).is_err());
assert!(validate_name("ab c", 3, 32).is_err());
// hyphen rules
assert!(validate_name("-abc", 3, 32).is_err());
assert!(validate_name("abc-", 3, 32).is_err());
assert!(validate_name("a--b", 3, 32).is_err());
}
#[test]
fn test_register_during_free_period() {
let (mut deps, env) = setup(60);
let info = mock_info("anyone", &[]);
let res = execute_register(
deps.as_mut(),
env.clone(),
info,
"chef".into(),
"owner1".into(),
)
.unwrap();
// No bank messages during free period when zero funds attached.
assert!(res.messages.is_empty());
// Resolve should now return owner.
let q = query_resolve(deps.as_ref(), "chef".into()).unwrap();
assert_eq!(q.owner.as_ref().map(|a| a.as_str()), Some("owner1"));
// Auto-primary should be set.
let r = query_reverse(deps.as_ref(), "owner1".into()).unwrap();
assert_eq!(r.primary.as_deref(), Some("chef"));
}
#[test]
fn test_double_registration_rejected() {
let (mut deps, env) = setup(60);
execute_register(
deps.as_mut(),
env.clone(),
mock_info("a", &[]),
"chef".into(),
"owner1".into(),
)
.unwrap();
let err = execute_register(
deps.as_mut(),
env.clone(),
mock_info("b", &[]),
"chef".into(),
"owner2".into(),
)
.unwrap_err();
match err {
ContractError::NameTaken { .. } => {}
other => panic!("expected NameTaken, got {:?}", other),
}
}
#[test]
fn test_post_free_pricing_via_oracle() {
// BWICK price = $0.50 = 500_000 micro-USD per BWICK.
// For a 5-8 char name, tier = $10 = 10_000_000 micro-USD.
// fee_ubwick = ceil(10_000_000 * 1_000_000 / 500_000) = 20_000_000 ubwick = 20 BWICK.
let (mut deps, env) = setup(-10); // free period in the past
install_oracle_price(&mut deps, 500_000);
let pricing = query_pricing(deps.as_ref(), env.clone(), "chefoo".into()).unwrap();
assert!(!pricing.free);
assert_eq!(pricing.tier_usd, 10_000_000);
assert_eq!(pricing.fee_ubwick.u128(), 20_000_000);
// Insufficient funds rejected.
let info = mock_info("anyone", &[Coin::new(1_000, BWICK_DENOM)]);
let err = execute_register(
deps.as_mut(),
env.clone(),
info,
"chefoo".into(),
"owner1".into(),
)
.unwrap_err();
match err {
ContractError::InsufficientFunds { required, sent } => {
assert_eq!(required, 20_000_000);
assert_eq!(sent, 1_000);
}
other => panic!("expected InsufficientFunds, got {:?}", other),
}
// Exact-amount succeeds; one bank message to treasury.
let info = mock_info("anyone", &[Coin::new(20_000_000, BWICK_DENOM)]);
let res = execute_register(
deps.as_mut(),
env.clone(),
info,
"chefoo".into(),
"owner1".into(),
)
.unwrap();
assert_eq!(res.messages.len(), 1);
// Overpay refunds.
let info = mock_info("anyone", &[Coin::new(25_000_000, BWICK_DENOM)]);
let res = execute_register(
deps.as_mut(),
env,
info,
"chefoo2".into(),
"owner1".into(),
)
.unwrap();
assert_eq!(res.messages.len(), 2); // treasury + refund
}
#[test]
fn test_transfer_clears_primary_when_was_primary() {
let (mut deps, env) = setup(60);
execute_register(
deps.as_mut(),
env.clone(),
mock_info("anyone", &[]),
"chef".into(),
"owner1".into(),
)
.unwrap();
// owner1's primary should currently be "chef"
assert_eq!(
query_reverse(deps.as_ref(), "owner1".into())
.unwrap()
.primary
.as_deref(),
Some("chef")
);
execute_transfer(
deps.as_mut(),
env.clone(),
mock_info("owner1", &[]),
"chef".into(),
"owner2".into(),
)
.unwrap();
// owner1 no longer has primary
assert!(query_reverse(deps.as_ref(), "owner1".into())
.unwrap()
.primary
.is_none());
// owner2 not auto-set
assert!(query_reverse(deps.as_ref(), "owner2".into())
.unwrap()
.primary
.is_none());
// resolve points to owner2
assert_eq!(
query_resolve(deps.as_ref(), "chef".into())
.unwrap()
.owner
.as_ref()
.map(|a| a.as_str()),
Some("owner2")
);
}
#[test]
fn test_set_primary_only_by_owner() {
let (mut deps, env) = setup(60);
execute_register(
deps.as_mut(),
env.clone(),
mock_info("anyone", &[]),
"chef".into(),
"owner1".into(),
)
.unwrap();
execute_register(
deps.as_mut(),
env.clone(),
mock_info("anyone", &[]),
"diner".into(),
"owner1".into(),
)
.unwrap();
// someone else cannot change owner1's primary
let err = execute_set_primary(
deps.as_mut(),
env.clone(),
mock_info("intruder", &[]),
"diner".into(),
)
.unwrap_err();
assert!(matches!(err, ContractError::Unauthorized { .. }));
// owner1 can switch primary
execute_set_primary(
deps.as_mut(),
env,
mock_info("owner1", &[]),
"diner".into(),
)
.unwrap();
assert_eq!(
query_reverse(deps.as_ref(), "owner1".into())
.unwrap()
.primary
.as_deref(),
Some("diner")
);
}
#[test]
fn test_release_clears_state() {
let (mut deps, env) = setup(60);
execute_register(
deps.as_mut(),
env.clone(),
mock_info("anyone", &[]),
"chef".into(),
"owner1".into(),
)
.unwrap();
execute_release(
deps.as_mut(),
env.clone(),
mock_info("owner1", &[]),
"chef".into(),
)
.unwrap();
assert!(query_resolve(deps.as_ref(), "chef".into())
.unwrap()
.owner
.is_none());
assert!(query_reverse(deps.as_ref(), "owner1".into())
.unwrap()
.primary
.is_none());
}
#[test]
fn test_owned_by_query() {
let (mut deps, env) = setup(60);
for n in ["abc", "xyz", "chef", "diner"].iter() {
execute_register(
deps.as_mut(),
env.clone(),
mock_info("anyone", &[]),
(*n).into(),
"owner1".into(),
)
.unwrap();
}
let owned = query_owned_by(deps.as_ref(), "owner1".into(), None, None).unwrap();
// Map key order is ascending alphabetic.
assert_eq!(owned.names, vec!["abc", "chef", "diner", "xyz"]);
}
#[test]
fn test_canonicalize_strips_tld() {
assert_eq!(canonicalize("chef.bwick", ".bwick"), "chef");
assert_eq!(canonicalize("chef", ".bwick"), "chef");
assert_eq!(canonicalize(".bwick", ".bwick"), "");
}
#[test]
fn test_query_resolve_handles_tld_suffix() {
let (mut deps, env) = setup(60);
execute_register(
deps.as_mut(),
env.clone(),
mock_info("anyone", &[]),
"chef".into(),
"owner1".into(),
)
.unwrap();
let r = query_resolve(deps.as_ref(), "chef.bwick".into()).unwrap();
assert_eq!(r.owner.as_ref().map(|a| a.as_str()), Some("owner1"));
}
#[test]
fn test_update_config_admin_only() {
let (mut deps, _env) = setup(60);
let err = execute_update_config(
deps.as_mut(),
mock_info("intruder", &[]),
None,
None,
None,
Some(42),
None,
)
.unwrap_err();
assert!(matches!(err, ContractError::Unauthorized { .. }));
// The admin defaulted to the instantiator ("admin0").
execute_update_config(
deps.as_mut(),
mock_info("admin0", &[]),
None,
None,
None,
Some(42),
None,
)
.unwrap();
let cfg = CONFIG.load(deps.as_ref().storage).unwrap();
assert_eq!(cfg.free_until_timestamp, 42);
}
#[test]
fn test_query_message_round_trip() {
// Sanity that QueryMsg variants parse cleanly via the entry point.
let (mut deps, env) = setup(60);
execute_register(
deps.as_mut(),
env.clone(),
mock_info("anyone", &[]),
"chef".into(),
"owner1".into(),
)
.unwrap();
let bin = query(
deps.as_ref(),
env.clone(),
QueryMsg::Resolve {
name: "chef".into(),
},
)
.unwrap();
let resp: ResolveResponse = from_binary(&bin).unwrap();
assert!(resp.owner.is_some());
let bin = query(deps.as_ref(), env, QueryMsg::Config {}).unwrap();
let cfg: Config = from_binary(&bin).unwrap();
assert_eq!(cfg.tld, ".bwick");
}
}

