Skip to content

Commit

Permalink
feat(gumball-machine): add SDK and small refactor program
Browse files Browse the repository at this point in the history
  • Loading branch information
samwise2 committed Jun 6, 2022
1 parent e8f35eb commit a7c983a
Show file tree
Hide file tree
Showing 11 changed files with 539 additions and 288 deletions.
106 changes: 61 additions & 45 deletions contracts/programs/gumball-machine/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ use spl_token::native_mint;
use bubblegum::program::Bubblegum;
use bubblegum::state::metaplex_adapter::UseMethod;
use bubblegum::state::metaplex_adapter::Uses;
use bubblegum::state::metaplex_adapter::MetadataArgs;
use bubblegum::state::leaf_schema::Version;
use bytemuck::cast_slice_mut;
use gummyroll::program::Gummyroll;
Expand Down Expand Up @@ -172,6 +173,50 @@ fn assert_valid_single_instruction_transaction<'info>(instruction_sysvar_account
return Ok(())
}

#[inline(always)]
// Preform a fisher_yates shuffle on the array of indices into the config lines data structure. Then return the
// metadata args corresponding to the chosen config line
fn fisher_yates_shuffle_and_fetch_nft_metadata<'info>(
recent_blockhashes: &UncheckedAccount<'info>,
gumball_header: &mut GumballMachineHeader,
indices: &mut [u32],
line_size: usize,
config_lines_data: &mut [u8]
) -> Result<(MetadataArgs)> {
// Get 8 bytes of entropy from the SlotHashes sysvar
let mut buf: [u8; 8] = [0; 8];
buf.copy_from_slice(
&hashv(&[
&recent_blockhashes.data.borrow(),
&(gumball_header.remaining as usize).to_le_bytes(),
])
.as_ref()[..8],
);
let entropy = u64::from_le_bytes(buf);
// Shuffle the list of indices using Fisher-Yates
let selected = entropy % gumball_header.remaining;
gumball_header.remaining -= 1;
indices.swap(selected as usize, gumball_header.remaining as usize);
// Pull out config line from the data
let random_config_index = indices[(gumball_header.remaining as usize)] as usize * line_size;
let config_line =
config_lines_data[random_config_index..random_config_index + line_size].to_vec();

let message = get_metadata_args(
gumball_header.url_base,
gumball_header.name_base,
gumball_header.symbol,
gumball_header.seller_fee_basis_points,
gumball_header.is_mutable != 0,
gumball_header.collection_key,
None,
gumball_header.creator_address,
random_config_index,
config_line,
);
return Ok(message);
}

#[inline(always)]
// For efficiency, this returns the GumballMachineHeader because it's required to validate
// payment parameters. But the main purpose of this function is to determine which config
Expand Down Expand Up @@ -204,47 +249,18 @@ fn find_and_mint_compressed_nft<'info>(
assert!(clock.unix_timestamp > gumball_header.go_live_date);
let size = gumball_header.max_items as usize;
let index_array_size = std::mem::size_of::<u32>() * size;
let config_size = gumball_header.extension_len * size;
let line_size = gumball_header.extension_len;
let config_size = gumball_header.extension_len as usize * size;
let line_size = gumball_header.extension_len as usize;

assert!(config_data.len() == index_array_size + config_size);
let (indices_data, config_lines_data) = config_data.split_at_mut(index_array_size);

// TODO: Validate data

