From 452111a8487a217c6b61f9952cef1caf40d7b514 Mon Sep 17 00:00:00 2001 From: Sam Orend Date: Tue, 7 Jun 2022 17:53:42 -0400 Subject: [PATCH] add sugar shack example compressed nft marketplace contract, test and sdk --- contracts/Anchor.toml | 3 +- contracts/Cargo.lock | 23 + contracts/Cargo.toml | 3 +- .../programs/gumball-machine/src/utils.rs | 9 + contracts/programs/sugar-shack/Cargo.toml | 25 + contracts/programs/sugar-shack/Xargo.toml | 2 + contracts/programs/sugar-shack/src/lib.rs | 440 +++++++++++++++++ .../programs/sugar-shack/src/state/mod.rs | 14 + .../bubblegum/src/generated/accounts/Nonce.ts | 2 +- .../src/generated/accounts/Voucher.ts | 4 +- .../src/generated/types/LeafSchema.ts | 55 +-- contracts/sdk/gumball-machine/index.ts | 2 +- .../sdk/gumball-machine/instructions/index.ts | 9 +- .../src/generated/types/EncodeMethod.ts | 24 + contracts/sdk/gumball-machine/utils/index.ts | 8 - contracts/sdk/gummyroll/index.ts | 1 + contracts/sdk/gummyroll/utils/index.ts | 20 + contracts/sdk/sugar-shack/README.md | 14 + .../sdk/sugar-shack/idl/sugar_shack.json | 398 +++++++++++++++ contracts/sdk/sugar-shack/index.ts | 2 + contracts/sdk/sugar-shack/package.json | 11 + contracts/sdk/sugar-shack/solita.js | 64 +++ .../accounts/MarketplaceProperties.ts | 157 ++++++ .../src/generated/accounts/index.ts | 1 + .../sdk/sugar-shack/src/generated/index.ts | 19 + .../instructions/createOrModifyListing.ts | 147 ++++++ .../src/generated/instructions/index.ts | 6 + .../instructions/initializeMarketplace.ts | 102 ++++ .../src/generated/instructions/purchase.ts | 160 ++++++ .../generated/instructions/removeListing.ts | 145 ++++++ .../updateMarketplaceProperties.ts | 98 ++++ .../generated/instructions/withdrawFees.ts | 114 +++++ .../src/generated/types/RoyaltyRecipient.ts | 28 ++ .../sugar-shack/src/generated/types/index.ts | 1 + contracts/sdk/sugar-shack/utils/index.ts | 12 + contracts/sdk/utils/index.ts | 8 + contracts/tests/bubblegum-test.ts | 47 +- contracts/tests/gumball-machine-serde.ts | 90 ---- contracts/tests/gumball-machine-test.ts | 46 +- contracts/tests/gummyroll-test.ts | 1 - contracts/tests/sugar-shack-test.ts | 465 ++++++++++++++++++ contracts/tests/utils.ts | 15 +- 42 files changed, 2564 insertions(+), 231 deletions(-) create mode 100644 contracts/programs/sugar-shack/Cargo.toml create mode 100644 contracts/programs/sugar-shack/Xargo.toml create mode 100644 contracts/programs/sugar-shack/src/lib.rs create mode 100644 contracts/programs/sugar-shack/src/state/mod.rs create mode 100644 contracts/sdk/gumball-machine/src/generated/types/EncodeMethod.ts create mode 100644 contracts/sdk/gummyroll/utils/index.ts create mode 100644 contracts/sdk/sugar-shack/README.md create mode 100644 contracts/sdk/sugar-shack/idl/sugar_shack.json create mode 100644 contracts/sdk/sugar-shack/index.ts create mode 100644 contracts/sdk/sugar-shack/package.json create mode 100644 contracts/sdk/sugar-shack/solita.js create mode 100644 contracts/sdk/sugar-shack/src/generated/accounts/MarketplaceProperties.ts create mode 100644 contracts/sdk/sugar-shack/src/generated/accounts/index.ts create mode 100644 contracts/sdk/sugar-shack/src/generated/index.ts create mode 100644 contracts/sdk/sugar-shack/src/generated/instructions/createOrModifyListing.ts create mode 100644 contracts/sdk/sugar-shack/src/generated/instructions/index.ts create mode 100644 contracts/sdk/sugar-shack/src/generated/instructions/initializeMarketplace.ts create mode 100644 contracts/sdk/sugar-shack/src/generated/instructions/purchase.ts create mode 100644 contracts/sdk/sugar-shack/src/generated/instructions/removeListing.ts create mode 100644 contracts/sdk/sugar-shack/src/generated/instructions/updateMarketplaceProperties.ts create mode 100644 contracts/sdk/sugar-shack/src/generated/instructions/withdrawFees.ts create mode 100644 contracts/sdk/sugar-shack/src/generated/types/RoyaltyRecipient.ts create mode 100644 contracts/sdk/sugar-shack/src/generated/types/index.ts create mode 100644 contracts/sdk/sugar-shack/utils/index.ts delete mode 100644 contracts/tests/gumball-machine-serde.ts create mode 100644 contracts/tests/sugar-shack-test.ts diff --git a/contracts/Anchor.toml b/contracts/Anchor.toml index 9be087b054f..9f21cdb6c23 100644 --- a/contracts/Anchor.toml +++ b/contracts/Anchor.toml @@ -3,6 +3,7 @@ gummyroll = "GRoLLMza82AiYN7W9S9KCCtCyyPRAQP2ifBy4v4D5RMD" gummyroll_crud = "Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS" bubblegum = "BGUMAp9Gq7iTEuizy4pqaxsTyUCBK68MDfK752saRPUY" gumball_machine = "GBALLoMcmimUutWvtNdFFGH5oguS7ghUUV6toQPppuTW" +sugar_shack = "9T5Xv2cJRydUBqvdK7rLGuNGqhkA8sU8Yq1rGN7hExNK" [[test.genesis]] address = "ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL" @@ -28,4 +29,4 @@ cluster = "localnet" wallet = "~/.config/solana/id.json" [scripts] -test = "yarn run ts-mocha -t 1000000 tests/**/gumball-machine-test.ts" +test = "yarn run ts-mocha -t 1000000 tests/**/*-test.ts" diff --git a/contracts/Cargo.lock b/contracts/Cargo.lock index 8e32258721a..f360ee2ecf5 100644 --- a/contracts/Cargo.lock +++ b/contracts/Cargo.lock @@ -1543,6 +1543,16 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "solana-safe-math" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccd6612e507fc0c6e17c51e3dfbed7b724f7c33cd4f5eb452d7dc4688ddaa888" +dependencies = [ + "solana-program", + "thiserror", +] + [[package]] name = "solana-sdk" version = "1.10.25" @@ -1697,6 +1707,19 @@ version = "2.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6bdef32e8150c2a081110b42772ffe7d7c9032b606bc226c8260fd97e0976601" +[[package]] +name = "sugar-shack" +version = "0.1.0" +dependencies = [ + "anchor-lang", + "anchor-spl", + "bubblegum", + "bytemuck", + "gummyroll", + "mpl-token-metadata", + "solana-safe-math", +] + [[package]] name = "syn" version = "1.0.96" diff --git a/contracts/Cargo.toml b/contracts/Cargo.toml index 702aeef66f7..7cd031df09d 100644 --- a/contracts/Cargo.toml +++ b/contracts/Cargo.toml @@ -3,5 +3,6 @@ members = [ "programs/gummyroll", "programs/gummyroll_crud", "programs/bubblegum", - "programs/gumball-machine" + "programs/gumball-machine", + "programs/sugar-shack" ] diff --git a/contracts/programs/gumball-machine/src/utils.rs b/contracts/programs/gumball-machine/src/utils.rs index 9487d82e5ee..d5bc9391127 100644 --- a/contracts/programs/gumball-machine/src/utils.rs +++ b/contracts/programs/gumball-machine/src/utils.rs @@ -72,6 +72,15 @@ pub fn get_metadata_args( }, uses, token_program_version: TokenProgramVersion::Token2022, + // TODO: change this placeholder to be more clear. Creators are akin to permanent secondary sale royalty recipients and are to be stored in the gumball header. + // We want something more like: + /* + Creator { + address: project_drop_pubkey, + verified: true, + share: 5, + } + */ creators: vec![Creator { address: creator, verified: true, diff --git a/contracts/programs/sugar-shack/Cargo.toml b/contracts/programs/sugar-shack/Cargo.toml new file mode 100644 index 00000000000..fb5188c41b2 --- /dev/null +++ b/contracts/programs/sugar-shack/Cargo.toml @@ -0,0 +1,25 @@ +[package] +name = "sugar-shack" +version = "0.1.0" +description = "Created with Anchor" +edition = "2021" + +[lib] +crate-type = ["cdylib", "lib"] +name = "sugar_shack" + +[features] +no-entrypoint = [] +no-idl = [] +no-log-ix-name = [] +cpi = ["no-entrypoint"] +default = [] + +[dependencies] +anchor-lang = { path="../../../deps/anchor/lang" } +anchor-spl = { path="../../../deps/anchor/spl" } +solana-safe-math = "0.1.0" +mpl-token-metadata = { git = "https://github.com/jarry-xiao/metaplex-program-library", rev="7e2810a", features = [ "no-entrypoint" ] } +bubblegum = { path = "../bubblegum", features = ["cpi"] } +gummyroll = { path = "../gummyroll", features = ["cpi"] } +bytemuck = "1.8.0" diff --git a/contracts/programs/sugar-shack/Xargo.toml b/contracts/programs/sugar-shack/Xargo.toml new file mode 100644 index 00000000000..475fb71ed15 --- /dev/null +++ b/contracts/programs/sugar-shack/Xargo.toml @@ -0,0 +1,2 @@ +[target.bpfel-unknown-unknown.dependencies.std] +features = [] diff --git a/contracts/programs/sugar-shack/src/lib.rs b/contracts/programs/sugar-shack/src/lib.rs new file mode 100644 index 00000000000..a5caaeb8679 --- /dev/null +++ b/contracts/programs/sugar-shack/src/lib.rs @@ -0,0 +1,440 @@ +use anchor_lang::{ + prelude::*, + solana_program::{ + log::sol_log_compute_units, + keccak::hashv, + pubkey::Pubkey, + instruction::{Instruction, AccountMeta}, + program::{invoke, invoke_signed}, + system_instruction, + }, +}; +use solana_safe_math::{SafeMath}; +use bubblegum::program::Bubblegum; +use gummyroll::program::Gummyroll; +pub mod state; +use crate::state::{MarketplaceProperties, MARKETPLACE_PROPERTIES_SIZE}; + +declare_id!("9T5Xv2cJRydUBqvdK7rLGuNGqhkA8sU8Yq1rGN7hExNK"); + +const MARKETPLACE_PROPERTIES_PREFIX: &str = "mymarketplace"; + +#[derive(Accounts)] +pub struct InitMarketplaceProperties<'info> { + #[account(mut)] + pub payer: Signer<'info>, + #[account( + init, + payer = payer, + space = MARKETPLACE_PROPERTIES_SIZE, + seeds = [MARKETPLACE_PROPERTIES_PREFIX.as_ref()], + bump, + )] + pub marketplace_props: Account<'info, MarketplaceProperties>, + pub system_program: Program<'info, System>, +} + +#[derive(Accounts)] +pub struct UpdateMarketplaceProperties<'info> { + /// CHECK: the authority over the marketplace, must correspond to the authority in marketplace_props, validated in instruction. + pub authority: Signer<'info>, + + /// CHECK: the PDA for this marketplace. + #[account( + seeds = [MARKETPLACE_PROPERTIES_PREFIX.as_ref()], + bump = marketplace_props.bump, + )] + #[account(mut)] + pub marketplace_props: Account<'info, MarketplaceProperties> +} + +#[derive(Accounts)] +#[instruction( + price: u64, +)] +pub struct CreateModifyListing<'info> { + /// CHECK: should own NFT, validated downstream in Gummyroll + #[account(mut)] + pub owner: Signer<'info>, + + /// CHECK: should be the current delegate of the NFT. Validated downstream in Gummyroll. + pub former_delegate: AccountInfo<'info>, + + #[account( + seeds = [price.to_le_bytes().as_ref()], + bump, + )] + /// CHECK: A PDA that encodes the price of the listing, will become the new delegate for the NFT. + pub new_delegate: AccountInfo<'info>, + + /// CHECK: PDA is checked in CPI from Bubblegum to Gummyroll + /// This key must sign for all write operations to the NFT Metadata stored in the Merkle slab + pub bubblegum_authority: AccountInfo<'info>, + pub gummyroll: Program<'info, Gummyroll>, + /// CHECK: Validation occurs in Gummyroll + #[account(mut)] + pub merkle_slab: AccountInfo<'info>, + pub bubblegum: Program<'info, Bubblegum> +} + +#[derive(Accounts)] +pub struct RemoveListing<'info> { + /// CHECK: should own NFT, validated downstream in Gummyroll + #[account(mut)] + pub owner: Signer<'info>, + + /// CHECK: should be the delegate for the NFT to be listed. Validated downstream in Gummyroll + pub former_delegate: AccountInfo<'info>, + + /// CHECK: any delegate desired by the owner + pub new_delegate: AccountInfo<'info>, + + /// CHECK: PDA is checked in CPI from Bubblegum to Gummyroll + /// This key must sign for all write operations to the NFT Metadata stored in the Merkle slab + pub bubblegum_authority: AccountInfo<'info>, + pub gummyroll: Program<'info, Gummyroll>, + /// CHECK: Validation occurs in Gummyroll + #[account(mut)] + pub merkle_slab: AccountInfo<'info>, + pub bubblegum: Program<'info, Bubblegum>, +} + +#[derive(Accounts)] +#[instruction( + price: u64, +)] +pub struct Purchase<'info> { + /// CHECK: should be the owner of an NFT with a current listing, validated downstream in Gummyroll + #[account(mut)] + pub former_owner: AccountInfo<'info>, + + /// CHECK: the purchaser of the NFT up for listing + #[account(mut)] + pub purchaser: Signer<'info>, + + /// CHECK: should be the delegate of the listed NFT. Must be a PDA owned by this program for operation to work. Validated downstream in Gummyroll. + #[account( + seeds = [price.to_le_bytes().as_ref()], + bump, + )] + pub listing_delegate: AccountInfo<'info>, + /// CHECK: PDA is checked in CPI from Bubblegum to Gummyroll + /// This key must sign for all write operations to the NFT Metadata stored in the Merkle slab + pub bubblegum_authority: AccountInfo<'info>, + pub gummyroll: Program<'info, Gummyroll>, + /// CHECK: Validation occurs in Gummyroll + #[account(mut)] + pub merkle_slab: AccountInfo<'info>, + pub bubblegum: Program<'info, Bubblegum>, + /// CHECK: the PDA for this marketplace with it's fee information. + #[account( + seeds = [MARKETPLACE_PROPERTIES_PREFIX.as_ref()], + bump = marketplace_props.bump, + )] + #[account(mut)] + pub marketplace_props: Account<'info, MarketplaceProperties>, + pub system_program: Program<'info, System> +} + +#[derive(Accounts)] +pub struct WithdrawFees<'info> { + /// CHECK: any pubkey the marketplace wants to withdraw fees to + #[account(mut)] + pub fee_payout_recipient: AccountInfo<'info>, + + /// CHECK: the authority over the marketplace, must correspond to the authority in marketplace_props, validated in instruction. + pub authority: Signer<'info>, + + /// CHECK: the PDA for this marketplace to withdraw fees from. + #[account( + seeds = [MARKETPLACE_PROPERTIES_PREFIX.as_ref()], + bump = marketplace_props.bump, + )] + #[account(mut)] + pub marketplace_props: Account<'info, MarketplaceProperties>, + pub system_program: Program<'info, System>, + pub sysvar_rent: Sysvar<'info, Rent> +} + +// A helper function to CPI to Bubblegum delegate. Used for listing creation, modification and removal. +#[inline(always)] +fn modify_compressed_nft_delegate<'info>( + owner: &Signer<'info>, + former_delegate: &AccountInfo<'info>, + new_delegate: &AccountInfo<'info>, + bubblegum_authority: &AccountInfo<'info>, + gummyroll: &Program<'info, Gummyroll>, + merkle_slab: &AccountInfo<'info>, + bubblegum: &Program<'info, Bubblegum>, + remaining_accounts: &[AccountInfo<'info>], + data_hash: [u8; 32], + creator_hash: [u8; 32], + nonce: u64, + index: u32, + root: [u8; 32] +) -> Result<()> { + let cpi_ctx = CpiContext::new( + bubblegum.to_account_info(), + bubblegum::cpi::accounts::Delegate { + authority: bubblegum_authority.to_account_info(), + owner: owner.to_account_info(), + previous_delegate: former_delegate.to_account_info(), + new_delegate: new_delegate.to_account_info(), + gummyroll_program: gummyroll.to_account_info(), + merkle_slab: merkle_slab.to_account_info(), + } + ) + .with_remaining_accounts(remaining_accounts.to_vec()); + bubblegum::cpi::delegate(cpi_ctx, root, data_hash, creator_hash, nonce, index)?; + Ok(()) +} + +#[program] +pub mod sugar_shack { + use super::*; + + /// Initialize the singleton PDA that will store the marketplace's admin info, mainly related to royalties. + pub fn initialize_marketplace( + ctx: Context, + royalty_share: u16, + authority: Pubkey + ) -> Result<()> { + let marketplace_props_data = &mut ctx.accounts.marketplace_props; + assert!(royalty_share <= 10000); + marketplace_props_data.share = royalty_share; + marketplace_props_data.authority = authority; + marketplace_props_data.bump = *ctx.bumps.get("marketplace_props").unwrap(); + Ok(()) + } + + /// Enables the authority of the marketplace to update admin properties + pub fn update_marketplace_properties<'info>( + ctx: Context, + authority: Option, + share: Option + ) -> Result<()> { + // This instruction must be signed by the authority to the marketplace + assert_eq!(ctx.accounts.authority.key(), ctx.accounts.marketplace_props.authority); + match authority { + Some(ay) => ctx.accounts.marketplace_props.authority = ay, + None => {} + } + match share { + Some(s) => ctx.accounts.marketplace_props.share = s, + None => {} + } + Ok(()) + } + + /// Enables the owner of a compressed NFT to list their NFT for sale, can also be used to modify the list price of an existing listing. + pub fn create_or_modify_listing<'info>( + ctx: Context<'_, '_, '_, 'info, CreateModifyListing<'info>>, + price: u64, + data_hash: [u8; 32], + creator_hash: [u8; 32], + nonce: u64, + index: u32, + root: [u8; 32] + ) -> Result<()> { + modify_compressed_nft_delegate( + &ctx.accounts.owner, + &ctx.accounts.former_delegate, + &ctx.accounts.new_delegate, + &ctx.accounts.bubblegum_authority, + &ctx.accounts.gummyroll, + &ctx.accounts.merkle_slab, + &ctx.accounts.bubblegum, + ctx.remaining_accounts, + data_hash, + creator_hash, + nonce, + index, + root + )?; + Ok(()) + } + + /// Enables the owner of a compressed NFT to remove their listing from the marketplace. The new_delegate specified in this instruction + /// should not be a PDA owned by this program for removal to be effective. + pub fn remove_listing<'info>( + ctx: Context<'_, '_, '_, 'info, RemoveListing<'info>>, + data_hash: [u8; 32], + creator_hash: [u8; 32], + nonce: u64, + index: u32, + root: [u8; 32] + ) -> Result<()> { + modify_compressed_nft_delegate( + &ctx.accounts.owner, + &ctx.accounts.former_delegate, + &ctx.accounts.new_delegate, + &ctx.accounts.bubblegum_authority, + &ctx.accounts.gummyroll, + &ctx.accounts.merkle_slab, + &ctx.accounts.bubblegum, + ctx.remaining_accounts, + data_hash, + creator_hash, + nonce, + index, + root + )?; + Ok(()) + } + + /// Enables any user to purchase an NFT listed on the marketplace. + pub fn purchase<'info>( + ctx: Context<'_, '_, '_, 'info, Purchase<'info>>, + price: u64, + data_hash: [u8; 32], + nonce: u64, + index: u32, + root: [u8; 32], + creator_shares: Vec + ) -> Result<()> { + // First, payout the marketplace's royalty fee. In this implementation, the marketplace is taking the fee off the top. + // However, the marketplace could also take its fee along side the "creators". However, if that approach is taken, just note + // that care should be taken that the sum of percentages across creators and marketplace does not exceed 100. + + // @notice: The risk here is that the NFT can be purchased for free (aside from gas cost) if the list price is less than 10,000 lamports. + let basis_point_of_price = price.safe_div(10000 as u64)?; + let amount_to_pay_marketplace = basis_point_of_price.safe_mul(ctx.accounts.marketplace_props.share as u64)?; + invoke( + &system_instruction::transfer( + &ctx.accounts.purchaser.key(), + &ctx.accounts.marketplace_props.key(), + amount_to_pay_marketplace + ), + &[ + ctx.accounts.purchaser.to_account_info(), + ctx.accounts.marketplace_props.to_account_info(), + ctx.accounts.system_program.to_account_info() + ] + )?; + + // Second, payout each "creator". Creators are an immutable set of secondary marketplace sale royalty recipients. + // Simultaneously, collect pairs to prepare to compute creator_hash + let mut total_remaining_price_allocation = price.safe_sub(amount_to_pay_marketplace)?; + let basis_point_of_remaining_price_allocation = total_remaining_price_allocation.safe_div(10000 as u64)?; + let mut amount_paid_out_to_creators = 0; + let mut creator_data: Vec> = Vec::new(); + let (creator_accounts, proof_accounts) = ctx.remaining_accounts.split_at(creator_shares.len()); + let creator_accounts_iter = &mut creator_accounts.iter(); + for share in creator_shares.into_iter() { + let current_creator_info = next_account_info(creator_accounts_iter)?; + let amount_to_pay_creator = basis_point_of_remaining_price_allocation.safe_mul((share as u64).safe_mul(100 as u64)?)?; + invoke( + &system_instruction::transfer( + &ctx.accounts.purchaser.key(), + ¤t_creator_info.key(), + amount_to_pay_creator + ), + &[ + ctx.accounts.purchaser.to_account_info(), + current_creator_info.clone(), + ctx.accounts.system_program.to_account_info() + ] + )?; + amount_paid_out_to_creators = amount_paid_out_to_creators.safe_add(amount_to_pay_creator)?; + creator_data.push([current_creator_info.key().as_ref(), &[share]].concat()); + } + total_remaining_price_allocation = total_remaining_price_allocation.safe_sub(amount_paid_out_to_creators)?; + + // Third, we payout all remaining lamports from "price" which were not paid to the marketplace/creators to the lister + invoke( + &system_instruction::transfer( + &ctx.accounts.purchaser.key(), + &ctx.accounts.former_owner.key(), + total_remaining_price_allocation + ), + &[ + ctx.accounts.purchaser.to_account_info(), + ctx.accounts.former_owner.to_account_info(), + ctx.accounts.system_program.to_account_info() + ] + )?; + + // Compute the creator hash using pairs + let creator_hash = hashv( + creator_data + .iter() + .map(|c| c.as_slice()) + .collect::>() + .as_ref(), + ); + + // CPI to Bubblegum to transfer the NFT to its new owner + let price_seed = price.to_le_bytes(); + let seeds: &[&[u8]] = &[price_seed.as_ref(), &[*ctx.bumps.get("listing_delegate").unwrap()]]; + let authority_pda_signer: &[&[&[u8]]] = &[&seeds[..]]; + + // Get the data for the CPI + let mut transfer_instruction_data = vec![163, 52, 200, 231, 140, 3, 69, 186]; + transfer_instruction_data.append( + &mut + bubblegum::instruction::Transfer { + root, + data_hash, + creator_hash: creator_hash.to_bytes(), + nonce, + index + } + .try_to_vec()? + ); + + // Get the account metas for the CPI call + // @notice: the reason why we need to manually call `to_account_metas` is because `Bubblegum::transfer` takes + // either the owner or the delegate as an optional signer. Since the delegate is a PDA in this case the + // client side code cannot set its is_signer flag to true, and Anchor drops it's is_signer flag when converting + // CpiContext to account metas on the CPI call since there is no Signer specified in the instructions context. + // @TODO: Consider TransferWithOwner and TransferWithDelegate instructions to avoid this slightly messy CPI + let transfer_accounts = bubblegum::cpi::accounts::Transfer { + authority: ctx.accounts.bubblegum_authority.to_account_info(), + owner: ctx.accounts.former_owner.to_account_info(), + delegate: ctx.accounts.listing_delegate.to_account_info(), + new_owner: ctx.accounts.purchaser.to_account_info(), + gummyroll_program: ctx.accounts.gummyroll.to_account_info(), + merkle_slab: ctx.accounts.merkle_slab.to_account_info(), + }; + let mut transfer_account_metas = transfer_accounts.to_account_metas(None); + for acct in transfer_account_metas.iter_mut() { + if acct.pubkey == ctx.accounts.listing_delegate.key() { + (*acct).is_signer = true; + } + } + for node in proof_accounts.iter() { + transfer_account_metas.push(AccountMeta::new_readonly(*node.key, false)); + } + + let mut transfer_cpi_account_infos = transfer_accounts.to_account_infos(); + transfer_cpi_account_infos.extend_from_slice(proof_accounts); + invoke_signed( + &Instruction { + program_id: ctx.accounts.bubblegum.key(), + accounts: transfer_account_metas, + data: transfer_instruction_data + }, + &(transfer_cpi_account_infos[..]), + authority_pda_signer + )?; + Ok(()) + } + + /// Enables marketplace authority to withdraw some collected fees to an external account + pub fn withdraw_fees<'info>( + ctx: Context<'_, '_, '_, 'info, WithdrawFees<'info>>, + lamports_to_withdraw: u64 + ) -> Result<()> { + // This instruction must be signed by the authority to the marketplace + assert_eq!(ctx.accounts.authority.key(), ctx.accounts.marketplace_props.authority); + let marketplace_props_balance_after_withdrawal = ctx.accounts.marketplace_props.to_account_info().lamports().safe_sub(lamports_to_withdraw)?; + + // The marketplace props account must be left with enough funds to be rent exempt after the withdrawal + assert!(Rent::get()?.is_exempt(marketplace_props_balance_after_withdrawal, MARKETPLACE_PROPERTIES_SIZE)); + + // Transfer lamports from props PDA to fee_payout_recipient + **ctx.accounts.marketplace_props.to_account_info().try_borrow_mut_lamports()? = marketplace_props_balance_after_withdrawal; + **ctx.accounts.fee_payout_recipient.try_borrow_mut_lamports()? = ctx.accounts.fee_payout_recipient.lamports().safe_add(lamports_to_withdraw)?; + Ok(()) + } +} \ No newline at end of file diff --git a/contracts/programs/sugar-shack/src/state/mod.rs b/contracts/programs/sugar-shack/src/state/mod.rs new file mode 100644 index 00000000000..3389d58df48 --- /dev/null +++ b/contracts/programs/sugar-shack/src/state/mod.rs @@ -0,0 +1,14 @@ +use anchor_lang::prelude::*; + +#[account] +#[derive(Copy)] +pub struct MarketplaceProperties { + // Address with admin authority to upgrade properties in this account + pub authority: Pubkey, + // The royalty percentage IN BASIS POINTS the marketplace will receive upon purchases through listings + pub share: u16, + pub bump: u8 +} + +// 8 bytes for discriminator + 32 byte pubkey + 1 share + 1 bump +pub const MARKETPLACE_PROPERTIES_SIZE: usize = 8 + 32 + 2 + 1; \ No newline at end of file diff --git a/contracts/sdk/bubblegum/src/generated/accounts/Nonce.ts b/contracts/sdk/bubblegum/src/generated/accounts/Nonce.ts index e80c37cd720..83132a7d1c8 100644 --- a/contracts/sdk/bubblegum/src/generated/accounts/Nonce.ts +++ b/contracts/sdk/bubblegum/src/generated/accounts/Nonce.ts @@ -17,7 +17,7 @@ export type NonceArgs = { count: beet.bignum } -export const nonceDiscriminator = [143, 197, 147, 95, 106, 165, 50, 43] +const nonceDiscriminator = [143, 197, 147, 95, 106, 165, 50, 43] /** * Holds the data for the {@link Nonce} Account and provides de/serialization * functionality for that data diff --git a/contracts/sdk/bubblegum/src/generated/accounts/Voucher.ts b/contracts/sdk/bubblegum/src/generated/accounts/Voucher.ts index 2e509c22a3f..207f7687264 100644 --- a/contracts/sdk/bubblegum/src/generated/accounts/Voucher.ts +++ b/contracts/sdk/bubblegum/src/generated/accounts/Voucher.ts @@ -21,7 +21,7 @@ export type VoucherArgs = { merkleSlab: web3.PublicKey } -export const voucherDiscriminator = [191, 204, 149, 234, 213, 165, 13, 65] +const voucherDiscriminator = [191, 204, 149, 234, 213, 165, 13, 65] /** * Holds the data for the {@link Voucher} Account and provides de/serialization * functionality for that data @@ -128,7 +128,7 @@ export class Voucher implements VoucherArgs { */ pretty() { return { - leafSchema: this.leafSchema.__kind, + leafSchema: 'LeafSchema.' + LeafSchema[this.leafSchema], index: this.index, merkleSlab: this.merkleSlab.toBase58(), } diff --git a/contracts/sdk/bubblegum/src/generated/types/LeafSchema.ts b/contracts/sdk/bubblegum/src/generated/types/LeafSchema.ts index 29ba3e09ab5..07389f65871 100644 --- a/contracts/sdk/bubblegum/src/generated/types/LeafSchema.ts +++ b/contracts/sdk/bubblegum/src/generated/types/LeafSchema.ts @@ -5,64 +5,19 @@ * See: https://github.com/metaplex-foundation/solita */ -import * as web3 from '@solana/web3.js' import * as beet from '@metaplex-foundation/beet' -import * as beetSolana from '@metaplex-foundation/beet-solana' /** - * This type is used to derive the {@link LeafSchema} type as well as the de/serializer. - * However don't refer to it in your code but use the {@link LeafSchema} type instead. - * - * @category userTypes * @category enums * @category generated - * @private */ -export type LeafSchemaRecord = { - V1: { - id: web3.PublicKey - owner: web3.PublicKey - delegate: web3.PublicKey - nonce: beet.bignum - dataHash: number[] /* size: 32 */ - creatorHash: number[] /* size: 32 */ - } +export enum LeafSchema { + V1, } -/** - * Union type respresenting the LeafSchema data enum defined in Rust. - * - * NOTE: that it includes a `__kind` property which allows to narrow types in - * switch/if statements. - * Additionally `isLeafSchema*` type guards are exposed below to narrow to a specific variant. - * - * @category userTypes - * @category enums - * @category generated - */ -export type LeafSchema = beet.DataEnumKeyAsKind - -export const isLeafSchemaV1 = ( - x: LeafSchema -): x is LeafSchema & { __kind: 'V1' } => x.__kind === 'V1' - /** * @category userTypes * @category generated */ -// @ts-ignore -export const leafSchemaBeet = beet.dataEnum([ - [ - 'V1', - new beet.BeetArgsStruct( - [ - ['id', beetSolana.publicKey], - ['owner', beetSolana.publicKey], - ['delegate', beetSolana.publicKey], - ['nonce', beet.u64], - ['dataHash', beet.uniformFixedSizeArray(beet.u8, 32)], - ['creatorHash', beet.uniformFixedSizeArray(beet.u8, 32)], - ], - 'LeafSchemaRecord["V1"]' - ), - ], -]) as beet.FixedSizeBeet +export const leafSchemaBeet = beet.fixedScalarEnum( + LeafSchema +) as beet.FixedSizeBeet diff --git a/contracts/sdk/gumball-machine/index.ts b/contracts/sdk/gumball-machine/index.ts index 8d6b9ea50d2..0b55a52fa2e 100644 --- a/contracts/sdk/gumball-machine/index.ts +++ b/contracts/sdk/gumball-machine/index.ts @@ -2,4 +2,4 @@ export * from './instructions'; export * from './accounts'; export * from './types'; export * from './utils'; -export * from './src/generated/index' +export * from './src/generated/index'; diff --git a/contracts/sdk/gumball-machine/instructions/index.ts b/contracts/sdk/gumball-machine/instructions/index.ts index c600f4965fc..6fb4e68a4fb 100644 --- a/contracts/sdk/gumball-machine/instructions/index.ts +++ b/contracts/sdk/gumball-machine/instructions/index.ts @@ -15,11 +15,13 @@ import { InitializeGumballMachineInstructionArgs, createInitializeGumballMachineInstruction, createDispenseNftSolInstruction, - createDispenseNftTokenInstruction + createDispenseNftTokenInstruction, } from "../src/generated"; +import { + getBubblegumAuthorityPDAKey +} from "../../utils/index"; import { getWillyWonkaPDAKey, - getBubblegumAuthorityPDAKey } from '../utils'; /** @@ -34,7 +36,6 @@ export async function createInitializeGumballMachineIxs( merkleRollAccountSize: number, gumballMachineInitArgs: InitializeGumballMachineInstructionArgs, mint: PublicKey, - noncePDAKey: PublicKey, gummyrollProgramId: PublicKey, bubblegumProgramId: PublicKey, gumballMachine: Program @@ -89,7 +90,6 @@ export async function createDispenseNFTForSolIx( receiver: PublicKey, gumballMachineAcctKeypair: Keypair, merkleRollKeypair: Keypair, - noncePDAKey: PublicKey, gummyrollProgramId: PublicKey, bubblegumProgramId: PublicKey, gumballMachine: Program, @@ -126,7 +126,6 @@ export async function createDispenseNFTForTokensIx( receiver: PublicKey, gumballMachineAcctKeypair: Keypair, merkleRollKeypair: Keypair, - noncePDAKey: PublicKey, gummyrollProgramId: PublicKey, bubblegumProgramId: PublicKey, gumballMachine: Program, diff --git a/contracts/sdk/gumball-machine/src/generated/types/EncodeMethod.ts b/contracts/sdk/gumball-machine/src/generated/types/EncodeMethod.ts new file mode 100644 index 00000000000..e987ce3863f --- /dev/null +++ b/contracts/sdk/gumball-machine/src/generated/types/EncodeMethod.ts @@ -0,0 +1,24 @@ +/** + * This code was GENERATED using the solita package. + * Please DO NOT EDIT THIS FILE, instead rerun solita to update it or write a wrapper to add functionality. + * + * See: https://github.com/metaplex-foundation/solita + */ + +import * as beet from '@metaplex-foundation/beet' +/** + * @category enums + * @category generated + */ +export enum EncodeMethod { + UTF8, + Base58Encode, +} + +/** + * @category userTypes + * @category generated + */ +export const encodeMethodBeet = beet.fixedScalarEnum( + EncodeMethod +) as beet.FixedSizeBeet diff --git a/contracts/sdk/gumball-machine/utils/index.ts b/contracts/sdk/gumball-machine/utils/index.ts index e0a8cb2dacd..2696af798d5 100644 --- a/contracts/sdk/gumball-machine/utils/index.ts +++ b/contracts/sdk/gumball-machine/utils/index.ts @@ -2,14 +2,6 @@ import { PublicKey } from "@solana/web3.js"; -export async function getBubblegumAuthorityPDAKey(merkleRollPubKey: PublicKey, bubblegumProgramId: PublicKey) { - const [bubblegumAuthorityPDAKey] = await PublicKey.findProgramAddress( - [merkleRollPubKey.toBuffer()], - bubblegumProgramId - ); - return bubblegumAuthorityPDAKey; - } - export async function getWillyWonkaPDAKey(gumballMachinePubkey: PublicKey, gumballMachineProgramId: PublicKey) { const [willyWonkaPDAKey] = await PublicKey.findProgramAddress( [gumballMachinePubkey.toBuffer()], diff --git a/contracts/sdk/gummyroll/index.ts b/contracts/sdk/gummyroll/index.ts index 6f9e53887ae..0528b4b5dba 100644 --- a/contracts/sdk/gummyroll/index.ts +++ b/contracts/sdk/gummyroll/index.ts @@ -2,6 +2,7 @@ import { PublicKey } from '@solana/web3.js'; export * from './instructions'; export * from './accounts'; export * from './types'; +export * from './utils'; /** * Program address diff --git a/contracts/sdk/gummyroll/utils/index.ts b/contracts/sdk/gummyroll/utils/index.ts new file mode 100644 index 00000000000..22e55d036cd --- /dev/null +++ b/contracts/sdk/gummyroll/utils/index.ts @@ -0,0 +1,20 @@ +import { + decodeMerkleRoll, + getMerkleRollAccountSize, +} from "../accounts"; +import { + PublicKey, + Keypair, + SystemProgram, + Transaction, + Connection as web3Connection, + LAMPORTS_PER_SOL, + Connection, + } from "@solana/web3.js"; + +export async function getRootOfOnChainMerkleRoot(connection: Connection, merkleRollAccountKey: PublicKey): Promise { + const merkleRootAcct = await connection.getAccountInfo(merkleRollAccountKey); + const merkleRoll = decodeMerkleRoll(merkleRootAcct.data); + return merkleRoll.roll.changeLogs[merkleRoll.roll.activeIndex].root.toBuffer(); +} + diff --git a/contracts/sdk/sugar-shack/README.md b/contracts/sdk/sugar-shack/README.md new file mode 100644 index 00000000000..3a02e6e7ec9 --- /dev/null +++ b/contracts/sdk/sugar-shack/README.md @@ -0,0 +1,14 @@ +# Bubblegum + +This SDK uses MPL's `Solita` to generate typescript SDK for `anchor` smart-contract. + +Solita is particularly helpful: +- Enums: (i.e. TokenProgramVersion) +- Complex types: (ie MetadataArgs support) +- Using typed system to identify issues with smart contract args + +### Install + +1. `yarn` +2. `node solita.js` +3. `import { ... } from '../sdk/bubblegum'` diff --git a/contracts/sdk/sugar-shack/idl/sugar_shack.json b/contracts/sdk/sugar-shack/idl/sugar_shack.json new file mode 100644 index 00000000000..4608a33d807 --- /dev/null +++ b/contracts/sdk/sugar-shack/idl/sugar_shack.json @@ -0,0 +1,398 @@ +{ + "version": "0.1.0", + "name": "sugar_shack", + "instructions": [ + { + "name": "initializeMarketplace", + "docs": [ + "Initialize the singleton PDA that will store the marketplace's admin info, mainly related to royalties." + ], + "accounts": [ + { + "name": "payer", + "isMut": true, + "isSigner": true + }, + { + "name": "marketplaceProps", + "isMut": true, + "isSigner": false + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "royaltyShare", + "type": "u16" + }, + { + "name": "authority", + "type": "publicKey" + } + ] + }, + { + "name": "updateMarketplaceProperties", + "docs": [ + "Enables the authority of the marketplace to update admin properties" + ], + "accounts": [ + { + "name": "authority", + "isMut": false, + "isSigner": true + }, + { + "name": "marketplaceProps", + "isMut": true, + "isSigner": false + } + ], + "args": [ + { + "name": "authority", + "type": { + "option": "publicKey" + } + }, + { + "name": "share", + "type": { + "option": "u16" + } + } + ] + }, + { + "name": "createOrModifyListing", + "docs": [ + "Enables the owner of a compressed NFT to list their NFT for sale, can also be used to modify the list price of an existing listing." + ], + "accounts": [ + { + "name": "owner", + "isMut": true, + "isSigner": true + }, + { + "name": "formerDelegate", + "isMut": false, + "isSigner": false + }, + { + "name": "newDelegate", + "isMut": false, + "isSigner": false + }, + { + "name": "bubblegumAuthority", + "isMut": false, + "isSigner": false, + "docs": [ + "This key must sign for all write operations to the NFT Metadata stored in the Merkle slab" + ] + }, + { + "name": "gummyroll", + "isMut": false, + "isSigner": false + }, + { + "name": "merkleSlab", + "isMut": true, + "isSigner": false + }, + { + "name": "bubblegum", + "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "price", + "type": "u64" + }, + { + "name": "dataHash", + "type": { + "array": [ + "u8", + 32 + ] + } + }, + { + "name": "creatorHash", + "type": { + "array": [ + "u8", + 32 + ] + } + }, + { + "name": "nonce", + "type": "u64" + }, + { + "name": "index", + "type": "u32" + }, + { + "name": "root", + "type": { + "array": [ + "u8", + 32 + ] + } + } + ] + }, + { + "name": "removeListing", + "docs": [ + "Enables the owner of a compressed NFT to remove their listing from the marketplace. The new_delegate specified in this instruction", + "should not be a PDA owned by this program for removal to be effective." + ], + "accounts": [ + { + "name": "owner", + "isMut": true, + "isSigner": true + }, + { + "name": "formerDelegate", + "isMut": false, + "isSigner": false + }, + { + "name": "newDelegate", + "isMut": false, + "isSigner": false + }, + { + "name": "bubblegumAuthority", + "isMut": false, + "isSigner": false, + "docs": [ + "This key must sign for all write operations to the NFT Metadata stored in the Merkle slab" + ] + }, + { + "name": "gummyroll", + "isMut": false, + "isSigner": false + }, + { + "name": "merkleSlab", + "isMut": true, + "isSigner": false + }, + { + "name": "bubblegum", + "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "dataHash", + "type": { + "array": [ + "u8", + 32 + ] + } + }, + { + "name": "creatorHash", + "type": { + "array": [ + "u8", + 32 + ] + } + }, + { + "name": "nonce", + "type": "u64" + }, + { + "name": "index", + "type": "u32" + }, + { + "name": "root", + "type": { + "array": [ + "u8", + 32 + ] + } + } + ] + }, + { + "name": "purchase", + "docs": [ + "Enables any user to purchase an NFT listed on the marketplace." + ], + "accounts": [ + { + "name": "formerOwner", + "isMut": true, + "isSigner": false + }, + { + "name": "purchaser", + "isMut": true, + "isSigner": true + }, + { + "name": "listingDelegate", + "isMut": false, + "isSigner": false + }, + { + "name": "bubblegumAuthority", + "isMut": false, + "isSigner": false, + "docs": [ + "This key must sign for all write operations to the NFT Metadata stored in the Merkle slab" + ] + }, + { + "name": "gummyroll", + "isMut": false, + "isSigner": false + }, + { + "name": "merkleSlab", + "isMut": true, + "isSigner": false + }, + { + "name": "bubblegum", + "isMut": false, + "isSigner": false + }, + { + "name": "marketplaceProps", + "isMut": true, + "isSigner": false + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "price", + "type": "u64" + }, + { + "name": "dataHash", + "type": { + "array": [ + "u8", + 32 + ] + } + }, + { + "name": "nonce", + "type": "u64" + }, + { + "name": "index", + "type": "u32" + }, + { + "name": "root", + "type": { + "array": [ + "u8", + 32 + ] + } + }, + { + "name": "creatorShares", + "type": "bytes" + } + ] + }, + { + "name": "withdrawFees", + "docs": [ + "Enables marketplace authority to withdraw some collected fees to an external account" + ], + "accounts": [ + { + "name": "feePayoutRecipient", + "isMut": true, + "isSigner": false + }, + { + "name": "authority", + "isMut": false, + "isSigner": true + }, + { + "name": "marketplaceProps", + "isMut": true, + "isSigner": false + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false + }, + { + "name": "sysvarRent", + "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "lamportsToWithdraw", + "type": "u64" + } + ] + } + ], + "accounts": [ + { + "name": "MarketplaceProperties", + "type": { + "kind": "struct", + "fields": [ + { + "name": "authority", + "type": "publicKey" + }, + { + "name": "share", + "type": "u16" + }, + { + "name": "bump", + "type": "u8" + } + ] + } + } + ], + "metadata": { + "address": "9T5Xv2cJRydUBqvdK7rLGuNGqhkA8sU8Yq1rGN7hExNK" + } +} \ No newline at end of file diff --git a/contracts/sdk/sugar-shack/index.ts b/contracts/sdk/sugar-shack/index.ts new file mode 100644 index 00000000000..ccc96af2b5b --- /dev/null +++ b/contracts/sdk/sugar-shack/index.ts @@ -0,0 +1,2 @@ +export * from './utils'; +export * from './src/generated/index'; \ No newline at end of file diff --git a/contracts/sdk/sugar-shack/package.json b/contracts/sdk/sugar-shack/package.json new file mode 100644 index 00000000000..a8f7b860600 --- /dev/null +++ b/contracts/sdk/sugar-shack/package.json @@ -0,0 +1,11 @@ +{ + "name": "sugar-shack", + "version": "1.0.0", + "description": "SDK for Sugar Shack contract", + "main": "index.js", + "license": "MIT", + "dependencies": { + "@metaplex-foundation/rustbin": "^0.3.1", + "@metaplex-foundation/solita": "^0.8.2" + } +} diff --git a/contracts/sdk/sugar-shack/solita.js b/contracts/sdk/sugar-shack/solita.js new file mode 100644 index 00000000000..2b720bab189 --- /dev/null +++ b/contracts/sdk/sugar-shack/solita.js @@ -0,0 +1,64 @@ +const path = require('path'); +const { + rustbinMatch, + confirmAutoMessageConsole, +} = require('@metaplex-foundation/rustbin') +const { spawn } = require('child_process'); +const { Solita } = require('@metaplex-foundation/solita'); +const { writeFile } = require('fs/promises'); +const { fstat, existsSync, realpath, realpathSync } = require('fs'); + +const PROGRAM_NAME = 'sugar-shack'; +const PROGRAM_ID = '9T5Xv2cJRydUBqvdK7rLGuNGqhkA8sU8Yq1rGN7hExNK'; + +const programDir = path.join(__dirname, '..', '..', 'programs', 'sugar-shack'); +const generatedIdlDir = path.join(__dirname, 'idl'); +const generatedSDKDir = path.join(__dirname, 'src', 'generated'); + +async function main() { + const anchorExecutable = realpathSync("../../../deps/anchor/target/debug/anchor"); + if (!existsSync(anchorExecutable)) { + console.log(`Could not find: ${anchorExecutable}`); + throw new Error("Please `cd candyland/deps/anchor/anchor-cli` && cargo build`") + } + const anchor = spawn(anchorExecutable, ['build', '--idl', generatedIdlDir], { cwd: programDir }) + .on('error', (err) => { + console.error(err); + // @ts-ignore this err does have a code + if (err.code === 'ENOENT') { + console.error( + 'Ensure that `anchor` is installed and in your path, see:\n https://project-serum.github.io/anchor/getting-started/installation.html#install-anchor\n', + ); + } + process.exit(1); + }) + .on('exit', () => { + console.log('IDL written to: %s', path.join(generatedIdlDir, `${PROGRAM_NAME.replace("-", '_')}.json`)); + generateTypeScriptSDK(); + }); + + anchor.stdout.on('data', (buf) => console.log(buf.toString('utf8'))); + anchor.stderr.on('data', (buf) => console.error(buf.toString('utf8'))); +} + +async function generateTypeScriptSDK() { + console.error('Generating TypeScript SDK to %s', generatedSDKDir); + const generatedIdlPath = path.join(generatedIdlDir, `${PROGRAM_NAME.replace("-", "_")}.json`); + + const idl = require(generatedIdlPath); + if (idl.metadata?.address == null) { + idl.metadata = { ...idl.metadata, address: PROGRAM_ID }; + await writeFile(generatedIdlPath, JSON.stringify(idl, null, 2)); + } + const gen = new Solita(idl, { formatCode: true }); + await gen.renderAndWriteTo(generatedSDKDir); + + console.error('Success!'); + + process.exit(0); +} + +main().catch((err) => { + console.error(err) + process.exit(1) +}) diff --git a/contracts/sdk/sugar-shack/src/generated/accounts/MarketplaceProperties.ts b/contracts/sdk/sugar-shack/src/generated/accounts/MarketplaceProperties.ts new file mode 100644 index 00000000000..9da18b8056c --- /dev/null +++ b/contracts/sdk/sugar-shack/src/generated/accounts/MarketplaceProperties.ts @@ -0,0 +1,157 @@ +/** + * This code was GENERATED using the solita package. + * Please DO NOT EDIT THIS FILE, instead rerun solita to update it or write a wrapper to add functionality. + * + * See: https://github.com/metaplex-foundation/solita + */ + +import * as web3 from '@solana/web3.js' +import * as beetSolana from '@metaplex-foundation/beet-solana' +import * as beet from '@metaplex-foundation/beet' + +/** + * Arguments used to create {@link MarketplaceProperties} + * @category Accounts + * @category generated + */ +export type MarketplacePropertiesArgs = { + authority: web3.PublicKey + share: number + bump: number +} + +const marketplacePropertiesDiscriminator = [31, 68, 0, 130, 46, 137, 61, 24] +/** + * Holds the data for the {@link MarketplaceProperties} Account and provides de/serialization + * functionality for that data + * + * @category Accounts + * @category generated + */ +export class MarketplaceProperties implements MarketplacePropertiesArgs { + private constructor( + readonly authority: web3.PublicKey, + readonly share: number, + readonly bump: number + ) {} + + /** + * Creates a {@link MarketplaceProperties} instance from the provided args. + */ + static fromArgs(args: MarketplacePropertiesArgs) { + return new MarketplaceProperties(args.authority, args.share, args.bump) + } + + /** + * Deserializes the {@link MarketplaceProperties} from the data of the provided {@link web3.AccountInfo}. + * @returns a tuple of the account data and the offset up to which the buffer was read to obtain it. + */ + static fromAccountInfo( + accountInfo: web3.AccountInfo, + offset = 0 + ): [MarketplaceProperties, number] { + return MarketplaceProperties.deserialize(accountInfo.data, offset) + } + + /** + * Retrieves the account info from the provided address and deserializes + * the {@link MarketplaceProperties} from its data. + * + * @throws Error if no account info is found at the address or if deserialization fails + */ + static async fromAccountAddress( + connection: web3.Connection, + address: web3.PublicKey + ): Promise { + const accountInfo = await connection.getAccountInfo(address) + if (accountInfo == null) { + throw new Error( + `Unable to find MarketplaceProperties account at ${address}` + ) + } + return MarketplaceProperties.fromAccountInfo(accountInfo, 0)[0] + } + + /** + * Deserializes the {@link MarketplaceProperties} from the provided data Buffer. + * @returns a tuple of the account data and the offset up to which the buffer was read to obtain it. + */ + static deserialize(buf: Buffer, offset = 0): [MarketplaceProperties, number] { + return marketplacePropertiesBeet.deserialize(buf, offset) + } + + /** + * Serializes the {@link MarketplaceProperties} into a Buffer. + * @returns a tuple of the created Buffer and the offset up to which the buffer was written to store it. + */ + serialize(): [Buffer, number] { + return marketplacePropertiesBeet.serialize({ + accountDiscriminator: marketplacePropertiesDiscriminator, + ...this, + }) + } + + /** + * Returns the byteSize of a {@link Buffer} holding the serialized data of + * {@link MarketplaceProperties} + */ + static get byteSize() { + return marketplacePropertiesBeet.byteSize + } + + /** + * Fetches the minimum balance needed to exempt an account holding + * {@link MarketplaceProperties} data from rent + * + * @param connection used to retrieve the rent exemption information + */ + static async getMinimumBalanceForRentExemption( + connection: web3.Connection, + commitment?: web3.Commitment + ): Promise { + return connection.getMinimumBalanceForRentExemption( + MarketplaceProperties.byteSize, + commitment + ) + } + + /** + * Determines if the provided {@link Buffer} has the correct byte size to + * hold {@link MarketplaceProperties} data. + */ + static hasCorrectByteSize(buf: Buffer, offset = 0) { + return buf.byteLength - offset === MarketplaceProperties.byteSize + } + + /** + * Returns a readable version of {@link MarketplaceProperties} properties + * and can be used to convert to JSON and/or logging + */ + pretty() { + return { + authority: this.authority.toBase58(), + share: this.share, + bump: this.bump, + } + } +} + +/** + * @category Accounts + * @category generated + */ +export const marketplacePropertiesBeet = new beet.BeetStruct< + MarketplaceProperties, + MarketplacePropertiesArgs & { + accountDiscriminator: number[] /* size: 8 */ + } +>( + [ + ['accountDiscriminator', beet.uniformFixedSizeArray(beet.u8, 8)], + ['authority', beetSolana.publicKey], + ['share', beet.u16], + ['bump', beet.u8], + ], + MarketplaceProperties.fromArgs, + 'MarketplaceProperties' +) diff --git a/contracts/sdk/sugar-shack/src/generated/accounts/index.ts b/contracts/sdk/sugar-shack/src/generated/accounts/index.ts new file mode 100644 index 00000000000..1c75e8cbc05 --- /dev/null +++ b/contracts/sdk/sugar-shack/src/generated/accounts/index.ts @@ -0,0 +1 @@ +export * from './MarketplaceProperties' diff --git a/contracts/sdk/sugar-shack/src/generated/index.ts b/contracts/sdk/sugar-shack/src/generated/index.ts new file mode 100644 index 00000000000..96671b8f5f0 --- /dev/null +++ b/contracts/sdk/sugar-shack/src/generated/index.ts @@ -0,0 +1,19 @@ +import { PublicKey } from '@solana/web3.js' +export * from './accounts' +export * from './instructions' + +/** + * Program address + * + * @category constants + * @category generated + */ +export const PROGRAM_ADDRESS = '9T5Xv2cJRydUBqvdK7rLGuNGqhkA8sU8Yq1rGN7hExNK' + +/** + * Program public key + * + * @category constants + * @category generated + */ +export const PROGRAM_ID = new PublicKey(PROGRAM_ADDRESS) diff --git a/contracts/sdk/sugar-shack/src/generated/instructions/createOrModifyListing.ts b/contracts/sdk/sugar-shack/src/generated/instructions/createOrModifyListing.ts new file mode 100644 index 00000000000..5600e5003a8 --- /dev/null +++ b/contracts/sdk/sugar-shack/src/generated/instructions/createOrModifyListing.ts @@ -0,0 +1,147 @@ +/** + * This code was GENERATED using the solita package. + * Please DO NOT EDIT THIS FILE, instead rerun solita to update it or write a wrapper to add functionality. + * + * See: https://github.com/metaplex-foundation/solita + */ + +import * as beet from '@metaplex-foundation/beet' +import * as web3 from '@solana/web3.js' + +/** + * @category Instructions + * @category CreateOrModifyListing + * @category generated + */ +export type CreateOrModifyListingInstructionArgs = { + price: beet.bignum + dataHash: number[] /* size: 32 */ + creatorHash: number[] /* size: 32 */ + nonce: beet.bignum + index: number + root: number[] /* size: 32 */ +} +/** + * @category Instructions + * @category CreateOrModifyListing + * @category generated + */ +export const createOrModifyListingStruct = new beet.BeetArgsStruct< + CreateOrModifyListingInstructionArgs & { + instructionDiscriminator: number[] /* size: 8 */ + } +>( + [ + ['instructionDiscriminator', beet.uniformFixedSizeArray(beet.u8, 8)], + ['price', beet.u64], + ['dataHash', beet.uniformFixedSizeArray(beet.u8, 32)], + ['creatorHash', beet.uniformFixedSizeArray(beet.u8, 32)], + ['nonce', beet.u64], + ['index', beet.u32], + ['root', beet.uniformFixedSizeArray(beet.u8, 32)], + ], + 'CreateOrModifyListingInstructionArgs' +) +/** + * Accounts required by the _createOrModifyListing_ instruction + * + * @property [_writable_, **signer**] owner + * @property [] formerDelegate + * @property [] newDelegate + * @property [] bubblegumAuthority + * @property [] gummyroll + * @property [_writable_] merkleSlab + * @property [] bubblegum + * @category Instructions + * @category CreateOrModifyListing + * @category generated + */ +export type CreateOrModifyListingInstructionAccounts = { + owner: web3.PublicKey + formerDelegate: web3.PublicKey + newDelegate: web3.PublicKey + bubblegumAuthority: web3.PublicKey + gummyroll: web3.PublicKey + merkleSlab: web3.PublicKey + bubblegum: web3.PublicKey +} + +export const createOrModifyListingInstructionDiscriminator = [ + 36, 247, 75, 240, 177, 100, 167, 57, +] + +/** + * Creates a _CreateOrModifyListing_ instruction. + * + * @param accounts that will be accessed while the instruction is processed + * @param args to provide as instruction data to the program + * + * @category Instructions + * @category CreateOrModifyListing + * @category generated + */ +export function createCreateOrModifyListingInstruction( + accounts: CreateOrModifyListingInstructionAccounts, + args: CreateOrModifyListingInstructionArgs +) { + const { + owner, + formerDelegate, + newDelegate, + bubblegumAuthority, + gummyroll, + merkleSlab, + bubblegum, + } = accounts + + const [data] = createOrModifyListingStruct.serialize({ + instructionDiscriminator: createOrModifyListingInstructionDiscriminator, + ...args, + }) + const keys: web3.AccountMeta[] = [ + { + pubkey: owner, + isWritable: true, + isSigner: true, + }, + { + pubkey: formerDelegate, + isWritable: false, + isSigner: false, + }, + { + pubkey: newDelegate, + isWritable: false, + isSigner: false, + }, + { + pubkey: bubblegumAuthority, + isWritable: false, + isSigner: false, + }, + { + pubkey: gummyroll, + isWritable: false, + isSigner: false, + }, + { + pubkey: merkleSlab, + isWritable: true, + isSigner: false, + }, + { + pubkey: bubblegum, + isWritable: false, + isSigner: false, + }, + ] + + const ix = new web3.TransactionInstruction({ + programId: new web3.PublicKey( + '9T5Xv2cJRydUBqvdK7rLGuNGqhkA8sU8Yq1rGN7hExNK' + ), + keys, + data, + }) + return ix +} diff --git a/contracts/sdk/sugar-shack/src/generated/instructions/index.ts b/contracts/sdk/sugar-shack/src/generated/instructions/index.ts new file mode 100644 index 00000000000..6e2deed17c0 --- /dev/null +++ b/contracts/sdk/sugar-shack/src/generated/instructions/index.ts @@ -0,0 +1,6 @@ +export * from './createOrModifyListing' +export * from './initializeMarketplace' +export * from './purchase' +export * from './removeListing' +export * from './updateMarketplaceProperties' +export * from './withdrawFees' diff --git a/contracts/sdk/sugar-shack/src/generated/instructions/initializeMarketplace.ts b/contracts/sdk/sugar-shack/src/generated/instructions/initializeMarketplace.ts new file mode 100644 index 00000000000..973d5f571b0 --- /dev/null +++ b/contracts/sdk/sugar-shack/src/generated/instructions/initializeMarketplace.ts @@ -0,0 +1,102 @@ +/** + * This code was GENERATED using the solita package. + * Please DO NOT EDIT THIS FILE, instead rerun solita to update it or write a wrapper to add functionality. + * + * See: https://github.com/metaplex-foundation/solita + */ + +import * as web3 from '@solana/web3.js' +import * as beet from '@metaplex-foundation/beet' +import * as beetSolana from '@metaplex-foundation/beet-solana' + +/** + * @category Instructions + * @category InitializeMarketplace + * @category generated + */ +export type InitializeMarketplaceInstructionArgs = { + royaltyShare: number + authority: web3.PublicKey +} +/** + * @category Instructions + * @category InitializeMarketplace + * @category generated + */ +export const initializeMarketplaceStruct = new beet.BeetArgsStruct< + InitializeMarketplaceInstructionArgs & { + instructionDiscriminator: number[] /* size: 8 */ + } +>( + [ + ['instructionDiscriminator', beet.uniformFixedSizeArray(beet.u8, 8)], + ['royaltyShare', beet.u16], + ['authority', beetSolana.publicKey], + ], + 'InitializeMarketplaceInstructionArgs' +) +/** + * Accounts required by the _initializeMarketplace_ instruction + * + * @property [_writable_, **signer**] payer + * @property [_writable_] marketplaceProps + * @category Instructions + * @category InitializeMarketplace + * @category generated + */ +export type InitializeMarketplaceInstructionAccounts = { + payer: web3.PublicKey + marketplaceProps: web3.PublicKey +} + +export const initializeMarketplaceInstructionDiscriminator = [ + 47, 81, 64, 0, 96, 56, 105, 7, +] + +/** + * Creates a _InitializeMarketplace_ instruction. + * + * @param accounts that will be accessed while the instruction is processed + * @param args to provide as instruction data to the program + * + * @category Instructions + * @category InitializeMarketplace + * @category generated + */ +export function createInitializeMarketplaceInstruction( + accounts: InitializeMarketplaceInstructionAccounts, + args: InitializeMarketplaceInstructionArgs +) { + const { payer, marketplaceProps } = accounts + + const [data] = initializeMarketplaceStruct.serialize({ + instructionDiscriminator: initializeMarketplaceInstructionDiscriminator, + ...args, + }) + const keys: web3.AccountMeta[] = [ + { + pubkey: payer, + isWritable: true, + isSigner: true, + }, + { + pubkey: marketplaceProps, + isWritable: true, + isSigner: false, + }, + { + pubkey: web3.SystemProgram.programId, + isWritable: false, + isSigner: false, + }, + ] + + const ix = new web3.TransactionInstruction({ + programId: new web3.PublicKey( + '9T5Xv2cJRydUBqvdK7rLGuNGqhkA8sU8Yq1rGN7hExNK' + ), + keys, + data, + }) + return ix +} diff --git a/contracts/sdk/sugar-shack/src/generated/instructions/purchase.ts b/contracts/sdk/sugar-shack/src/generated/instructions/purchase.ts new file mode 100644 index 00000000000..66dc859a919 --- /dev/null +++ b/contracts/sdk/sugar-shack/src/generated/instructions/purchase.ts @@ -0,0 +1,160 @@ +/** + * This code was GENERATED using the solita package. + * Please DO NOT EDIT THIS FILE, instead rerun solita to update it or write a wrapper to add functionality. + * + * See: https://github.com/metaplex-foundation/solita + */ + +import * as beet from '@metaplex-foundation/beet' +import * as web3 from '@solana/web3.js' + +/** + * @category Instructions + * @category Purchase + * @category generated + */ +export type PurchaseInstructionArgs = { + price: beet.bignum + dataHash: number[] /* size: 32 */ + nonce: beet.bignum + index: number + root: number[] /* size: 32 */ + creatorShares: Uint8Array +} +/** + * @category Instructions + * @category Purchase + * @category generated + */ +export const purchaseStruct = new beet.FixableBeetArgsStruct< + PurchaseInstructionArgs & { + instructionDiscriminator: number[] /* size: 8 */ + } +>( + [ + ['instructionDiscriminator', beet.uniformFixedSizeArray(beet.u8, 8)], + ['price', beet.u64], + ['dataHash', beet.uniformFixedSizeArray(beet.u8, 32)], + ['nonce', beet.u64], + ['index', beet.u32], + ['root', beet.uniformFixedSizeArray(beet.u8, 32)], + ['creatorShares', beet.bytes], + ], + 'PurchaseInstructionArgs' +) +/** + * Accounts required by the _purchase_ instruction + * + * @property [_writable_] formerOwner + * @property [_writable_, **signer**] purchaser + * @property [] listingDelegate + * @property [] bubblegumAuthority + * @property [] gummyroll + * @property [_writable_] merkleSlab + * @property [] bubblegum + * @property [_writable_] marketplaceProps + * @category Instructions + * @category Purchase + * @category generated + */ +export type PurchaseInstructionAccounts = { + formerOwner: web3.PublicKey + purchaser: web3.PublicKey + listingDelegate: web3.PublicKey + bubblegumAuthority: web3.PublicKey + gummyroll: web3.PublicKey + merkleSlab: web3.PublicKey + bubblegum: web3.PublicKey + marketplaceProps: web3.PublicKey +} + +export const purchaseInstructionDiscriminator = [ + 21, 93, 113, 154, 193, 160, 242, 168, +] + +/** + * Creates a _Purchase_ instruction. + * + * @param accounts that will be accessed while the instruction is processed + * @param args to provide as instruction data to the program + * + * @category Instructions + * @category Purchase + * @category generated + */ +export function createPurchaseInstruction( + accounts: PurchaseInstructionAccounts, + args: PurchaseInstructionArgs +) { + const { + formerOwner, + purchaser, + listingDelegate, + bubblegumAuthority, + gummyroll, + merkleSlab, + bubblegum, + marketplaceProps, + } = accounts + + const [data] = purchaseStruct.serialize({ + instructionDiscriminator: purchaseInstructionDiscriminator, + ...args, + }) + const keys: web3.AccountMeta[] = [ + { + pubkey: formerOwner, + isWritable: true, + isSigner: false, + }, + { + pubkey: purchaser, + isWritable: true, + isSigner: true, + }, + { + pubkey: listingDelegate, + isWritable: false, + isSigner: false, + }, + { + pubkey: bubblegumAuthority, + isWritable: false, + isSigner: false, + }, + { + pubkey: gummyroll, + isWritable: false, + isSigner: false, + }, + { + pubkey: merkleSlab, + isWritable: true, + isSigner: false, + }, + { + pubkey: bubblegum, + isWritable: false, + isSigner: false, + }, + { + pubkey: marketplaceProps, + isWritable: true, + isSigner: false, + }, + { + pubkey: web3.SystemProgram.programId, + isWritable: false, + isSigner: false, + }, + ] + + const ix = new web3.TransactionInstruction({ + programId: new web3.PublicKey( + '9T5Xv2cJRydUBqvdK7rLGuNGqhkA8sU8Yq1rGN7hExNK' + ), + keys, + data, + }) + return ix +} diff --git a/contracts/sdk/sugar-shack/src/generated/instructions/removeListing.ts b/contracts/sdk/sugar-shack/src/generated/instructions/removeListing.ts new file mode 100644 index 00000000000..3519455797d --- /dev/null +++ b/contracts/sdk/sugar-shack/src/generated/instructions/removeListing.ts @@ -0,0 +1,145 @@ +/** + * This code was GENERATED using the solita package. + * Please DO NOT EDIT THIS FILE, instead rerun solita to update it or write a wrapper to add functionality. + * + * See: https://github.com/metaplex-foundation/solita + */ + +import * as beet from '@metaplex-foundation/beet' +import * as web3 from '@solana/web3.js' + +/** + * @category Instructions + * @category RemoveListing + * @category generated + */ +export type RemoveListingInstructionArgs = { + dataHash: number[] /* size: 32 */ + creatorHash: number[] /* size: 32 */ + nonce: beet.bignum + index: number + root: number[] /* size: 32 */ +} +/** + * @category Instructions + * @category RemoveListing + * @category generated + */ +export const removeListingStruct = new beet.BeetArgsStruct< + RemoveListingInstructionArgs & { + instructionDiscriminator: number[] /* size: 8 */ + } +>( + [ + ['instructionDiscriminator', beet.uniformFixedSizeArray(beet.u8, 8)], + ['dataHash', beet.uniformFixedSizeArray(beet.u8, 32)], + ['creatorHash', beet.uniformFixedSizeArray(beet.u8, 32)], + ['nonce', beet.u64], + ['index', beet.u32], + ['root', beet.uniformFixedSizeArray(beet.u8, 32)], + ], + 'RemoveListingInstructionArgs' +) +/** + * Accounts required by the _removeListing_ instruction + * + * @property [_writable_, **signer**] owner + * @property [] formerDelegate + * @property [] newDelegate + * @property [] bubblegumAuthority + * @property [] gummyroll + * @property [_writable_] merkleSlab + * @property [] bubblegum + * @category Instructions + * @category RemoveListing + * @category generated + */ +export type RemoveListingInstructionAccounts = { + owner: web3.PublicKey + formerDelegate: web3.PublicKey + newDelegate: web3.PublicKey + bubblegumAuthority: web3.PublicKey + gummyroll: web3.PublicKey + merkleSlab: web3.PublicKey + bubblegum: web3.PublicKey +} + +export const removeListingInstructionDiscriminator = [ + 74, 5, 236, 7, 2, 104, 139, 114, +] + +/** + * Creates a _RemoveListing_ instruction. + * + * @param accounts that will be accessed while the instruction is processed + * @param args to provide as instruction data to the program + * + * @category Instructions + * @category RemoveListing + * @category generated + */ +export function createRemoveListingInstruction( + accounts: RemoveListingInstructionAccounts, + args: RemoveListingInstructionArgs +) { + const { + owner, + formerDelegate, + newDelegate, + bubblegumAuthority, + gummyroll, + merkleSlab, + bubblegum, + } = accounts + + const [data] = removeListingStruct.serialize({ + instructionDiscriminator: removeListingInstructionDiscriminator, + ...args, + }) + const keys: web3.AccountMeta[] = [ + { + pubkey: owner, + isWritable: true, + isSigner: true, + }, + { + pubkey: formerDelegate, + isWritable: false, + isSigner: false, + }, + { + pubkey: newDelegate, + isWritable: false, + isSigner: false, + }, + { + pubkey: bubblegumAuthority, + isWritable: false, + isSigner: false, + }, + { + pubkey: gummyroll, + isWritable: false, + isSigner: false, + }, + { + pubkey: merkleSlab, + isWritable: true, + isSigner: false, + }, + { + pubkey: bubblegum, + isWritable: false, + isSigner: false, + }, + ] + + const ix = new web3.TransactionInstruction({ + programId: new web3.PublicKey( + '9T5Xv2cJRydUBqvdK7rLGuNGqhkA8sU8Yq1rGN7hExNK' + ), + keys, + data, + }) + return ix +} diff --git a/contracts/sdk/sugar-shack/src/generated/instructions/updateMarketplaceProperties.ts b/contracts/sdk/sugar-shack/src/generated/instructions/updateMarketplaceProperties.ts new file mode 100644 index 00000000000..55f00382e25 --- /dev/null +++ b/contracts/sdk/sugar-shack/src/generated/instructions/updateMarketplaceProperties.ts @@ -0,0 +1,98 @@ +/** + * This code was GENERATED using the solita package. + * Please DO NOT EDIT THIS FILE, instead rerun solita to update it or write a wrapper to add functionality. + * + * See: https://github.com/metaplex-foundation/solita + */ + +import * as web3 from '@solana/web3.js' +import * as beet from '@metaplex-foundation/beet' +import * as beetSolana from '@metaplex-foundation/beet-solana' + +/** + * @category Instructions + * @category UpdateMarketplaceProperties + * @category generated + */ +export type UpdateMarketplacePropertiesInstructionArgs = { + authority: beet.COption + share: beet.COption +} +/** + * @category Instructions + * @category UpdateMarketplaceProperties + * @category generated + */ +export const updateMarketplacePropertiesStruct = new beet.FixableBeetArgsStruct< + UpdateMarketplacePropertiesInstructionArgs & { + instructionDiscriminator: number[] /* size: 8 */ + } +>( + [ + ['instructionDiscriminator', beet.uniformFixedSizeArray(beet.u8, 8)], + ['authority', beet.coption(beetSolana.publicKey)], + ['share', beet.coption(beet.u16)], + ], + 'UpdateMarketplacePropertiesInstructionArgs' +) +/** + * Accounts required by the _updateMarketplaceProperties_ instruction + * + * @property [**signer**] authority + * @property [_writable_] marketplaceProps + * @category Instructions + * @category UpdateMarketplaceProperties + * @category generated + */ +export type UpdateMarketplacePropertiesInstructionAccounts = { + authority: web3.PublicKey + marketplaceProps: web3.PublicKey +} + +export const updateMarketplacePropertiesInstructionDiscriminator = [ + 56, 7, 227, 235, 153, 143, 29, 213, +] + +/** + * Creates a _UpdateMarketplaceProperties_ instruction. + * + * @param accounts that will be accessed while the instruction is processed + * @param args to provide as instruction data to the program + * + * @category Instructions + * @category UpdateMarketplaceProperties + * @category generated + */ +export function createUpdateMarketplacePropertiesInstruction( + accounts: UpdateMarketplacePropertiesInstructionAccounts, + args: UpdateMarketplacePropertiesInstructionArgs +) { + const { authority, marketplaceProps } = accounts + + const [data] = updateMarketplacePropertiesStruct.serialize({ + instructionDiscriminator: + updateMarketplacePropertiesInstructionDiscriminator, + ...args, + }) + const keys: web3.AccountMeta[] = [ + { + pubkey: authority, + isWritable: false, + isSigner: true, + }, + { + pubkey: marketplaceProps, + isWritable: true, + isSigner: false, + }, + ] + + const ix = new web3.TransactionInstruction({ + programId: new web3.PublicKey( + '9T5Xv2cJRydUBqvdK7rLGuNGqhkA8sU8Yq1rGN7hExNK' + ), + keys, + data, + }) + return ix +} diff --git a/contracts/sdk/sugar-shack/src/generated/instructions/withdrawFees.ts b/contracts/sdk/sugar-shack/src/generated/instructions/withdrawFees.ts new file mode 100644 index 00000000000..3853156577e --- /dev/null +++ b/contracts/sdk/sugar-shack/src/generated/instructions/withdrawFees.ts @@ -0,0 +1,114 @@ +/** + * This code was GENERATED using the solita package. + * Please DO NOT EDIT THIS FILE, instead rerun solita to update it or write a wrapper to add functionality. + * + * See: https://github.com/metaplex-foundation/solita + */ + +import * as beet from '@metaplex-foundation/beet' +import * as web3 from '@solana/web3.js' + +/** + * @category Instructions + * @category WithdrawFees + * @category generated + */ +export type WithdrawFeesInstructionArgs = { + lamportsToWithdraw: beet.bignum +} +/** + * @category Instructions + * @category WithdrawFees + * @category generated + */ +export const withdrawFeesStruct = new beet.BeetArgsStruct< + WithdrawFeesInstructionArgs & { + instructionDiscriminator: number[] /* size: 8 */ + } +>( + [ + ['instructionDiscriminator', beet.uniformFixedSizeArray(beet.u8, 8)], + ['lamportsToWithdraw', beet.u64], + ], + 'WithdrawFeesInstructionArgs' +) +/** + * Accounts required by the _withdrawFees_ instruction + * + * @property [_writable_] feePayoutRecipient + * @property [**signer**] authority + * @property [_writable_] marketplaceProps + * @property [] sysvarRent + * @category Instructions + * @category WithdrawFees + * @category generated + */ +export type WithdrawFeesInstructionAccounts = { + feePayoutRecipient: web3.PublicKey + authority: web3.PublicKey + marketplaceProps: web3.PublicKey + sysvarRent: web3.PublicKey +} + +export const withdrawFeesInstructionDiscriminator = [ + 198, 212, 171, 109, 144, 215, 174, 89, +] + +/** + * Creates a _WithdrawFees_ instruction. + * + * @param accounts that will be accessed while the instruction is processed + * @param args to provide as instruction data to the program + * + * @category Instructions + * @category WithdrawFees + * @category generated + */ +export function createWithdrawFeesInstruction( + accounts: WithdrawFeesInstructionAccounts, + args: WithdrawFeesInstructionArgs +) { + const { feePayoutRecipient, authority, marketplaceProps, sysvarRent } = + accounts + + const [data] = withdrawFeesStruct.serialize({ + instructionDiscriminator: withdrawFeesInstructionDiscriminator, + ...args, + }) + const keys: web3.AccountMeta[] = [ + { + pubkey: feePayoutRecipient, + isWritable: true, + isSigner: false, + }, + { + pubkey: authority, + isWritable: false, + isSigner: true, + }, + { + pubkey: marketplaceProps, + isWritable: true, + isSigner: false, + }, + { + pubkey: web3.SystemProgram.programId, + isWritable: false, + isSigner: false, + }, + { + pubkey: sysvarRent, + isWritable: false, + isSigner: false, + }, + ] + + const ix = new web3.TransactionInstruction({ + programId: new web3.PublicKey( + '9T5Xv2cJRydUBqvdK7rLGuNGqhkA8sU8Yq1rGN7hExNK' + ), + keys, + data, + }) + return ix +} diff --git a/contracts/sdk/sugar-shack/src/generated/types/RoyaltyRecipient.ts b/contracts/sdk/sugar-shack/src/generated/types/RoyaltyRecipient.ts new file mode 100644 index 00000000000..d05a38e9ee4 --- /dev/null +++ b/contracts/sdk/sugar-shack/src/generated/types/RoyaltyRecipient.ts @@ -0,0 +1,28 @@ +/** + * This code was GENERATED using the solita package. + * Please DO NOT EDIT THIS FILE, instead rerun solita to update it or write a wrapper to add functionality. + * + * See: https://github.com/metaplex-foundation/solita + */ + +import * as web3 from '@solana/web3.js' +import * as beetSolana from '@metaplex-foundation/beet-solana' +import * as beet from '@metaplex-foundation/beet' +export type RoyaltyRecipient = { + address: web3.PublicKey + verified: boolean + share: number +} + +/** + * @category userTypes + * @category generated + */ +export const royaltyRecipientBeet = new beet.BeetArgsStruct( + [ + ['address', beetSolana.publicKey], + ['verified', beet.bool], + ['share', beet.u8], + ], + 'RoyaltyRecipient' +) diff --git a/contracts/sdk/sugar-shack/src/generated/types/index.ts b/contracts/sdk/sugar-shack/src/generated/types/index.ts new file mode 100644 index 00000000000..7c0651f2272 --- /dev/null +++ b/contracts/sdk/sugar-shack/src/generated/types/index.ts @@ -0,0 +1 @@ +export * from './RoyaltyRecipient' diff --git a/contracts/sdk/sugar-shack/utils/index.ts b/contracts/sdk/sugar-shack/utils/index.ts new file mode 100644 index 00000000000..c3342dc4319 --- /dev/null +++ b/contracts/sdk/sugar-shack/utils/index.ts @@ -0,0 +1,12 @@ +import { + PublicKey, +} from "@solana/web3.js"; +import { BN } from "@project-serum/anchor"; + +export async function getListingPDAKeyForPrice(price: BN, SugarShackProgramid: PublicKey): Promise { + const [key] = await PublicKey.findProgramAddress( + [price.toArrayLike(Buffer,"le",8)], + SugarShackProgramid + ); + return key; +} \ No newline at end of file diff --git a/contracts/sdk/utils/index.ts b/contracts/sdk/utils/index.ts index 649b2503aaa..cb37f86f0ae 100644 --- a/contracts/sdk/utils/index.ts +++ b/contracts/sdk/utils/index.ts @@ -29,3 +29,11 @@ export function strToByteUint8Array(str: string): Uint8Array { [...str].reduce((acc, c, ind) => acc.concat([str.charCodeAt(ind)]), []) ); } + +export async function getBubblegumAuthorityPDAKey(merkleRollPubKey: PublicKey, bubblegumProgramId: PublicKey) { + const [bubblegumAuthorityPDAKey] = await PublicKey.findProgramAddress( + [merkleRollPubKey.toBuffer()], + bubblegumProgramId + ); + return bubblegumAuthorityPDAKey; +} diff --git a/contracts/tests/bubblegum-test.ts b/contracts/tests/bubblegum-test.ts index 443dd0a6da6..9c61de42cc1 100644 --- a/contracts/tests/bubblegum-test.ts +++ b/contracts/tests/bubblegum-test.ts @@ -27,6 +27,7 @@ import {buildTree, Tree} from "./merkle-tree"; import { decodeMerkleRoll, getMerkleRollAccountSize, + getRootOfOnChainMerkleRoot, assertOnChainMerkleRollProperties, createTransferAuthorityIx, } from "../sdk/gummyroll"; @@ -36,23 +37,14 @@ import { TOKEN_PROGRAM_ID, Token, } from "@solana/spl-token"; -import {execute, logTx} from "./utils"; -import {TokenProgramVersion, Version} from "../sdk/bubblegum/src/generated"; +import { execute, logTx, bufferToArray } from "./utils"; +import { TokenProgramVersion, Version } from "../sdk/bubblegum/src/generated"; // @ts-ignore let Bubblegum; // @ts-ignore let GummyrollProgramId; -/// Converts to Uint8Array -function bufferToArray(buffer: Buffer): number[] { - const nums = []; - for (let i = 0; i < buffer.length; i++) { - nums.push(buffer.at(i)); - } - return nums; -} - describe("bubblegum", () => { // Configure the client to use the local cluster. let offChainTree: Tree; @@ -202,15 +194,9 @@ describe("bubblegum", () => { Buffer.from(keccak_256.digest(mintIx.data.slice(8))) ); const creatorHash = bufferToArray(Buffer.from(keccak_256.digest([]))); - let merkleRollAccount = - await Bubblegum.provider.connection.getAccountInfo( - merkleRollKeypair.publicKey - ); - let merkleRoll = decodeMerkleRoll(merkleRollAccount.data); - let onChainRoot = - merkleRoll.roll.changeLogs[merkleRoll.roll.activeIndex].root.toBuffer(); - - console.log(" - Transferring Ownership"); + let onChainRoot = await getRootOfOnChainMerkleRoot(connection, merkleRollKeypair.publicKey); + + console.log(" - Transferring Ownership"); const nonceInfo = await ( Bubblegum.provider.connection as web3Connection ).getAccountInfo(treeAuthority); @@ -236,12 +222,7 @@ describe("bubblegum", () => { ); await execute(Bubblegum.provider, [transferIx], [payer]); - merkleRollAccount = await Bubblegum.provider.connection.getAccountInfo( - merkleRollKeypair.publicKey - ); - merkleRoll = decodeMerkleRoll(merkleRollAccount.data); - onChainRoot = - merkleRoll.roll.changeLogs[merkleRoll.roll.activeIndex].root.toBuffer(); + onChainRoot = await getRootOfOnChainMerkleRoot(connection, merkleRollKeypair.publicKey); console.log(" - Delegating Ownership"); let delegateIx = await createDelegateInstruction( @@ -263,12 +244,7 @@ describe("bubblegum", () => { ); await execute(Bubblegum.provider, [delegateIx], [destination]); - merkleRollAccount = await Bubblegum.provider.connection.getAccountInfo( - merkleRollKeypair.publicKey - ); - merkleRoll = decodeMerkleRoll(merkleRollAccount.data); - onChainRoot = - merkleRoll.roll.changeLogs[merkleRoll.roll.activeIndex].root.toBuffer(); + onChainRoot = await getRootOfOnChainMerkleRoot(connection, merkleRollKeypair.publicKey); console.log(" - Transferring Ownership (through delegate)"); let delTransferIx = createTransferInstruction( @@ -298,12 +274,7 @@ describe("bubblegum", () => { } ); - merkleRollAccount = await Bubblegum.provider.connection.getAccountInfo( - merkleRollKeypair.publicKey - ); - merkleRoll = decodeMerkleRoll(merkleRollAccount.data); - onChainRoot = - merkleRoll.roll.changeLogs[merkleRoll.roll.activeIndex].root.toBuffer(); + onChainRoot = await getRootOfOnChainMerkleRoot(connection, merkleRollKeypair.publicKey); let [voucher] = await PublicKey.findProgramAddress( [ diff --git a/contracts/tests/gumball-machine-serde.ts b/contracts/tests/gumball-machine-serde.ts deleted file mode 100644 index 50c9bc39a2e..00000000000 --- a/contracts/tests/gumball-machine-serde.ts +++ /dev/null @@ -1,90 +0,0 @@ -import { - PublicKey -} from '@solana/web3.js'; -import * as borsh from 'borsh'; -import { BN } from '@project-serum/anchor'; - -/** - * Manually create a model for GumballMachine accounts to deserialize manually - */ -export type OnChainGumballMachine = { - header: GumballMachineHeader, - configData: ConfigData -} - -type GumballMachineHeader = { - urlBase: Buffer, // [u8; 64] - nameBase: Buffer, // [u8; 32] - symbol: Buffer, // [u8; 32] - sellerFeeBasisPoints: number, // u16 - isMutable: boolean, // u8 - retainAuthority: boolean, // u8 - _padding: Buffer, // [u8; 4], - price: BN, // u64 - goLiveDate: BN, // i64 - mint: PublicKey, - botWallet: PublicKey, - receiver: PublicKey, - authority: PublicKey, - collectionKey: PublicKey, - creatorAddress: PublicKey, - extensionLen: BN, // usize - maxMintSize: BN, // u64 - remaining: BN, // usize - maxItems: BN, // u64 - totalItemsAdded: BN, // usize -} - -type ConfigData = { - indexArray: Buffer, - configLines: Buffer -} - -function readPublicKey(reader: borsh.BinaryReader): PublicKey { - return new PublicKey(reader.readFixedArray(32)); -} - -// Deserialize on-chain gumball machine account to OnChainGumballMachine type -export function decodeGumballMachine(buffer: Buffer, accountSize: number): OnChainGumballMachine { - let reader = new borsh.BinaryReader(buffer); - - // Deserialize header - let header: GumballMachineHeader = { - urlBase: Buffer.from(reader.readFixedArray(64)), - nameBase: Buffer.from(reader.readFixedArray(32)), - symbol: Buffer.from(reader.readFixedArray(32)), - sellerFeeBasisPoints: reader.readU16(), - isMutable: !!reader.readU8(), - retainAuthority: !!reader.readU8(), - _padding: Buffer.from(reader.readFixedArray(4)), - price: reader.readU64(), - goLiveDate: new BN(reader.readFixedArray(8), null, 'le'), - mint: readPublicKey(reader), - botWallet: readPublicKey(reader), - receiver: readPublicKey(reader), - authority: readPublicKey(reader), - collectionKey: readPublicKey(reader), - creatorAddress: readPublicKey(reader), - extensionLen: new BN(reader.readFixedArray(8), null, 'le'), // Assume 8 byte size of usize...technically could break - maxMintSize: reader.readU64(), - remaining: new BN(reader.readFixedArray(8), null, 'le'), // Assume 8 byte size of usize...technically could break - maxItems: reader.readU64(), - totalItemsAdded: new BN(reader.readFixedArray(8), null, 'le'), // Assume 8 byte size of usize...technically could break - }; - - // Deserailize indices and config section - let numIndexArrayBytes = header.maxItems.toNumber() * 4; - let numConfigBytes = header.extensionLen.toNumber() * header.maxItems.toNumber(); - let configData: ConfigData = { - indexArray: Buffer.from(reader.readFixedArray(numIndexArrayBytes)), - configLines: Buffer.from(reader.readFixedArray(numConfigBytes)), - } - - if (accountSize != reader.offset) { - throw new Error("Reader processed different number of bytes than account size") - } - return { - header, - configData - } -} \ No newline at end of file diff --git a/contracts/tests/gumball-machine-test.ts b/contracts/tests/gumball-machine-test.ts index 3dbf45e6a5e..b82b12759b9 100644 --- a/contracts/tests/gumball-machine-test.ts +++ b/contracts/tests/gumball-machine-test.ts @@ -23,8 +23,7 @@ import { createDispenseNFTForSolIx, createDispenseNFTForTokensIx, createInitializeGumballMachineIxs, - getBubblegumAuthorityPDAKey, -} from "../sdk/gumball-machine"; +} from '../sdk/gumball-machine'; import { InitializeGumballMachineInstructionArgs, createAddConfigLinesInstruction, @@ -34,7 +33,12 @@ import { createUpdateHeaderMetadataInstruction, createDestroyInstruction, } from "../sdk/gumball-machine/src/generated/instructions"; -import { val, strToByteArray, strToByteUint8Array } from "../sdk/utils/index"; +import { + val, + strToByteArray, + strToByteUint8Array, + getBubblegumAuthorityPDAKey +} from "../sdk/utils/index"; import { GumballMachineHeader, gumballMachineHeaderBeet, @@ -192,8 +196,7 @@ describe("gumball-machine", () => { merkleRollKeypair: Keypair, merkleRollAccountSize: number, gumballMachineInitArgs: InitializeGumballMachineInstructionArgs, - mint: PublicKey, - nonce: PublicKey + mint: PublicKey ) { const bubblegumAuthorityPDAKey = await getBubblegumAuthorityPDAKey( merkleRollKeypair.publicKey, @@ -208,7 +211,6 @@ describe("gumball-machine", () => { merkleRollAccountSize, gumballMachineInitArgs, mint, - nonce, GummyrollProgramId, BubblegumProgramId, GumballMachine @@ -260,7 +262,7 @@ describe("gumball-machine", () => { botWallet: gumballMachineInitArgs.botWallet, receiver: gumballMachineInitArgs.receiver, authority: gumballMachineInitArgs.authority, - collectionKey: gumballMachineInitArgs.collectionKey, // 0x0 -> no collection key + collectionKey: gumballMachineInitArgs.collectionKey, creatorAddress: payer.publicKey, extensionLen: gumballMachineInitArgs.extensionLen, maxMintSize: gumballMachineInitArgs.maxMintSize, @@ -408,7 +410,6 @@ describe("gumball-machine", () => { receiver: PublicKey, gumballMachineAcctKeypair: Keypair, merkleRollKeypair: Keypair, - noncePDAKey: PublicKey, verbose?: boolean ) { const dispenseInstr = await createDispenseNFTForSolIx( @@ -417,7 +418,6 @@ describe("gumball-machine", () => { receiver, gumballMachineAcctKeypair, merkleRollKeypair, - noncePDAKey, GummyrollProgramId, BubblegumProgramId, GumballMachine @@ -438,7 +438,6 @@ describe("gumball-machine", () => { receiver: PublicKey, gumballMachineAcctKeypair: Keypair, merkleRollKeypair: Keypair, - noncePDAKey: PublicKey, verbose?: boolean ) { const dispenseInstr = await createDispenseNFTForTokensIx( @@ -448,7 +447,6 @@ describe("gumball-machine", () => { receiver, gumballMachineAcctKeypair, merkleRollKeypair, - noncePDAKey, GummyrollProgramId, BubblegumProgramId, GumballMachine @@ -500,7 +498,6 @@ describe("gumball-machine", () => { let creatorAddress: Keypair; let gumballMachineAcctKeypair: Keypair; let merkleRollKeypair: Keypair; - let noncePDAKey: PublicKey; let nftBuyer: Keypair; const GUMBALL_MACHINE_ACCT_CONFIG_INDEX_ARRAY_SIZE = 1000; const GUMBALL_MACHINE_ACCT_CONFIG_LINES_SIZE = 7000; @@ -530,10 +527,6 @@ describe("gumball-machine", () => { gumballMachineAcctKeypair = Keypair.generate(); merkleRollKeypair = Keypair.generate(); - [noncePDAKey] = await PublicKey.findProgramAddress( - [Buffer.from("bubblegum"), merkleRollKeypair.publicKey.toBuffer()], - BubblegumProgramId - ); baseGumballMachineInitProps = { maxDepth: 3, maxBufferSize: 8, @@ -571,8 +564,7 @@ describe("gumball-machine", () => { merkleRollKeypair, MERKLE_ROLL_ACCT_SIZE, baseGumballMachineInitProps, - NATIVE_MINT, - noncePDAKey + NATIVE_MINT ); await addConfigLines( creatorAddress, @@ -616,7 +608,6 @@ describe("gumball-machine", () => { baseGumballMachineInitProps.receiver, gumballMachineAcctKeypair, merkleRollKeypair, - noncePDAKey, GummyrollProgramId, BubblegumProgramId, GumballMachine @@ -699,8 +690,7 @@ describe("gumball-machine", () => { nftBuyer, baseGumballMachineInitProps.receiver, gumballMachineAcctKeypair, - merkleRollKeypair, - noncePDAKey + merkleRollKeypair ); const nftBuyerBalanceAfterPurchase = await connection.getBalance( nftBuyer.publicKey @@ -807,10 +797,6 @@ describe("gumball-machine", () => { nftBuyer = Keypair.generate(); botWallet = Keypair.generate(); - [noncePDAKey] = await PublicKey.findProgramAddress( - [Buffer.from("bubblegum"), merkleRollKeypair.publicKey.toBuffer()], - BubblegumProgramId - ); // Give creator enough funds to produce accounts for gumball-machine await GumballMachine.provider.connection.confirmTransaction( await GumballMachine.provider.connection.requestAirdrop( @@ -862,8 +848,7 @@ describe("gumball-machine", () => { merkleRollKeypair, MERKLE_ROLL_ACCT_SIZE, baseGumballMachineInitProps, - someMint, - noncePDAKey + someMint ); await addConfigLines( creatorAddress, @@ -908,7 +893,6 @@ describe("gumball-machine", () => { creatorReceiverTokenAccount.address, gumballMachineAcctKeypair, merkleRollKeypair, - noncePDAKey, GummyrollProgramId, BubblegumProgramId, GumballMachine @@ -970,8 +954,7 @@ describe("gumball-machine", () => { nftBuyerTokenAccount.address, creatorReceiverTokenAccount.address, gumballMachineAcctKeypair, - merkleRollKeypair, - noncePDAKey + merkleRollKeypair ); let newCreatorTokenAccount = await getAccount( @@ -1003,8 +986,7 @@ describe("gumball-machine", () => { nftBuyerTokenAccount.address, creatorReceiverTokenAccount.address, gumballMachineAcctKeypair, - merkleRollKeypair, - noncePDAKey + merkleRollKeypair ); creatorReceiverTokenAccount = newCreatorTokenAccount; diff --git a/contracts/tests/gummyroll-test.ts b/contracts/tests/gummyroll-test.ts index 95f473134f1..86a71376d06 100644 --- a/contracts/tests/gummyroll-test.ts +++ b/contracts/tests/gummyroll-test.ts @@ -30,7 +30,6 @@ import { assertOnChainMerkleRollProperties, } from "../sdk/gummyroll"; import { execute, logTx } from "./utils"; -import NodeWallet from "@project-serum/anchor/dist/cjs/nodewallet"; // @ts-ignore let Gummyroll; diff --git a/contracts/tests/sugar-shack-test.ts b/contracts/tests/sugar-shack-test.ts new file mode 100644 index 00000000000..845af80a5fa --- /dev/null +++ b/contracts/tests/sugar-shack-test.ts @@ -0,0 +1,465 @@ +import * as anchor from "@project-serum/anchor"; +import { keccak_256 } from "js-sha3"; +import { BN, Provider, Program } from "@project-serum/anchor"; +import { + PublicKey, + Keypair, + SystemProgram, + Transaction, + Connection as web3Connection, + LAMPORTS_PER_SOL, + SYSVAR_RENT_PUBKEY, + AccountMeta, +} from "@solana/web3.js"; +import { assert } from "chai"; +import { + createCreateTreeInstruction, + createMintV1Instruction, +} from '../sdk/bubblegum/src/generated/instructions'; +import { + MarketplaceProperties +} from "../sdk/sugar-shack/src/generated/accounts/index"; +import { + createInitializeMarketplaceInstruction, + createCreateOrModifyListingInstruction, + createRemoveListingInstruction, + createPurchaseInstruction, + createWithdrawFeesInstruction +} from "../sdk/sugar-shack/src/generated/instructions"; +import { + getListingPDAKeyForPrice +} from "../sdk/sugar-shack"; +import { + getBubblegumAuthorityPDAKey +} from "../sdk/utils/index"; +import { + MetadataArgs, + LeafSchema, + leafSchemaBeet +} from "../sdk/bubblegum/src/generated/types"; +import { + getMerkleRollAccountSize, + getRootOfOnChainMerkleRoot +} from "../sdk/gummyroll"; +import { + buildTree, + hash, + getProofOfLeaf, + updateTree, + Tree, + TreeNode, +} from "./merkle-tree"; +import NodeWallet from "@project-serum/anchor/dist/cjs/nodewallet"; +import { execute, logTx, bufferToArray } from "./utils"; +import { TokenProgramVersion, Version } from "../sdk/bubblegum/src/generated"; +import { SugarShack } from "../target/types/sugar_shack"; + +// @ts-ignore +let BubblegumProgramId; +let GummyrollProgramId; + +describe("sugar-shack", () => { + // In this test, payer is the merkle tree admin and payer for all txs. + const payer = Keypair.generate(); + + // Establish connection to localcluster + let connection = new web3Connection("http://localhost:8899", { + commitment: "confirmed", + }); + let wallet = new NodeWallet(payer); + anchor.setProvider( + new Provider(connection, wallet, { + commitment: connection.commitment, + skipPreflight: true, + }) + ); + + const SugarShack = anchor.workspace.SugarShack as Program; + BubblegumProgramId = anchor.workspace.Bubblegum.programId; + GummyrollProgramId = anchor.workspace.Gummyroll.programId; + + describe("test sugarshack", async () => { + let marketplaceAccountKey: PublicKey; + let marketplaceShareRecipient: Keypair; + let marketplaceAuthority: Keypair; + let merkleRollKeypair: Keypair; + let lister: Keypair; + let bubblegumAuthority: PublicKey; + let dataHashOfCompressedNFT: number[]; + let creatorHashOfCompressedNFT: number[]; + let leafNonce: BN; + let listingPDAKey: PublicKey; + let bufferOfProjectDropCreatorShare: Buffer; + let projectDropCreator: Keypair; + let listingPrice: BN; + let compressedNFTMetadata: MetadataArgs; + let originalProofToNFTLeaf: AccountMeta[]; + const marketplaceRoyaltyShare = 100; + + async function createOrModifyListing( + priceForListing: BN, + currentNFTOwner: Keypair, + previousNFTDelegate: PublicKey, + proofToLeaf: AccountMeta[] = null, + ) { + const onChainRoot = await getRootOfOnChainMerkleRoot(connection, merkleRollKeypair.publicKey); + const newListingPDAKey = await getListingPDAKeyForPrice(priceForListing, SugarShack.programId); + const createOrModifyListingIx = createCreateOrModifyListingInstruction( + { + owner: currentNFTOwner.publicKey, + formerDelegate: previousNFTDelegate, + newDelegate: newListingPDAKey, + bubblegumAuthority, + gummyroll: GummyrollProgramId, + merkleSlab: merkleRollKeypair.publicKey, + bubblegum: BubblegumProgramId + }, + { + price: priceForListing, + dataHash: dataHashOfCompressedNFT, + creatorHash: creatorHashOfCompressedNFT, + nonce: leafNonce, + index: 0, + root: bufferToArray(onChainRoot), + } + ); + if (proofToLeaf) { + proofToLeaf.forEach(acctMeta => createOrModifyListingIx.keys.push(acctMeta)); + } + await SugarShack.provider.send(new Transaction().add(createOrModifyListingIx), [currentNFTOwner], { + commitment: "confirmed", + }); + } + + async function removeListing( + currentNFTOwner: Keypair, + previousNFTDelegate: PublicKey, + desiredDelegate: PublicKey, + proofToLeaf: AccountMeta[] = null + ) { + const onChainRoot = await getRootOfOnChainMerkleRoot(connection, merkleRollKeypair.publicKey); + const removeListIx = createRemoveListingInstruction( + { + owner: currentNFTOwner.publicKey, + formerDelegate: previousNFTDelegate, + newDelegate: desiredDelegate, + bubblegumAuthority, + gummyroll: GummyrollProgramId, + merkleSlab: merkleRollKeypair.publicKey, + bubblegum: BubblegumProgramId + }, + { + dataHash: dataHashOfCompressedNFT, + creatorHash: creatorHashOfCompressedNFT, + nonce: leafNonce, + index: 0, + root: bufferToArray(onChainRoot), + } + ); + if (proofToLeaf) { + proofToLeaf.forEach(acctMeta => removeListIx.keys.push(acctMeta)); + } + await SugarShack.provider.send(new Transaction().add(removeListIx), [currentNFTOwner], { + commitment: "confirmed", + }); + } + + async function withdrawFees(feePayoutRecipient: PublicKey, authority: Keypair, lamportsToWithdraw: BN) { + const withdrawFeesIx = createWithdrawFeesInstruction( + { + feePayoutRecipient, + authority: authority.publicKey, + marketplaceProps: marketplaceAccountKey, + sysvarRent: SYSVAR_RENT_PUBKEY + }, + { + lamportsToWithdraw, + } + ); + await SugarShack.provider.send(new Transaction().add(withdrawFeesIx), [authority], { + commitment: "confirmed", + }); + } + + async function purchaseNFTFromListing( + purchasePrice: BN, + nftPurchaser: Keypair, + proofToLeaf: AccountMeta[] = null, + ) { + let onChainRoot = await getRootOfOnChainMerkleRoot(connection, merkleRollKeypair.publicKey); + let listedNFTDelegateKey: PublicKey = await getListingPDAKeyForPrice(purchasePrice, SugarShack.programId); + const purchaseIx = createPurchaseInstruction( + { + formerOwner: lister.publicKey, + purchaser: nftPurchaser.publicKey, + listingDelegate: listedNFTDelegateKey, + bubblegumAuthority, + gummyroll: GummyrollProgramId, + merkleSlab: merkleRollKeypair.publicKey, + bubblegum: BubblegumProgramId, + marketplaceProps: marketplaceAccountKey, + }, + { + price: purchasePrice, + dataHash: dataHashOfCompressedNFT, + nonce: leafNonce, + index: 0, + root: bufferToArray(onChainRoot), + creatorShares: bufferOfProjectDropCreatorShare, + } + ); + let remainingAccount = { + pubkey: projectDropCreator.publicKey, + isSigner: false, + isWritable: true, + }; + purchaseIx.keys.push(remainingAccount); + if (proofToLeaf) { + proofToLeaf.forEach(acctMeta => purchaseIx.keys.push(acctMeta)); + } + let tx = new Transaction().add(purchaseIx); + await SugarShack.provider.send(tx, [nftPurchaser], { + commitment: "confirmed", + }); + // For reference, a tree with depth 20 has a transaction size of 1123 with one creator and a canopy of depth 5 + // a tree with depth 24 has a transaction size of 1255 with one creator and a canopy of depth 5 + let txSize = tx.serialize().length; + console.log("Transaction Size", txSize); + } + + before(async () => { + // Fund the payer for the entire suite + await SugarShack.provider.connection.confirmTransaction( + await SugarShack.provider.connection.requestAirdrop(payer.publicKey, 75e9), + "confirmed" + ); + + // Setup one-time state that will be shared among tests: Marketplace Properties account, Nonce if not already init by another test + + // Initialize marketplace properties account + marketplaceAuthority = Keypair.generate(); + [marketplaceAccountKey] = await PublicKey.findProgramAddress( + [Buffer.from("mymarketplace")], + SugarShack.programId + ); + let initMarketplacePropsIx = createInitializeMarketplaceInstruction( + { + marketplaceProps: marketplaceAccountKey, + payer: payer.publicKey, + }, + { + royaltyShare: marketplaceRoyaltyShare, + authority: marketplaceAuthority.publicKey, + } + ); + + const initMarketplacePropsTx = new Transaction().add(initMarketplacePropsIx); + await SugarShack.provider.send(initMarketplacePropsTx, [payer], { + commitment: "confirmed", + }); + + // Confirm that properties of the onchain marketplace PDA match expectation + const onChainMarketplaceAccount: MarketplaceProperties = await MarketplaceProperties.fromAccountAddress(SugarShack.provider.connection, marketplaceAccountKey); + assert( + onChainMarketplaceAccount.authority.equals(marketplaceAuthority.publicKey), + "onchain marketplace account receiver does not match expectation" + ); + assert( + onChainMarketplaceAccount.share === marketplaceRoyaltyShare, + "onchain marketplace account share does not match expectation" + ); + }); + + describe("core instructions", async () => { + beforeEach(async () => { + // Setup unique state for each test: a new merkle roll tree with a new NFT in it + lister = Keypair.generate(); + merkleRollKeypair = Keypair.generate(); + const MERKLE_ROLL_MAX_DEPTH = 20; + const MERKLE_ROLL_MAX_BUFFER_SIZE = 2048; + + // Make use of CANOPY to enable larger project sizes and give more breathing room for additional accounts in marketplace instructions + const MERKLE_ROLL_CANOPY_DEPTH = 5; + const MERKLE_ROLL_ACCT_SIZE = getMerkleRollAccountSize(MERKLE_ROLL_MAX_DEPTH, MERKLE_ROLL_MAX_BUFFER_SIZE, MERKLE_ROLL_CANOPY_DEPTH); + + // Create the compressed NFT tree + // Instruction to alloc new merkle roll account + const allocMerkleRollAcctInstr = SystemProgram.createAccount({ + fromPubkey: payer.publicKey, + newAccountPubkey: merkleRollKeypair.publicKey, + lamports: + await SugarShack.provider.connection.getMinimumBalanceForRentExemption( + MERKLE_ROLL_ACCT_SIZE + ), + space: MERKLE_ROLL_ACCT_SIZE, + programId: GummyrollProgramId, + }); + bubblegumAuthority = await getBubblegumAuthorityPDAKey(merkleRollKeypair.publicKey, BubblegumProgramId); + + // Instruction to create merkle tree for compressed NFTs through Bubblegum + const createCompressedNFTTreeIx = createCreateTreeInstruction( + { + treeCreator: payer.publicKey, + payer: payer.publicKey, + authority: bubblegumAuthority, + gummyrollProgram: GummyrollProgramId, + merkleSlab: merkleRollKeypair.publicKey, + }, + { + maxDepth: MERKLE_ROLL_MAX_DEPTH, + maxBufferSize: MERKLE_ROLL_MAX_BUFFER_SIZE + } + ); + let createCompressedNFTTreeTx = new Transaction().add(allocMerkleRollAcctInstr).add(createCompressedNFTTreeIx); + await SugarShack.provider.send(createCompressedNFTTreeTx, [payer, merkleRollKeypair], { + commitment: "confirmed", + }); + + // build a corresponding off-chain tree...this allows us to fetch a proof + const leaves = Array(2 ** MERKLE_ROLL_MAX_DEPTH).fill(Buffer.alloc(32)); + const tree = buildTree(leaves); + + // @dev: notice that even as the hash of the leaf changes, we continue passing in the same stale proof to the original leaf + // before it was even listed. This works because Gummyroll has a fallback mechanism to reproduce a valid proof from the + // beginning of its buffer if the supplied proof is invalid. Thus, this allows us to present a proof of accurate *size* + // without needing to locally track how the proof actually changes which is harder with local tests. Note though, that after + // more than MAX_BUFFER_SIZE operations this is no longer valid, and in general is not good practice for a marketplace with + // access to indexing infra. + originalProofToNFTLeaf = getProofOfLeaf(tree, (2**MERKLE_ROLL_MAX_DEPTH)-1).slice(0, -1*MERKLE_ROLL_CANOPY_DEPTH).map((node) => { + return { + pubkey: new PublicKey(node.node), + isSigner: false, + isWritable: false, + }; + }); + + projectDropCreator = Keypair.generate(); + + // Mint an NFT to the tree, NFT to be owned by "lister" + compressedNFTMetadata = { + name: "test", + symbol: "test", + uri: "www.solana.com", + sellerFeeBasisPoints: 0, + primarySaleHappened: false, + isMutable: false, + editionNonce: null, + tokenStandard: null, + tokenProgramVersion: TokenProgramVersion.Original, + collection: null, + uses: null, + creators: [{address: projectDropCreator.publicKey, verified: true, share: 5}], + }; + const mintIx = createMintV1Instruction({ + mintAuthority: payer.publicKey, + authority: bubblegumAuthority, + gummyrollProgram: GummyrollProgramId, + owner: lister.publicKey, + delegate: lister.publicKey, + merkleSlab: merkleRollKeypair.publicKey, + }, { message: compressedNFTMetadata }); + + await SugarShack.provider.send(new Transaction().add(mintIx),[payer], + { + skipPreflight: true, + commitment: "confirmed", + } + ); + + // Get data_hash and creator_hash information for future transactions + let bufferOfProjectDropCreatorPubkey = projectDropCreator.publicKey.toBuffer(); + bufferOfProjectDropCreatorShare = Buffer.from([compressedNFTMetadata.creators[0].share]); + let bufferOfCreatorData = Buffer.concat([bufferOfProjectDropCreatorPubkey, bufferOfProjectDropCreatorShare]); + dataHashOfCompressedNFT = bufferToArray(Buffer.from(keccak_256.digest(mintIx.data.slice(8)))); + creatorHashOfCompressedNFT = bufferToArray(Buffer.from(keccak_256.digest(bufferOfCreatorData))); + + // Get the nonce for the minted leaf + const nonceInfo = await SugarShack.provider.connection.getAccountInfo(bubblegumAuthority); + leafNonce = (new BN(nonceInfo.data.slice(8, 16), "le")).sub(new BN(1)); + + // Record the PDA key that will be used as the "default" listing for each test + listingPrice = new BN(1*LAMPORTS_PER_SOL); + listingPDAKey = await getListingPDAKeyForPrice(listingPrice, SugarShack.programId); + + await createOrModifyListing(new BN(1*LAMPORTS_PER_SOL), lister, lister.publicKey, originalProofToNFTLeaf); + }); + it("can modify listing", async () => { + // Modify listing to have price 654321 + await createOrModifyListing(new BN(654321), lister, listingPDAKey, originalProofToNFTLeaf); + + // We can demonstrate that the modification worked by demonstrating that modifying using the old listingPDAKey will now fail + try { + await createOrModifyListing(new BN(555333), lister, listingPDAKey, originalProofToNFTLeaf); + assert(false, "Was able to update listing despite earlier modification of delegate key!") + } catch(e) {} + }); + it("can remove listing", async () => { + await removeListing(lister, listingPDAKey, lister.publicKey, originalProofToNFTLeaf); + + // Purchase after listing removal fails + let nftPurchaser = Keypair.generate(); + await SugarShack.provider.connection.confirmTransaction( + await SugarShack.provider.connection.requestAirdrop(nftPurchaser.publicKey, 2*LAMPORTS_PER_SOL), + "confirmed" + ); + + try { + await purchaseNFTFromListing(listingPrice, nftPurchaser, originalProofToNFTLeaf); + assert(false, "Unexpectedly, purchasing NFT after listing removal succeeded"); + } catch (e) {} + }); + it("can purchase listed NFT", async () => { + + // Create and fund the purchaser account + let nftPurchaser = Keypair.generate(); + await SugarShack.provider.connection.confirmTransaction( + await SugarShack.provider.connection.requestAirdrop(nftPurchaser.publicKey, 2*LAMPORTS_PER_SOL), + "confirmed" + ); + const originalMarketplacePDABalance = await SugarShack.provider.connection.getBalance(marketplaceAccountKey); + await purchaseNFTFromListing(listingPrice, nftPurchaser, originalProofToNFTLeaf); + + // Assert on expected balance changes after NFT purchase + const expectedMarketplaceFeePayout = listingPrice.toNumber() * marketplaceRoyaltyShare/10000; + assert( + originalMarketplacePDABalance + expectedMarketplaceFeePayout === await SugarShack.provider.connection.getBalance(marketplaceAccountKey), + "Marketplace did not recieve expected royalty" + ); + + const remainingLamportsToPayout = listingPrice.toNumber() - expectedMarketplaceFeePayout; + const expectedCreatorPayout = remainingLamportsToPayout * compressedNFTMetadata.creators[0].share/100; + assert( + expectedCreatorPayout === await SugarShack.provider.connection.getBalance(projectDropCreator.publicKey), + "Creator did not recieve expected royalty" + ); + const expectedListerPayout = remainingLamportsToPayout - expectedCreatorPayout; + assert( + expectedListerPayout === await SugarShack.provider.connection.getBalance(lister.publicKey), + "Lister did not recieve expected royalty" + ); + assert( + (2*LAMPORTS_PER_SOL)-listingPrice.toNumber() === await SugarShack.provider.connection.getBalance(nftPurchaser.publicKey), + "NFT purchaser balance did not change as expected" + ); + + // Create marketplace share recipient account + marketplaceShareRecipient = Keypair.generate(); + // Marketplace can now withdraw fee payout to external wallet + await withdrawFees(marketplaceShareRecipient.publicKey, marketplaceAuthority, new BN(expectedMarketplaceFeePayout)); + + // Assert that fee withdrawal occurred as expected + assert( + expectedMarketplaceFeePayout === await SugarShack.provider.connection.getBalance(marketplaceShareRecipient.publicKey), + "Marketplace share RECIPIENT balance did not increment as expected after fee withdrawal" + ); + assert( + originalMarketplacePDABalance === await SugarShack.provider.connection.getBalance(marketplaceAccountKey), + "Marketplace PDA balance did not decrease as expected after fee withdrawal" + ); + + // Purchaser is now able to list NFT + await createOrModifyListing(new BN(654321), nftPurchaser, nftPurchaser.publicKey, originalProofToNFTLeaf); + }); + }); + }); +}); diff --git a/contracts/tests/utils.ts b/contracts/tests/utils.ts index c728bb466e8..b411c99f824 100644 --- a/contracts/tests/utils.ts +++ b/contracts/tests/utils.ts @@ -1,6 +1,7 @@ import { Provider } from "@project-serum/anchor"; import { TransactionInstruction, Transaction, Signer } from "@solana/web3.js"; +/// Wait for a transaction of a certain id to confirm and optionally log its messages export async function logTx(provider: Provider, txId: string, verbose: boolean = true) { await provider.connection.confirmTransaction(txId, "confirmed"); if (verbose) { @@ -11,7 +12,7 @@ export async function logTx(provider: Provider, txId: string, verbose: boolean = } }; - +/// Execute a series of instructions in a txn export async function execute( provider: Provider, instructions: TransactionInstruction[], @@ -27,6 +28,8 @@ export async function execute( await logTx(provider, txid, false); return txid; } + +/// Convert a 32 bit number to a buffer of bytes export function num32ToBuffer(num: number) { const isU32 = (num >= 0 && num < Math.pow(2,32)); const isI32 = (num >= -1*Math.pow(2, 31) && num < Math.pow(2,31)) @@ -40,9 +43,19 @@ export function num32ToBuffer(num: number) { return Buffer.from([byte1, byte2, byte3, byte4]) } +/// Check if two Array types contain the same values in order export function arrayEquals(a, b) { return Array.isArray(a) && Array.isArray(b) && a.length === b.length && a.every((val, index) => val === b[index]); } + +/// Convert Buffer to Uint8Array +export function bufferToArray(buffer: Buffer): number[] { + const nums = []; + for (let i = 0; i < buffer.length; i++) { + nums.push(buffer.at(i)); + } + return nums; +}