Skip to content
Open
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
1,273 changes: 1,268 additions & 5 deletions nftopia-stellar/Cargo.lock

Large diffs are not rendered by default.

4 changes: 4 additions & 0 deletions nftopia-stellar/contracts/nft_contract/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,7 @@ version = "0.1.0"
edition = "2021"

[dependencies]
soroban-sdk = "25.0.2"

[lib]
crate-type = ["cdylib"]
36 changes: 36 additions & 0 deletions nftopia-stellar/contracts/nft_contract/src/access_control.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
use soroban_sdk::{contracttype, Address, Env};

use crate::error::ContractError;
use crate::storage::{self, DataKey};

#[contracttype]
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum Role {
Owner,
Admin,
Minter,
Burner,
MetadataUpdater,
Marketplace,
}

pub fn require_owner(env: &Env) -> Result<Address, ContractError> {
let owner = storage::read_owner(env)?;
owner.require_auth();
Ok(owner)
}

pub fn require_admin(env: &Env) -> Result<Address, ContractError> {
require_owner(env)
}

pub fn require_role(env: &Env, _role: Role) -> Result<Address, ContractError> {
require_owner(env)
}

pub fn set_role(env: &Env, role: Role, account: Address, enabled: bool) -> Result<(), ContractError> {
let _ = require_owner(env)?;
let key = DataKey::Role(role, account.clone());
env.storage().persistent().set(&key, &enabled);
Ok(())
}
22 changes: 22 additions & 0 deletions nftopia-stellar/contracts/nft_contract/src/error.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
use soroban_sdk::contracterror;

#[contracterror]
#[derive(Copy, Clone, Eq, PartialEq)]
pub enum ContractError {
AlreadyInitialized = 1,
NotInitialized = 2,
Unauthorized = 3,
TokenNotFound = 4,
NotOwner = 5,
NotApproved = 6,
TransferPaused = 7,
MintPaused = 8,
MetadataFrozen = 9,
InvalidInput = 10,
MaxSupplyReached = 11,
RoyaltyTooHigh = 12,
ConfirmRequired = 13,
ArrayLengthMismatch = 14,
WhitelistRequired = 15,
RevealNotReady = 16,
}
47 changes: 47 additions & 0 deletions nftopia-stellar/contracts/nft_contract/src/events.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
use soroban_sdk::{symbol_short, Address, Env, String};

use crate::token::RoyaltyInfo;

pub fn emit_mint(env: &Env, to: &Address, token_id: u64) {
env.events()
.publish((symbol_short!("mint"), to), token_id);
}

pub fn emit_burn(env: &Env, owner: &Address, token_id: u64) {
env.events()
.publish((symbol_short!("burn"), owner), token_id);
}

pub fn emit_transfer(env: &Env, from: &Address, to: &Address, token_id: u64) {
env.events()
.publish((symbol_short!("transfer"), from, to), token_id);
}

pub fn emit_approval(env: &Env, owner: &Address, approved: &Option<Address>, token_id: u64) {
env.events().publish(
(symbol_short!("approve"), owner),
(approved.clone(), token_id),
);
}

pub fn emit_approval_for_all(env: &Env, owner: &Address, operator: &Address, approved: bool) {
env.events().publish(
(symbol_short!("appr_all"), owner),
(operator, approved),
);
}

pub fn emit_metadata_update(env: &Env, token_id: u64, uri: &String) {
env.events()
.publish((symbol_short!("metadata"), token_id), uri.clone());
}

pub fn emit_base_uri_update(env: &Env, uri: &String) {
env.events()
.publish((symbol_short!("base_uri"),), uri.clone());
}

pub fn emit_royalty_update(env: &Env, token_id: Option<u64>, info: &RoyaltyInfo) {
env.events()
.publish((symbol_short!("royalty"), token_id), info.clone());
}
33 changes: 33 additions & 0 deletions nftopia-stellar/contracts/nft_contract/src/interface.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
use soroban_sdk::{BytesN, Env};

pub fn nft_received_magic(env: &Env) -> BytesN<32> {
BytesN::from_array(
env,
&[
0x4e, 0x46, 0x54, 0x4f, 0x50, 0x49, 0x41, 0x5f, 0x4e, 0x46, 0x54, 0x5f,
0x52, 0x45, 0x43, 0x45, 0x49, 0x56, 0x45, 0x44, 0x5f, 0x4d, 0x41, 0x47,
0x49, 0x43, 0x5f, 0x5f, 0x5f, 0x5f, 0x5f, 0x5f,
],
)
}

pub fn interface_id(env: &Env, name: &str) -> BytesN<32> {
let bytes = soroban_sdk::Bytes::from_slice(env, name.as_bytes());
env.crypto().sha256(&bytes).into()
}

pub fn nft_interface_id(env: &Env) -> BytesN<32> {
interface_id(env, "nftopia.nft.v1")
}

pub fn royalty_interface_id(env: &Env) -> BytesN<32> {
interface_id(env, "nftopia.royalty.v1")
}

pub fn metadata_interface_id(env: &Env) -> BytesN<32> {
interface_id(env, "nftopia.metadata.v1")
}

