diff --git a/onchain/Scarb.lock b/onchain/Scarb.lock index 89937d1f..2bee95e4 100644 --- a/onchain/Scarb.lock +++ b/onchain/Scarb.lock @@ -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" diff --git a/onchain/Scarb.toml b/onchain/Scarb.toml index 11dce338..3867ce6b 100644 --- a/onchain/Scarb.toml +++ b/onchain/Scarb.toml @@ -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] diff --git a/onchain/src/contracts/lyricsflipNFT.cairo b/onchain/src/contracts/lyricsflipNFT.cairo new file mode 100644 index 00000000..ea80a8e8 --- /dev/null +++ b/onchain/src/contracts/lyricsflipNFT.cairo @@ -0,0 +1,75 @@ +use starknet::ContractAddress; + +#[starknet::interface] +pub trait ILyricsFlipNFT { + 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; + #[abi(embed_v0)] + impl OwnableMixinImpl = OwnableComponent::OwnableMixinImpl; + + // Internal + impl ERC721InternalImpl = ERC721Component::InternalImpl; + impl OwnableInternalImpl = OwnableComponent::InternalImpl; + + #[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 { + 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); + } + } +} diff --git a/onchain/src/lib.cairo b/onchain/src/lib.cairo index 0fe0e7e8..f9eba9bb 100644 --- a/onchain/src/lib.cairo +++ b/onchain/src/lib.cairo @@ -4,6 +4,7 @@ pub mod interfaces { pub mod contracts { pub mod lyricsflip; + pub mod lyricsflipNFT; } pub mod utils { @@ -14,4 +15,5 @@ pub mod utils { #[cfg(test)] mod tests { mod test_lyricsflip; + mod test_lyricsflipNFT; } diff --git a/onchain/src/tests/test_lyricsflipNFT.cairo b/onchain/src/tests/test_lyricsflipNFT.cairo new file mode 100644 index 00000000..795d2fa8 --- /dev/null +++ b/onchain/src/tests/test_lyricsflipNFT.cairo @@ -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 = 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); +}