1
0

NFT now requests unlock from asset node

This commit is contained in:
Jon Michael Aanes 2025-04-03 00:26:15 +02:00
parent 275495c8e8
commit 14bf6a72cd
6 changed files with 109 additions and 466 deletions

View File

@ -113,20 +113,25 @@ pub fn delete_asset(
} }
#[action(shortname = 0x07, zk = true)] #[action(shortname = 0x07, zk = true)]
pub fn publish_asset( pub fn publish_assets(
context: ContractContext, context: ContractContext,
state: AssetContractState, state: AssetContractState,
zk_state: ZkState<AssetMetadata>, zk_state: ZkState<AssetMetadata>,
asset_id: AssetId, asset_ids: Vec<AssetId>,
) -> (AssetContractState, Vec<EventGroup>, Vec<ZkStateChange>) { ) -> (AssetContractState, Vec<EventGroup>, Vec<ZkStateChange>) {
state.permissions.assert_has_permission(&context.sender, Role::PUBLISHER {}); state.permissions.assert_has_permission(&context.sender, Role::PUBLISHER {});
let variable_id = state.asset_id_to_variable_id.get(&asset_id).unwrap(); let variable_ids: Vec<SecretVarId> =
asset_ids.into_iter().filter_map(
|asset_id|
state.asset_id_to_variable_id.get(&asset_id)
).collect();
( (
state, state,
vec![], vec![],
vec![ZkStateChange::OpenVariables { vec![ZkStateChange::OpenVariables {
variables: vec![variable_id], variables: variable_ids,
}], }],
) )
} }

View File