let mut indices = cast_slice_mut::<u8, u32>(indices_data);
for _ in 0..(num_items as usize).max(1).min(gumball_header.remaining) {
// Get 8 bytes of entropy from the SlotHashes sysvar
let mut buf: [u8; 8] = [0; 8];
buf.copy_from_slice(
&hashv(&[
&recent_blockhashes.data.borrow(),
&gumball_header.remaining.to_le_bytes(),
])
.as_ref()[..8],
);
let entropy = u64::from_le_bytes(buf);
// Shuffle the list of indices using Fisher-Yates
let selected = entropy % gumball_header.remaining as u64;
gumball_header.remaining -= 1;
(&mut indices).swap(selected as usize, gumball_header.remaining);
// Pull out config line from the data
let random_config_index = indices[gumball_header.remaining] as usize * line_size;
let config_line =
config_lines_data[random_config_index..random_config_index + line_size].to_vec();

let message = get_metadata_args(
gumball_header.url_base,
gumball_header.name_base,
gumball_header.symbol,
gumball_header.seller_fee_basis_points,
gumball_header.is_mutable != 0,
gumball_header.collection_key,
None,
gumball_header.creator_address,
random_config_index,
config_line,
);
for _ in 0..(num_items as usize).max(1).min(gumball_header.remaining as usize) {

let message = fisher_yates_shuffle_and_fetch_nft_metadata(recent_blockhashes, gumball_header, indices, line_size, config_lines_data)?;

let seed = gumball_machine.key();
let seeds = &[seed.as_ref(), &[*willy_wonka_bump]];
Expand Down Expand Up @@ -280,7 +296,7 @@ pub mod gumball_machine {
max_buffer_size: u32,
url_base: [u8; 64],
name_base: [u8; 32],
symbol: [u8; 32],
symbol: [u8; 8],
seller_fee_basis_points: u16,
is_mutable: bool,
retain_authority: bool,
Expand Down Expand Up @@ -315,7 +331,7 @@ pub mod gumball_machine {
mint: ctx.accounts.mint.key(),
collection_key,
creator_address: ctx.accounts.creator.key(),
extension_len: extension_len as usize,
extension_len: extension_len,
max_mint_size: max_mint_size.max(1).min(max_items),
remaining: 0,
max_items,
Expand Down Expand Up @@ -357,10 +373,10 @@ pub mod gumball_machine {
let mut gumball_header = GumballMachineHeader::load_mut_bytes(&mut header_bytes)?;
let size = gumball_header.max_items as usize;
let index_array_size = std::mem::size_of::<u32>() * size;
let config_size = gumball_header.extension_len * size;
let line_size = gumball_header.extension_len;
let config_size = gumball_header.extension_len as usize * size;
let line_size = gumball_header.extension_len as usize;
let num_lines = new_config_lines_data.len() / line_size; // unchecked divide by zero? maybe we don't care since this will throw and the instr will fail
let start_index = gumball_header.total_items_added;
let start_index = gumball_header.total_items_added as usize;
assert_eq!(gumball_header.authority, ctx.accounts.authority.key());
assert_eq!(new_config_lines_data.len() % line_size, 0);
assert!(start_index + num_lines <= gumball_header.max_items as usize);
Expand All @@ -370,8 +386,8 @@ pub mod gumball_machine {
.take(new_config_lines_data.len())
.enumerate()
.for_each(|(i, l)| *l = new_config_lines_data[i]);
gumball_header.total_items_added += num_lines;
gumball_header.remaining += num_lines;
gumball_header.total_items_added += num_lines as u64;
gumball_header.remaining += num_lines as u64;
Ok(())
}

Expand All @@ -387,14 +403,14 @@ pub mod gumball_machine {
let gumball_header = GumballMachineHeader::load_mut_bytes(&mut header_bytes)?;
let size = gumball_header.max_items as usize;
let index_array_size = std::mem::size_of::<u32>() * size;
let config_size = gumball_header.extension_len * size;
let line_size = gumball_header.extension_len;
let config_size = gumball_header.extension_len as usize * size;
let line_size = gumball_header.extension_len as usize;
let num_lines = new_config_lines_data.len() / line_size; // unchecked divide by zero? maybe we don't care since this will throw and the instr will fail
assert_eq!(gumball_header.authority, ctx.accounts.authority.key());
assert_eq!(new_config_lines_data.len() % line_size, 0);
assert!(config_data.len() == index_array_size + config_size);
assert_eq!(new_config_lines_data.len(), num_lines * line_size);
assert!(starting_line as usize + num_lines <= gumball_header.total_items_added);
assert!(starting_line as usize + num_lines <= gumball_header.total_items_added as usize);
let (_, config_lines_data) = config_data.split_at_mut(index_array_size);
config_lines_data[starting_line as usize * line_size..]
.iter_mut()
Expand All @@ -408,7 +424,7 @@ pub mod gumball_machine {
ctx: Context<UpdateHeaderMetadata>,
url_base: Option<[u8; 64]>,
name_base: Option<[u8; 32]>,
symbol: Option<[u8; 32]>,
symbol: Option<[u8; 8]>,
seller_fee_basis_points: Option<u16>,
is_mutable: Option<bool>,
retain_authority: Option<bool>,
Expand Down
9 changes: 4 additions & 5 deletions contracts/programs/gumball-machine/src/state/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ use std::mem::size_of;

#[derive(AnchorSerialize, AnchorDeserialize, Clone, Copy, Zeroable, Pod)]
#[repr(C)]
// Current Size: 384 bytes
pub struct GumballMachineHeader {
// TODO: Add more fields
// Used to programmatically create the url and name for each field.
Expand All @@ -15,7 +14,7 @@ pub struct GumballMachineHeader {
// Unlike candy machine, each NFT minted has its name programmatically generated
// from the config line index as format!("{} #{}", name_base, index)
pub name_base: [u8; 32],
pub symbol: [u8; 32],
pub symbol: [u8; 8],
pub seller_fee_basis_points: u16,
pub is_mutable: u8,
pub retain_authority: u8,
Expand All @@ -33,11 +32,11 @@ pub struct GumballMachineHeader {
pub collection_key: Pubkey,
// Force a single creator (use Hydra)
pub creator_address: Pubkey,
pub extension_len: usize,
pub extension_len: u64,
pub max_mint_size: u64,
pub remaining: usize,
pub remaining: u64,
pub max_items: u64,
pub total_items_added: usize,
pub total_items_added: u64,
}

impl ZeroCopy for GumballMachineHeader {}
Expand Down
2 changes: 1 addition & 1 deletion contracts/programs/gumball-machine/src/utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ pub fn error_msg<T>(data_len: usize) -> impl Fn(PodCastError) -> ProgramError {
pub fn get_metadata_args(
url_base: [u8; 64],
name_base: [u8; 32],
symbol: [u8; 32],
symbol: [u8; 8],
seller_fee_basis_points: u16,
is_mutable: bool,
collection: Pubkey,
Expand Down
89 changes: 89 additions & 0 deletions contracts/sdk/gumball-machine/accounts/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import {
PublicKey
} from '@solana/web3.js';
import * as borsh from 'borsh';
import { BN } from '@project-serum/anchor';
import { readPublicKey } from '../../utils';

/**
* Manually create a model for GumballMachine accounts to deserialize manually
*/
export type OnChainGumballMachine = {
header: GumballMachineHeader,
configData: ConfigData
}

export const GUMBALL_MACHINE_HEADER_SIZE = 360;

type GumballMachineHeader = {
urlBase: Buffer, // [u8; 64]
nameBase: Buffer, // [u8; 32]
symbol: Buffer, // [u8; 8]
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
}

// 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(8)),
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
}
}
4 changes: 4 additions & 0 deletions contracts/sdk/gumball-machine/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export * from './instructions';
export * from './accounts';
export * from './types';
export * from './utils';
Loading

0 comments on commit a7c983a

Please sign in to comment.