A complete blockchain implementation featuring: - synord: Full node with GHOSTDAG consensus - explorer-web: Modern React blockchain explorer with 3D DAG visualization - CLI wallet and tools - Smart contract SDK and example contracts (DEX, NFT, token) - WASM crypto library for browser/mobile
502 lines
16 KiB
Rust
502 lines
16 KiB
Rust
//! 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<u8> {
|
|
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::<bool>(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<Address> {
|
|
storage::get::<Address>(keys::OWNER)
|
|
}
|
|
|
|
// ==================== Events ====================
|
|
|
|
struct TransferNFT {
|
|
from: Address,
|
|
to: Address,
|
|
token_id: u64,
|
|
}
|
|
|
|
impl synor_sdk::events::Event for TransferNFT {
|
|
fn topics(&self) -> Vec<synor_sdk::events::Topic> {
|
|
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<u8> {
|
|
Vec::new()
|
|
}
|
|
}
|
|
|
|
struct ApprovalNFT {
|
|
owner: Address,
|
|
approved: Address,
|
|
token_id: u64,
|
|
}
|
|
|
|
impl synor_sdk::events::Event for ApprovalNFT {
|
|
fn topics(&self) -> Vec<synor_sdk::events::Topic> {
|
|
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<u8> {
|
|
Vec::new()
|
|
}
|
|
}
|
|
|
|
struct ApprovalForAll {
|
|
owner: Address,
|
|
operator: Address,
|
|
approved: bool,
|
|
}
|
|
|
|
impl synor_sdk::events::Event for ApprovalForAll {
|
|
fn topics(&self) -> Vec<synor_sdk::events::Topic> {
|
|
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<u8> {
|
|
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<Vec<u8>> {
|
|
// 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(())
|
|
}
|