6. Smart Contract Specification
6.1 Exhibition Contract
use cosmwasm_std::{
entry_point, to_json_binary, Binary, Deps, DepsMut, Env,
MessageInfo, Response, StdError, StdResult, Addr, Timestamp, Uint128,
};
use cw_storage_plus::{Item, Map};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
// ============================================================================
// STATE DEFINITIONS
// ============================================================================
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
pub struct Config {
pub admin: Addr,
pub license_contract: Addr,
pub governance_contract: Addr,
pub next_exhibition_id: u64,
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
pub struct Exhibition {
pub id: u64,
pub artist: Addr,
pub curator: Addr,
pub title: String,
pub metadata_uri: String, // IPFS hash
pub content_hash: String, // SHA-256 of primary media
pub created_at: Timestamp,
pub total_licenses: u64,
pub active_licenses: u64,
pub lifetime_revenue: Uint128,
pub status: ExhibitionStatus,
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
pub enum ExhibitionStatus {
Draft,
Active,
Paused, // Artist can pause licensing temporarily
Archived, // No new licenses, existing honored
}
pub const CONFIG: Item<Config> = Item::new("config");
pub const EXHIBITIONS: Map<u64, Exhibition> = Map::new("exhibitions");
pub const ARTIST_EXHIBITIONS: Map<&Addr, Vec<u64>> = Map::new("artist_exhibitions");
pub const CURATOR_EXHIBITIONS: Map<&Addr, Vec<u64>> = Map::new("curator_exhibitions");
// ============================================================================
// INSTANTIATE
// ============================================================================
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
pub struct InstantiateMsg {
pub license_contract: String,
pub governance_contract: String,
}
#[entry_point]
pub fn instantiate(
deps: DepsMut,
_env: Env,
info: MessageInfo,
msg: InstantiateMsg,
) -> StdResult<Response> {
let config = Config {
admin: info.sender.clone(),
license_contract: deps.api.addr_validate(&msg.license_contract)?,
governance_contract: deps.api.addr_validate(&msg.governance_contract)?,
next_exhibition_id: 1,
};
CONFIG.save(deps.storage, &config)?;
Ok(Response::new()
.add_attribute("method", "instantiate")
.add_attribute("admin", info.sender))
}
// ============================================================================
// EXECUTE MESSAGES
// ============================================================================
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum ExecuteMsg {
// Artist creates new exhibition
CreateExhibition {
title: String,
curator: String,
metadata_uri: String,
content_hash: String,
},
// Artist updates metadata (not content hash - immutable)
UpdateMetadata {
exhibition_id: u64,
metadata_uri: String,
},
// Artist controls availability
SetStatus {
exhibition_id: u64,
status: ExhibitionStatus,
},
// Called by License contract to update stats
RecordLicense {
exhibition_id: u64,
fee_amount: Uint128,
},
// Called by License contract when license completes
CompleteLicense {
exhibition_id: u64,
},
// NOTE: No Transfer message - by design
// NOTE: No Burn message - exhibitions are permanent records
}
#[entry_point]
pub fn execute(
deps: DepsMut,
env: Env,
info: MessageInfo,
msg: ExecuteMsg,
) -> Result<Response, ContractError> {
match msg {
ExecuteMsg::CreateExhibition { title, curator, metadata_uri, content_hash } => {
execute_create_exhibition(deps, env, info, title, curator, metadata_uri, content_hash)
}
ExecuteMsg::UpdateMetadata { exhibition_id, metadata_uri } => {
execute_update_metadata(deps, info, exhibition_id, metadata_uri)
}
ExecuteMsg::SetStatus { exhibition_id, status } => {
execute_set_status(deps, info, exhibition_id, status)
}
ExecuteMsg::RecordLicense { exhibition_id, fee_amount } => {
execute_record_license(deps, info, exhibition_id, fee_amount)
}
ExecuteMsg::CompleteLicense { exhibition_id } => {
execute_complete_license(deps, info, exhibition_id)
}
}
}
fn execute_create_exhibition(
deps: DepsMut,
env: Env,
info: MessageInfo,
title: String,
curator: String,
metadata_uri: String,
content_hash: String,
) -> Result<Response, ContractError> {
let mut config = CONFIG.load(deps.storage)?;
let curator_addr = deps.api.addr_validate(&curator)?;
let exhibition = Exhibition {
id: config.next_exhibition_id,
artist: info.sender.clone(),
curator: curator_addr.clone(),
title: title.clone(),
metadata_uri,
content_hash,
created_at: env.block.time,
total_licenses: 0,
active_licenses: 0,
lifetime_revenue: Uint128::zero(),
status: ExhibitionStatus::Draft,
};
EXHIBITIONS.save(deps.storage, config.next_exhibition_id, &exhibition)?;
// Update artist index
let mut artist_exh = ARTIST_EXHIBITIONS
.may_load(deps.storage, &info.sender)?
.unwrap_or_default();
artist_exh.push(config.next_exhibition_id);
ARTIST_EXHIBITIONS.save(deps.storage, &info.sender, &artist_exh)?;
// Update curator index
let mut curator_exh = CURATOR_EXHIBITIONS
.may_load(deps.storage, &curator_addr)?
.unwrap_or_default();
curator_exh.push(config.next_exhibition_id);
CURATOR_EXHIBITIONS.save(deps.storage, &curator_addr, &curator_exh)?;
config.next_exhibition_id += 1;
CONFIG.save(deps.storage, &config)?;
Ok(Response::new()
.add_attribute("method", "create_exhibition")
.add_attribute("exhibition_id", exhibition.id.to_string())
.add_attribute("artist", info.sender)
.add_attribute("title", title))
}
fn execute_update_metadata(
deps: DepsMut,
info: MessageInfo,
exhibition_id: u64,
metadata_uri: String,
) -> Result<Response, ContractError> {
let mut exhibition = EXHIBITIONS.load(deps.storage, exhibition_id)?;
// Only artist can update metadata
if info.sender != exhibition.artist {
return Err(ContractError::Unauthorized {});
}
exhibition.metadata_uri = metadata_uri;
EXHIBITIONS.save(deps.storage, exhibition_id, &exhibition)?;
Ok(Response::new()
.add_attribute("method", "update_metadata")
.add_attribute("exhibition_id", exhibition_id.to_string()))
}
fn execute_set_status(
deps: DepsMut,
info: MessageInfo,
exhibition_id: u64,
status: ExhibitionStatus,
) -> Result<Response, ContractError> {
let mut exhibition = EXHIBITIONS.load(deps.storage, exhibition_id)?;
// Only artist can change status
if info.sender != exhibition.artist {
return Err(ContractError::Unauthorized {});
}
exhibition.status = status.clone();
EXHIBITIONS.save(deps.storage, exhibition_id, &exhibition)?;
Ok(Response::new()
.add_attribute("method", "set_status")
.add_attribute("exhibition_id", exhibition_id.to_string())
.add_attribute("status", format!("{:?}", status)))
}
fn execute_record_license(
deps: DepsMut,
info: MessageInfo,
exhibition_id: u64,
fee_amount: Uint128,
) -> Result<Response, ContractError> {
let config = CONFIG.load(deps.storage)?;
// Only license contract can record
if info.sender != config.license_contract {
return Err(ContractError::Unauthorized {});
}
let mut exhibition = EXHIBITIONS.load(deps.storage, exhibition_id)?;
exhibition.total_licenses += 1;
exhibition.active_licenses += 1;
exhibition.lifetime_revenue += fee_amount;
EXHIBITIONS.save(deps.storage, exhibition_id, &exhibition)?;
Ok(Response::new()
.add_attribute("method", "record_license")
.add_attribute("exhibition_id", exhibition_id.to_string()))
}
fn execute_complete_license(
deps: DepsMut,
info: MessageInfo,
exhibition_id: u64,
) -> Result<Response, ContractError> {
let config = CONFIG.load(deps.storage)?;
if info.sender != config.license_contract {
return Err(ContractError::Unauthorized {});
}
let mut exhibition = EXHIBITIONS.load(deps.storage, exhibition_id)?;
exhibition.active_licenses = exhibition.active_licenses.saturating_sub(1);
EXHIBITIONS.save(deps.storage, exhibition_id, &exhibition)?;
Ok(Response::new()
.add_attribute("method", "complete_license")
.add_attribute("exhibition_id", exhibition_id.to_string()))
}
// ============================================================================
// QUERY MESSAGES
// ============================================================================
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum QueryMsg {
GetExhibition { exhibition_id: u64 },
GetArtistExhibitions { artist: String },
GetCuratorExhibitions { curator: String },
ListActiveExhibitions { start_after: Option<u64>, limit: Option<u32> },
GetConfig {},
}
#[entry_point]
pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> StdResult<Binary> {
match msg {
QueryMsg::GetExhibition { exhibition_id } => {
to_json_binary(&EXHIBITIONS.load(deps.storage, exhibition_id)?)
}
QueryMsg::GetArtistExhibitions { artist } => {
let addr = deps.api.addr_validate(&artist)?;
let ids = ARTIST_EXHIBITIONS.may_load(deps.storage, &addr)?.unwrap_or_default();
to_json_binary(&ids)
}
QueryMsg::GetCuratorExhibitions { curator } => {
let addr = deps.api.addr_validate(&curator)?;
let ids = CURATOR_EXHIBITIONS.may_load(deps.storage, &addr)?.unwrap_or_default();
to_json_binary(&ids)
}
QueryMsg::ListActiveExhibitions { start_after, limit } => {
let limit = limit.unwrap_or(30).min(100) as usize;
let start = start_after.unwrap_or(0);
let exhibitions: Vec<Exhibition> = EXHIBITIONS
.range(deps.storage, None, None, cosmwasm_std::Order::Ascending)
.filter(|r| {
if let Ok((id, exh)) = r {
*id > start && exh.status == ExhibitionStatus::Active
} else {
false
}
})
.take(limit)
.filter_map(|r| r.ok().map(|(_, e)| e))
.collect();
to_json_binary(&exhibitions)
}
QueryMsg::GetConfig {} => to_json_binary(&CONFIG.load(deps.storage)?),
}
}
// ============================================================================
// ERRORS
// ============================================================================
#[derive(Debug)]
pub enum ContractError {
Std(StdError),
Unauthorized {},
ExhibitionNotFound {},
InvalidStatus {},
}
impl From<StdError> for ContractError {
fn from(err: StdError) -> Self {
ContractError::Std(err)
}
}6.2 License Contract
6.3 Payment Contract
6.4 Governance Contract
6.5 Provenance Contract
Last updated