@ -8,35 +8,60 @@ use std::fmt::Debug;
mod permission; mod permission;
pub use permission::{Permission, Permissions}; pub use permission::{Permission, Permissions};
#[derive(ReadWriteState, ReadWriteRPC, Debug, CreateTypeSpec, PartialEq, Eq)] #[derive(ReadWriteState, ReadWriteRPC, Debug, CreateTypeSpec, PartialEq, Eq, Clone)]
pub struct AssetId{
id: u32,
}
#[derive(ReadWriteState, ReadWriteRPC, Debug, CreateTypeSpec, PartialEq, Eq)]
pub struct NotamonId{ pub struct NotamonId{
id: u128, id: u128,
} }
#[derive(ReadWriteState, ReadWriteRPC, Debug, CreateTypeSpec, PartialEq, Eq)] #[derive(ReadWriteState, ReadWriteRPC, Debug, CreateTypeSpec, PartialEq, Eq, Clone)]
pub struct AssetId{
id: u32,
}
trait ToAssetId {
fn asset_id(&self) -> AssetId;
}
#[derive(ReadWriteState, ReadWriteRPC, Debug, CreateTypeSpec, PartialEq, Eq, Clone)]
pub struct SpeciesId{ pub struct SpeciesId{
id: u16, id: u16,
} }
#[derive(ReadWriteState, ReadWriteRPC, Debug, CreateTypeSpec, PartialEq, Eq)] #[derive(ReadWriteState, ReadWriteRPC, Debug, CreateTypeSpec, PartialEq, Eq, Clone)]
pub struct SkinId{ pub struct SkinId{
id: u8, id: u8,
} }
#[derive(ReadWriteState, ReadWriteRPC, Debug, CreateTypeSpec, PartialEq, Eq)] #[derive(ReadWriteState, ReadWriteRPC, Debug, CreateTypeSpec, PartialEq, Eq, Clone)]
pub struct EffectId{ pub struct EffectId{
id: u8, id: u8,
} }
const ASSET_ID_OFFSET_SPECIES: u32 = 0x0100_0000;
const ASSET_ID_OFFSET_SKIN: u32 = 0x0200_0000;
const ASSET_ID_OFFSET_EFFECT: u32 = 0x0300_0000;
impl ToAssetId for SpeciesId {
fn asset_id(&self) -> AssetId {
AssetId { id: self.id as u32 | ASSET_ID_OFFSET_SPECIES }
}
}
impl ToAssetId for SkinId {
fn asset_id(&self) -> AssetId {
AssetId { id: self.id as u32 | ASSET_ID_OFFSET_SKIN}
}
}
impl ToAssetId for EffectId {
fn asset_id(&self) -> AssetId {
AssetId { id: self.id as u32 | ASSET_ID_OFFSET_EFFECT}
}
}
type StatAmount = u8; type StatAmount = u8;
#[derive(ReadWriteState, ReadWriteRPC, Debug, CreateTypeSpec)] #[derive(ReadWriteState, ReadWriteRPC, Debug, CreateTypeSpec, Clone)]
pub struct NotamonAttributes{ pub struct NotamonAttributes{
species_id: SpeciesId, species_id: SpeciesId,
skin_id: SkinId, skin_id: SkinId,
@ -45,3 +70,9 @@ pub struct NotamonAttributes{
stat_attack: StatAmount, stat_attack: StatAmount,
stat_defense: StatAmount, stat_defense: StatAmount,
} }
impl NotamonAttributes {
pub fn asset_ids(&self) -> [AssetId; 3] {
[self.species_id.asset_id(), self.skin_id.asset_id(), self.effect_id.asset_id()]
}
}

View File

@ -1,25 +0,0 @@
[package]
name = "nft-v2-contract"
readme = "README.md"
version.workspace = true
description.workspace = true
homepage.workspace = true
repository.workspace = true
documentation.workspace = true
edition.workspace = true
license.workspace = true
[lib]
crate-type = ['rlib', 'cdylib']
[dependencies]
pbc_contract_common.workspace = true
pbc_traits.workspace = true
pbc_lib.workspace = true
read_write_rpc_derive.workspace = true
read_write_state_derive.workspace = true
create_type_spec_derive.workspace = true
pbc_contract_codegen.workspace = true
[features]
abi = ["pbc_contract_common/abi", "pbc_contract_codegen/abi", "pbc_traits/abi", "create_type_spec_derive/abi", "pbc_lib/abi"]

View File

@ -1,42 +0,0 @@
# NFT v2 (MPC-721-v2)
An example of a NFT (Non-Fungible Token) smart contract for Partisia
Blockchain, implementing the MPC-721-v2 standard.
## Background, NFT
An NFT is a unique identifier managed by an NFT contract, that can be
transfered between accounts on the blockchain. NFTs can be used in much the
same way as [MPC-20 tokens](../token-v2) can, but NFTs represent specific
instances of an object (non-fungible like a physical book; there are many like
it, but the one sitting on your bookshelf is yours and has a history), whereas
[tokens](../token-v2) are interchangable (fungible; like money in a bank
account).
NFTs are often associated with specific artworks, which are publically
accessible by a unique link stored in the contract; artwork is rarely stored
on-chain.
Some NFT contracts also manage additional attributes associated with each NFT,
for example their history of ownership. This functionality is not implemented
by `nft-v2`.
## Implementation
This example follows the mpc-721-v2 standard contract interface. You can read more about this standard here: [https://partisiablockchain.gitlab.io/documentation/smart-contracts/integration/mpc-721-nft-contract.html](https://partisiablockchain.gitlab.io/documentation/smart-contracts/integration/mpc-721-nft-contract.html)
The contract is inspired by the ERC721 NFT contract with extensions for Metadata and Burnable\
[https://github.com/ethereum/EIPs/blob/master/EIPS/eip-721.md](https://github.com/ethereum/EIPs/blob/master/EIPS/eip-721.md)
## Extensions
This contract is meant as a jumping off point to making your own NFTs. Here are
some ideas:
- NFT attributes: Track anything you want! This can include ownership history,
rarity, game stats, etc.
- On-chain generation: Partisia Blockchain REAL/ZK allows for true randomness.
Generate your attributes on-chain and store your images off-chain.
- User-requested minting: With on-chain generation your can allow your users to
mint their own NFTs. Then you can limit them to a certain amount, or let them
run amok.

View File

@ -1,382 +0,0 @@
#![doc = include_str!("../README.md")]
#![allow(unused_variables)]
#[macro_use]
extern crate pbc_contract_codegen;
use create_type_spec_derive::CreateTypeSpec;
use pbc_contract_common::address::Address;
use pbc_contract_common::avl_tree_map::AvlTreeMap;
use pbc_contract_common::context::ContractContext;
use read_write_state_derive::ReadWriteState;
/// 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,
}
/// Unit
#[derive(CreateTypeSpec, ReadWriteState)]
pub struct Unit {}
/// State of the contract.
#[state]
pub struct NFTContractState {
/// Descriptive name for the collection of NFTs in this contract.
name: String,
/// Abbreviated name for NFTs in this contract.
symbol: String,
/// Mapping from token_id to the owner of the token.
owners: AvlTreeMap<u128, Address>,
/// Mapping from token_id to the approved address who can transfer the token.
token_approvals: AvlTreeMap<u128, Address>,
/// Containing approved operators of owners. Operators can transfer and change approvals on all tokens owned by owner.
operator_approvals: AvlTreeMap<OperatorApproval, Unit>,
/// Template which the uri's of the NFTs fit into.
uri_template: String,
/// Mapping from token_id to the URI of the token.
token_uri_details: AvlTreeMap<u128, String>,
/// Owner of the contract. Is allowed to mint new NFTs.
contract_owner: Address,
}
impl NFTContractState {
/// Find the owner of an NFT.
/// Throws if no such token exists.
///
/// ### Parameters:
///
/// * `token_id`: [`u128`] The identifier for an NFT.
///
/// ### Returns:
///
/// An [`Address`] for the owner of the NFT.
pub fn owner_of(&self, token_id: u128) -> 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`: [`u128`] 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: u128) -> 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`: [`u128`] The tokenId that is checked.
///
/// ### Returns:
///
/// A [`bool`] True if `token_id` is in use, false otherwise.
pub fn exists(&self, token_id: u128) -> 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`: [`u128`] 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: u128) -> 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`: [`u128`], The NFT to approve.
pub fn _approve(&mut self, approved: Option<Address>, token_id: u128) {
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`: [`u128`], The NFT to transfer
pub fn _transfer(&mut self, from: Address, to: Address, token_id: u128) {
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,
) -> 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,
}
}
/// 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`: [`u128`], 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: u128,
) -> 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, Unit {});
} 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`: [`u128`], 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: u128,
) -> 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`: [`u128`], 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: u128,
token_uri: String,
) -> NFTContractState {
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 {
state.owners.insert(token_id, to);
state.token_uri_details.insert(token_id, token_uri);
state
}
}
/// 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`: [`u128`], 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: u128) -> 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
}
}

