Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: added an ERC721 NFT Contract #214

Closed
wants to merge 15 commits into from
199 changes: 199 additions & 0 deletions listings/applications/ERC721/ERC721.cairo
Original file line number Diff line number Diff line change
@@ -0,0 +1,199 @@
#[starknet::contract]
mod ERC721Contract {

use starknet::ContractAddress;
use starknet::get_caller_address;
use zeroable::Zeroable;
use starknet::contract_address_to_felt252;
use traits::Into;
use traits::TryInto;
use option::OptionTrait;

#[storage]
struct Storage {
name: felt252,
symbol: felt252,
owners: LegacyMap::<u256, ContractAddress>,
balances: LegacyMap::<ContractAddress, u256>,
token_approvals: LegacyMap::<u256, ContractAddress>,
raizo07 marked this conversation as resolved.
Show resolved Hide resolved
operator_approvals: LegacyMap::<(ContractAddress, ContractAddress), bool>,
token_uri: LegacyMap::<u256, felt252>,
}

#[event]
#[derive(Drop, starknet::Event)]
enum Event {
Approval: Approval,
Transfer: Transfer,
ApprovalForAll: ApprovalForAll
}

#[derive(Drop, starknet::Event)]
struct Approval {
owner: ContractAddress,
to: ContractAddress,
token_id: u256
}

#[derive(Drop, starknet::Event)]
struct Transfer {
from: ContractAddress,
to: ContractAddress,
token_id: u256
}

#[derive(Drop, starknet::Event)]
struct ApprovalForAll {
owner: ContractAddress,
operator: ContractAddress,
approved: bool
}

#[constructor]
fn constructor(ref self: ContractState, _name: felt252, _symbol: felt252) {
self.name.write(_name);
self.symbol.write(_symbol);
}

#[external(v0)]
#[generate_trait]
raizo07 marked this conversation as resolved.
Show resolved Hide resolved
impl IERC721Impl of IERC721Trait {
// Returns the name of the token collection
fn get_name(self: @ContractState) -> felt252 {
self.name.read()
}

// Returns the symbol of the token collection
fn get_symbol(self: @ContractState) -> felt252 {
self.symbol.read()
}

// Returns the metadata URI for a given token ID
fn get_token_uri(self: @ContractState, token_id: u256) -> felt252 {
assert(self._exists(token_id), 'ERC721: invalid token ID');
self.token_uri.read(token_id)
}

// Returns the number of tokens owned by a specific address
fn balance_of(self: @ContractState, account: ContractAddress) -> u256 {
assert(account.is_non_zero(), 'ERC721: address zero');
raizo07 marked this conversation as resolved.
Show resolved Hide resolved
self.balances.read(account)
}

// Returns the owner of the specified token ID
fn owner_of(self: @ContractState, token_id: u256) -> ContractAddress {
let owner = self.owners.read(token_id);
assert(owner.is_non_zero(), 'ERC721: invalid token ID');
owner
}

// Returns the approved address for a specific token ID
fn get_approved(self: @ContractState, token_id: u256) -> ContractAddress {
assert(self._exists(token_id), 'ERC721: invalid token ID');
self.token_approvals.read(token_id)
}

// Checks if an operator is approved to manage all of the assets of an owner
fn is_approved_for_all(self: @ContractState, owner: ContractAddress, operator: ContractAddress) -> bool {
self.operator_approvals.read((owner, operator))
}

// Approves another address to transfer the given token ID
fn approve(ref self: ContractState, to: ContractAddress, token_id: u256) {
let owner = self.owner_of(token_id);
assert(to != owner, 'Approval to current owner');
assert(get_caller_address() == owner || self.is_approved_for_all(owner, get_caller_address()), 'Not token owner');
self.token_approvals.write(token_id, to);
self.emit(
Approval{ owner: self.owner_of(token_id), to: to, token_id: token_id }
);
}

// Sets or unsets the approval of a given operator
fn set_approval_for_all(ref self: ContractState, operator: ContractAddress, approved: bool) {
let owner = get_caller_address();
assert(owner != operator, 'ERC721: approve to caller');
self.operator_approvals.write((owner, operator), approved);
self.emit(
ApprovalForAll{ owner: owner, operator: operator, approved: approved }
);
}

// Transfers a specific token ID to another address
fn transfer_from(ref self: ContractState, from: ContractAddress, to: ContractAddress, token_id: u256) {
assert(self._is_approved_or_owner(get_caller_address(), token_id), 'neither owner nor approved');
self._transfer(from, to, token_id);
}
}

#[generate_trait]
impl ERC721HelperImpl of ERC721HelperTrait {
// Checks if a specific token ID exists
fn _exists(self: @ContractState, token_id: u256) -> bool {
self.owner_of(token_id).is_non_zero()
}

// Checks if a spender is the owner or an approved operator of a specific token ID
fn _is_approved_or_owner(self: @ContractState, spender: ContractAddress, token_id: u256) -> bool {
let owner = self.owners.read(token_id);
spender == owner
|| self.is_approved_for_all(owner, spender)
|| self.get_approved(token_id) == spender
}

// Sets the metadata URI for a specific token ID
fn _set_token_uri(ref self: ContractState, token_id: u256, token_uri: felt252) {
assert(self._exists(token_id), 'ERC721: invalid token ID');
self.token_uri.write(token_id, token_uri)
}

// Transfers a specific token ID from one address to another
fn _transfer(ref self: ContractState, from: ContractAddress, to: ContractAddress, token_id: u256) {
assert(from == self.owner_of(token_id), 'ERC721: Caller is not owner');
assert(to.is_non_zero(), 'ERC721: transfer to 0 address');

self.token_approvals.write(token_id, Zeroable::zero());

self.balances.write(from, self.balances.read(from) - 1.into());
self.balances.write(to, self.balances.read(to) + 1.into());

self.owners.write(token_id, to);

self.emit(
Transfer{ from: from, to: to, token_id: token_id }
);
}

// Mints a new token with a specific ID to a specified address
fn _mint(ref self: ContractState, to: ContractAddress, token_id: u256) {
assert(to.is_non_zero(), 'TO_IS_ZERO_ADDRESS');
assert(!self.owner_of(token_id).is_non_zero(), 'ERC721: Token already minted');

let receiver_balance = self.balances.read(to);
self.balances.write(to, receiver_balance + 1.into());

self.owners.write(token_id, to);

self.emit(
Transfer{ from: Zeroable::zero(), to: to, token_id: token_id }
);
}

// Burns a specific token ID, removing it from existence
fn _burn(ref self: ContractState, token_id: u256) {
let owner = self.owner_of(token_id);

self.token_approvals.write(token_id, Zeroable::zero());

let owner_balance = self.balances.read(owner);
self.balances.write(owner, owner_balance - 1.into());

self.owners.write(token_id, Zeroable::zero());

self.emit(
Transfer{ from: owner, to: Zeroable::zero(), token_id: token_id }
);
}
}
}

