1
0
notamon/rust/notamon-nft/src/lib.rs

470 lines
14 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#![doc = include_str!("../README.md")]
#![allow(unused_variables)]
#[macro_use]
extern crate pbc_contract_codegen;
use pbc_contract_common::shortname::Shortname;
use pbc_contract_common::events::EventGroup;
use create_type_spec_derive::CreateTypeSpec;
use pbc_contract_common::address::Address;
use pbc_contract_common::avl_tree_map::{AvlTreeMap, AvlTreeSet};
use pbc_contract_common::context::ContractContext;
use read_write_state_derive::ReadWriteState;
use notamon_common::{NotamonAttributes, AssetId};
/// A permission to transfer and approve NFTs given from an NFT owner to a separate address, called an operator.
#[derive(ReadWriteState, CreateTypeSpec, PartialEq, Copy, Clone, Ord, PartialOrd, Eq)]
struct OperatorApproval {
/// NFT owner.
owner: Address,
/// Operator of the owner's tokens.
operator: Address,
}
/// Must correspond with [`NotamonId`].
///
/// ```
/// let _: TokenId = notamon_common::NotamonId { id : 1 }.id;
/// ```
type TokenId = u128;
/// Unit2
#[derive(CreateTypeSpec, ReadWriteState)]
pub struct Unit2 {}
/// State of the contract.
#[state]
pub struct NFTContractState {
/// Descriptive name for the collection of NFTs in this contract.
///
/// Required by MPC-721.
name: String,
/// Abbreviated name for NFTs in this contract.
///
/// Required by MPC-721.
symbol: String,
/// Mapping from token_id to the owner of the token.
///
/// Required by MPC-721.
owners: AvlTreeMap<TokenId, Address>,
/// Mapping from token_id to the approved address who can transfer the token.
///
/// Required by MPC-721.
token_approvals: AvlTreeMap<TokenId, Address>,
/// Containing approved operators of owners. Operators can transfer and change approvals on all tokens owned by owner.
///
/// Required by MPC-721.
operator_approvals: AvlTreeMap<OperatorApproval, Unit2>,
/// Template which the uri's of the NFTs fit into.
///
/// Required by MPC-721.
uri_template: String,
/// Mapping from token_id to the URI of the token.
///
/// Required by MPC-721.
token_uri_details: AvlTreeMap<TokenId, String>,
/// Owner of the contract. Is allowed to mint new NFTs.
///
/// Required by MPC-721.
contract_owner: Address,
/// Notamon attributes
notamon_attributes: AvlTreeMap<TokenId, NotamonAttributes>,
cached_asset_unlocks: AvlTreeSet<AssetId>,
asset_contract: Address,
}
impl NFTContractState {
/// Find the owner of an NFT.
/// Throws if no such token exists.
///
/// ### Parameters:
///
/// * `token_id`: [`TokenId`] The identifier for an NFT.
///
/// ### Returns:
///
/// An [`Address`] for the owner of the NFT.
pub fn owner_of(&self, token_id: TokenId) -> Address {
self.owners
.get(&token_id)
.expect("MPC-721: owner query for nonexistent token")
}
/// Get the approved address for a single NFT.
///
/// ### Parameters:
///
/// * `token_id`: [`TokenId`] The NFT to find the approved address for.
///
/// ### Returns:
///
/// An [`Option<Address>`] The approved address for this NFT, or none if there is none.
pub fn get_approved(&self, token_id: TokenId) -> Option<Address> {
self.token_approvals.get(&token_id)
}
/// Query if an address is an authorized operator for another address.
///
/// ### Parameters:
///
/// * `owner`: [`Address`] The address that owns the NFTs.
///
/// * `operator`: [`Address`] The address that acts on behalf of the owner.
///
/// ### Returns:
///
/// A [`bool`] true if `operator` is an approved operator for `owner`, false otherwise.
pub fn is_approved_for_all(&self, owner: Address, operator: Address) -> bool {
let as_operator_approval: OperatorApproval = OperatorApproval { owner, operator };
self.operator_approvals.contains_key(&as_operator_approval)
}
/// Helper function to check whether a tokenId exists.
///
/// Tokens start existing when they are minted (`mint`),
/// and stop existing when they are burned (`burn`).
///
/// ### Parameters:
///
/// * `token_id`: [`TokenId`] The tokenId that is checked.
///
/// ### Returns:
///
/// A [`bool`] True if `token_id` is in use, false otherwise.
pub fn exists(&self, token_id: TokenId) -> bool {
self.owners.contains_key(&token_id)
}
/// Helper function to check whether a spender is owner or approved for a given token.
/// Throws if token_id does not exist.
///
/// ### Parameters:
///
/// * `spender`: [`Address`] The address to check ownership for.
///
/// * `token_id`: [`TokenId`] The tokenId which is checked.
///
/// ### Returns:
///
/// A [`bool`] True if `token_id` is owned or approved for `spender`, false otherwise.
pub fn is_approved_or_owner(&self, spender: Address, token_id: TokenId) -> bool {
let owner = self.owner_of(token_id);
spender == owner
|| self.get_approved(token_id) == Some(spender)
|| self.is_approved_for_all(owner, spender)
}
/// Mutates the state by approving `to` to operate on `token_id`.
/// None indicates there is no approved address.
///
/// ### Parameters:
///
/// * `approved`: [`Option<Address>`], The new approved NFT controller.
///
/// * `token_id`: [`TokenId`], The NFT to approve.
pub fn _approve(&mut self, approved: Option<Address>, token_id: TokenId) {
if let Some(appr) = approved {
self.token_approvals.insert(token_id, appr);
} else {
self.token_approvals.remove(&token_id);
}
}
/// Mutates the state by transferring `token_id` from `from` to `to`.
/// As opposed to {transfer_from}, this imposes no restrictions on `ctx.sender`.
///
/// Throws if `from` is not the owner of `token_id`.
///
/// ### Parameters:
///
/// * `from`: [`Address`], The current owner of the NFT
///
/// * `to`: [`Address`], The new owner
///
/// * `token_id`: [`TokenId`], The NFT to transfer
pub fn _transfer(&mut self, from: Address, to: Address, token_id: TokenId) {
if self.owner_of(token_id) != from {
panic!("MPC-721: transfer from incorrect owner")
} else {
// clear approvals from the previous owner
self._approve(None, token_id);
self.owners.insert(token_id, to);
}
}
}
/// Create a new NFT contract, with a name, symbol and URI template. No NFTs are minted initially.
///
/// ### Parameters:
///
/// * `ctx`: [`ContractContext`], initial context.
///
/// * `name`: [`String`], A descriptive name for a collection of NFTs in this contract.
///
/// * `symbol`: [`String`], An abbreviated name for NFTs in this contract.
///
/// * `uri_template`: [`String`], Template for uri´s associated with NFTs in this contract.
///
/// ### Returns:
///
/// The new state object of type [`NFTContractState`].
#[init]
pub fn initialize(
ctx: ContractContext,
name: String,
symbol: String,
uri_template: String,
asset_contract: Address,
) -> NFTContractState {
NFTContractState {
name,
symbol,
owners: AvlTreeMap::new(),
token_approvals: AvlTreeMap::new(),
operator_approvals: AvlTreeMap::new(),
uri_template,
token_uri_details: AvlTreeMap::new(),
contract_owner: ctx.sender,
notamon_attributes: AvlTreeMap::new(),
cached_asset_unlocks: AvlTreeSet::new(),
asset_contract,
}
}
/// User allows another user or contract to [`transfer_from`] their NFT. Each NFT can only have a single approved account.
///
/// Change or reaffirm the approved address for an NFT.
/// `approved==None` revokes any existing approved address.
/// Throws unless `ctx.sender` is the current NFT owner, or an authorized
/// operator of the current owner.
///
/// ### Parameters:
///
/// * `ctx`: [`ContractContext`], the context for the action call.
///
/// * `state`: [`NFTContractState`], the current state of the contract.
///
/// * `approved`: [`Option<Address>`], The new approved NFT controller.
///
/// * `token_id`: [`TokenId`], The NFT to approve.
///
/// ### Returns
///
/// The new state object of type [`NFTContractState`] with an updated ledger.
#[action(shortname = 0x05)]
pub fn approve(
ctx: ContractContext,
mut state: NFTContractState,
approved: Option<Address>,
token_id: TokenId,
) -> NFTContractState {
let owner = state.owner_of(token_id);
if ctx.sender != owner && !state.is_approved_for_all(owner, ctx.sender) {
panic!("MPC-721: approve caller is not owner nor authorized operator")
}
state._approve(approved, token_id);
state
}
/// User allows another user or contract ("operator") to manage all of their NFTs for them. Operators can do everything the real owner can.
/// Each token can only have a single operator.
///
/// Throws if `operator` == `ctx.sender`.
///
/// ### Parameters:
///
/// * `context`: [`ContractContext`], the context for the action call.
///
/// * `state`: [`NFTContractState`], the current state of the contract.
///
/// * `operator`: [`Address`], Address to add to the set of authorized operators.
///
/// * `approved`: [`bool`], True if the operator is approved, false to revoke approval.
///
/// ### Returns
///
/// The new state object of type [`NFTContractState`] with an updated ledger.
#[action(shortname = 0x07)]
pub fn set_approval_for_all(
ctx: ContractContext,
mut state: NFTContractState,
operator: Address,
approved: bool,
) -> NFTContractState {
if operator == ctx.sender {
panic!("MPC-721: approve to caller")
}
let operator_approval = OperatorApproval {
owner: ctx.sender,
operator,
};
if approved {
state.operator_approvals.insert(operator_approval, Unit2 {});
} else {
state.operator_approvals.remove(&operator_approval);
}
state
}
/// Transfer ownership of an NFT to recipient.
///
/// User must have permission to perform the transfer: Throws unless `ctx.sender` is the current owner, an authorized
/// operator, or the approved address for this NFT. Throws if `from` is
/// not the current owner. Throws if `token_id` is not a valid NFT.
///
/// ### Parameters:
///
/// * `ctx`: [`ContractContext`], the context for the action call.
///
/// * `state`: [`NFTContractState`], the current state of the contract.
///
/// * `from`: [`Address`], The current owner of the NFT
///
/// * `to`: [`Address`], The new owner
///
/// * `token_id`: [`TokenId`], The NFT to transfer
///
/// ### Returns
///
/// The new state object of type [`NFTContractState`] with an updated ledger.
#[action(shortname = 0x03)]
pub fn transfer_from(
ctx: ContractContext,
mut state: NFTContractState,
from: Address,
to: Address,
token_id: TokenId,
) -> NFTContractState {
if !state.is_approved_or_owner(ctx.sender, token_id) {
panic!("MPC-721: transfer caller is not owner nor approved")
} else {
state._transfer(from, to, token_id);
state
}
}
/// Create a new NFT, and send it to the specified recipient. Only the contract owner can use this.
///
/// Requirements:
///
/// - `token_id` must not exist
/// - `ctx.sender` owns the contract
///
/// ### Parameters:
///
/// * `ctx`: [`ContractContext`], the context for the action call.
///
/// * `state`: [`NFTContractState`], the current state of the contract.
///
/// * `to`: [`Address`], the owner of the minted token.
///
/// * `token_id`: [`TokenId`], The new id for the minted token.
///
/// ### Returns
///
/// The new state object of type [`NFTContractState`] with an updated ledger.
#[action(shortname = 0x01)]
pub fn mint(
ctx: ContractContext,
mut state: NFTContractState,
to: Address,
token_id: TokenId,
token_uri: String,
) -> NFTContractState {
panic!("Default NFT minting is not supported")
}
fn unlock_assets(
ctx: ContractContext,
mut state: NFTContractState,
notamon_attributes: NotamonAttributes,
) -> (
NFTContractState,
Vec<EventGroup>,
) {
let ids_of_not_yet_unlocked_assets: Vec<AssetId> = notamon_attributes.asset_ids().into_iter().filter(
|id| !state.cached_asset_unlocks.contains(id)
) .collect();
if ids_of_not_yet_unlocked_assets.is_empty() {
return (state, vec![]);
}
for id in ids_of_not_yet_unlocked_assets.clone() {
state.cached_asset_unlocks.insert(id)
}
let mut event_group_builder = EventGroup::builder();
event_group_builder
.call(state.asset_contract, Shortname::from_u32(0x07))
.argument(ids_of_not_yet_unlocked_assets)
.done();
(
state,
vec![event_group_builder.build()]
)
}
#[action(shortname = 0x10)]
pub fn mint_notamon(
ctx: ContractContext,
mut state: NFTContractState,
to: Address,
token_id: TokenId,
notamon_attributes: NotamonAttributes,
) -> (
NFTContractState,
Vec<EventGroup>,
) {
if ctx.sender != state.contract_owner {
panic!("MPC-721: mint only callable by the contract owner")
} else if state.exists(token_id) {
panic!("MPC-721: token already minted")
} else {
let token_uri: String = "TODO".into();
state.owners.insert(token_id, to);
state.notamon_attributes.insert(token_id, notamon_attributes.clone());
state.token_uri_details.insert(token_id, token_uri);
unlock_assets(ctx, state, notamon_attributes)
}
}
/// Destroy a NFT with id `token_id`.
///
/// Requires that the `token_id` exists and `ctx.sender` is approved or owner of the token.
///
/// The approval is cleared when the token is burned.
///
/// ### Parameters:
///
/// * `ctx`: [`ContractContext`], the context for the action call.
///
/// * `state`: [`NFTContractState`], the current state of the contract.
///
/// * `token_id`: [`TokenId`], The id of the NFT to be burned.
///
/// ### Returns
///
/// The new state object of type [`NFTContractState`] with an updated ledger.
#[action(shortname = 0x08)]
pub fn burn(ctx: ContractContext, mut state: NFTContractState, token_id: TokenId) -> NFTContractState {
if !state.is_approved_or_owner(ctx.sender, token_id) {
panic!("MPC-721: burn caller is not owner nor approved")
} else {
let owner = state.owner_of(token_id);
// Clear approvals
state._approve(None, token_id);
state.owners.remove(&token_id);
state.token_uri_details.remove(&token_id);
state
}
}