View File

@ -4,13 +4,16 @@
#[macro_use] #[macro_use]
extern crate pbc_contract_codegen; extern crate pbc_contract_codegen;
use pbc_contract_common::shortname::Shortname;
use pbc_contract_common::events::EventGroup;
use create_type_spec_derive::CreateTypeSpec; use create_type_spec_derive::CreateTypeSpec;
use pbc_contract_common::address::Address; use pbc_contract_common::address::Address;
use pbc_contract_common::avl_tree_map::AvlTreeMap; use pbc_contract_common::avl_tree_map::{AvlTreeMap, AvlTreeSet};
use pbc_contract_common::context::ContractContext; use pbc_contract_common::context::ContractContext;
use read_write_state_derive::ReadWriteState; use read_write_state_derive::ReadWriteState;
use notamon_common::NotamonAttributes; use notamon_common::{NotamonAttributes, AssetId};
/// A permission to transfer and approve NFTs given from an NFT owner to a separate address, called an operator. /// 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)] #[derive(ReadWriteState, CreateTypeSpec, PartialEq, Copy, Clone, Ord, PartialOrd, Eq)]
@ -70,6 +73,10 @@ pub struct NFTContractState {
/// Notamon attributes /// Notamon attributes
notamon_attributes: AvlTreeMap<TokenId, NotamonAttributes>, notamon_attributes: AvlTreeMap<TokenId, NotamonAttributes>,
cached_asset_unlocks: AvlTreeSet<AssetId>,
asset_contract: Address,
} }
impl NFTContractState { impl NFTContractState {
@ -224,6 +231,7 @@ pub fn initialize(
token_uri_details: AvlTreeMap::new(), token_uri_details: AvlTreeMap::new(),
contract_owner: ctx.sender, contract_owner: ctx.sender,
notamon_attributes: AvlTreeMap::new(), notamon_attributes: AvlTreeMap::new(),
cached_asset_unlocks: AvlTreeSet::new(),
} }
} }
@ -367,14 +375,62 @@ pub fn mint(
token_id: TokenId, token_id: TokenId,
token_uri: String, token_uri: String,
) -> NFTContractState { ) -> NFTContractState {
panic!("Default NFT minting is not supported")
}
fn unlock_assets(
ctx: ContractContext,
mut state: NFTContractState,
notamon_attributes: NotamonAttributes,
) -> (
NFTContractState,
Vec<EventGroup>,
) {
let asset_ids: Vec<AssetId> = notamon_attributes.asset_ids().into_iter().filter(
|id| state.cached_asset_unlocks.contains(id)
) .collect();
if asset_ids.is_empty() {
return (state, vec![]);
}
for id in asset_ids.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(asset_ids)
.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 { if ctx.sender != state.contract_owner {
panic!("MPC-721: mint only callable by the contract owner") panic!("MPC-721: mint only callable by the contract owner")
} else if state.exists(token_id) { } else if state.exists(token_id) {
panic!("MPC-721: token already minted") panic!("MPC-721: token already minted")
} else { } else {
let token_uri: String = "TODO".into();
state.owners.insert(token_id, to); state.owners.insert(token_id, to);
state.notamon_attributes.insert(token_id, notamon_attributes.clone());
state.token_uri_details.insert(token_id, token_uri); state.token_uri_details.insert(token_id, token_uri);
state unlock_assets(ctx, state, notamon_attributes)
} }
} }