pub fn access_control_interface_id(env: &Env) -> BytesN<32> {
interface_id(env, "nftopia.access.v1")
}
3 changes: 0 additions & 3 deletions nftopia-stellar/contracts/nft_contract/src/main.rs

This file was deleted.

89 changes: 89 additions & 0 deletions nftopia-stellar/contracts/nft_contract/src/metadata.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
use soroban_sdk::{Env, String};

use crate::access_control::require_owner;
use crate::error::ContractError;
use crate::events;
use crate::storage;

pub fn token_uri(env: &Env, token_id: u64) -> Result<String, ContractError> {
let token = storage::read_token(env, token_id)?;
let config = storage::read_config(env)?;
if config.is_revealed || is_reveal_ready(env, &config) {
Ok(token.metadata_uri)
} else {
Ok(config.base_uri)
}
}

pub fn token_metadata(env: &Env, token_id: u64) -> Result<crate::token::TokenData, ContractError> {
storage::read_token(env, token_id)
}

pub fn set_token_uri(
env: &Env,
token_id: u64,
uri: String,
) -> Result<(), ContractError> {
let config = storage::read_config(env)?;
if config.metadata_is_frozen {
return Err(ContractError::MetadataFrozen);
}

let mut token = storage::read_token(env, token_id)?;
token.owner.require_auth();

token.metadata_uri = uri.clone();
storage::write_token(env, &token);
events::emit_metadata_update(env, token_id, &uri);
Ok(())
}

pub fn set_base_uri(env: &Env, uri: String) -> Result<(), ContractError> {
let _ = require_owner(env)?;
let mut config = storage::read_config(env)?;
if config.metadata_is_frozen {
return Err(ContractError::MetadataFrozen);
}
config.base_uri = uri.clone();
storage::write_config(env, &config);
events::emit_base_uri_update(env, &uri);
Ok(())
}

pub fn freeze_metadata(env: &Env) -> Result<(), ContractError> {
let _ = require_owner(env)?;
let mut config = storage::read_config(env)?;
if config.metadata_is_frozen {
return Ok(());
}
config.metadata_is_frozen = true;
storage::write_config(env, &config);
Ok(())
}

pub fn set_reveal_time(env: &Env, reveal_time: Option<u64>) -> Result<(), ContractError> {
let _ = require_owner(env)?;
let mut config = storage::read_config(env)?;
config.reveal_time = reveal_time;
storage::write_config(env, &config);
Ok(())
}

pub fn set_revealed(env: &Env, revealed: bool) -> Result<(), ContractError> {
let _ = require_owner(env)?;
let mut config = storage::read_config(env)?;
if revealed && !is_reveal_ready(env, &config) {
return Err(ContractError::RevealNotReady);
}
config.is_revealed = revealed;
storage::write_config(env, &config);
Ok(())
}

fn is_reveal_ready(env: &Env, config: &crate::token::CollectionConfig) -> bool {
if let Some(reveal_time) = config.reveal_time {
env.ledger().timestamp() >= reveal_time
} else {
true
}
}
74 changes: 74 additions & 0 deletions nftopia-stellar/contracts/nft_contract/src/royalty.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
use soroban_sdk::{Address, Env};

use crate::access_control::{require_admin, require_role, Role};
use crate::error::ContractError;
use crate::events;
use crate::storage;
use crate::token::RoyaltyInfo;

const MAX_BPS: u32 = 10_000;

pub fn set_default_royalty(
env: &Env,
recipient: Address,
percentage: u32,
) -> Result<(), ContractError> {
let _ = require_admin(env)?;
if percentage > MAX_BPS {
return Err(ContractError::RoyaltyTooHigh);
}
let mut config = storage::read_config(env)?;
config.royalty_default = RoyaltyInfo {
recipient: recipient.clone(),
percentage,
};
storage::write_config(env, &config);
events::emit_royalty_update(env, None, &config.royalty_default);
Ok(())
}

pub fn set_token_royalty(
env: &Env,
token_id: u64,
recipient: Address,
percentage: u32,
) -> Result<(), ContractError> {
let mut token = storage::read_token(env, token_id)?;
token.owner.require_auth();
if percentage > MAX_BPS {
return Err(ContractError::RoyaltyTooHigh);
}
token.royalty_recipient = recipient.clone();
token.royalty_percentage = percentage;
storage::write_token(env, &token);
events::emit_royalty_update(
env,
Some(token_id),
&RoyaltyInfo {
recipient,
percentage,
},
);
Ok(())
}

pub fn get_royalty_info(
env: &Env,
token_id: u64,
sale_price: i128,
) -> Result<(Address, i128), ContractError> {
if sale_price < 0 {
return Err(ContractError::InvalidInput);
}
let token = storage::read_token(env, token_id)?;
let percentage = token.royalty_percentage as i128;
let royalty_amount = sale_price
.saturating_mul(percentage)
.checked_div(MAX_BPS as i128)
.unwrap_or(0);
Ok((token.royalty_recipient, royalty_amount))
}

pub fn require_marketplace(env: &Env) -> Result<Address, ContractError> {
require_role(env, Role::Marketplace)
}
Loading
Loading