diff --git a/contracts/programs/bubblegum/src/lib.rs b/contracts/programs/bubblegum/src/lib.rs index 24896e3084a..a10d9cf6ed4 100644 --- a/contracts/programs/bubblegum/src/lib.rs +++ b/contracts/programs/bubblegum/src/lib.rs @@ -30,10 +30,10 @@ const NONCE_PREFIX: &str = "bubblegum"; declare_id!("BGUMzZr2wWfD2yzrXFEWTK2HbdYhqQCP2EZoPEkZBD6o"); #[derive(Accounts)] -pub struct InitNonce<'info> { +pub struct CreateTree<'info> { #[account( init, - seeds = [NONCE_PREFIX.as_ref()], + seeds = [NONCE_PREFIX.as_ref(), merkle_slab.key().as_ref()], payer = payer, space = NONCE_SIZE, bump, @@ -41,12 +41,8 @@ pub struct InitNonce<'info> { pub nonce: Account<'info, Nonce>, #[account(mut)] pub payer: Signer<'info>, - pub system_program: Program<'info, System>, -} - -#[derive(Accounts)] -pub struct CreateTree<'info> { pub tree_creator: Signer<'info>, + pub system_program: Program<'info, System>, #[account( seeds = [merkle_slab.key().as_ref()], bump, @@ -71,7 +67,7 @@ pub struct Mint<'info> { pub authority: UncheckedAccount<'info>, #[account( mut, - seeds = [NONCE_PREFIX.as_ref()], + seeds = [NONCE_PREFIX.as_ref(), merkle_slab.key.as_ref()], bump, )] pub nonce: Account<'info, Nonce>, @@ -306,10 +302,6 @@ pub fn get_instruction_type(full_bytes: &[u8]) -> InstructionName { pub mod bubblegum { use super::*; - pub fn initialize_nonce(_ctx: Context) -> Result<()> { - Ok(()) - } - pub fn create_tree( ctx: Context, max_depth: u32, diff --git a/contracts/programs/gumball-machine/src/lib.rs b/contracts/programs/gumball-machine/src/lib.rs index a90d5730827..d14c57f086e 100644 --- a/contracts/programs/gumball-machine/src/lib.rs +++ b/contracts/programs/gumball-machine/src/lib.rs @@ -1,25 +1,17 @@ use anchor_lang::{ prelude::*, solana_program::{ - keccak::hashv, - sysvar, - sysvar::instructions::{load_instruction_at_checked}, - sysvar::SysvarId, - pubkey::Pubkey, - program::{invoke, invoke_signed}, - system_instruction, - instruction::{Instruction}, + keccak::hashv, program::invoke, pubkey::Pubkey, system_instruction, sysvar, + sysvar::instructions::load_instruction_at_checked, sysvar::SysvarId, }, }; -use anchor_spl::token::{Mint, TokenAccount, Token, Transfer, transfer}; -use spl_token::native_mint; +use anchor_spl::token::{transfer, Mint, Token, TokenAccount, Transfer}; 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 bubblegum::state::metaplex_adapter::MetadataArgs; use bytemuck::cast_slice_mut; use gummyroll::program::Gummyroll; +use spl_token::native_mint; pub mod state; pub mod utils; @@ -33,6 +25,7 @@ pub struct InitGumballMachine<'info> { /// CHECK: Validation occurs in instruction #[account(zero)] gumball_machine: AccountInfo<'info>, + #[account(mut)] creator: Signer<'info>, mint: Account<'info, Mint>, /// CHECK: Mint/append authority to the merkle slab @@ -41,6 +34,9 @@ pub struct InitGumballMachine<'info> { bump, )] willy_wonka: AccountInfo<'info>, + /// CHECK: Tree nonce, checked in Bubblegum + #[account(mut)] + nonce: UncheckedAccount<'info>, /// CHECK: Tree authority to the merkle slab, PDA owned by BubbleGum bubblegum_authority: AccountInfo<'info>, gummyroll: Program<'info, Gummyroll>, @@ -48,6 +44,7 @@ pub struct InitGumballMachine<'info> { #[account(zero)] merkle_slab: AccountInfo<'info>, bubblegum: Program<'info, Bubblegum>, + system_program: Program<'info, System>, } #[derive(Accounts)] @@ -97,7 +94,7 @@ pub struct DispenseSol<'info> { bubblegum_authority: AccountInfo<'info>, /// CHECK: PDA is checked in Bubblegum #[account(mut)] - nonce: AccountInfo<'info>, + nonce: UncheckedAccount<'info>, gummyroll: Program<'info, Gummyroll>, /// CHECK: Validation occurs in Gummyroll #[account(mut)] @@ -112,7 +109,7 @@ pub struct DispenseToken<'info> { gumball_machine: AccountInfo<'info>, payer: Signer<'info>, - + #[account(mut)] payer_tokens: Account<'info, TokenAccount>, @@ -137,7 +134,7 @@ pub struct DispenseToken<'info> { bubblegum_authority: AccountInfo<'info>, /// CHECK: PDA is checked in Bubblegum #[account(mut)] - nonce: AccountInfo<'info>, + nonce: UncheckedAccount<'info>, gummyroll: Program<'info, Gummyroll>, /// CHECK: Validation occurs in Gummyroll #[account(mut)] @@ -157,9 +154,11 @@ pub struct Destroy<'info> { #[inline(always)] // Bots may try to buy only valuable NFTs by sending instructions to dispense an NFT along with // instructions that fail if they do not get the one that they want. We prevent this by forcing -// all transactions that hit the "dispense" functions to have a single instruction body, and +// all transactions that hit the "dispense" functions to have a single instruction body, and // that the call to "dispense" is the top level of the single instruction (not a CPI) -fn assert_valid_single_instruction_transaction<'info>(instruction_sysvar_account: &AccountInfo<'info>) -> Result<()> { +fn assert_valid_single_instruction_transaction<'info>( + instruction_sysvar_account: &AccountInfo<'info>, +) -> Result<()> { // There should only be one instruction in this transaction (the current call to dispense_...) let instruction_sysvar = instruction_sysvar_account.try_borrow_data()?; let mut fixed_data = [0u8; 2]; @@ -170,7 +169,7 @@ fn assert_valid_single_instruction_transaction<'info>(instruction_sysvar_account // We should not be executing dispense... from a CPI let only_instruction = load_instruction_at_checked(0, instruction_sysvar_account)?; assert_eq!(only_instruction.program_id, id()); - return Ok(()) + return Ok(()); } #[inline(always)] @@ -181,8 +180,8 @@ fn fisher_yates_shuffle_and_fetch_nft_metadata<'info>( gumball_header: &mut GumballMachineHeader, indices: &mut [u32], line_size: usize, - config_lines_data: &mut [u8] -) -> Result<(MetadataArgs)> { + config_lines_data: &mut [u8], +) -> Result { // Get 8 bytes of entropy from the SlotHashes sysvar let mut buf: [u8; 8] = [0; 8]; buf.copy_from_slice( @@ -233,9 +232,8 @@ fn find_and_mint_compressed_nft<'info>( gummyroll: &Program<'info, Gummyroll>, merkle_slab: &AccountInfo<'info>, bubblegum: &Program<'info, Bubblegum>, - num_items: u64 + num_items: u64, ) -> Result { - // Prevent atomic transaction exploit attacks // TODO: potentially record information about botting now as pretains to payments to bot_wallet assert_valid_single_instruction_transaction(instruction_sysvar_account)?; @@ -257,10 +255,18 @@ fn find_and_mint_compressed_nft<'info>( // TODO: Validate data - let mut indices = cast_slice_mut::(indices_data); - 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 indices = cast_slice_mut::(indices_data); + 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]]; @@ -352,10 +358,13 @@ pub mod gumball_machine { let cpi_ctx = CpiContext::new_with_signer( ctx.accounts.bubblegum.to_account_info(), bubblegum::cpi::accounts::CreateTree { + nonce: ctx.accounts.nonce.to_account_info(), tree_creator: ctx.accounts.willy_wonka.to_account_info(), authority: ctx.accounts.bubblegum_authority.to_account_info(), gummyroll_program: ctx.accounts.gummyroll.to_account_info(), merkle_slab: ctx.accounts.merkle_slab.to_account_info(), + payer: ctx.accounts.creator.to_account_info(), + system_program: ctx.accounts.system_program.to_account_info(), }, authority_pda_signer, ); @@ -373,7 +382,6 @@ 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::() * size; - 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 as usize; @@ -488,7 +496,7 @@ pub mod gumball_machine { } /// Request to purchase a random NFT from GumballMachine for a specific project. - /// @notice: the project must have specified the native mint (Wrapped SOL) for "mint" + /// @notice: the project must have specified the native mint (Wrapped SOL) for "mint" /// in its GumballMachineHeader for this method to succeed. If mint is anything /// else dispense_nft_token should be used. pub fn dispense_nft_sol(ctx: Context, num_items: u64) -> Result<()> { @@ -504,7 +512,7 @@ pub mod gumball_machine { &ctx.accounts.gummyroll, &ctx.accounts.merkle_slab, &ctx.accounts.bubblegum, - num_items + num_items, )?; // Process payment for NFT @@ -518,13 +526,13 @@ pub mod gumball_machine { &system_instruction::transfer( &ctx.accounts.payer.key(), &ctx.accounts.receiver.key(), - gumball_header.price + gumball_header.price, ), &[ ctx.accounts.payer.to_account_info(), ctx.accounts.receiver.to_account_info(), - ctx.accounts.system_program.to_account_info() - ] + ctx.accounts.system_program.to_account_info(), + ], )?; Ok(()) @@ -547,7 +555,7 @@ pub mod gumball_machine { &ctx.accounts.gummyroll, &ctx.accounts.merkle_slab, &ctx.accounts.bubblegum, - num_items + num_items, )?; // Process payment for NFT @@ -560,9 +568,9 @@ pub mod gumball_machine { from: ctx.accounts.payer_tokens.to_account_info(), to: ctx.accounts.receiver.to_account_info(), authority: ctx.accounts.payer.to_account_info(), - } + }, ), - gumball_header.price + gumball_header.price, )?; Ok(()) diff --git a/contracts/sdk/bubblegum/idl/bubblegum.json b/contracts/sdk/bubblegum/idl/bubblegum.json index d2d08e05237..1411f777901 100644 --- a/contracts/sdk/bubblegum/idl/bubblegum.json +++ b/contracts/sdk/bubblegum/idl/bubblegum.json @@ -3,7 +3,7 @@ "name": "bubblegum", "instructions": [ { - "name": "initializeNonce", + "name": "createTree", "accounts": [ { "name": "nonce", @@ -15,22 +15,16 @@ "isMut": true, "isSigner": true }, - { - "name": "systemProgram", - "isMut": false, - "isSigner": false - } - ], - "args": [] - }, - { - "name": "createTree", - "accounts": [ { "name": "treeCreator", "isMut": false, "isSigner": true }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false + }, { "name": "authority", "isMut": false, diff --git a/contracts/sdk/bubblegum/src/generated/accounts/Nonce.ts b/contracts/sdk/bubblegum/src/generated/accounts/Nonce.ts index 0b4fbc5031a..b09ce1b9036 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 } -const nonceDiscriminator = [143, 197, 147, 95, 106, 165, 50, 43] +export 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 335a1c3fc72..eb2fb758c42 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 } -const voucherDiscriminator = [191, 204, 149, 234, 213, 165, 13, 65] +export 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 diff --git a/contracts/sdk/bubblegum/src/generated/instructions/createTree.ts b/contracts/sdk/bubblegum/src/generated/instructions/createTree.ts index f794dd828a2..d31ebd58b7f 100644 --- a/contracts/sdk/bubblegum/src/generated/instructions/createTree.ts +++ b/contracts/sdk/bubblegum/src/generated/instructions/createTree.ts @@ -37,6 +37,8 @@ export const createTreeStruct = new beet.BeetArgsStruct< /** * Accounts required by the _createTree_ instruction * + * @property [_writable_] nonce + * @property [_writable_, **signer**] payer * @property [**signer**] treeCreator * @property [] authority * @property [] gummyrollProgram @@ -46,6 +48,8 @@ export const createTreeStruct = new beet.BeetArgsStruct< * @category generated */ export type CreateTreeInstructionAccounts = { + nonce: web3.PublicKey + payer: web3.PublicKey treeCreator: web3.PublicKey authority: web3.PublicKey gummyrollProgram: web3.PublicKey @@ -70,18 +74,34 @@ export function createCreateTreeInstruction( accounts: CreateTreeInstructionAccounts, args: CreateTreeInstructionArgs ) { - const { treeCreator, authority, gummyrollProgram, merkleSlab } = accounts + const { nonce, payer, treeCreator, authority, gummyrollProgram, merkleSlab } = + accounts const [data] = createTreeStruct.serialize({ instructionDiscriminator: createTreeInstructionDiscriminator, ...args, }) const keys: web3.AccountMeta[] = [ + { + pubkey: nonce, + isWritable: true, + isSigner: false, + }, + { + pubkey: payer, + isWritable: true, + isSigner: true, + }, { pubkey: treeCreator, isWritable: false, isSigner: true, }, + { + pubkey: web3.SystemProgram.programId, + isWritable: false, + isSigner: false, + }, { pubkey: authority, isWritable: false, diff --git a/contracts/sdk/bubblegum/src/generated/instructions/index.ts b/contracts/sdk/bubblegum/src/generated/instructions/index.ts index 2ba9192b72c..067ede8488d 100644 --- a/contracts/sdk/bubblegum/src/generated/instructions/index.ts +++ b/contracts/sdk/bubblegum/src/generated/instructions/index.ts @@ -4,7 +4,6 @@ export * from './compress' export * from './createTree' export * from './decompress' export * from './delegate' -export * from './initializeNonce' export * from './mint' export * from './redeem' export * from './transfer' diff --git a/contracts/sdk/bubblegum/src/generated/instructions/initializeNonce.ts b/contracts/sdk/bubblegum/src/generated/instructions/initializeNonce.ts deleted file mode 100644 index bde9fbd1e24..00000000000 --- a/contracts/sdk/bubblegum/src/generated/instructions/initializeNonce.ts +++ /dev/null @@ -1,82 +0,0 @@ -/** - * 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 InitializeNonce - * @category generated - */ -export const initializeNonceStruct = new beet.BeetArgsStruct<{ - instructionDiscriminator: number[] /* size: 8 */ -}>( - [['instructionDiscriminator', beet.uniformFixedSizeArray(beet.u8, 8)]], - 'InitializeNonceInstructionArgs' -) -/** - * Accounts required by the _initializeNonce_ instruction - * - * @property [_writable_] nonce - * @property [_writable_, **signer**] payer - * @category Instructions - * @category InitializeNonce - * @category generated - */ -export type InitializeNonceInstructionAccounts = { - nonce: web3.PublicKey - payer: web3.PublicKey -} - -export const initializeNonceInstructionDiscriminator = [ - 64, 206, 214, 231, 20, 15, 231, 41, -] - -/** - * Creates a _InitializeNonce_ instruction. - * - * @param accounts that will be accessed while the instruction is processed - * @category Instructions - * @category InitializeNonce - * @category generated - */ -export function createInitializeNonceInstruction( - accounts: InitializeNonceInstructionAccounts -) { - const { nonce, payer } = accounts - - const [data] = initializeNonceStruct.serialize({ - instructionDiscriminator: initializeNonceInstructionDiscriminator, - }) - const keys: web3.AccountMeta[] = [ - { - pubkey: nonce, - isWritable: true, - isSigner: false, - }, - { - pubkey: payer, - isWritable: true, - isSigner: true, - }, - { - pubkey: web3.SystemProgram.programId, - isWritable: false, - isSigner: false, - }, - ] - - const ix = new web3.TransactionInstruction({ - programId: new web3.PublicKey( - 'BGUMzZr2wWfD2yzrXFEWTK2HbdYhqQCP2EZoPEkZBD6o' - ), - keys, - data, - }) - return ix -} diff --git a/contracts/sdk/gumball-machine/idl/gumball_machine.json b/contracts/sdk/gumball-machine/idl/gumball_machine.json index c101a58ad24..e487ff857f3 100644 --- a/contracts/sdk/gumball-machine/idl/gumball_machine.json +++ b/contracts/sdk/gumball-machine/idl/gumball_machine.json @@ -12,7 +12,7 @@ }, { "name": "creator", - "isMut": false, + "isMut": true, "isSigner": true }, { @@ -25,6 +25,11 @@ "isMut": false, "isSigner": false }, + { + "name": "nonce", + "isMut": true, + "isSigner": false + }, { "name": "bubblegumAuthority", "isMut": false, @@ -44,6 +49,11 @@ "name": "bubblegum", "isMut": false, "isSigner": false + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false } ], "args": [ diff --git a/contracts/sdk/gumball-machine/instructions/index.ts b/contracts/sdk/gumball-machine/instructions/index.ts index 6f323925e72..f2694f5cb58 100644 --- a/contracts/sdk/gumball-machine/instructions/index.ts +++ b/contracts/sdk/gumball-machine/instructions/index.ts @@ -34,10 +34,11 @@ export async function createInitializeGumballMachineIxs( merkleRollAccountSize: number, gumballMachineInitArgs: InitializeGumballMachineInstructionArgs, mint: PublicKey, + noncePDAKey: PublicKey, gummyrollProgramId: PublicKey, bubblegumProgramId: PublicKey, gumballMachine: Program -): Promise<[TransactionInstruction]> { +): Promise { const allocGumballMachineAcctInstr = SystemProgram.createAccount({ fromPubkey: payer.publicKey, newAccountPubkey: gumballMachineAcctKeypair.publicKey, @@ -68,6 +69,7 @@ export async function createInitializeGumballMachineIxs( gumballMachine: gumballMachineAcctKeypair.publicKey, creator: payer.publicKey, mint, + nonce: noncePDAKey, willyWonka: willyWonkaPDAKey, bubblegumAuthority: bubblegumAuthorityPDAKey, gummyroll: gummyrollProgramId, diff --git a/contracts/sdk/gumball-machine/src/generated/instructions/initializeGumballMachine.ts b/contracts/sdk/gumball-machine/src/generated/instructions/initializeGumballMachine.ts index 62935efd427..484f32ec1f4 100644 --- a/contracts/sdk/gumball-machine/src/generated/instructions/initializeGumballMachine.ts +++ b/contracts/sdk/gumball-machine/src/generated/instructions/initializeGumballMachine.ts @@ -69,9 +69,10 @@ export const initializeGumballMachineStruct = new beet.BeetArgsStruct< * Accounts required by the _initializeGumballMachine_ instruction * * @property [_writable_] gumballMachine - * @property [**signer**] creator + * @property [_writable_, **signer**] creator * @property [] mint * @property [] willyWonka + * @property [_writable_] nonce * @property [] bubblegumAuthority * @property [] gummyroll * @property [_writable_] merkleSlab @@ -85,6 +86,7 @@ export type InitializeGumballMachineInstructionAccounts = { creator: web3.PublicKey mint: web3.PublicKey willyWonka: web3.PublicKey + nonce: web3.PublicKey bubblegumAuthority: web3.PublicKey gummyroll: web3.PublicKey merkleSlab: web3.PublicKey @@ -114,6 +116,7 @@ export function createInitializeGumballMachineInstruction( creator, mint, willyWonka, + nonce, bubblegumAuthority, gummyroll, merkleSlab, @@ -132,7 +135,7 @@ export function createInitializeGumballMachineInstruction( }, { pubkey: creator, - isWritable: false, + isWritable: true, isSigner: true, }, { @@ -145,6 +148,11 @@ export function createInitializeGumballMachineInstruction( isWritable: false, isSigner: false, }, + { + pubkey: nonce, + isWritable: true, + isSigner: false, + }, { pubkey: bubblegumAuthority, isWritable: false, @@ -165,6 +173,11 @@ export function createInitializeGumballMachineInstruction( isWritable: false, isSigner: false, }, + { + pubkey: web3.SystemProgram.programId, + isWritable: false, + isSigner: false, + }, ] const ix = new web3.TransactionInstruction({ diff --git a/contracts/sdk/indexer/db.ts b/contracts/sdk/indexer/db.ts index ed6a1f91cc0..cfd840ad899 100644 --- a/contracts/sdk/indexer/db.ts +++ b/contracts/sdk/indexer/db.ts @@ -64,7 +64,7 @@ export class NFTDatabaseConnection { }); } - async updateChangeLogs(changeLog: ChangeLogEvent, txId: string) { + async updateChangeLogs(changeLog: ChangeLogEvent, txId: string, treeId: string) { console.log("Update Change Log"); if (changeLog.seq == 0) { return; @@ -73,10 +73,11 @@ export class NFTDatabaseConnection { this.connection.run( ` INSERT INTO - merkle(transaction_id, node_idx, seq, level, hash) - VALUES (?, ?, ?, ?, ?) + merkle(transaction_id, tree_id, node_idx, seq, level, hash) + VALUES (?, ?, ?, ?, ?, ?) `, txId, + treeId, pathNode.index, changeLog.seq, i, @@ -85,13 +86,14 @@ export class NFTDatabaseConnection { } } - async updateLeafSchema(leafSchema: LeafSchema, leafHash: PublicKey, txId: string) { + async updateLeafSchema(leafSchema: LeafSchema, leafHash: PublicKey, txId: string, treeId: string) { console.log("Update Leaf Schema"); this.connection.run( ` INSERT INTO leaf_schema( nonce, + tree_id, transaction_id, owner, delegate, @@ -99,8 +101,8 @@ export class NFTDatabaseConnection { creator_hash, leaf_hash ) - VALUES (?, ?, ?, ?, ?, ?, ?) - ON CONFLICT (nonce) + VALUES (?, ?, ?, ?, ?, ?, ?, ?) + ON CONFLICT (nonce, tree_id) DO UPDATE SET owner = excluded.owner, delegate = excluded.delegate, @@ -109,6 +111,7 @@ export class NFTDatabaseConnection { leaf_hash = excluded.leaf_hash `, (leafSchema.nonce.valueOf() as BN).toNumber(), + treeId, txId, leafSchema.owner.toBase58(), leafSchema.delegate.toBase58(), @@ -118,7 +121,7 @@ export class NFTDatabaseConnection { ); } - async updateNFTMetadata(newLeafEvent: NewLeafEvent, nonce: bignum) { + async updateNFTMetadata(newLeafEvent: NewLeafEvent, nonce: bignum, treeId: string) { console.log("Update NFT"); const uri = newLeafEvent.metadata.uri; const name = newLeafEvent.metadata.name; @@ -143,6 +146,7 @@ export class NFTDatabaseConnection { INSERT INTO nft( nonce, + tree_id, uri, name, symbol, @@ -165,8 +169,8 @@ export class NFTDatabaseConnection { share4, verified4 ) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) - ON CONFLICT (nonce) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + ON CONFLICT (nonce, tree_id) DO UPDATE SET uri = excluded.uri, name = excluded.name, @@ -176,6 +180,7 @@ export class NFTDatabaseConnection { is_mutable = excluded.is_mutable `, (nonce as BN).toNumber(), + treeId, uri, name, symbol, @@ -279,7 +284,7 @@ export class NFTDatabaseConnection { return leafIdxs; } - async getProof(hash: Buffer, check: boolean = true): Promise { + async getProof(hash: Buffer, treeId: string, check: boolean = true): Promise { let hashString = bs58.encode(hash); let res = await this.connection.all( ` @@ -293,11 +298,12 @@ export class NFTDatabaseConnection { max(m.seq) as seq FROM merkle m JOIN leaf_schema l - ON m.hash = l.leaf_hash - WHERE hash = ? and level = 0 + ON m.hash = l.leaf_hash and m.tree_id = l.tree_id + WHERE hash = ? and m.tree_id = ? and level = 0 GROUP BY node_idx `, - hashString + hashString, + treeId, ); if (res.length == 1) { let data = res[0] @@ -397,6 +403,7 @@ export class NFTDatabaseConnection { let rawNftMetadata = await this.connection.all( ` SELECT + ls.tree_id as treeId, ls.nonce as nonce, n.uri as uri, n.name as name, @@ -422,7 +429,7 @@ export class NFTDatabaseConnection { n.verified4 as verified4 FROM leaf_schema ls JOIN nft n - ON ls.nonce = n.nonce + ON ls.nonce = n.nonce and ls.tree_id = n.tree_id WHERE owner = ? `, owner @@ -467,6 +474,7 @@ export class NFTDatabaseConnection { } assets.push({ nonce: metadata.nonce, + treeId: metadata.treeId, uri: metadata.uri, name: metadata.name, symbol: metadata.symbol, @@ -512,6 +520,7 @@ export async function bootstrap( ` CREATE TABLE IF NOT EXISTS merkle ( id INTEGER PRIMARY KEY, + tree_id TEXT, transaction_id TEXT, node_idx INT, seq INT, @@ -524,7 +533,8 @@ export async function bootstrap( await db.run( ` CREATE TABLE IF NOT EXISTS nft ( - nonce BIGINT PRIMARY KEY, + tree_id TEXT, + nonce BIGINT, name TEXT, symbol TEXT, uri TEXT, @@ -545,30 +555,23 @@ export async function bootstrap( verified3 BOOLEAN, creator4 TEXT, share4 INT, - verified4 BOOLEAN + verified4 BOOLEAN, + PRIMARY KEY (tree_id, nonce) ); ` ); await db.run( ` CREATE TABLE IF NOT EXISTS leaf_schema ( - nonce BIGINT PRIMARY KEY, + tree_id TEXT, + nonce BIGINT, transaction_id TEXT, owner TEXT, delegate TEXT, data_hash TEXT, creator_hash TEXT, - leaf_hash TEXT - ); - ` - ); - await db.run( - ` - CREATE TABLE IF NOT EXISTS creators ( - nonce BIGINT, - creator TEXT, - share INT, - verifed BOOLEAN + leaf_hash TEXT, + PRIMARY KEY (tree_id, nonce) ); ` ); diff --git a/contracts/sdk/indexer/indexer/bubblegum.ts b/contracts/sdk/indexer/indexer/bubblegum.ts index 3faaa84628d..db201aa6723 100644 --- a/contracts/sdk/indexer/indexer/bubblegum.ts +++ b/contracts/sdk/indexer/indexer/bubblegum.ts @@ -92,13 +92,15 @@ export function parseBubblegumMint( .data as NewLeafEvent; const leafSchema = parseEventFromLog(logs[2] as string, parser.Bubblegum.idl) .data as LeafSchema; - db.updateNFTMetadata(newLeafData, leafSchema.nonce); + let treeId = changeLog.id.toBase58(); + db.updateNFTMetadata(newLeafData, leafSchema.nonce, treeId); db.updateLeafSchema( leafSchema, new PublicKey(changeLog.path[0].node), - optionalInfo.txId + optionalInfo.txId, + treeId, ); - db.updateChangeLogs(changeLog, optionalInfo.txId); + db.updateChangeLogs(changeLog, optionalInfo.txId, treeId); } export function parseBubblegumTransfer( @@ -110,12 +112,14 @@ export function parseBubblegumTransfer( const changeLog = findGummyrollEvent(logs, parser); const leafSchema = parseEventFromLog(logs[1] as string, parser.Bubblegum.idl) .data as LeafSchema; + let treeId = changeLog.id.toBase58(); db.updateLeafSchema( leafSchema, new PublicKey(changeLog.path[0].node), - optionalInfo.txId + optionalInfo.txId, + treeId ); - db.updateChangeLogs(changeLog, optionalInfo.txId); + db.updateChangeLogs(changeLog, optionalInfo.txId, treeId); } export function parseBubblegumCreateTree( @@ -125,7 +129,8 @@ export function parseBubblegumCreateTree( optionalInfo: OptionalInfo ) { const changeLog = findGummyrollEvent(logs, parser); - db.updateChangeLogs(changeLog, optionalInfo.txId); + let treeId = changeLog.id.toBase58(); + db.updateChangeLogs(changeLog, optionalInfo.txId, treeId); } export function parseBubblegumDelegate( @@ -137,12 +142,14 @@ export function parseBubblegumDelegate( const changeLog = findGummyrollEvent(logs, parser); const leafSchema = parseEventFromLog(logs[1] as string, parser.Bubblegum.idl) .data as LeafSchema; + let treeId = changeLog.id.toBase58(); db.updateLeafSchema( leafSchema, new PublicKey(changeLog.path[0].node), - optionalInfo.txId + optionalInfo.txId, + treeId ); - db.updateChangeLogs(changeLog, optionalInfo.txId); + db.updateChangeLogs(changeLog, optionalInfo.txId, treeId); } export function parseBubblegumRedeem( @@ -152,7 +159,8 @@ export function parseBubblegumRedeem( optionalInfo: OptionalInfo ) { const changeLog = findGummyrollEvent(logs, parser); - db.updateChangeLogs(changeLog, optionalInfo.txId); + let treeId = changeLog.id.toBase58(); + db.updateChangeLogs(changeLog, optionalInfo.txId, treeId); } export function parseBubblegumCancelRedeem( @@ -164,12 +172,14 @@ export function parseBubblegumCancelRedeem( const changeLog = findGummyrollEvent(logs, parser); const leafSchema = parseEventFromLog(logs[1] as string, parser.Bubblegum.idl) .data as LeafSchema; + let treeId = changeLog.id.toBase58(); db.updateLeafSchema( leafSchema, new PublicKey(changeLog.path[0].node), - optionalInfo.txId + optionalInfo.txId, + treeId ); - db.updateChangeLogs(changeLog, optionalInfo.txId); + db.updateChangeLogs(changeLog, optionalInfo.txId, treeId); } export function parseBubblegumDecompress( diff --git a/contracts/sdk/indexer/indexer/utils.ts b/contracts/sdk/indexer/indexer/utils.ts index cf040be175b..ee9e982c9f8 100644 --- a/contracts/sdk/indexer/indexer/utils.ts +++ b/contracts/sdk/indexer/indexer/utils.ts @@ -1,108 +1,119 @@ -import * as anchor from '@project-serum/anchor'; -import { PublicKey } from '@solana/web3.js'; -import { readFileSync } from 'fs'; -import { Bubblegum } from '../../../target/types/bubblegum'; -import { Gummyroll } from '../../../target/types/gummyroll'; +import * as anchor from "@project-serum/anchor"; +import { PublicKey } from "@solana/web3.js"; +import { readFileSync } from "fs"; +import { Bubblegum } from "../../../target/types/bubblegum"; +import { Gummyroll } from "../../../target/types/gummyroll"; const startRegEx = /Program (\w*) invoke \[(\d)\]/; const endRegEx = /Program (\w*) success/; -const dataRegEx = /Program data: ((?:[A-Za-z\d+/]{4})*(?:[A-Za-z\d+/]{3}=|[A-Za-z\d+/]{2}==)?$)/; +const dataRegEx = + /Program data: ((?:[A-Za-z\d+/]{4})*(?:[A-Za-z\d+/]{3}=|[A-Za-z\d+/]{2}==)?$)/; export const ixRegEx = /Program log: Instruction: (\w+)/; export type ParserState = { - Gummyroll: anchor.Program, - Bubblegum: anchor.Program, -} + Gummyroll: anchor.Program; + Bubblegum: anchor.Program; +}; export type ParsedLog = { - programId: PublicKey, - logs: (string | ParsedLog)[] - depth: number, -} + programId: PublicKey; + logs: (string | ParsedLog)[]; + depth: number; +}; export type OptionalInfo = { - txId: string -} + txId: string; +}; /** * Recursively parses the logs of a program instruction execution - * @param programId - * @param depth - * @param logs - * @returns + * @param programId + * @param depth + * @param logs + * @returns */ -function parseInstructionLog(programId: PublicKey, depth: number, logs: string[]) { - const parsedLog: ParsedLog = { - programId, - depth, - logs: [], - } - let instructionComplete = false; - while (!instructionComplete) { - const logLine = logs[0]; - logs = logs.slice(1); - let result = logLine.match(endRegEx) - if (result) { - if (result[1] != programId.toString()) { - throw Error(`Unexpected program id finished: ${result[1]}`) - } - instructionComplete = true; - } else { - result = logLine.match(startRegEx) - if (result) { - const programId = new PublicKey(result[1]); - const depth = Number(result[2]) - 1; - const parsedInfo = parseInstructionLog(programId, depth, logs); - parsedLog.logs.push(parsedInfo.parsedLog); - logs = parsedInfo.logs; - } else { - parsedLog.logs.push(logLine); - } - } +function parseInstructionLog( + programId: PublicKey, + depth: number, + logs: string[] +) { + const parsedLog: ParsedLog = { + programId, + depth, + logs: [], + }; + let instructionComplete = false; + while (!instructionComplete) { + const logLine = logs[0]; + logs = logs.slice(1); + let result = logLine.match(endRegEx); + if (result) { + if (result[1] != programId.toString()) { + throw Error(`Unexpected program id finished: ${result[1]}`); + } + instructionComplete = true; + } else { + result = logLine.match(startRegEx); + if (result) { + const programId = new PublicKey(result[1]); + const depth = Number(result[2]) - 1; + const parsedInfo = parseInstructionLog(programId, depth, logs); + parsedLog.logs.push(parsedInfo.parsedLog); + logs = parsedInfo.logs; + } else { + parsedLog.logs.push(logLine); + } } - return { parsedLog, logs }; + } + return { parsedLog, logs }; } /** - * Parses logs so that emitted event data can be tied to its execution context - * @param logs - * @returns + * Parses logs so that emitted event data can be tied to its execution context + * @param logs + * @returns */ export function parseLogs(logs: string[]): ParsedLog[] { - let parsedLogs: ParsedLog[] = []; - while (logs && logs.length) { - const logLine = logs[0]; - logs = logs.slice(1); - const result = logLine.match(startRegEx); - const programId = new PublicKey(result[1]); - const depth = Number(result[2]) - 1; - const parsedInfo = parseInstructionLog(programId, depth, logs) - parsedLogs.push(parsedInfo.parsedLog); - logs = parsedInfo.logs; - } - return parsedLogs; + let parsedLogs: ParsedLog[] = []; + while (logs && logs.length) { + const logLine = logs[0]; + logs = logs.slice(1); + const result = logLine.match(startRegEx); + const programId = new PublicKey(result[1]); + const depth = Number(result[2]) - 1; + const parsedInfo = parseInstructionLog(programId, depth, logs); + parsedLogs.push(parsedInfo.parsedLog); + logs = parsedInfo.logs; + } + return parsedLogs; } -export function parseEventFromLog(log: string, idl: anchor.Idl): anchor.Event | null { - return decodeEvent(log.match(dataRegEx)[1], idl) +export function parseEventFromLog( + log: string, + idl: anchor.Idl +): anchor.Event | null { + return decodeEvent(log.match(dataRegEx)[1], idl); } /** - * Example: + * Example: * ``` * let event = decodeEvent(dataString, Gummyroll.idl) ?? decodeEvent(dataString, Bubblegum.idl); * ``` - * @param data - * @param idl - * @returns + * @param data + * @param idl + * @returns */ function decodeEvent(data: string, idl: anchor.Idl): anchor.Event | null { - let eventCoder = new anchor.BorshEventCoder(idl); - return eventCoder.decode(data); + let eventCoder = new anchor.BorshEventCoder(idl); + return eventCoder.decode(data); } -export function loadProgram(provider: anchor.Provider, programId: PublicKey, idlPath: string) { - const IDL = JSON.parse(readFileSync(idlPath).toString()); - return new anchor.Program(IDL, programId, provider) +export function loadProgram( + provider: anchor.Provider, + programId: PublicKey, + idlPath: string +) { + const IDL = JSON.parse(readFileSync(idlPath).toString()); + return new anchor.Program(IDL, programId, provider); } - diff --git a/contracts/sdk/indexer/server.ts b/contracts/sdk/indexer/server.ts index cf67c45de9f..09642cf7b60 100644 --- a/contracts/sdk/indexer/server.ts +++ b/contracts/sdk/indexer/server.ts @@ -36,10 +36,11 @@ function stringifyProof(proof: Proof): string { app.get("/proof", async (req, res) => { const leafHashString = req.query.leafHash; + const treeId = req.query.treeId; console.log("GET request:", leafHashString); const nftDb = await bootstrap(false); const leafHash: Buffer = bs58.decode(leafHashString); - const proof = await nftDb.getProof(leafHash, false); + const proof = await nftDb.getProof(leafHash, treeId, false); res.send(stringifyProof(proof)); }); diff --git a/contracts/sdk/indexer/smokeTest.ts b/contracts/sdk/indexer/smokeTest.ts index bdad4a1a1b2..af7f3695550 100644 --- a/contracts/sdk/indexer/smokeTest.ts +++ b/contracts/sdk/indexer/smokeTest.ts @@ -1,9 +1,6 @@ import { Program, web3 } from "@project-serum/anchor"; import fetch from "node-fetch"; -import { - getMerkleRollAccountSize, - Gummyroll, -} from "../gummyroll"; +import { getMerkleRollAccountSize, Gummyroll } from "../gummyroll"; import * as anchor from "@project-serum/anchor"; import { AccountMeta, @@ -16,13 +13,13 @@ import NodeWallet from "@project-serum/anchor/dist/cjs/nodewallet"; import { Bubblegum } from "../../target/types/bubblegum"; import { createCreateTreeInstruction, - createInitializeNonceInstruction, createMintInstruction, createTransferInstruction, TokenProgramVersion, Version, } from "../bubblegum/src/generated"; import { bs58 } from "@project-serum/anchor/dist/cjs/utils/bytes"; +import { logTx } from "../../tests/utils"; async function main() { const connection = new web3.Connection("http://127.0.0.1:8899", { @@ -98,44 +95,34 @@ async function main() { [merkleRollKeypair.publicKey.toBuffer()], BubblegumCtx.programId ); + let [nonce] = await PublicKey.findProgramAddress( + [Buffer.from("bubblegum"), merkleRollKeypair.publicKey.toBuffer()], + BubblegumCtx.programId + ); let createTreeIx = createCreateTreeInstruction( { treeCreator: payer.publicKey, + payer: payer.publicKey, authority: authority, gummyrollProgram: GummyrollCtx.programId, merkleSlab: merkleRollKeypair.publicKey, + nonce: nonce, }, { maxDepth, maxBufferSize: maxSize, } ); - let [nonce] = await PublicKey.findProgramAddress( - [Buffer.from("bubblegum")], - BubblegumCtx.programId - ); let tx = new Transaction(); - try { - const nonceAccount = await BubblegumCtx.provider.connection.getAccountInfo( - nonce - ); - if (nonceAccount.data.length === 0 || nonceAccount.lamports === 0) { - throw new Error("Nonce account not yet initialized"); - } - } catch { - // Only initialize the nonce if it does not exist - const initNonceIx = BubblegumCtx.instruction.initializeNonce({ - accounts: { - nonce: nonce, - payer: payer.publicKey, - systemProgram: SystemProgram.programId, - }, - signers: [payer], - }); - tx = tx.add(initNonceIx); - } tx = tx.add(allocAccountIx).add(createTreeIx); - await GummyrollCtx.provider.send(tx, [payer, merkleRollKeypair]); + let txId = await BubblegumCtx.provider.connection.sendTransaction( + tx, + [payer, merkleRollKeypair], + { + skipPreflight: true, + } + ); + await logTx(BubblegumCtx.provider, txId); let numMints = 0; while (1) { let i = Math.floor(Math.random() * wallets.length); @@ -192,7 +179,7 @@ async function main() { } let k = Math.floor(Math.random() * assets.length); response = await fetch( - `${proofServerUrl}?leafHash=${assets[k].leafHash}`, + `${proofServerUrl}?leafHash=${assets[k].leafHash}&treeId=${assets[k].treeId}`, { method: "GET" } ); const proof = await response.json(); @@ -203,13 +190,17 @@ async function main() { isSigner: false, }; }); + let [merkleAuthority] = await PublicKey.findProgramAddress( + [bs58.decode(assets[k].treeId)], + BubblegumCtx.programId + ); let replaceIx = createTransferInstruction( { owner: wallets[i].publicKey, delegate: new PublicKey(proof.delegate), newOwner: wallets[j].publicKey, - authority: authority, - merkleSlab: merkleRollKeypair.publicKey, + authority: merkleAuthority, + merkleSlab: new PublicKey(assets[k].treeId), gummyrollProgram: GummyrollCtx.programId, }, { @@ -226,7 +217,15 @@ async function main() { let tx = new Transaction().add(replaceIx); await BubblegumCtx.provider.connection .sendTransaction(tx, [wallets[i]]) - .then(() => console.log("Successfully transferred asset")) + .then(() => + console.log( + `Successfully transferred asset (${assets[k].leafHash} from tree: ${ + assets[k].treeId + }) - ${wallets[i].publicKey.toBase58()} -> ${wallets[ + j + ].publicKey.toBase58()}` + ) + ) .catch((e) => console.log("Encountered Error when transferring", e)); } } diff --git a/contracts/tests/bubblegum-test.ts b/contracts/tests/bubblegum-test.ts index fcc32b6685a..67e04007ba7 100644 --- a/contracts/tests/bubblegum-test.ts +++ b/contracts/tests/bubblegum-test.ts @@ -124,6 +124,10 @@ describe("bubblegum", () => { [merkleRollKeypair.publicKey.toBuffer()], Bubblegum.programId ); + let [nonce] = await PublicKey.findProgramAddress( + [Buffer.from("bubblegum"), merkleRollKeypair.publicKey.toBuffer()], + Bubblegum.programId + ); const initGummyrollIx = Bubblegum.instruction.createTree( MAX_DEPTH, @@ -131,9 +135,12 @@ describe("bubblegum", () => { { accounts: { treeCreator: payer.publicKey, + payer: payer.publicKey, + nonce: nonce, authority: authority, gummyrollProgram: GummyrollProgramId, merkleSlab: merkleRollKeypair.publicKey, + systemProgram: SystemProgram.programId, }, signers: [payer], } @@ -143,29 +150,6 @@ describe("bubblegum", () => { .add(allocAccountIx) .add(initGummyrollIx); - let [nonce] = await PublicKey.findProgramAddress( - [Buffer.from("bubblegum")], - Bubblegum.programId - ); - try { - const nonceAccount = await Bubblegum.provider.connection.getAccountInfo( - nonce - ); - if (nonceAccount.data.length === 0 || nonceAccount.lamports === 0) { - throw new Error("Nonce account not yet initialized"); - } - } catch { - // Only initialize the nonce if it does not exist - const initNonceIx = Bubblegum.instruction.initializeNonce({ - accounts: { - nonce: nonce, - payer: payer.publicKey, - systemProgram: SystemProgram.programId, - }, - signers: [payer], - }); - tx = tx.add(initNonceIx); - } await Bubblegum.provider.send(tx, [payer, merkleRollKeypair], { commitment: "confirmed", diff --git a/contracts/tests/continuous_gummyroll-test.ts b/contracts/tests/continuous_gummyroll-test.ts index c66caa0753f..59d7ff54cc8 100644 --- a/contracts/tests/continuous_gummyroll-test.ts +++ b/contracts/tests/continuous_gummyroll-test.ts @@ -30,7 +30,7 @@ function chunk(arr: T[], size: number): T[][] { ); } -describe("gummyroll-continuous", () => { +describe.skip("gummyroll-continuous", () => { let connection: web3Connection; let wallet: NodeWallet; let offChainTree: ReturnType; diff --git a/contracts/tests/gumball-machine-test.ts b/contracts/tests/gumball-machine-test.ts index 37846eff8d9..b068a0e62c3 100644 --- a/contracts/tests/gumball-machine-test.ts +++ b/contracts/tests/gumball-machine-test.ts @@ -14,7 +14,7 @@ import { assert } from "chai"; import { buildTree } from "./merkle-tree"; import { getMerkleRollAccountSize, - assertOnChainMerkleRollProperties + assertOnChainMerkleRollProperties, } from "../sdk/gummyroll"; import { GumballMachine, @@ -24,7 +24,7 @@ import { createDispenseNFTForTokensIx, createInitializeGumballMachineIxs, getBubblegumAuthorityPDAKey, -} from '../sdk/gumball-machine'; +} from "../sdk/gumball-machine"; import { InitializeGumballMachineInstructionArgs, createAddConfigLinesInstruction, @@ -34,20 +34,19 @@ import { createUpdateHeaderMetadataInstruction, createDestroyInstruction, } from "../sdk/gumball-machine/src/generated/instructions"; -import { - val, - strToByteArray, - strToByteUint8Array -} from "../sdk/utils/index"; +import { val, strToByteArray, strToByteUint8Array } from "../sdk/utils/index"; import { GumballMachineHeader, - gumballMachineHeaderBeet + gumballMachineHeaderBeet, } from "../sdk/gumball-machine/src/generated/types/index"; import NodeWallet from "@project-serum/anchor/dist/cjs/nodewallet"; -import { createMint, getOrCreateAssociatedTokenAccount, mintTo, getAccount } from "../../deps/solana-program-library/token/js/src"; import { - NATIVE_MINT -} from "@solana/spl-token"; + createMint, + getOrCreateAssociatedTokenAccount, + mintTo, + getAccount, +} from "../../deps/solana-program-library/token/js/src"; +import { NATIVE_MINT } from "@solana/spl-token"; import { logTx, num32ToBuffer, arrayEquals } from "./utils"; // @ts-ignore @@ -61,7 +60,7 @@ describe("gumball-machine", () => { // Configure the client to use the local cluster. const payer = Keypair.generate(); - + let connection = new web3Connection("http://localhost:8899", { commitment: "confirmed", }); @@ -78,8 +77,11 @@ describe("gumball-machine", () => { Bubblegum = anchor.workspace.Bubblegum as Program; GummyrollProgramId = anchor.workspace.Gummyroll.programId; BubblegumProgramId = anchor.workspace.Bubblegum.programId; - - function assertGumballMachineHeaderProperties(gm: OnChainGumballMachine, expectedHeader: GumballMachineHeader) { + + function assertGumballMachineHeaderProperties( + gm: OnChainGumballMachine, + expectedHeader: GumballMachineHeader + ) { assert( arrayEquals(gm.header.urlBase, expectedHeader.urlBase), "Gumball Machine has incorrect url base" @@ -154,20 +156,32 @@ describe("gumball-machine", () => { ); } - function assertGumballMachineConfigProperties(gm: OnChainGumballMachine, expectedIndexArray: Buffer, expectedConfigLines: Buffer, onChainConfigLinesNumBytes: number) { + function assertGumballMachineConfigProperties( + gm: OnChainGumballMachine, + expectedIndexArray: Buffer, + expectedConfigLines: Buffer, + onChainConfigLinesNumBytes: number + ) { assert( gm.configData.indexArray.equals(expectedIndexArray), "Onchain index array doesn't match expectation" - ) + ); // Calculate full-sized on-chain config bytes buffer, we must null pad the buffer up to the end of the account size - const numExpectedInitializedBytesInConfig = expectedConfigLines.byteLength - const bufferOfNonInitializedConfigLineBytes = Buffer.from("\0".repeat(onChainConfigLinesNumBytes-numExpectedInitializedBytesInConfig)) - const actualExpectedConfigLinesBuffer = Buffer.concat([expectedConfigLines, bufferOfNonInitializedConfigLineBytes]) + const numExpectedInitializedBytesInConfig = expectedConfigLines.byteLength; + const bufferOfNonInitializedConfigLineBytes = Buffer.from( + "\0".repeat( + onChainConfigLinesNumBytes - numExpectedInitializedBytesInConfig + ) + ); + const actualExpectedConfigLinesBuffer = Buffer.concat([ + expectedConfigLines, + bufferOfNonInitializedConfigLineBytes, + ]); assert( gm.configData.configLines.equals(actualExpectedConfigLinesBuffer), "Config lines on gumball machine do not match expectation" - ) + ); } async function initializeGumballMachine( @@ -177,33 +191,67 @@ describe("gumball-machine", () => { merkleRollKeypair: Keypair, merkleRollAccountSize: number, gumballMachineInitArgs: InitializeGumballMachineInstructionArgs, - mint: PublicKey + mint: PublicKey, + nonce: PublicKey ) { - const bubblegumAuthorityPDAKey = await getBubblegumAuthorityPDAKey(merkleRollKeypair.publicKey, BubblegumProgramId); - const initializeGumballMachineInstrs = await createInitializeGumballMachineIxs(payer, gumballMachineAcctKeypair, gumballMachineAcctSize, merkleRollKeypair, merkleRollAccountSize, gumballMachineInitArgs, mint, GummyrollProgramId, BubblegumProgramId, GumballMachine); + const bubblegumAuthorityPDAKey = await getBubblegumAuthorityPDAKey( + merkleRollKeypair.publicKey, + BubblegumProgramId + ); + const initializeGumballMachineInstrs = + await createInitializeGumballMachineIxs( + payer, + gumballMachineAcctKeypair, + gumballMachineAcctSize, + merkleRollKeypair, + merkleRollAccountSize, + gumballMachineInitArgs, + mint, + nonce, + GummyrollProgramId, + BubblegumProgramId, + GumballMachine + ); const tx = new Transaction(); initializeGumballMachineInstrs.forEach((instr) => tx.add(instr)); - await GumballMachine.provider.send(tx, [payer, gumballMachineAcctKeypair, merkleRollKeypair], { - commitment: "confirmed", - }); + await GumballMachine.provider.send( + tx, + [payer, gumballMachineAcctKeypair, merkleRollKeypair], + { + commitment: "confirmed", + } + ); - const tree = buildTree(Array(2 ** gumballMachineInitArgs.maxDepth).fill(Buffer.alloc(32))); - await assertOnChainMerkleRollProperties(GumballMachine.provider.connection, gumballMachineInitArgs.maxDepth, gumballMachineInitArgs.maxBufferSize, bubblegumAuthorityPDAKey, new PublicKey(tree.root), merkleRollKeypair.publicKey); + const tree = buildTree( + Array(2 ** gumballMachineInitArgs.maxDepth).fill(Buffer.alloc(32)) + ); + await assertOnChainMerkleRollProperties( + GumballMachine.provider.connection, + gumballMachineInitArgs.maxDepth, + gumballMachineInitArgs.maxBufferSize, + bubblegumAuthorityPDAKey, + new PublicKey(tree.root), + merkleRollKeypair.publicKey + ); - const onChainGumballMachineAccount = await GumballMachine.provider.connection.getAccountInfo( - gumballMachineAcctKeypair.publicKey + const onChainGumballMachineAccount = + await GumballMachine.provider.connection.getAccountInfo( + gumballMachineAcctKeypair.publicKey + ); + + const gumballMachine = decodeGumballMachine( + onChainGumballMachineAccount.data, + gumballMachineAcctSize ); - const gumballMachine = decodeGumballMachine(onChainGumballMachineAccount.data, gumballMachineAcctSize); - let expectedOnChainHeader: GumballMachineHeader = { urlBase: gumballMachineInitArgs.urlBase, nameBase: gumballMachineInitArgs.nameBase, - symbol: gumballMachineInitArgs.symbol, + symbol: gumballMachineInitArgs.symbol, sellerFeeBasisPoints: gumballMachineInitArgs.sellerFeeBasisPoints, isMutable: gumballMachineInitArgs.isMutable ? 1 : 0, retainAuthority: gumballMachineInitArgs.retainAuthority ? 1 : 0, - padding: [0,0,0,0], + padding: [0, 0, 0, 0], price: gumballMachineInitArgs.price, goLiveDate: gumballMachineInitArgs.goLiveDate, mint, @@ -216,8 +264,8 @@ describe("gumball-machine", () => { maxMintSize: gumballMachineInitArgs.maxMintSize, remaining: new BN(0), maxItems: gumballMachineInitArgs.maxItems, - totalItemsAdded: new BN(0) - } + totalItemsAdded: new BN(0), + }; assertGumballMachineHeaderProperties(gumballMachine, expectedOnChainHeader); } @@ -233,28 +281,40 @@ describe("gumball-machine", () => { const addConfigLinesInstr = createAddConfigLinesInstruction( { gumballMachine: gumballMachineAcctKey, - authority: authority.publicKey + authority: authority.publicKey, }, { - newConfigLinesData: configLinesToAdd + newConfigLinesData: configLinesToAdd, } ); - const tx = new Transaction().add(addConfigLinesInstr) + const tx = new Transaction().add(addConfigLinesInstr); await GumballMachine.provider.send(tx, [authority], { commitment: "confirmed", }); - const onChainGumballMachineAccount = await GumballMachine.provider.connection.getAccountInfo( - gumballMachineAcctKey + const onChainGumballMachineAccount = + await GumballMachine.provider.connection.getAccountInfo( + gumballMachineAcctKey + ); + const gumballMachine = decodeGumballMachine( + onChainGumballMachineAccount.data, + gumballMachineAcctSize ); - const gumballMachine = decodeGumballMachine(onChainGumballMachineAccount.data, gumballMachineAcctSize); // Create the expected buffer for the indices of the account - const expectedIndexArrBuffer = [...Array(gumballMachineAcctConfigIndexArrSize/4).keys()].reduce( - (prevVal, curVal) => Buffer.concat([prevVal, Buffer.from(num32ToBuffer(curVal))]), + const expectedIndexArrBuffer = [ + ...Array(gumballMachineAcctConfigIndexArrSize / 4).keys(), + ].reduce( + (prevVal, curVal) => + Buffer.concat([prevVal, Buffer.from(num32ToBuffer(curVal))]), Buffer.from([]) ); - assertGumballMachineConfigProperties(gumballMachine, expectedIndexArrBuffer, allExpectedInitializedConfigLines, gumballMachineAcctConfigLinesSize); + assertGumballMachineConfigProperties( + gumballMachine, + expectedIndexArrBuffer, + allExpectedInitializedConfigLines, + gumballMachineAcctConfigLinesSize + ); } async function updateConfigLines( @@ -267,30 +327,45 @@ describe("gumball-machine", () => { allExpectedInitializedConfigLines: Buffer, indexOfFirstLineToUpdate: BN ) { - const args: UpdateConfigLinesInstructionArgs = { startingLine: indexOfFirstLineToUpdate, newConfigLinesData: updatedConfigLines }; + const args: UpdateConfigLinesInstructionArgs = { + startingLine: indexOfFirstLineToUpdate, + newConfigLinesData: updatedConfigLines, + }; const updateConfigLinesInstr = createUpdateConfigLinesInstruction( { - authority: authority.publicKey, - gumballMachine: gumballMachineAcctKey, + authority: authority.publicKey, + gumballMachine: gumballMachineAcctKey, }, args ); - const tx = new Transaction().add(updateConfigLinesInstr) + const tx = new Transaction().add(updateConfigLinesInstr); await GumballMachine.provider.send(tx, [authority], { commitment: "confirmed", }); - const onChainGumballMachineAccount = await GumballMachine.provider.connection.getAccountInfo( - gumballMachineAcctKey + const onChainGumballMachineAccount = + await GumballMachine.provider.connection.getAccountInfo( + gumballMachineAcctKey + ); + const gumballMachine = decodeGumballMachine( + onChainGumballMachineAccount.data, + gumballMachineAcctSize ); - const gumballMachine = decodeGumballMachine(onChainGumballMachineAccount.data, gumballMachineAcctSize); - + // Create the expected buffer for the indices of the account - const expectedIndexArrBuffer = [...Array(gumballMachineAcctConfigIndexArrSize/4).keys()].reduce( - (prevVal, curVal) => Buffer.concat([prevVal, Buffer.from(num32ToBuffer(curVal))]), + const expectedIndexArrBuffer = [ + ...Array(gumballMachineAcctConfigIndexArrSize / 4).keys(), + ].reduce( + (prevVal, curVal) => + Buffer.concat([prevVal, Buffer.from(num32ToBuffer(curVal))]), Buffer.from([]) - ) - assertGumballMachineConfigProperties(gumballMachine, expectedIndexArrBuffer, allExpectedInitializedConfigLines, gumballMachineAcctConfigLinesSize); + ); + assertGumballMachineConfigProperties( + gumballMachine, + expectedIndexArrBuffer, + allExpectedInitializedConfigLines, + gumballMachineAcctConfigLinesSize + ); } async function updateHeaderMetadata( @@ -303,19 +378,26 @@ describe("gumball-machine", () => { const updateHeaderMetadataInstr = createUpdateHeaderMetadataInstruction( { gumballMachine: gumballMachineAcctKey, - authority: authority.publicKey + authority: authority.publicKey, }, newHeader ); - const tx = new Transaction().add(updateHeaderMetadataInstr) + const tx = new Transaction().add(updateHeaderMetadataInstr); await GumballMachine.provider.send(tx, [authority], { commitment: "confirmed", }); - const onChainGumballMachineAccount = await GumballMachine.provider.connection.getAccountInfo( - gumballMachineAcctKey + const onChainGumballMachineAccount = + await GumballMachine.provider.connection.getAccountInfo( + gumballMachineAcctKey + ); + const gumballMachine = decodeGumballMachine( + onChainGumballMachineAccount.data, + gumballMachineAcctSize + ); + assertGumballMachineHeaderProperties( + gumballMachine, + resultingExpectedOnChainHeader ); - const gumballMachine = decodeGumballMachine(onChainGumballMachineAccount.data, gumballMachineAcctSize); - assertGumballMachineHeaderProperties(gumballMachine, resultingExpectedOnChainHeader); } async function dispenseCompressedNFTForSol( @@ -340,7 +422,7 @@ describe("gumball-machine", () => { const tx = new Transaction().add(dispenseInstr); await GumballMachine.provider.send(tx, [payer], { commitment: "confirmed", - }); + }); } async function dispenseCompressedNFTForTokens( @@ -367,34 +449,38 @@ describe("gumball-machine", () => { const tx = new Transaction().add(dispenseInstr); await GumballMachine.provider.send(tx, [payer], { commitment: "confirmed", - }); + }); } async function destroyGumballMachine( gumballMachineAcctKeypair: Keypair, authorityKeypair: Keypair ) { - const originalGumballMachineAcctBalance = await connection.getBalance(gumballMachineAcctKeypair.publicKey); - const originalAuthorityAcctBalance = await connection.getBalance(authorityKeypair.publicKey); - const destroyInstr = createDestroyInstruction( - { - gumballMachine: gumballMachineAcctKeypair.publicKey, - authority: authorityKeypair.publicKey - } + const originalGumballMachineAcctBalance = await connection.getBalance( + gumballMachineAcctKeypair.publicKey + ); + const originalAuthorityAcctBalance = await connection.getBalance( + authorityKeypair.publicKey ); + const destroyInstr = createDestroyInstruction({ + gumballMachine: gumballMachineAcctKeypair.publicKey, + authority: authorityKeypair.publicKey, + }); const tx = new Transaction().add(destroyInstr); await GumballMachine.provider.send(tx, [authorityKeypair], { commitment: "confirmed", }); assert( - 0 === await connection.getBalance(gumballMachineAcctKeypair.publicKey), + 0 === (await connection.getBalance(gumballMachineAcctKeypair.publicKey)), "Failed to remove lamports from gumball machine acct" ); - const expectedAuthorityAcctBalance = originalAuthorityAcctBalance + originalGumballMachineAcctBalance + const expectedAuthorityAcctBalance = + originalAuthorityAcctBalance + originalGumballMachineAcctBalance; assert( - expectedAuthorityAcctBalance === await connection.getBalance(authorityKeypair.publicKey), + expectedAuthorityAcctBalance === + (await connection.getBalance(authorityKeypair.publicKey)), "Failed to transfer correct balance to authority" ); } @@ -408,37 +494,21 @@ describe("gumball-machine", () => { let nftBuyer: Keypair; const GUMBALL_MACHINE_ACCT_CONFIG_INDEX_ARRAY_SIZE = 1000; const GUMBALL_MACHINE_ACCT_CONFIG_LINES_SIZE = 7000; - const GUMBALL_MACHINE_ACCT_SIZE = gumballMachineHeaderBeet.byteSize + GUMBALL_MACHINE_ACCT_CONFIG_INDEX_ARRAY_SIZE + GUMBALL_MACHINE_ACCT_CONFIG_LINES_SIZE; - const MERKLE_ROLL_ACCT_SIZE = getMerkleRollAccountSize(3,8); + const GUMBALL_MACHINE_ACCT_SIZE = + gumballMachineHeaderBeet.byteSize + + GUMBALL_MACHINE_ACCT_CONFIG_INDEX_ARRAY_SIZE + + GUMBALL_MACHINE_ACCT_CONFIG_LINES_SIZE; + const MERKLE_ROLL_ACCT_SIZE = getMerkleRollAccountSize(3, 8); before(async () => { - // Give funds to the payer for the whole suite await GumballMachine.provider.connection.confirmTransaction( - await GumballMachine.provider.connection.requestAirdrop(payer.publicKey, 25e9), + await GumballMachine.provider.connection.requestAirdrop( + payer.publicKey, + 25e9 + ), "confirmed" ); - - [noncePDAKey] = await PublicKey.findProgramAddress( - [Buffer.from("bubblegum")], - BubblegumProgramId - ); - - // Attempt to initialize the nonce account. Since localnet is not torn down between suites, - // there is some shared state. Specifically, the Bubblegum suite may initialize this account - // if it is run first. Thus even in the case of an error, we proceed. - try { - await Bubblegum.rpc.initializeNonce({ - accounts: { - nonce: noncePDAKey, - payer: payer.publicKey, - systemProgram: SystemProgram.programId, - }, - signers: [payer], - }); - } catch(e) { - console.log("Bubblegum nonce PDA already initialized by other suite") - } }); describe("native sol projects", async () => { @@ -450,12 +520,18 @@ 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, - urlBase: strToByteArray("https://arweave.net/Rmg4pcIv-0FQ7M7X838p2r592Q4NU63Fj7o7XsvBHEEl"), + urlBase: strToByteArray( + "https://arweave.net/Rmg4pcIv-0FQ7M7X838p2r592Q4NU63Fj7o7XsvBHEEl" + ), nameBase: strToByteArray("zfgfsxrwieciemyavrpkuqehkmhqmnim"), - symbol: strToByteArray("12345678"), + symbol: strToByteArray("12345678"), sellerFeeBasisPoints: 100, isMutable: true, retainAuthority: true, @@ -469,27 +545,53 @@ describe("gumball-machine", () => { maxMintSize: new BN(10), maxItems: new BN(250), }; - + // Give creator enough funds to produce accounts for NFT await GumballMachine.provider.connection.confirmTransaction( - await GumballMachine.provider.connection.requestAirdrop(creatorAddress.publicKey, LAMPORTS_PER_SOL), + await GumballMachine.provider.connection.requestAirdrop( + creatorAddress.publicKey, + LAMPORTS_PER_SOL + ), "confirmed" ); - - await initializeGumballMachine(creatorAddress, gumballMachineAcctKeypair, GUMBALL_MACHINE_ACCT_SIZE, merkleRollKeypair, MERKLE_ROLL_ACCT_SIZE, baseGumballMachineInitProps, NATIVE_MINT); - await addConfigLines(creatorAddress, gumballMachineAcctKeypair.publicKey, GUMBALL_MACHINE_ACCT_SIZE, GUMBALL_MACHINE_ACCT_CONFIG_INDEX_ARRAY_SIZE, GUMBALL_MACHINE_ACCT_CONFIG_LINES_SIZE, strToByteUint8Array("uluvnpwncgchwnbqfpbtdlcpdthc"), Buffer.from("uluvnpwncgchwnbqfpbtdlcpdthc")); + + await initializeGumballMachine( + creatorAddress, + gumballMachineAcctKeypair, + GUMBALL_MACHINE_ACCT_SIZE, + merkleRollKeypair, + MERKLE_ROLL_ACCT_SIZE, + baseGumballMachineInitProps, + NATIVE_MINT, + noncePDAKey + ); + await addConfigLines( + creatorAddress, + gumballMachineAcctKeypair.publicKey, + GUMBALL_MACHINE_ACCT_SIZE, + GUMBALL_MACHINE_ACCT_CONFIG_INDEX_ARRAY_SIZE, + GUMBALL_MACHINE_ACCT_CONFIG_LINES_SIZE, + strToByteUint8Array("uluvnpwncgchwnbqfpbtdlcpdthc"), + Buffer.from("uluvnpwncgchwnbqfpbtdlcpdthc") + ); }); describe("dispense nft sol instruction", async () => { beforeEach(async () => { // Give the recipient address enough money to not get rent exempt await GumballMachine.provider.connection.confirmTransaction( - await GumballMachine.provider.connection.requestAirdrop(baseGumballMachineInitProps.receiver, LAMPORTS_PER_SOL), + await GumballMachine.provider.connection.requestAirdrop( + baseGumballMachineInitProps.receiver, + LAMPORTS_PER_SOL + ), "confirmed" ); - + // Fund the NFT Buyer await GumballMachine.provider.connection.confirmTransaction( - await GumballMachine.provider.connection.requestAirdrop(nftBuyer.publicKey, LAMPORTS_PER_SOL), + await GumballMachine.provider.connection.requestAirdrop( + nftBuyer.publicKey, + LAMPORTS_PER_SOL + ), "confirmed" ); }); @@ -499,7 +601,17 @@ describe("gumball-machine", () => { let dummyInstr; beforeEach(async () => { - dispenseNFTForSolInstr = await createDispenseNFTForSolIx(new BN(1), nftBuyer, baseGumballMachineInitProps.receiver, gumballMachineAcctKeypair, merkleRollKeypair, noncePDAKey, GummyrollProgramId, BubblegumProgramId, GumballMachine); + dispenseNFTForSolInstr = await createDispenseNFTForSolIx( + new BN(1), + nftBuyer, + baseGumballMachineInitProps.receiver, + gumballMachineAcctKeypair, + merkleRollKeypair, + noncePDAKey, + GummyrollProgramId, + BubblegumProgramId, + GumballMachine + ); dummyNewAcctKeypair = Keypair.generate(); dummyInstr = SystemProgram.createAccount({ fromPubkey: payer.publicKey, @@ -510,53 +622,96 @@ describe("gumball-machine", () => { }); }); it("Cannot dispense NFT for SOL with subsequent instructions in transaction", async () => { - const tx = new Transaction().add(dispenseNFTForSolInstr).add(dummyInstr); + const tx = new Transaction() + .add(dispenseNFTForSolInstr) + .add(dummyInstr); try { - await GumballMachine.provider.send(tx, [nftBuyer, payer, dummyNewAcctKeypair], { - commitment: "confirmed", - }) - assert(false, "Dispense should fail when part of transaction with multiple instructions, but it succeeded"); - } catch(e) {} + await GumballMachine.provider.send( + tx, + [nftBuyer, payer, dummyNewAcctKeypair], + { + commitment: "confirmed", + } + ); + assert( + false, + "Dispense should fail when part of transaction with multiple instructions, but it succeeded" + ); + } catch (e) {} }); it("Cannot dispense NFT for SOL with prior instructions in transaction", async () => { - const tx = new Transaction().add(dummyInstr).add(dispenseNFTForSolInstr); + const tx = new Transaction() + .add(dummyInstr) + .add(dispenseNFTForSolInstr); try { - await GumballMachine.provider.send(tx, [nftBuyer, payer, dummyNewAcctKeypair], { - commitment: "confirmed", - }) - assert(false, "Dispense should fail when part of transaction with multiple instructions, but it succeeded"); - } catch(e) {} + await GumballMachine.provider.send( + tx, + [nftBuyer, payer, dummyNewAcctKeypair], + { + commitment: "confirmed", + } + ); + assert( + false, + "Dispense should fail when part of transaction with multiple instructions, but it succeeded" + ); + } catch (e) {} }); }); it("Can dispense single NFT paid in sol", async () => { // Give the recipient address enough money to not get rent exempt await GumballMachine.provider.connection.confirmTransaction( - await GumballMachine.provider.connection.requestAirdrop(baseGumballMachineInitProps.receiver, LAMPORTS_PER_SOL), + await GumballMachine.provider.connection.requestAirdrop( + baseGumballMachineInitProps.receiver, + LAMPORTS_PER_SOL + ), "confirmed" ); - + // Fund the NFT Buyer await GumballMachine.provider.connection.confirmTransaction( - await GumballMachine.provider.connection.requestAirdrop(nftBuyer.publicKey, LAMPORTS_PER_SOL), + await GumballMachine.provider.connection.requestAirdrop( + nftBuyer.publicKey, + LAMPORTS_PER_SOL + ), "confirmed" ); - - const nftBuyerBalanceBeforePurchase = await connection.getBalance(nftBuyer.publicKey); - const creatorBalanceBeforePurchase = await connection.getBalance(baseGumballMachineInitProps.receiver); - + + const nftBuyerBalanceBeforePurchase = await connection.getBalance( + nftBuyer.publicKey + ); + const creatorBalanceBeforePurchase = await connection.getBalance( + baseGumballMachineInitProps.receiver + ); + // Purchase the compressed NFT with SOL - await dispenseCompressedNFTForSol(new BN(1), nftBuyer, baseGumballMachineInitProps.receiver, gumballMachineAcctKeypair, merkleRollKeypair, noncePDAKey); - const nftBuyerBalanceAfterPurchase = await connection.getBalance(nftBuyer.publicKey); - const creatorBalanceAfterPurchase = await connection.getBalance(baseGumballMachineInitProps.receiver); - + await dispenseCompressedNFTForSol( + new BN(1), + nftBuyer, + baseGumballMachineInitProps.receiver, + gumballMachineAcctKeypair, + merkleRollKeypair, + noncePDAKey + ); + const nftBuyerBalanceAfterPurchase = await connection.getBalance( + nftBuyer.publicKey + ); + const creatorBalanceAfterPurchase = await connection.getBalance( + baseGumballMachineInitProps.receiver + ); + // Assert on how the creator and buyer's balances changed assert( - await creatorBalanceAfterPurchase === (creatorBalanceBeforePurchase + val(baseGumballMachineInitProps.price).toNumber()), + (await creatorBalanceAfterPurchase) === + creatorBalanceBeforePurchase + + val(baseGumballMachineInitProps.price).toNumber(), "Creator balance did not update as expected after NFT purchase" ); - + assert( - await nftBuyerBalanceAfterPurchase === (nftBuyerBalanceBeforePurchase - val(baseGumballMachineInitProps.price).toNumber()), + (await nftBuyerBalanceAfterPurchase) === + nftBuyerBalanceBeforePurchase - + val(baseGumballMachineInitProps.price).toNumber(), "NFT purchaser balance did not decrease as expected after NFT purchase" ); }); @@ -564,13 +719,24 @@ describe("gumball-machine", () => { // @notice: We only test admin instructions on SOL projects because they are completely (for now) independent of project mint describe("admin instructions", async () => { it("Can update config lines", async () => { - await updateConfigLines(creatorAddress, gumballMachineAcctKeypair.publicKey, GUMBALL_MACHINE_ACCT_SIZE, GUMBALL_MACHINE_ACCT_CONFIG_INDEX_ARRAY_SIZE, GUMBALL_MACHINE_ACCT_CONFIG_LINES_SIZE, Buffer.from("aaavnpwncgchwnbqfpbtdlcpdaaa"), Buffer.from("aaavnpwncgchwnbqfpbtdlcpdaaa"), new BN(0)); + await updateConfigLines( + creatorAddress, + gumballMachineAcctKeypair.publicKey, + GUMBALL_MACHINE_ACCT_SIZE, + GUMBALL_MACHINE_ACCT_CONFIG_INDEX_ARRAY_SIZE, + GUMBALL_MACHINE_ACCT_CONFIG_LINES_SIZE, + Buffer.from("aaavnpwncgchwnbqfpbtdlcpdaaa"), + Buffer.from("aaavnpwncgchwnbqfpbtdlcpdaaa"), + new BN(0) + ); }); it("Can update gumball header", async () => { const newGumballMachineHeader: UpdateHeaderMetadataInstructionArgs = { - urlBase: strToByteArray("https://arweave.net/bzdjillretjcraaxawlnhqrhmexzbsixyajrlzhfcvcc"), + urlBase: strToByteArray( + "https://arweave.net/bzdjillretjcraaxawlnhqrhmexzbsixyajrlzhfcvcc" + ), nameBase: strToByteArray("wmqeslreeondhmcmtfebrwqnqcoasbye"), - symbol: strToByteArray("abcdefgh"), + symbol: strToByteArray("abcdefgh"), sellerFeeBasisPoints: 50, isMutable: false, retainAuthority: false, @@ -578,17 +744,17 @@ describe("gumball-machine", () => { goLiveDate: new BN(5678.0), botWallet: Keypair.generate().publicKey, authority: Keypair.generate().publicKey, - maxMintSize: new BN(15) + maxMintSize: new BN(15), }; const expectedOnChainHeader: GumballMachineHeader = { urlBase: newGumballMachineHeader.urlBase, nameBase: newGumballMachineHeader.nameBase, - symbol: newGumballMachineHeader.symbol, + symbol: newGumballMachineHeader.symbol, sellerFeeBasisPoints: newGumballMachineHeader.sellerFeeBasisPoints, isMutable: newGumballMachineHeader.isMutable ? 1 : 0, retainAuthority: newGumballMachineHeader.retainAuthority ? 1 : 0, - padding: [0,0,0,0], + padding: [0, 0, 0, 0], price: newGumballMachineHeader.price, goLiveDate: newGumballMachineHeader.goLiveDate, mint: NATIVE_MINT, @@ -601,12 +767,21 @@ describe("gumball-machine", () => { maxMintSize: newGumballMachineHeader.maxMintSize, remaining: new BN(0), maxItems: baseGumballMachineInitProps.maxItems, - totalItemsAdded: new BN(0) - } - await updateHeaderMetadata(creatorAddress, gumballMachineAcctKeypair.publicKey, GUMBALL_MACHINE_ACCT_SIZE, newGumballMachineHeader, expectedOnChainHeader); + totalItemsAdded: new BN(0), + }; + await updateHeaderMetadata( + creatorAddress, + gumballMachineAcctKeypair.publicKey, + GUMBALL_MACHINE_ACCT_SIZE, + newGumballMachineHeader, + expectedOnChainHeader + ); }); it("Can destroy gumball machine and reclaim lamports", async () => { - await destroyGumballMachine(gumballMachineAcctKeypair, creatorAddress); + await destroyGumballMachine( + gumballMachineAcctKeypair, + creatorAddress + ); }); }); }); @@ -623,21 +798,41 @@ 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(creatorAddress.publicKey, 4 * LAMPORTS_PER_SOL), + await GumballMachine.provider.connection.requestAirdrop( + creatorAddress.publicKey, + 4 * LAMPORTS_PER_SOL + ), "confirmed" ); - someMint = await createMint(connection, payer, payer.publicKey, null, 9); - creatorReceiverTokenAccount = await getOrCreateAssociatedTokenAccount(connection, payer, someMint, creatorAddress.publicKey); + someMint = await createMint( + connection, + payer, + payer.publicKey, + null, + 9 + ); + creatorReceiverTokenAccount = await getOrCreateAssociatedTokenAccount( + connection, + payer, + someMint, + creatorAddress.publicKey + ); baseGumballMachineInitProps = { maxDepth: 3, maxBufferSize: 8, - urlBase: strToByteArray("https://arweave.net/Rmg4pcIv-0FQ7M7X838p2r592Q4NU63Fj7o7XsvBHEEl"), + urlBase: strToByteArray( + "https://arweave.net/Rmg4pcIv-0FQ7M7X838p2r592Q4NU63Fj7o7XsvBHEEl" + ), nameBase: strToByteArray("zfgfsxrwieciemyavrpkuqehkmhqmnim"), - symbol: strToByteArray("12345678"), + symbol: strToByteArray("12345678"), sellerFeeBasisPoints: 100, isMutable: true, retainAuthority: true, @@ -651,13 +846,46 @@ describe("gumball-machine", () => { maxMintSize: new BN(10), maxItems: new BN(250), }; - - await initializeGumballMachine(creatorAddress, gumballMachineAcctKeypair, GUMBALL_MACHINE_ACCT_SIZE, merkleRollKeypair, MERKLE_ROLL_ACCT_SIZE, baseGumballMachineInitProps, someMint); - await addConfigLines(creatorAddress, gumballMachineAcctKeypair.publicKey, GUMBALL_MACHINE_ACCT_SIZE, GUMBALL_MACHINE_ACCT_CONFIG_INDEX_ARRAY_SIZE, GUMBALL_MACHINE_ACCT_CONFIG_LINES_SIZE, Buffer.from("uluvnpwncgchwnbqfpbtdlcpdthc" + "aauvnpwncgchwnbqfpbtdlcpdthc"), Buffer.from("uluvnpwncgchwnbqfpbtdlcpdthc" + "aauvnpwncgchwnbqfpbtdlcpdthc")); + + await initializeGumballMachine( + creatorAddress, + gumballMachineAcctKeypair, + GUMBALL_MACHINE_ACCT_SIZE, + merkleRollKeypair, + MERKLE_ROLL_ACCT_SIZE, + baseGumballMachineInitProps, + someMint, + noncePDAKey, + ); + await addConfigLines( + creatorAddress, + gumballMachineAcctKeypair.publicKey, + GUMBALL_MACHINE_ACCT_SIZE, + GUMBALL_MACHINE_ACCT_CONFIG_INDEX_ARRAY_SIZE, + GUMBALL_MACHINE_ACCT_CONFIG_LINES_SIZE, + Buffer.from( + "uluvnpwncgchwnbqfpbtdlcpdthc" + "aauvnpwncgchwnbqfpbtdlcpdthc" + ), + Buffer.from( + "uluvnpwncgchwnbqfpbtdlcpdthc" + "aauvnpwncgchwnbqfpbtdlcpdthc" + ) + ); // Create and fund the NFT pruchaser - nftBuyerTokenAccount = await getOrCreateAssociatedTokenAccount(connection, payer, someMint, nftBuyer.publicKey); - await mintTo(connection, payer, someMint, nftBuyerTokenAccount.address, payer, 50); + nftBuyerTokenAccount = await getOrCreateAssociatedTokenAccount( + connection, + payer, + someMint, + nftBuyer.publicKey + ); + await mintTo( + connection, + payer, + someMint, + nftBuyerTokenAccount.address, + payer, + 50 + ); }); describe("transaction atomicity attacks fail", async () => { let dispenseNFTForTokensInstr; @@ -665,7 +893,18 @@ describe("gumball-machine", () => { let dummyInstr; beforeEach(async () => { - dispenseNFTForTokensInstr = await createDispenseNFTForTokensIx(new BN(1), nftBuyer, nftBuyerTokenAccount.address, creatorReceiverTokenAccount.address, gumballMachineAcctKeypair, merkleRollKeypair, noncePDAKey, GummyrollProgramId, BubblegumProgramId, GumballMachine); + dispenseNFTForTokensInstr = await createDispenseNFTForTokensIx( + new BN(1), + nftBuyer, + nftBuyerTokenAccount.address, + creatorReceiverTokenAccount.address, + gumballMachineAcctKeypair, + merkleRollKeypair, + noncePDAKey, + GummyrollProgramId, + BubblegumProgramId, + GumballMachine + ); dummyNewAcctKeypair = Keypair.generate(); dummyInstr = SystemProgram.createAccount({ fromPubkey: payer.publicKey, @@ -676,55 +915,112 @@ describe("gumball-machine", () => { }); }); it("Cannot dispense NFT for tokens with subsequent instructions in transaction", async () => { - const tx = new Transaction().add(dispenseNFTForTokensInstr).add(dummyInstr); + const tx = new Transaction() + .add(dispenseNFTForTokensInstr) + .add(dummyInstr); try { - await GumballMachine.provider.send(tx, [nftBuyer, payer, dummyNewAcctKeypair], { - commitment: "confirmed", - }) - assert(false, "Dispense should fail when part of transaction with multiple instructions, but it succeeded"); - } catch(e) {} + await GumballMachine.provider.send( + tx, + [nftBuyer, payer, dummyNewAcctKeypair], + { + commitment: "confirmed", + } + ); + assert( + false, + "Dispense should fail when part of transaction with multiple instructions, but it succeeded" + ); + } catch (e) {} }); it("Cannot dispense NFT for SOL with prior instructions in transaction", async () => { - const tx = new Transaction().add(dummyInstr).add(dispenseNFTForTokensInstr); + const tx = new Transaction() + .add(dummyInstr) + .add(dispenseNFTForTokensInstr); try { - await GumballMachine.provider.send(tx, [nftBuyer, payer, dummyNewAcctKeypair], { - commitment: "confirmed", - }) - assert(false, "Dispense should fail when part of transaction with multiple instructions, but it succeeded"); - } catch(e) {} + await GumballMachine.provider.send( + tx, + [nftBuyer, payer, dummyNewAcctKeypair], + { + commitment: "confirmed", + } + ); + assert( + false, + "Dispense should fail when part of transaction with multiple instructions, but it succeeded" + ); + } catch (e) {} }); }); it("Can dispense multiple NFTs paid in token", async () => { - let buyerTokenAccount = await getAccount(connection, nftBuyerTokenAccount.address); - await dispenseCompressedNFTForTokens(new BN(1), nftBuyer, nftBuyerTokenAccount.address, creatorReceiverTokenAccount.address, gumballMachineAcctKeypair, merkleRollKeypair, noncePDAKey); - - let newCreatorTokenAccount = await getAccount(connection, creatorReceiverTokenAccount.address); - let newBuyerTokenAccount = await getAccount(connection, nftBuyerTokenAccount.address); - + let buyerTokenAccount = await getAccount( + connection, + nftBuyerTokenAccount.address + ); + await dispenseCompressedNFTForTokens( + new BN(1), + nftBuyer, + nftBuyerTokenAccount.address, + creatorReceiverTokenAccount.address, + gumballMachineAcctKeypair, + merkleRollKeypair, + noncePDAKey + ); + + let newCreatorTokenAccount = await getAccount( + connection, + creatorReceiverTokenAccount.address + ); + let newBuyerTokenAccount = await getAccount( + connection, + nftBuyerTokenAccount.address + ); + assert( - Number(newCreatorTokenAccount.amount) === Number(creatorReceiverTokenAccount.amount) + val(baseGumballMachineInitProps.price).toNumber(), + Number(newCreatorTokenAccount.amount) === + Number(creatorReceiverTokenAccount.amount) + + val(baseGumballMachineInitProps.price).toNumber(), "The creator did not receive their payment as expected" ); - + assert( - Number(newBuyerTokenAccount.amount) === Number(buyerTokenAccount.amount) - val(baseGumballMachineInitProps.price).toNumber(), + Number(newBuyerTokenAccount.amount) === + Number(buyerTokenAccount.amount) - + val(baseGumballMachineInitProps.price).toNumber(), "The nft buyer did not pay for the nft as expected" ); - await dispenseCompressedNFTForTokens(new BN(1), nftBuyer, nftBuyerTokenAccount.address, creatorReceiverTokenAccount.address, gumballMachineAcctKeypair, merkleRollKeypair, noncePDAKey); + await dispenseCompressedNFTForTokens( + new BN(1), + nftBuyer, + nftBuyerTokenAccount.address, + creatorReceiverTokenAccount.address, + gumballMachineAcctKeypair, + merkleRollKeypair, + noncePDAKey + ); creatorReceiverTokenAccount = newCreatorTokenAccount; buyerTokenAccount = newBuyerTokenAccount; - newCreatorTokenAccount = await getAccount(connection, creatorReceiverTokenAccount.address); - newBuyerTokenAccount = await getAccount(connection, nftBuyerTokenAccount.address); - + newCreatorTokenAccount = await getAccount( + connection, + creatorReceiverTokenAccount.address + ); + newBuyerTokenAccount = await getAccount( + connection, + nftBuyerTokenAccount.address + ); + assert( - Number(newCreatorTokenAccount.amount) === Number(creatorReceiverTokenAccount.amount) + val(baseGumballMachineInitProps.price).toNumber(), + Number(newCreatorTokenAccount.amount) === + Number(creatorReceiverTokenAccount.amount) + + val(baseGumballMachineInitProps.price).toNumber(), "The creator did not receive their payment as expected" ); - + assert( - Number(newBuyerTokenAccount.amount) === Number(buyerTokenAccount.amount) - val(baseGumballMachineInitProps.price).toNumber(), + Number(newBuyerTokenAccount.amount) === + Number(buyerTokenAccount.amount) - + val(baseGumballMachineInitProps.price).toNumber(), "The nft buyer did not pay for the nft as expected" ); }); diff --git a/contracts/yarn.lock b/contracts/yarn.lock index fcdc0b5e7cf..2893801ef2e 100644 --- a/contracts/yarn.lock +++ b/contracts/yarn.lock @@ -89,6 +89,14 @@ "@metaplex-foundation/beet" ">=0.1.0" "@solana/web3.js" "^1.31.0" +"@metaplex-foundation/beet-solana@^0.2.0": + version "0.2.0" + resolved "https://registry.yarnpkg.com/@metaplex-foundation/beet-solana/-/beet-solana-0.2.0.tgz#7b32c3374ea421e133e3c2c11feea34574831194" + integrity sha512-swab167CcE2dEGpXKdh2h/SI0TntF/9J7qijQDwOtHSgCInu7KJqecpAcp3QYInaczTFlC6Om3GxsxLZllAugQ== + dependencies: + "@metaplex-foundation/beet" ">=0.1.0" + "@solana/web3.js" "^1.44.0" + "@metaplex-foundation/beet@>=0.1.0", "@metaplex-foundation/beet@^0.2.0": version "0.2.0" resolved "https://registry.yarnpkg.com/@metaplex-foundation/beet/-/beet-0.2.0.tgz#e543e17fd1c4dc1251e9aea481a7429bc73f70b8" @@ -107,6 +115,15 @@ bn.js "^5.2.0" debug "^4.3.3" +"@metaplex-foundation/beet@^0.3.0": + version "0.3.0" + resolved "https://registry.yarnpkg.com/@metaplex-foundation/beet/-/beet-0.3.0.tgz#e62dabbb93a9b4d0d87d5f1e5443c64c8c97abad" + integrity sha512-G/rNFbgK8py0PZe5oTHFNKzJTcPC859T4nV2ntDPQe8MHHlOv9SKjVShnYa0BH7uddJ9PYI+5mU8jX2G0mcxyw== + dependencies: + ansicolors "^0.3.2" + bn.js "^5.2.0" + debug "^4.3.3" + "@metaplex-foundation/cusper@^0.0.2": version "0.0.2" resolved "https://registry.npmjs.org/@metaplex-foundation/cusper/-/cusper-0.0.2.tgz" @@ -133,6 +150,32 @@ bn.js "^5.2.0" debug "^4.3.3" +"@metaplex-foundation/rustbin@^0.3.0": + version "0.3.1" + resolved "https://registry.yarnpkg.com/@metaplex-foundation/rustbin/-/rustbin-0.3.1.tgz#bbcd61e8699b73c0b062728c6f5e8d52e8145042" + integrity sha512-hWd2JPrnt2/nJzkBpZD3Y6ZfCUlJujv2K7qUfsxdS0jSwLrSrOvYwmNWFw6mc3lbULj6VP4WDyuy9W5/CHU/lQ== + dependencies: + debug "^4.3.3" + semver "^7.3.7" + text-table "^0.2.0" + toml "^3.0.0" + +"@metaplex-foundation/solita@^0.9.0": + version "0.9.0" + resolved "https://registry.yarnpkg.com/@metaplex-foundation/solita/-/solita-0.9.0.tgz#98ebf97ad44889ce19fbfafdc9d90f8679ac3299" + integrity sha512-qMBZxMKSTe3JR7EX3f9tDnG2F/4HN2eOEU6jM09dSoiUhN0Xtq2TZEiEZ6zZEUkEQLQinF5gIpEQIVDpWFGDMA== + dependencies: + "@metaplex-foundation/beet" "^0.3.0" + "@metaplex-foundation/beet-solana" "^0.2.0" + "@metaplex-foundation/rustbin" "^0.3.0" + "@solana/web3.js" "^1.36.0" + camelcase "^6.2.1" + debug "^4.3.3" + js-sha256 "^0.9.0" + prettier "^2.5.1" + snake-case "^3.0.4" + spok "^1.4.3" + "@npmcli/fs@^1.0.0": version "1.1.1" resolved "https://registry.npmjs.org/@npmcli/fs/-/fs-1.1.1.tgz" @@ -323,6 +366,28 @@ superstruct "^0.14.2" tweetnacl "^1.0.0" +"@solana/web3.js@^1.36.0", "@solana/web3.js@^1.44.0": + version "1.44.0" + resolved "https://registry.yarnpkg.com/@solana/web3.js/-/web3.js-1.44.0.tgz#233f7bd268520a0ce852ff7f92ded150c5fad0f5" + integrity sha512-KHf7o8sM5FlxYGHGroD7IJeCCOmjFITdBIXq4cO5xPFQ8O6Y26FWfYqIXqY1dXI29t240g0m1GYPssCp5UVgZg== + dependencies: + "@babel/runtime" "^7.12.5" + "@ethersproject/sha2" "^5.5.0" + "@solana/buffer-layout" "^4.0.0" + bigint-buffer "^1.1.5" + bn.js "^5.0.0" + borsh "^0.7.0" + bs58 "^4.0.1" + buffer "6.0.1" + fast-stable-stringify "^1.0.0" + jayson "^3.4.4" + js-sha3 "^0.8.0" + node-fetch "2" + rpc-websockets "^7.4.2" + secp256k1 "^4.0.2" + superstruct "^0.14.2" + tweetnacl "^1.0.0" + "@solana/web3.js@^1.37.0": version "1.37.0" resolved "https://registry.npmjs.org/@solana/web3.js/-/web3.js-1.37.0.tgz" @@ -485,7 +550,7 @@ ansi-styles@^4.0.0, ansi-styles@^4.1.0: dependencies: color-convert "^2.0.1" -ansicolors@^0.3.2: +ansicolors@^0.3.2, ansicolors@~0.3.2: version "0.3.2" resolved "https://registry.npmjs.org/ansicolors/-/ansicolors-0.3.2.tgz" integrity sha1-ZlWX3oap/+Oqm/vmyuXG6kJrSXk= @@ -763,9 +828,9 @@ camelcase@^5.3.1: resolved "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz" integrity sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg== -camelcase@^6.0.0: +camelcase@^6.0.0, camelcase@^6.2.1: version "6.3.0" - resolved "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz" + resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-6.3.0.tgz#5685b95eb209ac9c0c177467778c9c84df58ba9a" integrity sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA== chai@^4.3.4: @@ -1920,18 +1985,18 @@ node-addon-api@^4.2.0: resolved "https://registry.npmjs.org/node-addon-api/-/node-addon-api-4.3.0.tgz" integrity sha512-73sE9+3UaLYYFmDsFZnqCInzPyh3MqIwZO9cw58yIqAZhONrrabrYyYe3TuIqtIiOuTXVhsGau8hcrhhwSsDIQ== -node-fetch@2.6.1: - version "2.6.1" - resolved "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.1.tgz" - integrity sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw== - -node-fetch@2.6.7, node-fetch@^2.6.7: +node-fetch@2, node-fetch@2.6.7, node-fetch@^2.6.7: version "2.6.7" - resolved "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz" + resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.7.tgz#24de9fba827e3b4ae44dc8b20256a379160052ad" integrity sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ== dependencies: whatwg-url "^5.0.0" +node-fetch@2.6.1: + version "2.6.1" + resolved "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.1.tgz" + integrity sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw== + node-gyp-build@^4.2.0, node-gyp-build@^4.3.0: version "4.3.0" resolved "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.3.0.tgz" @@ -2184,6 +2249,11 @@ postgres-interval@^1.1.0: dependencies: xtend "^4.0.0" +prettier@^2.5.1: + version "2.7.0" + resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.7.0.tgz#a4fdae07e5596c51c9857ea676cd41a0163879d6" + integrity sha512-nwoX4GMFgxoPC6diHvSwmK/4yU8FFH3V8XWtLQrbj4IBsK2pkYhG4kf/ljF/haaZ/aii+wNJqISrCDPgxGWDVQ== + promise-inflight@^1.0.1: version "1.0.1" resolved "https://registry.npmjs.org/promise-inflight/-/promise-inflight-1.0.1.tgz" @@ -2338,9 +2408,9 @@ semver@^6.0.0: resolved "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz" integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw== -semver@^7.3.5: +semver@^7.3.5, semver@^7.3.7: version "7.3.7" - resolved "https://registry.npmjs.org/semver/-/semver-7.3.7.tgz" + resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.7.tgz#12c5b649afdbf9049707796e22a4028814ce523f" integrity sha512-QlYTucUYOews+WeEujDoEGziz4K6c47V/Bd+LjSSYcA94p+DmINdf7ncaUinThfvZyu13lN9OY1XDxt8C0Tw0g== dependencies: lru-cache "^6.0.0" @@ -2482,9 +2552,16 @@ split@0.3: dependencies: through "2" +spok@^1.4.3: + version "1.4.3" + resolved "https://registry.yarnpkg.com/spok/-/spok-1.4.3.tgz#8516234e6bd8caf0e10567bd675e15fd03b5ceb8" + integrity sha512-5wFGctwrk638aDs+44u99kohxFNByUq2wo0uShQ9yqxSmsxqx7zKbMo1Busy4s7stZQXU+PhJ/BlVf2XWFEGIw== + dependencies: + ansicolors "~0.3.2" + sqlite3@^5.0.8: version "5.0.8" - resolved "https://registry.npmjs.org/sqlite3/-/sqlite3-5.0.8.tgz" + resolved "https://registry.yarnpkg.com/sqlite3/-/sqlite3-5.0.8.tgz#b4b7eab7156debec80866ef492e01165b4688272" integrity sha512-f2ACsbSyb2D1qFFcqIXPfFscLtPVOWJr5GmUzYxf4W+0qelu5MWrR+FAQE1d5IUArEltBrzSDxDORG8P/IkqyQ== dependencies: "@mapbox/node-pre-gyp" "^1.0.0" @@ -2495,7 +2572,7 @@ sqlite3@^5.0.8: sqlite@^4.1.1: version "4.1.1" - resolved "https://registry.npmjs.org/sqlite/-/sqlite-4.1.1.tgz" + resolved "https://registry.yarnpkg.com/sqlite/-/sqlite-4.1.1.tgz#e79c94d44e4381e30d5b3e885bfb9d802d757ecb" integrity sha512-qssVl58Q4ytWabIK7e3lIjDuiXu0sq+M2foXFILrlJwpHisTgywQ5wDB5ImcOPMbuZHX3Q5gmlcDgX3m+VBfdw== ssri@^8.0.0, ssri@^8.0.1: @@ -2604,6 +2681,11 @@ text-encoding-utf-8@^1.0.2: resolved "https://registry.npmjs.org/text-encoding-utf-8/-/text-encoding-utf-8-1.0.2.tgz" integrity sha512-8bw4MY9WjdsD2aMtO0OzOCY3pXGYNx2d2FfHRVUKkiCPDWjKuOlhLVASS+pD7VkLTVjW268LYJHwsnPFlBpbAg== +text-table@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4" + integrity sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw== + through@2, "through@>=2.2.7 <3", through@~2.3, through@~2.3.1: version "2.3.8" resolved "https://registry.npmjs.org/through/-/through-2.3.8.tgz"