This document elaborates on the process of integrating the Starknet contract for the Terracon Prestige Card NFT minting system with a web application. Crafted in Cairo, this contract harnesses the robust functionalities of the OpenZeppelin library.
- Implements the ERC721 token standard for NFTs.
- Features ERC721 metadata, SRC5, and ownership management components.
- Contains a storage struct detailing public sale status, whitelist Merkle root, next token ID, and whitelisted addresses.
- The constructor sets up the contract with essential details and mints initial tokens for the owner.
- Facilitates NFT minting for users based on whitelist and public sale conditions.
- Includes functions for the contract owner to manage public sale and whitelist addresses.
Introduce a total_supply
function to track the total tokens minted. This is derived by subtracting one from the next_token_id
.
Add a token_uri
function to link a token_id
to its corresponding URI, making use of the _set_token_uri
for URI retrieval.
Ensure external accessibility of balance_of
by applying #[abi(embed_v0)]
in ERC721Impl
.
Create a token_of_owner_by_index
function for obtaining a token ID associated with an owner’s address and index.
Develop functions to return the token’s name and symbol respectively.
Advocate for storing metadata on-chain as opposed to IPFS. Implement a TokenMetadata
struct for detailed token info and modify the _mint
function to include metadata input.
Utilize events for significant contract activities, employing event structs and emit
for communication.
To store metadata directly on the blockchain, you can define a structure representing the metadata and adjust the contract for storage and retrieval. This guide outlines the implementation using Cairo 2.0 syntax:
First, define a TokenMetadata
struct that includes the necessary fields for an NFT's metadata, as well as an array of Attribute
structs for customizable properties.
#[derive(Drop, Serde)]
struct Attribute {
trait_type: felt252,
value: felt252,
}
#[derive(Drop, Serde)]
struct TokenMetadata {
name: felt252,
description: felt252,
image: felt252,
external_url: felt252,
attributes: Array<Attribute>,
}
This struct encompasses fields for the name, description, image URL, external URL, and an array of attributes.
Integrate a new storage variable within the Storage
struct to link token IDs with their corresponding metadata.
struct Storage {
// ...
token_metadata: LegacyMap<u256, TokenMetadata>,
// ...
}
Adjust the _mint
function to accept metadata as an input parameter and store it using the token's ID as the key.
fn _mint(ref self: ContractState, recipient: ContractAddress, token_id: u256, metadata: TokenMetadata) {
// ...
self.token_metadata.write(token_id, metadata);
// ...
}
Revise the token_uri
function to retrieve the metadata from storage and convert it to a JSON string before returning.
fn token_uri(self: @ContractState, token_id: u256) -> felt252 {
let metadata = self.token_metadata.read(token_id);
// Convert the metadata to a JSON string and return it
return metadata.to_json();
}
Modify the minting process to include metadata for each minted token, ensuring the metadata is passed to the _mint
function.
fn mint(ref self: ContractState, recipient: ContractAddress, quantity: u256, metadata: TokenMetadata) {
// ...
let mut token_id = self.next_token_id.read();
for _ in 0 until quantity {
self._mint(recipient, token_id, metadata);
token_id += 1;
}
self.next_token_id.write(token_id);
// ...
}
Note: The to_json
function in the token_uri
function is a placeholder for a method that converts the TokenMetadata
struct to a JSON string. This function must be implemented separately or utilize a JSON serialization library compatible with Cairo.
Remember to revise the INFTMint
interface and any other relevant contract sections to reflect the incorporation of on-chain metadata storage.
To enable the modification of NFT attributes by a proxy contract or an admin, it's essential to introduce a storage mapping that permits authorized entities to update token attributes. Below, we detail the modifications necessary to incorporate this functionality into your Starknet contract, using Cairo 2.0 syntax for clarity.
Firstly, a new storage variable is added to track addresses authorized to modify token attributes:
struct Storage {
// Other storage variables...
authorized_addresses: LegacyMap<ContractAddress, bool>,
}
This mapping, authorized_addresses
, determines which addresses are permitted to update token attributes, ensuring only designated entities can make such changes.
To add or remove addresses from this list of authorized entities, implement the following functions:
fn add_authorized_address(ref self: ContractState, address: ContractAddress) {
self.ownable.assert_only_owner();
self.authorized_addresses.write(address, true);
}
fn remove_authorized_address(ref self: ContractState, address: ContractAddress) {
self.ownable.assert_only_owner();
self.authorized_addresses.write(address, false);
}
fn is_authorized(self: @ContractState, address: ContractAddress) -> bool {
return self.authorized_addresses.read(address);
}
These functions enable the contract owner to manage which addresses are authorized to update NFT attributes efficiently.
To update the attributes of a specific token, the update_token_attributes
function is introduced:
fn update_token_attributes(
ref self: ContractState,
token_id: u256,
new_attributes: Array<Attribute>
) {
assert(self.is_authorized(get_caller_address()), 'Caller is not authorized');
let mut metadata = self.token_metadata.read(token_id);
metadata.attributes = new_attributes;
self.token_metadata.write(token_id, metadata);
}
This function allows authorized addresses to modify the attributes of a given token by specifying the token ID and a new set of attributes. It verifies the caller's authorization before proceeding with the update.
Optionally, for enhanced transparency and traceability, emit an event whenever a token's attributes are updated:
#[derive(Drop, starknet::Event)]
struct AttributesUpdated {
token_id: u256,
updater: ContractAddress,
}
// In the event enum...
#[derive(Drop, starknet::Event)]
enum Event {
// Other events...
AttributesUpdated: AttributesUpdated,
}
// Within update_token_attributes...
self.emit(Event::AttributesUpdated(AttributesUpdated {
token_id,
updater: get_caller_address(),
}));
Emitting the AttributesUpdated
event serves to notify external observers about the update, including information about the token and the entity that made the change.
By incorporating these modifications, your contract will support updating NFT attributes post-minting, enhancing flexibility in managing token metadata. Ensure to extend the INFTMint
interface and review other contract parts for consistency with these new functionalities.
Note: Carefully test and review the security implications of allowing external modifications to token attributes. Depending on your specific requirements, you may need to implement further access control or validation mechanisms.