Skip to content

Commit

Permalink
feat: added ERC721 contract (#135)
Browse files Browse the repository at this point in the history
* feat: added ERC721 contract

* apply requested changes

* change variable name
  • Loading branch information
od-hunter authored Jan 31, 2025
1 parent a76d883 commit 5cc1549
Show file tree
Hide file tree
Showing 5 changed files with 269 additions and 0 deletions.
119 changes: 119 additions & 0 deletions onchain/Scarb.lock
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,128 @@ version = 1
name = "lyricsflip"
version = "0.1.0"
dependencies = [
"openzeppelin",
"snforge_std",
]

[[package]]
name = "openzeppelin"
version = "0.19.0"
source = "registry+https://scarbs.xyz/"
checksum = "sha256:33174cc8f66cd2c1a527fd7f13a800dcb107d59f5c77e998e0c896a1da9cf1df"
dependencies = [
"openzeppelin_access",
"openzeppelin_account",
"openzeppelin_finance",
"openzeppelin_governance",
"openzeppelin_introspection",
"openzeppelin_merkle_tree",
"openzeppelin_presets",
"openzeppelin_security",
"openzeppelin_token",
"openzeppelin_upgrades",
"openzeppelin_utils",
]

[[package]]
name = "openzeppelin_access"
version = "0.19.0"
source = "registry+https://scarbs.xyz/"
checksum = "sha256:0f5055ef443327bb613a56a812ccf31157abfd7d36a18739556f78b67f5b1116"
dependencies = [
"openzeppelin_introspection",
"openzeppelin_utils",
]

[[package]]
name = "openzeppelin_account"
version = "0.19.0"
source = "registry+https://scarbs.xyz/"
checksum = "sha256:0c92c856e44080e3280788d1c46f89ac707c64fa555eb02c343e492709a1ee50"
dependencies = [
"openzeppelin_introspection",
"openzeppelin_utils",
]

[[package]]
name = "openzeppelin_finance"
version = "0.19.0"
source = "registry+https://scarbs.xyz/"
checksum = "sha256:3d38c8aff02478431ddbb0538be5281a89eb159016105195bf6409bf6c3c4fc4"
dependencies = [
"openzeppelin_access",
"openzeppelin_token",
]

[[package]]
name = "openzeppelin_governance"
version = "0.19.0"
source = "registry+https://scarbs.xyz/"
checksum = "sha256:fc6afb45e3cdcb5e843bbc80c6e12bb2536a34f557b74787c256872b86f2a81a"
dependencies = [
"openzeppelin_access",
"openzeppelin_account",
"openzeppelin_introspection",
"openzeppelin_token",
]

[[package]]
name = "openzeppelin_introspection"
version = "0.19.0"
source = "registry+https://scarbs.xyz/"
checksum = "sha256:a1dda07a91c447b83ccfcc4895897ec134917f0ff6d2ca876b93ea27466d7693"

[[package]]
name = "openzeppelin_merkle_tree"
version = "0.19.0"
source = "registry+https://scarbs.xyz/"
checksum = "sha256:e7aaa00b9ea0f73938d3be6351aaa88efd21304bf6d5fd1b66c61e048a7a2375"

[[package]]
name = "openzeppelin_presets"
version = "0.19.0"
source = "registry+https://scarbs.xyz/"
checksum = "sha256:57d5c48724025072419c63a929903d71b949287338dc86d561e52b52d869c06f"
dependencies = [
"openzeppelin_access",
"openzeppelin_account",
"openzeppelin_finance",
"openzeppelin_introspection",
"openzeppelin_token",
"openzeppelin_upgrades",
"openzeppelin_utils",
]

[[package]]
name = "openzeppelin_security"
version = "0.19.0"
source = "registry+https://scarbs.xyz/"
checksum = "sha256:0f1462d6de898cd28199cde0110304b4248fb19c7e788d4121d26c93b290e991"

[[package]]
name = "openzeppelin_token"
version = "0.19.0"
source = "registry+https://scarbs.xyz/"
checksum = "sha256:9cba10f666ca6dd83b581367438d04b244bd5bbf0cfad6a28d193d373c0498b8"
dependencies = [
"openzeppelin_access",
"openzeppelin_account",
"openzeppelin_introspection",
"openzeppelin_utils",
]

[[package]]
name = "openzeppelin_upgrades"
version = "0.19.0"
source = "registry+https://scarbs.xyz/"
checksum = "sha256:3f2badf764a2219b0ea5b567b039daeb4c1707331f98e4f7b985ca2b562b4e10"

[[package]]
name = "openzeppelin_utils"
version = "0.19.0"
source = "registry+https://scarbs.xyz/"
checksum = "sha256:0e0e6f6b20b3c4075b92941a2c124430a59f1c207f8fbdfd56ce9239e6d666a8"

[[package]]
name = "snforge_scarb_plugin"
version = "0.31.0"
Expand Down
1 change: 1 addition & 0 deletions onchain/Scarb.toml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ edition = "2023_11"
# See more keys and their definitions at https://docs.swmansion.com/scarb/docs/reference/manifest.html

[dependencies]
openzeppelin = "0.19.0"
starknet = "2.8.4"

[dev-dependencies]
Expand Down
75 changes: 75 additions & 0 deletions onchain/src/contracts/lyricsflipNFT.cairo
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
use starknet::ContractAddress;

#[starknet::interface]
pub trait ILyricsFlipNFT<TContractState> {
fn mint(ref self: TContractState, recipient: ContractAddress);
}

#[starknet::contract]
mod LyricsFlipNFT {
use openzeppelin::access::ownable::OwnableComponent;
use openzeppelin::introspection::src5::SRC5Component;
use openzeppelin::token::erc721::{ERC721Component, ERC721HooksEmptyImpl};
use starknet::storage::StoragePointerReadAccess;
use starknet::storage::StoragePointerWriteAccess;
use starknet::{ContractAddress};

component!(path: ERC721Component, storage: erc721, event: ERC721Event);
component!(path: SRC5Component, storage: src5, event: SRC5Event);
component!(path: OwnableComponent, storage: ownable, event: OwnableEvent);

// External
#[abi(embed_v0)]
impl ERC721MixinImpl = ERC721Component::ERC721MixinImpl<ContractState>;
#[abi(embed_v0)]
impl OwnableMixinImpl = OwnableComponent::OwnableMixinImpl<ContractState>;

// Internal
impl ERC721InternalImpl = ERC721Component::InternalImpl<ContractState>;
impl OwnableInternalImpl = OwnableComponent::InternalImpl<ContractState>;

#[storage]
struct Storage {
#[substorage(v0)]
erc721: ERC721Component::Storage,
#[substorage(v0)]
src5: SRC5Component::Storage,
#[substorage(v0)]
ownable: OwnableComponent::Storage,
token_count: u256,
}

#[event]
#[derive(Drop, starknet::Event)]
enum Event {
#[flat]
ERC721Event: ERC721Component::Event,
#[flat]
SRC5Event: SRC5Component::Event,
#[flat]
OwnableEvent: OwnableComponent::Event,
}

#[constructor]
fn constructor(
ref self: ContractState,
owner: ContractAddress,
token_name: ByteArray,
token_symbol: ByteArray,
base_uri: ByteArray
) {
self.erc721.initializer(token_name, token_symbol, base_uri);
self.ownable.initializer(owner);
}

#[abi(embed_v0)]
impl ILyricsFlipNFTImpl of super::ILyricsFlipNFT<ContractState> {
fn mint(ref self: ContractState, recipient: ContractAddress) {
let mut token_id = self.token_count.read() + 1;
self.ownable.assert_only_owner();
assert(!self.erc721.exists(token_id), 'NFT with id already exists');
self.erc721.mint(recipient, token_id);
self.token_count.write(token_id);
}
}
}
2 changes: 2 additions & 0 deletions onchain/src/lib.cairo
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ pub mod interfaces {

pub mod contracts {
pub mod lyricsflip;
pub mod lyricsflipNFT;
}

pub mod utils {
Expand All @@ -14,4 +15,5 @@ pub mod utils {
#[cfg(test)]
mod tests {
mod test_lyricsflip;
mod test_lyricsflipNFT;
}
72 changes: 72 additions & 0 deletions onchain/src/tests/test_lyricsflipNFT.cairo
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
use core::array::ArrayTrait;
use core::byte_array::ByteArray;
use core::result::ResultTrait;
use core::traits::Into;
use lyricsflip::contracts::lyricsflipNFT::{
ILyricsFlipNFTDispatcher as NFTDispatcher, ILyricsFlipNFTDispatcherTrait as NFTDispatcherTrait
};
use openzeppelin::token::erc721::interface::{IERC721Dispatcher, IERC721DispatcherTrait};
use snforge_std::{
declare, ContractClassTrait, DeclareResultTrait, start_cheat_caller_address,
stop_cheat_caller_address
};
use starknet::{ContractAddress, contract_address_const};

// Account functions
fn owner() -> ContractAddress {
contract_address_const::<'OWNER'>()
}

fn caller() -> ContractAddress {
contract_address_const::<'CALLER'>()
}

fn setup_dispatcher() -> (ContractAddress, NFTDispatcher) {
// Declare the contract
let contract = declare("LyricsFlipNFT").unwrap().contract_class();

// Prepare constructor calldata
let mut calldata: Array<felt252> = ArrayTrait::new();

// Add constructor arguments
calldata.append(owner().into());

let name: ByteArray = "TestNFT";
let symbol: ByteArray = "LYNFT";
let base_uri: ByteArray = "baseuri";

name.serialize(ref calldata);
symbol.serialize(ref calldata);
base_uri.serialize(ref calldata);

// Deploy contract
let (address, _) = contract.deploy(@calldata).unwrap();

// Create dispatcher
(address, NFTDispatcher { contract_address: address })
}

#[test]
fn test_successful_mint() {
let (contract_address, dispatcher) = setup_dispatcher();
let recipient = contract_address_const::<'RECIPIENT'>();

start_cheat_caller_address(contract_address, owner());
dispatcher.mint(recipient);
stop_cheat_caller_address(contract_address);

let erc721 = IERC721Dispatcher { contract_address };
assert(erc721.owner_of(1) == recipient, 'Wrong owner');
assert(erc721.balance_of(recipient) == 1, 'Wrong balance');
}

#[test]
#[should_panic(expected: ('Caller is not the owner',))]
fn test_mint_not_owner() {
let (contract_address, dispatcher) = setup_dispatcher();
let recipient = contract_address_const::<'RECIPIENT'>();

start_cheat_caller_address(contract_address, caller());
dispatcher.mint(recipient);
stop_cheat_caller_address(contract_address);
}

0 comments on commit 5cc1549

Please sign in to comment.