diff --git a/Cargo.lock b/Cargo.lock index ef869c6..6e73687 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2094,8 +2094,20 @@ dependencies = [ ] [[package]] -name = "marketplace-native" +name = "marketplace_native" version = "0.1.0" +dependencies = [ + "bytemuck", + "five8_const", + "mollusk-svm", + "mollusk-token", + "pinocchio", + "pinocchio-system", + "pinocchio-token", + "solana-nostd-sha256", + "solana-sdk", + "spl-token", +] [[package]] name = "memchr" diff --git a/marketplace-native/Cargo.toml b/marketplace-native/Cargo.toml index a569aee..ea603d8 100644 --- a/marketplace-native/Cargo.toml +++ b/marketplace-native/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "marketplace-native" +name = "marketplace_native" version = "0.1.0" edition = "2021" authors.workspace = true @@ -7,4 +7,28 @@ homepage.workspace = true repository.workspace = true license.workspace = true +[package.metadata.solana] +program-id = "22222222222222222222222222222222222222222222" +program-dependencies = [] +account-dependencies = [] + +[lib] +crate-type = ["cdylib", "lib"] + [dependencies] +pinocchio = { workspace = true } +pinocchio-system = { workspace = true} +pinocchio-token = { workspace = true} +bytemuck = { workspace = true } +five8_const = { workspace = true } +solana-nostd-sha256 = { workspace = true } + +[dev-dependencies] +mollusk-svm = { workspace = true } +mollusk-token = { workspace = true } +solana-sdk = { workspace = true } +spl-token = { workspace = true } + +[features] +test-sbf = [] +no-entrypoint = [] \ No newline at end of file diff --git a/marketplace-native/README.md b/marketplace-native/README.md new file mode 100644 index 0000000..7014bc8 --- /dev/null +++ b/marketplace-native/README.md @@ -0,0 +1,7 @@ +Flow + +- Maker creates a Marketplace +- Producer wants to sell something and publish it +- Consumer wants to buy something in the marketplace +- Consumer sends money to vault and recieves the NFT +- Producer claims sell recieving money but fee diff --git a/marketplace-native/src/instructions/initialize.rs b/marketplace-native/src/instructions/initialize.rs new file mode 100644 index 0000000..4fc3a54 --- /dev/null +++ b/marketplace-native/src/instructions/initialize.rs @@ -0,0 +1,20 @@ +use pinocchio::{ + account_info::AccountInfo, program_error::ProgramError, pubkey::Pubkey, ProgramResult, +}; + +pub fn initialize(accounts: &[AccountInfo], data: &[u8]) -> ProgramResult { + let [marketplace] = accounts else { + return Err(ProgramError::NotEnoughAccountKeys); + }; + + unsafe { + let marketplace_account = marketplace.borrow_mut_data_unchecked().as_mut_ptr(); + + *(marketplace_account.add(0) as *mut Pubkey) = *((data.as_ptr() as * const Pubkey)); // maker + *(marketplace_account.add(32) as *mut u64) = *((data.as_ptr() as * const u64)); // fee + *(marketplace_account.add(32) as *mut u8) = *((data.as_ptr() as * const u8)); // bump + *(marketplace_account.add(32) as *mut u8) = *((data.as_ptr() as * const u8)); // treasury_bump + } + + Ok(()) +} diff --git a/marketplace-native/src/instructions/mod.rs b/marketplace-native/src/instructions/mod.rs new file mode 100644 index 0000000..bf16b40 --- /dev/null +++ b/marketplace-native/src/instructions/mod.rs @@ -0,0 +1,35 @@ +pub mod initialize; +pub use initialize::*; + +pub mod publish; +pub use publish::*; + +pub mod unpublish; +pub use unpublish::*; + +pub mod purchase; +pub use purchase::*; + +use pinocchio::program_error::ProgramError; + +#[derive(Clone, Copy, Debug)] +pub enum MarketplaceInstruction { + Initialize, + Publish, + Unpublish, + Purchase, +} + +impl TryFrom<&u8> for MarketplaceInstruction { + type Error = ProgramError; + + fn try_from(value: &u8) -> Result { + match value { + 0 => Ok(MarketplaceInstruction::Initialize), + 1 => Ok(MarketplaceInstruction::Publish), + 2 => Ok(MarketplaceInstruction::Unpublish), + 3 => Ok(MarketplaceInstruction::Purchase), + _ => Err(ProgramError::InvalidInstructionData), + } + } +} diff --git a/marketplace-native/src/instructions/publish.rs b/marketplace-native/src/instructions/publish.rs new file mode 100644 index 0000000..ab1b22d --- /dev/null +++ b/marketplace-native/src/instructions/publish.rs @@ -0,0 +1,20 @@ +use pinocchio::{ + account_info::AccountInfo, program_error::ProgramError, pubkey::Pubkey, ProgramResult, +}; + +// +// Publish instruction +// +// A publisher who owns a NFT, wants to sell it in a marketplace +// It will require to pass: +// > publisher +// > publisher_ta +// > the marketplace +// > the token account +// > price +// +pub fn publish(accounts: &[AccountInfo], data: &[u8]) -> ProgramResult { + let [publisher, marketplace, , _token_program] = accounts + + Ok(()) +} diff --git a/marketplace-native/src/instructions/purchase.rs b/marketplace-native/src/instructions/purchase.rs new file mode 100644 index 0000000..08c1f8e --- /dev/null +++ b/marketplace-native/src/instructions/purchase.rs @@ -0,0 +1,8 @@ +use pinocchio::{ + account_info::AccountInfo, program_error::ProgramError, pubkey::Pubkey, ProgramResult, +}; + +pub fn purchase(accounts: &[AccountInfo], data: &[u8]) -> ProgramResult { + + Ok(()) +} diff --git a/marketplace-native/src/instructions/unpublish.rs b/marketplace-native/src/instructions/unpublish.rs new file mode 100644 index 0000000..528971e --- /dev/null +++ b/marketplace-native/src/instructions/unpublish.rs @@ -0,0 +1,8 @@ +use pinocchio::{ + account_info::AccountInfo, program_error::ProgramError, pubkey::Pubkey, ProgramResult, +}; + +pub fn unpublish(accounts: &[AccountInfo], data: &[u8]) -> ProgramResult { + + Ok(()) +} diff --git a/marketplace-native/src/lib.rs b/marketplace-native/src/lib.rs index b93cf3f..ac249d7 100644 --- a/marketplace-native/src/lib.rs +++ b/marketplace-native/src/lib.rs @@ -1,14 +1,32 @@ -pub fn add(left: u64, right: u64) -> u64 { - left + right -} +#![feature(asm_experimental_arch)] +use pinocchio::{ + account_info::AccountInfo, entrypoint, program_error::ProgramError, pubkey::Pubkey, + ProgramResult, +}; + +mod instructions; +use instructions::*; + +mod state; +pub use state::*; + +const ID: Pubkey = five8_const::decode_32_const("22222222222222222222222222222222222222222222"); + +entrypoint!(process_instruction); -#[cfg(test)] -mod tests { - use super::*; +pub fn process_instruction( + _program_id: &Pubkey, + accounts: &[AccountInfo], + instruction_data: &[u8], +) -> ProgramResult { + let (discriminator, data) = instruction_data + .split_first() + .ok_or(ProgramError::InvalidInstructionData)?; - #[test] - fn it_works() { - let result = add(2, 2); - assert_eq!(result, 4); + match MarketplaceInstruction::try_from(discriminator)? { + MarketplaceInstruction::Initialize => initialize(accounts, data), + MarketplaceInstruction::Publish => publish(accounts, data), + MarketplaceInstruction::Unpublish => unpublish(accounts, data), + MarketplaceInstruction::Purchase => purchase(accounts, data), } } diff --git a/marketplace-native/src/state/marketplace.rs b/marketplace-native/src/state/marketplace.rs new file mode 100644 index 0000000..0c378c0 --- /dev/null +++ b/marketplace-native/src/state/marketplace.rs @@ -0,0 +1,62 @@ +use pinocchio::{account_info::AccountInfo, program_error::ProgramError, pubkey::Pubkey, ProgramResult}; + +/// # Marketplace State +/// +/// -- Data -- +/// > Maker: Pubkey +/// > fee: u64 +/// > bump: u8 +/// > treasury_bump: u8 +/// +/// -- Data Logic -- +/// [...] +/// +pub struct Marketplace(*const u8); + +impl Marketplace { + pub const LEN: usize = 32 // maker + + 8 // fee + + 1 // bump + + 1; // treasury_bump + + #[inline(always)] + pub fn init(&self, data: &[u8; Self::LEN]) -> ProgramResult { + unsafe { *(self.0 as *mut [u8; Self::LEN]) = *data }; + Ok(()) + } + + #[inline(always)] + pub fn from_account_info_unchecked(account_info: &AccountInfo) -> Self { + unsafe { Self(account_info.borrow_data_unchecked().as_ptr()) } + } + + #[inline(always)] + pub fn from_account_info(account_info: &AccountInfo) -> Result { + assert_eq!(account_info.data_len(), Self::LEN); + assert_eq!(account_info.owner(), &crate::ID); + Ok(Self::from_account_info_unchecked(account_info)) + } + + // We store who owns the marketplace + #[inline(always)] + pub fn maker(&self) -> Pubkey { + unsafe { *(self.0 as *const Pubkey) } + } + + // How much the marketplace will retain per purchase + #[inline(always)] + pub fn fee(&self) -> u64 { + unsafe { *(self.0.add(32) as *const u64) } + } + + #[inline(always)] + pub fn bump(&self) -> u8 { + unsafe { *(self.0.add(40) as *const u8) } + } + + // To store the SOL + #[inline(always)] + pub fn treasury_bump(&self) -> u8 { + unsafe { *(self.0.add(41) as *const u8) } + } +} diff --git a/marketplace-native/src/state/mod.rs b/marketplace-native/src/state/mod.rs new file mode 100644 index 0000000..d8e99c0 --- /dev/null +++ b/marketplace-native/src/state/mod.rs @@ -0,0 +1,5 @@ +pub mod marketplace; +pub use marketplace::*; + +pub mod publish; +pub use publish::*; diff --git a/marketplace-native/src/state/publish.rs b/marketplace-native/src/state/publish.rs new file mode 100644 index 0000000..288b89e --- /dev/null +++ b/marketplace-native/src/state/publish.rs @@ -0,0 +1,45 @@ +use pinocchio::{account_info::AccountInfo, pubkey::Pubkey}; + +/// # Publish State +/// +/// -- Data -- +/// > Maker: Pubkey +/// > fee: u64 +/// > bump: u8 +/// > treasury_bump: u8 +/// +/// -- Data Logic -- +/// [...] +/// +pub struct Publish(*const u8); + +impl Publish { + pub const LEN: usize = 32 + 32 + 8 + 1; + + #[inline(always)] + pub fn from_account_info_unchecked(account_info: &AccountInfo) -> Self { + unsafe { Self(account_info.borrow_data_unchecked().as_ptr()) } + } + + pub fn from_account_info(account_info: &AccountInfo) -> Self { + assert_eq!(account_info.data_len(), Self::LEN); + assert_eq!(account_info.owner(), &crate::ID); + Self::from_account_info_unchecked(account_info) + } + + pub fn publisher(&self) -> Pubkey { + unsafe { *(self.0 as *const Pubkey) } + } + + pub fn mint(&self) -> Pubkey { + unsafe { *(self.0.add(32) as *const Pubkey) } + } + + pub fn price(&self) -> u64 { + unsafe { *(self.0.add(64) as *const u64) } + } + + pub fn bump(&self) -> u8 { + unsafe { *(self.0.add(72) as *const u8) } + } +} diff --git a/marketplace-native/tests/initialize.rs b/marketplace-native/tests/initialize.rs new file mode 100644 index 0000000..b11ecf7 --- /dev/null +++ b/marketplace-native/tests/initialize.rs @@ -0,0 +1,93 @@ +#[path = "./shared.rs"] +mod shared; + +#[cfg(test)] +mod initialize_tests { + use crate::shared; + use marketplace_native::Marketplace; + + use mollusk_svm::result::Check; + use solana_sdk::{ + account::{ AccountSharedData, ReadableAccount }, + instruction::{AccountMeta, Instruction}, + pubkey::Pubkey, + }; + + #[test] + fn initialize() { + let (mollusk, program_id) = shared::setup(); + + let marker = Pubkey::new_unique(); + let marketplace = Pubkey::new_unique(); + + let data = [ + vec![0], // Instruction + marker.to_bytes().to_vec(), // marker + u64::MAX.to_le_bytes().to_vec(), // fee + u8::MAX.to_le_bytes().to_vec(), // authority bump + u8::MAX.to_le_bytes().to_vec(), // authority bump + ] + .concat(); + + let instruction = + Instruction::new_with_bytes(program_id, &data, vec![AccountMeta::new(marketplace, false)]); + + let lamports = mollusk.sysvars.rent.minimum_balance(Marketplace::LEN); + + let result = mollusk.process_and_validate_instruction( + &instruction, + &vec![( + marketplace, + AccountSharedData::new(lamports, Marketplace::LEN, &program_id), + )], + &[Check::success()], + ); + + assert!(!result.program_result.is_err()); + + // We could add some tests to the config account created + let marketplace_result_account = result + .get_account(&marketplace) + .expect("Failed to find marketplace account"); + + // Fundraiser should be own by the program id to be able to modify it + assert_eq!(*marketplace_result_account.owner(), program_id); + + // Fundraiser should have a length of 81 + assert_eq!(marketplace_result_account.data().len(), Marketplace::LEN); + + // Let's verify the data + /* + let data = marketplace_result_account.data(); + + // Maker Pubkey + let pubkey_bytes: [u8; 32] = data[0..32] + .try_into() + .expect("Expected 32 bytes for pubkey"); + let maker_pubkey = Pubkey::from(pubkey_bytes); + assert_eq!(maker_pubkey.to_string(), maker.to_string()); + + // Mint Pubkey + let mint_bytes: [u8; 32] = data[32..64].try_into().expect("Expecting 8 bytes for mint"); + let mint_pubkey = Pubkey::from(mint_bytes); + assert_eq!(mint_pubkey.to_string(), mint.to_string()); + + // Remaining Amount + let remaining_amount_bytes: [u8; 8] = data[64..72] + .try_into() + .expect("Expecting 8 bytes for remaining_amount"); + let remaining_amount_result = u64::from_le_bytes(remaining_amount_bytes); + assert_eq!(remaining_amount_result, 100_000_000u64); + + // Slot + let slot_bytes: [u8; 8] = data[72..80].try_into().expect("Expecting 8 bytes for slot"); + let slot_result = u64::from_le_bytes(slot_bytes); + assert_eq!(slot_result, slot); + + // authority bump + let bump_bytes: [u8; 1] = data[80..81].try_into().expect("Expecting 1 bytes for bump"); + let bump_result = u8::from_le_bytes(bump_bytes); + + assert_eq!(bump_result, bump); */ + } +} diff --git a/marketplace-native/tests/shared.rs b/marketplace-native/tests/shared.rs new file mode 100644 index 0000000..4eb4cb2 --- /dev/null +++ b/marketplace-native/tests/shared.rs @@ -0,0 +1,129 @@ +use std::mem; + +use marketplace_native::Marketplace; +use mollusk_svm::{result::InstructionResult, Mollusk}; +use solana_sdk::{ + account::{AccountSharedData, ReadableAccount, WritableAccount}, + program_option::COption, + program_pack::Pack, + pubkey::Pubkey, +}; +use spl_token::state::AccountState; + +pub fn setup() -> (Mollusk, Pubkey) { + let program_id = Pubkey::new_from_array(five8_const::decode_32_const( + "22222222222222222222222222222222222222222222", + )); + + let project_name = format!("../target/deploy/{}", env!("CARGO_PKG_NAME")); + let mut mollusk = Mollusk::new(&program_id, &project_name); + + mollusk_token::token::add_program(&mut mollusk); + (mollusk, program_id) +} + +pub fn create_mint_account( + mollusk: &Mollusk, + authority: Pubkey, + supply: u64, + decimals: u8, + is_initialized: bool, + token_program: Pubkey, +) -> AccountSharedData { + let mut account = AccountSharedData::new( + mollusk + .sysvars + .rent + .minimum_balance(spl_token::state::Mint::LEN), + spl_token::state::Mint::LEN, + &token_program, + ); + + spl_token::state::Mint { + mint_authority: COption::Some(authority), + supply, + decimals, + is_initialized, + freeze_authority: COption::None, + } + .pack_into_slice(account.data_as_mut_slice()); + + account +} + +pub fn create_token_account( + mollusk: &Mollusk, + mint: Pubkey, + owner: Pubkey, + amount: u64, + token_program_id: Pubkey, +) -> AccountSharedData /* (PubKey, AccountSharedData) */ { + let mut account = AccountSharedData::new( + mollusk + .sysvars + .rent + .minimum_balance(spl_token::state::Account::LEN), + spl_token::state::Account::LEN, + &token_program_id, + ); + + spl_token::state::Account::pack( + spl_token::state::Account { + mint, + owner, + amount, + delegate: COption::None, + state: AccountState::Initialized, + is_native: COption::None, + delegated_amount: 0, + close_authority: COption::None, + }, + account.data_as_mut_slice(), + ) + .unwrap(); + + account +} + +pub fn create_marketplace( + mollusk: &Mollusk, + maker: Pubkey, + fee: u64, + bump: u8, + treasury_bump: u8, + program_id: Pubkey, +) -> AccountSharedData { + let mut account = AccountSharedData::new( + mollusk + .sysvars + .rent + .minimum_balance(mem::size_of::()), + mem::size_of::(), + &program_id, + ); + account.set_data_from_slice( + &[ + maker.to_bytes().to_vec(), + fee.to_le_bytes().to_vec(), + bump.to_le_bytes().to_vec(), + treasury_bump.to_le_bytes().to_vec(), + ] + .concat(), + ); + + account +} +/* +#[inline] +pub fn expect_token_balance(result: &InstructionResult, account: Pubkey, expected_balance: u64) { + let account_shared_data = result + .get_account(&account) + .expect("Failed to find contributor token account"); + + let account_data: spl_token::state::Account = + solana_sdk::program_pack::Pack::unpack(&account_shared_data.data()) + .expect("Failed to unpack contributor token account"); + + assert_eq!(account_data.amount, expected_balance); +} + */ \ No newline at end of file