//! SYN-721 NFT Contract //! //! A standard non-fungible token implementation for Synor, similar to ERC-721. //! //! # Features //! - Minting NFTs with metadata URI //! - Burning //! - Transfers //! - Approvals (per-token and operator) //! - Ownership management //! //! # Methods //! - `init(name, symbol)` - Initialize NFT collection //! - `name() -> String` - Collection name //! - `symbol() -> String` - Collection symbol //! - `total_supply() -> u64` - Total minted tokens //! - `balance_of(owner) -> u64` - Token count for owner //! - `owner_of(token_id) -> Address` - Owner of token //! - `token_uri(token_id) -> String` - Metadata URI //! - `transfer(to, token_id)` - Transfer token //! - `approve(to, token_id)` - Approve transfer //! - `get_approved(token_id) -> Address` - Get approved address //! - `set_approval_for_all(operator, approved)` - Set operator approval //! - `is_approved_for_all(owner, operator) -> bool` - Check operator //! - `mint(to, token_uri) -> u64` - Mint new token //! - `burn(token_id)` - Burn token #![no_std] extern crate alloc; use alloc::string::String; use alloc::vec::Vec; use borsh::BorshDeserialize; use synor_sdk::prelude::*; use synor_sdk::{require, require_auth}; // ==================== Storage Keys ==================== mod keys { pub const NAME: &[u8] = b"syn721:name"; pub const SYMBOL: &[u8] = b"syn721:symbol"; pub const TOTAL_SUPPLY: &[u8] = b"syn721:total_supply"; pub const NEXT_TOKEN_ID: &[u8] = b"syn721:next_token_id"; pub const OWNER: &[u8] = b"syn721:owner"; pub const OWNERS: &[u8] = b"syn721:owners"; // token_id -> Address pub const BALANCES: &[u8] = b"syn721:balances"; // Address -> count pub const TOKEN_URIS: &[u8] = b"syn721:uris"; // token_id -> String pub const APPROVALS: &[u8] = b"syn721:approvals"; // token_id -> Address pub const OPERATORS: &[u8] = b"syn721:operators"; // owner+operator -> bool } // ==================== Storage Helpers ==================== fn owners() -> storage::Map<[u8; 8], Address> { storage::Map::new(keys::OWNERS) } fn balances() -> storage::Map<[u8; 34], u64> { storage::Map::new(keys::BALANCES) } fn token_uris() -> storage::Map<[u8; 8], String> { storage::Map::new(keys::TOKEN_URIS) } fn approvals() -> storage::Map<[u8; 8], Address> { storage::Map::new(keys::APPROVALS) } fn token_id_key(token_id: u64) -> [u8; 8] { token_id.to_le_bytes() } fn operator_key(owner: &Address, operator: &Address) -> Vec { let mut key = Vec::with_capacity(68); key.extend_from_slice(owner.as_bytes()); key.extend_from_slice(operator.as_bytes()); key } fn get_operator(owner: &Address, operator: &Address) -> bool { let key = operator_key(owner, operator); storage::get_with_suffix::(keys::OPERATORS, &key).unwrap_or(false) } fn set_operator(owner: &Address, operator: &Address, approved: bool) { let key = operator_key(owner, operator); storage::set_with_suffix(keys::OPERATORS, &key, &approved); } fn get_owner() -> Option
{ storage::get::
(keys::OWNER) } // ==================== Events ==================== struct TransferNFT { from: Address, to: Address, token_id: u64, } impl synor_sdk::events::Event for TransferNFT { fn topics(&self) -> Vec { vec![ synor_sdk::events::Topic::from_name("Transfer"), synor_sdk::events::Topic::from(&self.from), synor_sdk::events::Topic::from(&self.to), synor_sdk::events::Topic::from(self.token_id), ] } fn data(&self) -> Vec { Vec::new() } } struct ApprovalNFT { owner: Address, approved: Address, token_id: u64, } impl synor_sdk::events::Event for ApprovalNFT { fn topics(&self) -> Vec { vec![ synor_sdk::events::Topic::from_name("Approval"), synor_sdk::events::Topic::from(&self.owner), synor_sdk::events::Topic::from(&self.approved), synor_sdk::events::Topic::from(self.token_id), ] } fn data(&self) -> Vec { Vec::new() } } struct ApprovalForAll { owner: Address, operator: Address, approved: bool, } impl synor_sdk::events::Event for ApprovalForAll { fn topics(&self) -> Vec { vec![ synor_sdk::events::Topic::from_name("ApprovalForAll"), synor_sdk::events::Topic::from(&self.owner), synor_sdk::events::Topic::from(&self.operator), ] } fn data(&self) -> Vec { vec![if self.approved { 1 } else { 0 }] } } // ==================== Entry Points ==================== synor_sdk::entry_point!(init, call); /// Initialize the NFT collection. fn init(params: &[u8]) -> Result<()> { #[derive(BorshDeserialize)] struct InitParams { name: String, symbol: String, } let params = InitParams::try_from_slice(params) .map_err(|_| Error::invalid_args("Invalid init params"))?; // Store collection metadata storage::set(keys::NAME, ¶ms.name); storage::set(keys::SYMBOL, ¶ms.symbol); storage::set(keys::TOTAL_SUPPLY, &0u64); storage::set(keys::NEXT_TOKEN_ID, &1u64); // Set owner let owner = caller(); storage::set(keys::OWNER, &owner); Ok(()) } /// Handle contract calls. fn call(selector: &[u8], params: &[u8]) -> Result> { // Method selectors (computed at runtime) let name_sel = synor_sdk::method_selector("name"); let symbol_sel = synor_sdk::method_selector("symbol"); let total_supply_sel = synor_sdk::method_selector("total_supply"); let balance_of_sel = synor_sdk::method_selector("balance_of"); let owner_of_sel = synor_sdk::method_selector("owner_of"); let token_uri_sel = synor_sdk::method_selector("token_uri"); let transfer_sel = synor_sdk::method_selector("transfer"); let approve_sel = synor_sdk::method_selector("approve"); let get_approved_sel = synor_sdk::method_selector("get_approved"); let set_approval_for_all_sel = synor_sdk::method_selector("set_approval_for_all"); let is_approved_for_all_sel = synor_sdk::method_selector("is_approved_for_all"); let mint_sel = synor_sdk::method_selector("mint"); let burn_sel = synor_sdk::method_selector("burn"); let owner_sel = synor_sdk::method_selector("owner"); let transfer_ownership_sel = synor_sdk::method_selector("transfer_ownership"); match selector { s if s == name_sel => { let name: String = storage::get(keys::NAME).unwrap_or_default(); Ok(borsh::to_vec(&name).unwrap()) } s if s == symbol_sel => { let symbol: String = storage::get(keys::SYMBOL).unwrap_or_default(); Ok(borsh::to_vec(&symbol).unwrap()) } s if s == total_supply_sel => { let supply: u64 = storage::get(keys::TOTAL_SUPPLY).unwrap_or(0); Ok(borsh::to_vec(&supply).unwrap()) } s if s == balance_of_sel => { #[derive(BorshDeserialize)] struct Args { owner: Address, } let args = Args::try_from_slice(params) .map_err(|_| Error::invalid_args("Expected owner address"))?; let balance = balances().get_or_default(&args.owner.0); Ok(borsh::to_vec(&balance).unwrap()) } s if s == owner_of_sel => { #[derive(BorshDeserialize)] struct Args { token_id: u64, } let args = Args::try_from_slice(params) .map_err(|_| Error::invalid_args("Expected token_id"))?; let owner = owners() .get(&token_id_key(args.token_id)) .ok_or(Error::not_found("Token does not exist"))?; Ok(borsh::to_vec(&owner).unwrap()) } s if s == token_uri_sel => { #[derive(BorshDeserialize)] struct Args { token_id: u64, } let args = Args::try_from_slice(params) .map_err(|_| Error::invalid_args("Expected token_id"))?; let uri = token_uris() .get(&token_id_key(args.token_id)) .unwrap_or_default(); Ok(borsh::to_vec(&uri).unwrap()) } s if s == transfer_sel => { #[derive(BorshDeserialize)] struct Args { to: Address, token_id: u64, } let args = Args::try_from_slice(params) .map_err(|_| Error::invalid_args("Expected to, token_id"))?; do_transfer(&caller(), &args.to, args.token_id)?; Ok(Vec::new()) } s if s == approve_sel => { #[derive(BorshDeserialize)] struct Args { to: Address, token_id: u64, } let args = Args::try_from_slice(params) .map_err(|_| Error::invalid_args("Expected to, token_id"))?; let token_owner = owners() .get(&token_id_key(args.token_id)) .ok_or(Error::not_found("Token does not exist"))?; let sender = caller(); require!( sender == token_owner || get_operator(&token_owner, &sender), Error::Unauthorized ); approvals().set(&token_id_key(args.token_id), &args.to); emit(&ApprovalNFT { owner: token_owner, approved: args.to, token_id: args.token_id, }); Ok(Vec::new()) } s if s == get_approved_sel => { #[derive(BorshDeserialize)] struct Args { token_id: u64, } let args = Args::try_from_slice(params) .map_err(|_| Error::invalid_args("Expected token_id"))?; let approved = approvals() .get(&token_id_key(args.token_id)) .unwrap_or(Address::zero()); Ok(borsh::to_vec(&approved).unwrap()) } s if s == set_approval_for_all_sel => { #[derive(BorshDeserialize)] struct Args { operator: Address, approved: bool, } let args = Args::try_from_slice(params) .map_err(|_| Error::invalid_args("Expected operator, approved"))?; let owner = caller(); set_operator(&owner, &args.operator, args.approved); emit(&ApprovalForAll { owner, operator: args.operator, approved: args.approved, }); Ok(Vec::new()) } s if s == is_approved_for_all_sel => { #[derive(BorshDeserialize)] struct Args { owner: Address, operator: Address, } let args = Args::try_from_slice(params) .map_err(|_| Error::invalid_args("Expected owner, operator"))?; let approved = get_operator(&args.owner, &args.operator); Ok(borsh::to_vec(&approved).unwrap()) } s if s == mint_sel => { #[derive(BorshDeserialize)] struct Args { to: Address, token_uri: String, } let args = Args::try_from_slice(params) .map_err(|_| Error::invalid_args("Expected to, token_uri"))?; // Only owner can mint let contract_owner = get_owner().ok_or(Error::Unauthorized)?; require_auth!(contract_owner); // Get next token ID let token_id: u64 = storage::get(keys::NEXT_TOKEN_ID).unwrap_or(1); storage::set(keys::NEXT_TOKEN_ID, &(token_id + 1)); // Set ownership owners().set(&token_id_key(token_id), &args.to); // Store token URI token_uris().set(&token_id_key(token_id), &args.token_uri); // Update balance let balance = balances().get_or_default(&args.to.0); balances().set(&args.to.0, &(balance + 1)); // Update total supply let supply: u64 = storage::get(keys::TOTAL_SUPPLY).unwrap_or(0); storage::set(keys::TOTAL_SUPPLY, &(supply + 1)); emit(&TransferNFT { from: Address::zero(), to: args.to, token_id, }); Ok(borsh::to_vec(&token_id).unwrap()) } s if s == burn_sel => { #[derive(BorshDeserialize)] struct Args { token_id: u64, } let args = Args::try_from_slice(params) .map_err(|_| Error::invalid_args("Expected token_id"))?; let sender = caller(); let token_owner = owners() .get(&token_id_key(args.token_id)) .ok_or(Error::not_found("Token does not exist"))?; // Must be owner or approved require!( is_approved_or_owner(&sender, &token_owner, args.token_id), Error::Unauthorized ); // Remove ownership owners().delete(&token_id_key(args.token_id)); token_uris().delete(&token_id_key(args.token_id)); approvals().delete(&token_id_key(args.token_id)); // Update balance let balance = balances().get_or_default(&token_owner.0); balances().set(&token_owner.0, &balance.saturating_sub(1)); // Update total supply let supply: u64 = storage::get(keys::TOTAL_SUPPLY).unwrap_or(0); storage::set(keys::TOTAL_SUPPLY, &supply.saturating_sub(1)); emit(&TransferNFT { from: token_owner, to: Address::zero(), token_id: args.token_id, }); Ok(Vec::new()) } s if s == owner_sel => { let owner = get_owner().unwrap_or(Address::zero()); Ok(borsh::to_vec(&owner).unwrap()) } s if s == transfer_ownership_sel => { #[derive(BorshDeserialize)] struct Args { new_owner: Address, } let args = Args::try_from_slice(params) .map_err(|_| Error::invalid_args("Expected new_owner"))?; let current_owner = get_owner().ok_or(Error::Unauthorized)?; require_auth!(current_owner); storage::set(keys::OWNER, &args.new_owner); emit(&synor_sdk::events::OwnershipTransferred { previous_owner: current_owner, new_owner: args.new_owner, }); Ok(Vec::new()) } _ => Err(Error::InvalidMethod), } } // ==================== Internal Functions ==================== fn is_approved_or_owner(spender: &Address, owner: &Address, token_id: u64) -> bool { *spender == *owner || approvals().get(&token_id_key(token_id)).as_ref() == Some(spender) || get_operator(owner, spender) } fn do_transfer(from: &Address, to: &Address, token_id: u64) -> Result<()> { let token_owner = owners() .get(&token_id_key(token_id)) .ok_or(Error::not_found("Token does not exist"))?; require!( is_approved_or_owner(from, &token_owner, token_id), Error::Unauthorized ); require!( *to != Address::zero(), Error::invalid_args("Cannot transfer to zero address") ); // Clear approval approvals().delete(&token_id_key(token_id)); // Update owner owners().set(&token_id_key(token_id), to); // Update balances let from_balance = balances().get_or_default(&token_owner.0); balances().set(&token_owner.0, &from_balance.saturating_sub(1)); let to_balance = balances().get_or_default(&to.0); balances().set(&to.0, &(to_balance + 1)); emit(&TransferNFT { from: token_owner, to: *to, token_id, }); Ok(()) }