90 changes: 90 additions & 0 deletions listings/applications/ERC721/test_erc721.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
#[cfg(test)]
mod tests {
raizo07 marked this conversation as resolved.
Show resolved Hide resolved
use super::*;
use starknet::testing::ContractState;
use starknet::ContractAddress;
use starknet::get_caller_address;

#[test]
fn test_constructor() {
let contract_state = ContractState::default();
let contract = ERC721Contract::constructor(&contract_state, "MyToken".into(), "MTK".into());

assert_eq!(contract.get_name(), "MyToken".into());
assert_eq!(contract.get_symbol(), "MTK".into());
}

#[test]
fn test_mint() {
let contract_state = ContractState::default();
let mut contract = ERC721Contract::constructor(&contract_state, "MyToken".into(), "MTK".into());

let to = ContractAddress::from(1);
let token_id = 1.into();

contract._mint(to, token_id);

assert_eq!(contract.owner_of(token_id), to);
assert_eq!(contract.balance_of(to), 1.into());
}

#[test]
fn test_transfer() {
let contract_state = ContractState::default();
let mut contract = ERC721Contract::constructor(&contract_state, "MyToken".into(), "MTK".into());

let from = ContractAddress::from(1);
let to = ContractAddress::from(2);
let token_id = 1.into();

contract._mint(from, token_id);
contract._transfer(from, to, token_id);

assert_eq!(contract.owner_of(token_id), to);
assert_eq!(contract.balance_of(to), 1.into());
assert_eq!(contract.balance_of(from), 0.into());
}

#[test]
fn test_approve() {
let contract_state = ContractState::default();
let mut contract = ERC721Contract::constructor(&contract_state, "MyToken".into(), "MTK".into());

let owner = ContractAddress::from(1);
let approved = ContractAddress::from(2);
let token_id = 1.into();

contract._mint(owner, token_id);
contract.approve(approved, token_id);

assert_eq!(contract.get_approved(token_id), approved);
}

#[test]
fn test_set_approval_for_all() {
let contract_state = ContractState::default();
let mut contract = ERC721Contract::constructor(&contract_state, "MyToken".into(), "MTK".into());

let owner = get_caller_address();
let operator = ContractAddress::from(2);

contract.set_approval_for_all(operator, true);

assert!(contract.is_approved_for_all(owner, operator));
}

#[test]
fn test_burn() {
let contract_state = ContractState::default();
let mut contract = ERC721Contract::constructor(&contract_state, "MyToken".into(), "MTK".into());

let owner = ContractAddress::from(1);
let token_id = 1.into();

contract._mint(owner, token_id);
contract._burn(token_id);

assert_eq!(contract.owner_of(token_id).is_zero(), true);
assert_eq!(contract.balance_of(owner), 0.into());
}
}