Skip to content

Instantly share code, notes, and snippets.

@staccDOTsol
Last active March 19, 2025 14:49
Show Gist options
  • Save staccDOTsol/556e30a74d554092128d32861beb68a7 to your computer and use it in GitHub Desktop.
Save staccDOTsol/556e30a74d554092128d32861beb68a7 to your computer and use it in GitHub Desktop.
import {
Connection,
Keypair,
PublicKey,
Transaction,
sendAndConfirmTransaction,
SystemProgram,
ComputeBudgetProgram,
TransactionInstruction,
SYSVAR_SLOT_HASHES_PUBKEY,
SYSVAR_INSTRUCTIONS_PUBKEY,
LAMPORTS_PER_SOL,
TransactionMessage,
VersionedTransaction,
AddressLookupTableProgram,
} from '@solana/web3.js';
import * as fs from 'fs';
import * as path from 'path';
import axios from 'axios';
import { Program, AnchorProvider, web3, BN, utils, Wallet } from '@coral-xyz/anchor';
import * as dotenv from 'dotenv';
import * as bs58 from 'bs58';
import { Buffer } from 'buffer';
import { getAssociatedTokenAddressSync } from '@solana/spl-token';
dotenv.config();
// Constants
const NFT_BEATER_PROGRAM_ID = new PublicKey('6w7wtTfqpjjijaKhZ5PYVRepqeVjGBvvkyir9HVtQskN');
const MENAGERIE_PROGRAM_ID = new PublicKey('F9SixdqdmEBP5kprp2gZPZNeMmfHJRCTMFjN22dx3akf');
const MPL_CORE_PROGRAM_ID = new PublicKey('CoREENxT6tW1HoK8ypY1SxRMZTcVPm7R94rH4PZNhX7d');
const TOKEN_PROGRAM_ID = new PublicKey('TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA');
const TOKEN_2022_PROGRAM_ID = new PublicKey('TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb');
const TOKEN_METADATA_PROGRAM_ID = new PublicKey('metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s');
const STATE_COMPRESSION_PROGRAM_ID = new PublicKey('cmtDvXumGCrqC1Age74AVPhSRVXJMd8PJS91L8KbNCK');
const NOOP_PROGRAM_ID = new PublicKey('noopb9bkMVfRPU8AsbpTUg8AQkHtKwMYZiFUjNRtMmV');
const BUBBLEGUM_PROGRAM_ID = new PublicKey('BGUMAp9Gq7iTEuizy4pqaxsTyUCBK68MDfK752saRPUY');
const ASSOCIATED_TOKEN_PROGRAM_ID = new PublicKey('ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL');
// Fixed addresses from the successful transaction
const FIXED_ADDRESSES = {
authority: new PublicKey('22W96vbcDd1HaX6QUsuwJzoBNSWnBV3uHUAMWGiLUyyb'),
artistWallet: new PublicKey('BYhzyAdSwF9Zg14t91gzAMtvHXewHVYpNeWXTDB9Cgqw'),
feeReceiver: new PublicKey('33nQCgievSd3jJLSWFBefH3BJRN7h6sAoS82VFFdJGF5'),
collectionMint: new PublicKey('44xq2PwsXAWk3vsYbPKvXv9EmxjebCYqCshAtToa43NH'),
royaltyReceiver: new PublicKey('BYhzyAdSwF9Zg14t91gzAMtvHXewHVYpNeWXTDB9Cgqw'),
};
// Get RPC URL from environment variables
const RPC_URL = process.env.RPC_URL || 'https://api.mainnet-beta.solana.com';
// MintCore instruction discriminator (from transaction)
const MINT_CORE_DISCRIMINATOR = 'b7317780a38b2df8';
/**
* Creates a MintCore instruction with proper formatting based on the successful transaction
*/
function createMintCoreInstruction(
payer: PublicKey,
nftIndex: number
): { instruction: TransactionInstruction, nftMintKeypair: Keypair } {
// Create a new account keypair for the NFT asset
const nftMintKeypair = Keypair.generate();
// This formats the data exactly as seen in the successful transaction
const data = Buffer.from("b7317780a38b2df80000000080f0fa020000000001000000000100", "hex")
// Create a temporary account keypair for the asset
const tempNftAccount = PublicKey.findProgramAddressSync(
[Buffer.from("mint_state"), nftMintKeypair.publicKey.toBuffer()],
MENAGERIE_PROGRAM_ID
)[0];
// Account list exactly matching the successful transaction
return {
instruction: new TransactionInstruction({
keys: [
{ pubkey: payer, isSigner: true, isWritable: true }, // #1
{ pubkey: payer, isSigner: true, isWritable: true }, // #2
{ pubkey: FIXED_ADDRESSES.authority, isSigner: false, isWritable: true }, // #3
{ pubkey: new PublicKey("HCrfgM6EPmALTxae6toVe3GFihCn4jFp43NHHo6DdGvF"), isSigner: false, isWritable: true }, // #4 - Will be created in an inner instruction
{ pubkey: FIXED_ADDRESSES.artistWallet, isSigner: false, isWritable: true }, // #5
{ pubkey: FIXED_ADDRESSES.feeReceiver, isSigner: false, isWritable: true }, // #6
{ pubkey: SystemProgram.programId, isSigner: false, isWritable: false }, // #7 - System Program
{ pubkey: MENAGERIE_PROGRAM_ID, isSigner: false, isWritable: false }, // #8
{ pubkey: MENAGERIE_PROGRAM_ID, isSigner: false, isWritable: false }, // #9
{ pubkey: MENAGERIE_PROGRAM_ID, isSigner: false, isWritable: false }, // #10
{ pubkey: MENAGERIE_PROGRAM_ID, isSigner: false, isWritable: false }, // #11
{ pubkey: MENAGERIE_PROGRAM_ID, isSigner: false, isWritable: false }, // #12
{ pubkey: SYSVAR_INSTRUCTIONS_PUBKEY, isSigner: false, isWritable: false }, // #13
{ pubkey: TOKEN_PROGRAM_ID, isSigner: false, isWritable: false }, // #14
{ pubkey: TOKEN_2022_PROGRAM_ID, isSigner: false, isWritable: false }, // #15
{ pubkey: TOKEN_METADATA_PROGRAM_ID, isSigner: false, isWritable: false }, // #16
{ pubkey: STATE_COMPRESSION_PROGRAM_ID, isSigner: false, isWritable: false }, // #17
{ pubkey: NOOP_PROGRAM_ID, isSigner: false, isWritable: false }, // #18
{ pubkey: SYSVAR_SLOT_HASHES_PUBKEY, isSigner: false, isWritable: false }, // #19
{ pubkey: BUBBLEGUM_PROGRAM_ID, isSigner: false, isWritable: false }, // #20
{ pubkey: MENAGERIE_PROGRAM_ID, isSigner: false, isWritable: false }, // #21
{ pubkey: MENAGERIE_PROGRAM_ID, isSigner: false, isWritable: false }, // #22
{ pubkey: ASSOCIATED_TOKEN_PROGRAM_ID, isSigner: false, isWritable: false }, // #23
{ pubkey: MPL_CORE_PROGRAM_ID, isSigner: false, isWritable: false }, // #24
{ pubkey: new PublicKey("44xq2PwsXAWk3vsYbPKvXv9EmxjebCYqCshAtToa43NH"), isSigner: false, isWritable: true }, // #25
{ pubkey: nftMintKeypair.publicKey, isSigner: true, isWritable: true }, // #26
{ pubkey: TOKEN_2022_PROGRAM_ID, isSigner: false, isWritable: false }, // #27
{ pubkey: new PublicKey("BYhzyAdSwF9Zg14t91gzAMtvHXewHVYpNeWXTDB9Cgqw"), isSigner: false, isWritable: true }, // #28
{ pubkey: new PublicKey("BYhzyAdSwF9Zg14t91gzAMtvHXewHVYpNeWXTDB9Cgqw"), isSigner: false, isWritable: true }, // #29
{ pubkey: new PublicKey("D9MvUbo9tfzqddLJZhfGVeLRaByravHgPTSPY15B7xMV"), isSigner: false, isWritable: true }, // #30
],
programId: MENAGERIE_PROGRAM_ID,
data,
}),
nftMintKeypair
};
}
/**
* Mint an NFT using the Menagerie program's MintCore instruction
*/
async function mintNFT(nftIndex: number) {
try {
// Load wallet from file or environment
let payer: Keypair;
const walletPath = path.join(process.env.HOME || '', 'ddgb.json');
if (fs.existsSync(walletPath)) {
const walletData = JSON.parse(fs.readFileSync(walletPath, 'utf-8'));
payer = Keypair.fromSecretKey(Uint8Array.from(walletData));
console.log(`Loaded wallet from ${walletPath}`);
} else if (process.env.WALLET_PRIVATE_KEY) {
payer = Keypair.fromSecretKey(
Buffer.from(JSON.parse(process.env.WALLET_PRIVATE_KEY))
);
console.log("Loaded wallet from environment variable");
} else {
console.log("No wallet found, generating new keypair");
payer = Keypair.generate();
}
// Connect to the Solana network
const connection = new Connection(RPC_URL, 'confirmed');
// Check wallet balance
const balance = await connection.getBalance(payer.publicKey);
console.log(`Wallet balance: ${balance / 1_000_000_000} SOL`);
// Create transaction
const transaction = new Transaction();
// Add compute budget instructions exactly as in the successful transaction
transaction.add(
ComputeBudgetProgram.setComputeUnitPrice({ microLamports: 40000 }),
ComputeBudgetProgram.setComputeUnitLimit({ units: 175000 })
);
// Create the Menagerie MintCore instruction with the correct NFT index
const { instruction, nftMintKeypair } = createMintCoreInstruction(
payer.publicKey,
nftIndex
);
// Add the MintCore instruction to transaction
transaction.add(instruction);
// add the verify
// Process chunks
// Initialize Anchor provider and program
const provider = new AnchorProvider(
connection,
new Wallet(payer),
{ commitment: 'confirmed', skipPreflight: true }
);
// Load Anchor IDL
let idl;
try {
idl = JSON.parse(fs.readFileSync('./target/idl/nfting.json', 'utf-8'));
} catch (e) {
console.error("IDL file not found. Make sure you've built the program with 'anchor build'");
process.exit(1);
}
// Create the program
// @ts-ignore
const program = new Program(idl, NFT_BEATER_PROGRAM_ID, provider);
// Initialize with rarity thresholds instead of the full array
// This fixes the "Blob.encode[data] requires Buffer as src" error
// Get the NFT Beater PDA
const [nftBeaterPDA] = PublicKey.findProgramAddressSync(
[
Buffer.from('nft-beater'),
new PublicKey("44xq2PwsXAWk3vsYbPKvXv9EmxjebCYqCshAtToa43NH").toBuffer(),
],
NFT_BEATER_PROGRAM_ID
);
const feeReceiver = new PublicKey("89VB5UmvopuCFmp5Mf8YPX28fGvvqn79afCgouQuPyhY");
try {
const ix = await program.methods
.validateMintCore(new BN(50))
.accounts({
assetAccount: nftMintKeypair.publicKey,
state: nftBeaterPDA,
instructionsSysvar: SYSVAR_INSTRUCTIONS_PUBKEY,
merkleTree: new PublicKey("44xq2PwsXAWk3vsYbPKvXv9EmxjebCYqCshAtToa43NH"),
merkleTreeAccount: new PublicKey("44xq2PwsXAWk3vsYbPKvXv9EmxjebCYqCshAtToa43NH"),
authority: payer.publicKey,
feeReceiver: feeReceiver,
systemProgram: SystemProgram.programId,
})
.instruction();
transaction.add(ix);
} catch (e) {
console.error('Error validating mint:', e);
process.exit(1);
}
// Set fee payer
transaction.feePayer = payer.publicKey;
// Get recent blockhash
const { blockhash } = await connection.getLatestBlockhash('confirmed');
transaction.recentBlockhash = blockhash;
console.log(`Minting Core NFT #${nftIndex}`);
console.log(`Generated asset keypair: ${nftMintKeypair.publicKey.toString()}`);
console.log(`MintCore instruction data: ${bs58.encode(instruction.data)}`);
// Sign transaction
transaction.sign(payer, nftMintKeypair);
// Send and confirm transaction
const signature = await connection.sendRawTransaction(transaction.serialize(), {
skipPreflight: true
});
console.log(`Transaction signature: ${signature}`);
console.log(`https://solscan.io/tx/${signature}`);
// Wait for confirmation
await connection.confirmTransaction(signature, 'confirmed');
console.log(`Transaction confirmed! NFT minted successfully.`);
return {
signature,
nftMint: nftMintKeypair.publicKey.toString()
};
} catch (error) {
console.error('Error minting NFT:', error);
throw error;
}
}
// Run mint function with command line arg for NFT index
async function main() {
const args = process.argv.slice(2);
const nftIndex = args.length > 0 ? parseInt(args[0]) : 886; // Default to index 886
try {
const result = await mintNFT(nftIndex);
console.log('Mint result:', result);
} catch (error) {
console.error('Error in main execution:', error);
}
}
// Only execute if this is the main file
if (require.main === module) {
main();
}
export { mintNFT };
import {
Connection,
Keypair,
PublicKey,
SystemProgram,
Transaction,
TransactionInstruction,
sendAndConfirmTransaction,
} from '@solana/web3.js';
import * as fs from 'fs';
import * as path from 'path';
import axios from 'axios';
import { PromisePool } from '@supercharge/promise-pool';
import { Program, Wallet } from '@coral-xyz/anchor';
import { AnchorProvider } from '@coral-xyz/anchor';
import { BN } from 'bn.js';
// Constants
const NFT_BEATER_PROGRAM_ID = new PublicKey("6w7wtTfqpjjijaKhZ5PYVRepqeVjGBvvkyir9HVtQskN");
const MERKLE_TREE_ADDRESS = new PublicKey("44xq2PwsXAWk3vsYbPKvXv9EmxjebCYqCshAtToa43NH");
const FEE_RECEIVER = new PublicKey("89VB5UmvopuCFmp5Mf8YPX28fGvvqn79afCgouQuPyhY");
const BASE_URI = "https://gateway.pinit.io/ipfs/Qmd4SP1MEzoXQK5ZcEsMnUpmEMuvqJF8H6VMLyJfLAwrtw";
// Parameters
const CONCURRENCY = 25; // Number of concurrent requests
const MAX_NFTS = 10000; // Maximum number of NFTs to check
const CHUNK_SIZE = 500; // Size of chunks for uploading
interface NFTMetadata {
index: number;
metadata: any;
score?: number;
}
/**
* Fetch all NFT metadata using PromisePool for concurrent processing
*/
async function fetchAllNFTMetadata() {
console.log("Starting to fetch all NFT metadata using PromisePool...");
const nftMetadata: NFTMetadata[] = [];
const attributeCounts: Record<string, number> = {};
// Create an array of indices from 0 to MAX_NFTS-1
const indices = Array.from({ length: MAX_NFTS }, (_, i) => i);
// Track number of 404s to detect end of collection
let notFoundCount = 0;
// Use PromisePool to process concurrently with rate limiting
const { results, errors } = await PromisePool
.withConcurrency(CONCURRENCY)
.for(indices)
.process(async (index) => {
try {
// Check if we've hit too many 404s in a row
if (notFoundCount > 50) {
return null; // Skip processing
}
const uri = `${BASE_URI}/${index}.json`;
const response = await axios.get(uri, { timeout: 10000 });
if (response.status === 200 && response.data) {
console.log(`Successfully fetched NFT #${index}`);
notFoundCount = 0; // Reset counter on success
const metadata = response.data;
nftMetadata.push({
index,
metadata
});
// Track attribute occurrences for rarity calculation
if (metadata.attributes) {
metadata.attributes.forEach((attr: { trait_type: string, value: string }) => {
const key = `${attr.trait_type}:${attr.value}`;
attributeCounts[key] = (attributeCounts[key] || 0) + 1;
});
}
// Save progress every 100 successful fetches
if (nftMetadata.length % 100 === 0) {
saveProgress(nftMetadata, attributeCounts);
}
return { index, success: true };
}
} catch (error) {
if (axios.isAxiosError(error) && error.response?.status === 404) {
notFoundCount++;
console.log(`NFT #${index} not found (404). Not found count: ${notFoundCount}`);
if (notFoundCount > 50) {
console.log(`Detected probable end of collection after ${notFoundCount} consecutive 404s`);
}
} else {
console.error(`Error fetching NFT #${index}:`, error);
}
return { index, success: false, error };
}
});
// Calculate total fetched
const successCount = results.filter(r => r && r.success).length;
console.log(`Completed fetching metadata. Successfully fetched ${successCount} NFTs.`);
console.log(`Encountered ${errors.length} errors.`);
// Calculate rarity scores
calculateRarityScores(nftMetadata, attributeCounts);
// Save final results
saveProgress(nftMetadata, attributeCounts, true);
return nftMetadata;
}
/**
* Calculate rarity scores for each NFT based on attribute rarity
*/
function calculateRarityScores(nftMetadata: NFTMetadata[], attributeCounts: Record<string, number>) {
console.log("Calculating rarity scores...");
const totalNFTs = nftMetadata.length;
// Calculate raw rarity scores for each NFT
for (const nft of nftMetadata) {
let rarityScore = 0;
if (nft.metadata.attributes) {
for (const attr of nft.metadata.attributes) {
const key = `${attr.trait_type}:${attr.value}`;
const attributeCount = attributeCounts[key] || 0;
// Rarity score formula: rarer attributes have higher scores
if (attributeCount > 0) {
const attributeRarity = 1 / (attributeCount / totalNFTs);
rarityScore += attributeRarity;
}
}
}
// Store raw score for normalization
nft.score = rarityScore;
}
// Normalize scores to 0-100 range using min-max scaling
const scores = nftMetadata.map(nft => nft.score || 0);
const minScore = Math.min(...scores);
const maxScore = Math.max(...scores);
const range = maxScore - minScore;
for (const nft of nftMetadata) {
if (nft.score !== undefined) {
// Normalize to 0-100 range
const normalizedScore = range === 0 ? 50 : // If all items have same rarity, assign 50
Math.round(((nft.score - minScore) / range) * 100);
nft.score = normalizedScore;
}
}
// Sort by rarity score in descending order
nftMetadata.sort((a, b) => (b.score || 0) - (a.score || 0));
// Display top 10 rarest NFTs
console.log("Top 10 Rarest NFTs:");
for (let i = 0; i < Math.min(10, nftMetadata.length); i++) {
const nft = nftMetadata[i];
console.log(`${i + 1}. NFT #${nft.index} - Score: ${nft.score} - Name: ${nft.metadata?.name || 'Unknown'}`);
}
}
/**
* Save progress to file
*/
function saveProgress(nftMetadata: NFTMetadata[], attributeCounts: Record<string, number>, isFinal = false) {
const filename = isFinal ? 'menagerie-rarity-data.json' : 'menagerie-metadata-progress.json';
// Prepare data for saving
const data = {
lastUpdated: new Date().toISOString(),
totalNFTs: nftMetadata.length,
rarityScores: nftMetadata.map(nft => ({
index: nft.index,
score: nft.score || 0,
metadata: nft.metadata
}))
};
fs.writeFileSync(filename, JSON.stringify(data, null, 2));
console.log(`Progress saved to ${filename}: ${nftMetadata.length} NFTs`);
}
/**
* Upload rarity data to the on-chain program
*/
async function uploadRarityData(nftMetadata: NFTMetadata[]) {
console.log("Starting rarity data upload...");
try {
// Connect to Solana
const connection = new Connection("https://api.mainnet-beta.solana.com", "confirmed");
// Load wallet from keypair file
const wallet = Keypair.fromSecretKey(
Uint8Array.from(JSON.parse(fs.readFileSync(
path.resolve(process.env.KEYPAIR_PATH || '/Users/jarettdunn/ddgb.json'),
'utf-8'
)))
);
console.log(`Using wallet: ${wallet.publicKey.toString()}`);
// Get the NFT Beater PDA
const [nftBeaterPDA] = PublicKey.findProgramAddressSync(
[
Buffer.from('nft-beater'),
MERKLE_TREE_ADDRESS.toBuffer(),
],
NFT_BEATER_PROGRAM_ID
);
console.log(`NFT Beater PDA: ${nftBeaterPDA.toString()}`);
// Find maximum NFT index
const maxIndex = Math.max(...nftMetadata.map(item => item.index));
console.log(`Maximum NFT index: ${maxIndex}`);
// Create a rarity array with all indices from 0 to maxIndex
const rarityArray = new Array(3000).fill(0);
// Fill in the rarity scores we have
nftMetadata.forEach(item => {
if (item.index !== undefined && item.score !== undefined) {
rarityArray[item.index] = Math.floor(item.score);
}
});
console.log(`Uploading ${rarityArray.length} rarity scores in chunks of ${CHUNK_SIZE}...`);
// Process chunks
// Initialize Anchor provider and program
const provider = new AnchorProvider(
connection,
new Wallet(wallet),
{ commitment: 'confirmed', skipPreflight: true }
);
// Load Anchor IDL
let idl;
try {
idl = JSON.parse(fs.readFileSync('./target/idl/nfting.json', 'utf-8'));
} catch (e) {
console.error("IDL file not found. Make sure you've built the program with 'anchor build'");
process.exit(1);
}
// Create the program
// @ts-ignore
const program = new Program(idl, NFT_BEATER_PROGRAM_ID, provider);
// Initialize with rarity thresholds instead of the full array
// This fixes the "Blob.encode[data] requires Buffer as src" error
const rarityThresholds = [50, 75, 90]; // Common, Rare, Legendary thresholds
try {
const tx = await program.methods
.initialize(Buffer.from(rarityThresholds))
.accounts({
state: nftBeaterPDA,
merkleTree: MERKLE_TREE_ADDRESS,
merkleTreeAccount: MERKLE_TREE_ADDRESS,
authority: wallet.publicKey,
feeReceiver: FEE_RECEIVER,
systemProgram: SystemProgram.programId,
})
.signers([wallet])
.rpc({skipPreflight: true});
console.log(`Initialization transaction confirmed: https://solscan.io/tx/${tx}`);
} catch (error) {
// If account already initialized, we can proceed
if (error?.message?.includes('already in use')) {
console.log('NFT Beater state already initialized');
} else {
console.error("Error initializing NFT Beater state:", error);
}
}
// Upload each chunk
for (let i = 0; i < rarityArray.length; i += CHUNK_SIZE) {
const chunk = rarityArray.slice(i, i + CHUNK_SIZE);
const buffer = Buffer.from(chunk);
console.log(`Uploading chunk ${i} to ${i + chunk.length - 1} (${chunk.length} items)`);
try {
// Create and send the transaction
// @ts-ignore
const tx = await program.methods
.updateRarityData(new BN(i), buffer)
.accounts({
merkleTree: MERKLE_TREE_ADDRESS,
state: nftBeaterPDA,
authority: wallet.publicKey,
feeReceiver: FEE_RECEIVER,
systemProgram: SystemProgram.programId,
})
.signers([wallet])
.rpc({skipPreflight: true});
console.log(`Transaction confirmed: https://solscan.io/tx/${tx}`);
console.log(`Updated rarity data for indices ${i} through ${i + chunk.length - 1}`);
// Add a small delay between transactions
if (i + CHUNK_SIZE < rarityArray.length) {
console.log("Waiting for 2 seconds before next chunk...");
await new Promise(resolve => setTimeout(resolve, 2000));
}
} catch (e) {
console.error(`Error uploading chunk starting at ${i}:`, e);
console.error(e);
// Continue with next chunk even if this one fails
}
}
console.log("Rarity data upload complete!");
} catch (error) {
console.error("Error uploading rarity data:", error);
throw error;
}
}
// Main execution
async function main() {
try {
// First fetch all metadata and calculate rarity
const nftMetadata = await fetchAllNFTMetadata();
// Then upload the rarity data
await uploadRarityData(nftMetadata);
} catch (error) {
console.error("Error in main execution:", error);
}
}
// Run the script
main().then(
() => process.exit(0),
(error) => {
console.error("Error in main function:", error);
process.exit(1);
}
);
import {
Connection,
PublicKey,
Transaction,
TransactionInstruction,
Keypair,
sendAndConfirmTransaction,
} from '@solana/web3.js';
import { Program, AnchorProvider, web3, BN, Wallet } from '@coral-xyz/anchor';
import * as fs from 'fs';
import * as path from 'path';
import axios from 'axios';
import * as idl from '../target/idl/nft_beater.json';
import { NFT_BEATER_PROGRAM_ID } from './constants';
import {
CandyMachine,
CandyMachineConfigLineSettings,
CandyMachineHiddenSettings
} from '@metaplex-foundation/js/dist/types/plugins/candyMachineModule/models/CandyMachine';
import { Metaplex } from '@metaplex-foundation/js';
// Constants for parallel processing
const BATCH_SIZE = 256; // Number of concurrent requests
const RATE_LIMIT_DELAY = 50; // ms between batches
// NFT metadata structure
interface NFTMetadata {
name: string;
symbol: string;
description: string;
seller_fee_basis_points: number;
image: string;
attributes: Array<{
trait_type: string;
value: string;
}>;
}
interface MetadataFetchResult {
index: number;
metadata: NFTMetadata | null;
error?: any;
}
// This class indexes a candy machine's NFTs and calculates rarity scores
export class CandyMachineIndexer {
private connection: Connection;
private program: Program;
private wallet: Keypair;
private candyMachineId: PublicKey;
private candyMachine?: CandyMachine;
private metaplex: Metaplex;
// Maps to store trait frequencies and rarity scores
private traitFrequencies: Map<string, Map<string, number>> = new Map();
private rarityScores: Map<number, number> = new Map();
private metadataByIndex: Map<number, NFTMetadata> = new Map();
// NFT count for percentage calculations
private totalNFTs: number = 0;
constructor(
connection: Connection,
wallet: Keypair,
candyMachineId: string | PublicKey
) {
this.connection = connection;
this.wallet = wallet;
this.candyMachineId = typeof candyMachineId === 'string'
? new PublicKey(candyMachineId)
: candyMachineId;
// Create wallet adapter from keypair
const walletAdapter = new Wallet(wallet);
// Initialize the program
const provider = new AnchorProvider(
connection,
walletAdapter,
{ commitment: 'confirmed' }
);
// Initialize Metaplex
this.metaplex = new Metaplex(connection);
// Initialize the NFT Beater program
// @ts-ignore
this.program = new Program(idl as any,new PublicKey("7wnvw6e5S8P3Yo7rU2krZ9VN5yp2z1Wrn74PhLdHLQVf"), provider);
}
/**
* Fetch candy machine data and config lines
*/
async fetchCandyMachineData(): Promise<void> {
console.log(`Fetching candy machine data for: ${this.candyMachineId.toString()}`);
try {
// Use Metaplex to fetch candy machine
this.candyMachine = await this.metaplex
.candyMachines()
.findByAddress({ address: this.candyMachineId })
console.log(`Items Available: ${this.candyMachine.itemsAvailable}`);
console.log(`Items Minted: ${this.candyMachine.itemsMinted}`);
this.totalNFTs = Number(this.candyMachine.itemsAvailable);
} catch (error) {
console.error('Error fetching candy machine:', error);
throw new Error('Failed to fetch candy machine data');
}
}
/**
* Fetch metadata in parallel with rate limiting
*/
private async fetchMetadataBatch(
uris: { index: number; uri: string }[]
): Promise<MetadataFetchResult[]> {
const promises = uris.map(async ({ index, uri }) => {
try {
const response = await axios.get(uri);
return { index, metadata: response.data };
} catch (error) {
console.error(`Error fetching metadata for index ${index}:`, error);
return { index, metadata: null, error };
}
});
return Promise.all(promises);
}
/**
* Process metadata in batches
*/
private async processMetadataBatches(items: { uri: string }[]): Promise<void> {
const total = items.length;
const batches = Math.ceil(total / BATCH_SIZE);
console.log(`Processing ${total} items in ${batches} batches of ${BATCH_SIZE}`);
for (let i = 0; i < total; i += BATCH_SIZE) {
const batchItems = items.slice(i, i + BATCH_SIZE).map((item, idx) => ({
index: i + idx,
uri: item.uri
}));
console.log(`Processing batch ${Math.floor(i / BATCH_SIZE) + 1}/${batches}`);
const results = await this.fetchMetadataBatch(batchItems);
// Store successful results
results.forEach(({ index, metadata }) => {
if (metadata) {
this.metadataByIndex.set(index, metadata);
}
});
// Add delay between batches to avoid rate limiting
if (i + BATCH_SIZE < total) {
await new Promise(resolve => setTimeout(resolve, RATE_LIMIT_DELAY));
}
// Log progress
const progress = Math.min(100, Math.round((i + BATCH_SIZE) / total * 100));
console.log(`Progress: ${progress}% (${this.metadataByIndex.size}/${total} items indexed)`);
}
}
/**
* Fetch and index metadata for all NFTs in the candy machine
*/
async indexAllMetadata(): Promise<void> {
if (!this.candyMachine) {
await this.fetchCandyMachineData();
}
console.log('Indexing all metadata...');
// Check if using hidden settings
if (this.candyMachine?.itemSettings.type === 'hidden') {
await this.indexHiddenCollection();
return;
}
if (!this.candyMachine) {
throw new Error('Candy machine not found');
}
// For non-hidden collections, fetch each config line
const items = this.candyMachine.items;
await this.processMetadataBatches(items);
// Calculate trait frequencies and rarity scores
this.calculateTraitFrequencies();
this.calculateRarityScores();
}
/**
* Index a hidden collection by parsing the pattern URI
*/
private async indexHiddenCollection(): Promise<void> {
if (!this.candyMachine || this.candyMachine.itemSettings.type !== 'hidden') {
throw new Error('No hidden settings found');
}
const hiddenSettings = this.candyMachine.itemSettings as CandyMachineHiddenSettings;
const baseUri = hiddenSettings.uri;
console.log(`Hidden collection with base URI: ${baseUri}`);
// Generate all URIs first
const items = Array.from(
{ length: Number(this.candyMachine.itemsAvailable) },
(_, i) => ({ uri: this.formatHiddenUri(baseUri, i) })
);
// Process in batches
await this.processMetadataBatches(items);
if (this.metadataByIndex.size === 0) {
console.log('No revealed NFTs found for hidden collection');
return;
}
// Calculate trait frequencies and rarity scores
this.calculateTraitFrequencies();
this.calculateRarityScores();
}
/**
* Format URI for hidden settings using proper variables
*/
private formatHiddenUri(baseUri: string, index: number): string {
return baseUri
.replace(/\$ID\$/g, index.toString())
.replace(/\$ID\+1\$/g, (index + 1).toString());
}
/**
* Calculate the frequency of each trait type and value
*/
private calculateTraitFrequencies(): void {
console.log('Calculating trait frequencies...');
// Reset frequencies
this.traitFrequencies.clear();
// Count occurrences of each trait
this.metadataByIndex.forEach((metadata) => {
metadata.attributes.forEach((attribute) => {
if (!this.traitFrequencies.has(attribute.trait_type)) {
this.traitFrequencies.set(attribute.trait_type, new Map());
}
const traitValues = this.traitFrequencies.get(attribute.trait_type)!;
traitValues.set(
attribute.value,
(traitValues.get(attribute.value) || 0) + 1
);
});
});
}
/**
* Calculate rarity scores for each NFT
*/
private calculateRarityScores(): void {
console.log('Calculating rarity scores...');
// First pass - calculate raw rarity scores
const rawScores = new Map<number, number>();
this.metadataByIndex.forEach((metadata, index) => {
let score = 0;
metadata.attributes.forEach((attribute) => {
const traitFreq = this.traitFrequencies.get(attribute.trait_type)!;
const valueFreq = traitFreq.get(attribute.value)!;
// Calculate rarity score as 1 / frequency
score += 1 / (valueFreq / this.metadataByIndex.size);
});
rawScores.set(index, score);
});
// Find min and max scores for normalization
const scores = Array.from(rawScores.values());
const minScore = Math.min(...scores);
const maxScore = Math.max(...scores);
const range = maxScore - minScore;
// Second pass - normalize to 0-100 scale
rawScores.forEach((score, index) => {
// Normalize using min-max scaling to 0-100
const normalizedScore = range === 0 ? 50 : // If all items have same rarity, assign 50
Math.round(((score - minScore) / range) * 100);
this.rarityScores.set(index, normalizedScore);
});
console.log(`Normalized ${this.rarityScores.size} rarity scores from ${minScore.toFixed(2)} to ${maxScore.toFixed(2)}`);
}
/**
* Save rarity data to a file
*/
saveRarityData(filePath: string): void {
const data = {
candyMachine: this.candyMachineId.toString(),
totalNFTs: this.totalNFTs,
rarityScores: Array.from(this.rarityScores.entries()).map(([index, score]) => ({
index,
score,
metadata: this.metadataByIndex.get(index),
})),
};
fs.writeFileSync(filePath, JSON.stringify(data, null, 2));
console.log(`Rarity data saved to ${filePath}`);
}
/**
* Initialize the NFT Beater program state for this candy machine
*/
async initialize(): Promise<void> {
console.log('Initializing NFT Beater state...');
try {
// Pass thresholds directly as a number array
const rarity_thresholds = [50, 75, 90];
await this.program.methods
.initialize(this.candyMachineId, Buffer.from(rarity_thresholds))
.accounts({
state: await this.getNftBeaterPda(),
candyMachineId: this.candyMachineId,
feeReceiver: new PublicKey("89VB5UmvopuCFmp5Mf8YPX28fGvvqn79afCgouQuPyhY"),
authority: this.wallet.publicKey,
systemProgram: web3.SystemProgram.programId,
})
.signers([this.wallet])
.rpc();
console.log('NFT Beater state initialized successfully');
} catch (error: any) {
// If account already initialized, we can proceed
if (error?.message?.includes('already in use')) {
console.log('NFT Beater state already initialized');
return;
}
throw error;
}
}
/**
* Update rarity data on-chain
*/
async updateOnChainRarityData(): Promise<void> {
if (this.rarityScores.size === 0) {
throw new Error('No rarity scores calculated');
}
console.log('Updating on-chain rarity data...');
// Initialize if not already initialized
await this.initialize();
// Convert rarity scores to u8 array (0-100)
const rarityData = Array.from(this.rarityScores.entries())
.sort(([a], [b]) => a - b)
.map(([_, score]) => Math.floor(score));
// Update in smaller chunks to fit within state size limits
const chunkSize = 50; // Smaller chunks to ensure we stay within limits
for (let i = 0; i < rarityData.length; i += chunkSize) {
const chunk = rarityData.slice(i, i + chunkSize);
try {
// No need to pad to 100 bytes anymore since we're using smaller chunks
const buffer = Buffer.from(chunk);
await this.program.methods
.updateRarityData(new BN(i), buffer)
.accounts({
state: await this.getNftBeaterPda(),
authority: this.wallet.publicKey,
feeReceiver: new PublicKey("89VB5UmvopuCFmp5Mf8YPX28fGvvqn79afCgouQuPyhY"),
systemProgram: web3.SystemProgram.programId,
})
.signers([this.wallet])
.rpc();
console.log(`Updated rarity data for indices ${i} to ${i + chunk.length - 1}`);
} catch (error) {
console.error(`Error updating rarity data for chunk ${i}:`, error);
throw error;
}
}
console.log('Rarity data updated on-chain successfully');
}
/**
* Get the NFT Beater PDA for this candy machine
*/
private async getNftBeaterPda(): Promise<PublicKey> {
const [pda] = await PublicKey.findProgramAddress(
[
Buffer.from('nft-beater'),
this.candyMachineId.toBuffer(),
],
this.program.programId
);
return pda;
}
}
use std::str::FromStr;
use anchor_lang::prelude::*;
use arrayref::array_ref;
use mpl_candy_machine_core::{
CandyMachine,
};
use solana_program::{
sysvar::clock::Clock,
system_instruction,
};
use crate::state::RarityState;
mod state;
declare_id!("7wnvw6e5S8P3Yo7rU2krZ9VN5yp2z1Wrn74PhLdHLQVf");
const FEE_LAMPORTS: u64 = 100_000_000; // 0.1 SOL
// Candy Machine account data offsets
const ITEMS_AVAILABLE_OFFSET: usize = 8 + // discriminator
1 + // version (u8)
1 + // token_standard (u8)
6 + // features [u8; 6]
32 + // authority
32 + // mint_authority
32 + // collection_mint
8 + // items_redeemed
8; // items_available (from CandyMachineData)
#[program]
pub mod nft_beater {
use std::str::FromStr;
use super::*;
/// Initialize the rarity checker for a specific candy machine
pub fn initialize(
ctx: Context<Initialize>,
candy_machine_id: Pubkey,
rarity_thresholds: Vec<u8>,
) -> Result<()> {
msg!("Processing fee transfer of {} lamports", FEE_LAMPORTS);
// Transfer fee
let transfer_ix = system_instruction::transfer(
&ctx.accounts.authority.key(),
&ctx.accounts.fee_receiver.key(),
FEE_LAMPORTS,
);
solana_program::program::invoke(
&transfer_ix,
&[
ctx.accounts.fee_receiver.to_account_info(),
ctx.accounts.authority.to_account_info(),
ctx.accounts.system_program.to_account_info(),
],
)?;
msg!("Fee transfer successful");
let state = &mut ctx.accounts.state;
state.authority = ctx.accounts.authority.key();
state.candy_machine = candy_machine_id;
state.rarity_thresholds = rarity_thresholds;
state.bump = *ctx.bumps.get("state").unwrap();
Ok(())
}
/// Add or update rarity data for mint indices
pub fn update_rarity_data(
ctx: Context<UpdateRarityData>,
start_index: u64,
rarity_data: Vec<u8>,
) -> Result<()> {
msg!("Processing fee transfer of {} lamports", FEE_LAMPORTS);
// Transfer fee
let transfer_ix = system_instruction::transfer(
&ctx.accounts.authority.key(),
&ctx.accounts.fee_receiver.key(),
FEE_LAMPORTS,
);
solana_program::program::invoke(
&transfer_ix,
&[
ctx.accounts.fee_receiver.to_account_info(),
ctx.accounts.authority.to_account_info(),
ctx.accounts.system_program.to_account_info(),
],
)?;
msg!("Fee transfer successful");
let state = &mut ctx.accounts.state;
// Make sure we stay within bounds
let end_index = start_index + rarity_data.len() as u64;
if end_index > u16::MAX as u64 {
return Err(error!(ErrorCode::IndexOutOfBounds));
}
// Extend rarity_map if needed
if end_index > state.rarity_map.len() as u64 {
state.rarity_map.resize(end_index as usize, 0);
}
// Update the rarity map with new data
for (i, rarity) in rarity_data.iter().enumerate() {
let index = (start_index as usize) + i;
state.rarity_map[index] = *rarity;
}
Ok(())
}
/// Validate that the next mint meets rarity threshold
pub fn validate_mint(
ctx: Context<ValidateMint>,
min_rarity_percentage: u8,
) -> Result<()> {
msg!("Starting validate_mint with min_rarity_percentage: {}", min_rarity_percentage);
msg!("Processing fee transfer of {} lamports", FEE_LAMPORTS);
// Transfer fee
let transfer_ix = system_instruction::transfer(
&ctx.accounts.minter.key(),
&ctx.accounts.fee_receiver.key(),
FEE_LAMPORTS,
);
solana_program::program::invoke(
&transfer_ix,
&[
ctx.accounts.fee_receiver.to_account_info(),
ctx.accounts.minter.to_account_info(),
ctx.accounts.system_program.to_account_info(),
],
)?;
msg!("Fee transfer successful");
let state = &ctx.accounts.state;
msg!("Loaded state for candy machine: {}", state.candy_machine);
// Verify we're targeting the correct candy machine
if state.candy_machine != ctx.accounts.candy_machine.key() {
msg!("Invalid candy machine: expected {}, got {}",
state.candy_machine, ctx.accounts.candy_machine.key());
return Err(error!(ErrorCode::InvalidCandyMachine));
}
// Parse candy machine data using MPL types
let candy_machine = Account::<CandyMachine>::try_from(&ctx.accounts.candy_machine.to_account_info())?;
let items_available = candy_machine.data.items_available;
let items_redeemed = candy_machine.items_redeemed;
msg!("Candy machine stats: {} items available, {} items redeemed",
items_available, items_redeemed);
// Calculate mint index
msg!("Calculating mint index from recent slot hashes");
let recent_slothashes = &ctx.accounts.recent_slothashes;
let data = recent_slothashes.try_borrow_data()?;
let most_recent = array_ref![data, 12, 8];
let clock = Clock::get()?;
let seed = u64::from_le_bytes(*most_recent).saturating_sub(clock.unix_timestamp as u64);
msg!("Generated seed value: {}", seed);
let remaining_items = items_available.saturating_sub(items_redeemed);
msg!("Remaining items to mint: {}", remaining_items);
// Check if there are any items left to mint
if remaining_items == 0 {
msg!("No items available to mint");
return Err(error!(ErrorCode::NoItemsAvailable));
}
let mint_index = seed % remaining_items;
msg!("Calculated mint index: {}", mint_index);
// Get and validate rarity
if mint_index as usize >= state.rarity_map.len() {
msg!("Mint index {} out of bounds (rarity map length: {})",
mint_index, state.rarity_map.len());
return Err(error!(ErrorCode::IndexOutOfBounds));
}
let rarity = state.rarity_map[mint_index as usize];
msg!("NFT at index {} has rarity score: {}", mint_index, rarity);
if rarity < min_rarity_percentage {
msg!("Rarity {} below threshold {}", rarity, min_rarity_percentage);
return Err(error!(ErrorCode::RarityBelowThreshold));
}
msg!("Validation successful: NFT meets rarity threshold");
Ok(())
}
}
// Helper functions to parse candy machine data
fn parse_items_redeemed(data: &[u8]) -> Result<u64> {
if data.len() < ITEMS_AVAILABLE_OFFSET - 8 {
return Err(error!(ErrorCode::InvalidCandyMachineData));
}
let items_redeemed_bytes = array_ref![data, ITEMS_AVAILABLE_OFFSET - 8, 8];
Ok(u64::from_le_bytes(*items_redeemed_bytes))
}
fn parse_items_available(data: &[u8]) -> Result<u64> {
if data.len() < ITEMS_AVAILABLE_OFFSET {
return Err(error!(ErrorCode::InvalidCandyMachineData));
}
let items_available_bytes = array_ref![data, ITEMS_AVAILABLE_OFFSET, 8];
Ok(u64::from_le_bytes(*items_available_bytes))
}
#[derive(Accounts)]
pub struct Initialize<'info> {
#[account(
init,
payer = authority,
space = 8 + state::STATE_SIZE,
seeds = [b"nft-beater", candy_machine_id.key().as_ref()],
bump
)]
pub state: Account<'info, RarityState>,
/// CHECK: This is just used as a seed for PDA derivation
pub candy_machine_id: UncheckedAccount<'info>,
#[account(mut)]
pub authority: Signer<'info>,
#[account(mut, address=Pubkey::from_str("89VB5UmvopuCFmp5Mf8YPX28fGvvqn79afCgouQuPyhY").unwrap())]
pub fee_receiver: AccountInfo<'info>,
pub system_program: Program<'info, System>,
}
#[derive(Accounts)]
pub struct UpdateRarityData<'info> {
#[account(
mut,
seeds = [b"nft-beater", state.candy_machine.as_ref()],
bump = state.bump,
has_one = authority
)]
pub state: Account<'info, RarityState>,
#[account(mut)]
pub authority: Signer<'info>,
#[account(mut, address=Pubkey::from_str("89VB5UmvopuCFmp5Mf8YPX28fGvvqn79afCgouQuPyhY").unwrap())]
pub fee_receiver: AccountInfo<'info>,
pub system_program: Program<'info, System>,
}
#[derive(Accounts)]
pub struct ValidateMint<'info> {
#[account(
seeds = [b"nft-beater", state.candy_machine.as_ref()],
bump = state.bump
)]
pub state: Account<'info, RarityState>,
/// The candy machine account
pub candy_machine: Account<'info, CandyMachine>,
/// CHECK: Account constraints ensure this is the SlotHashes sysvar
#[account(address = solana_program::sysvar::slot_hashes::id())]
pub recent_slothashes: UncheckedAccount<'info>,
#[account(mut)]
pub minter: Signer<'info>,
#[account(mut, address=Pubkey::from_str("89VB5UmvopuCFmp5Mf8YPX28fGvvqn79afCgouQuPyhY").unwrap())]
pub fee_receiver: AccountInfo<'info>,
pub system_program: Program<'info, System>,
}
#[error_code]
pub enum ErrorCode {
#[msg("Rarity below specified threshold")]
RarityBelowThreshold,
#[msg("Mint index out of bounds")]
IndexOutOfBounds,
#[msg("Invalid candy machine")]
InvalidCandyMachine,
#[msg("Invalid candy machine data")]
InvalidCandyMachineData,
#[msg("No items available to mint")]
NoItemsAvailable,
}
use std::str::FromStr;
use anchor_lang::prelude::*;
use solana_program::{
system_instruction,
keccak,
program_pack::Pack,
instruction::Instruction,
sysvar::{instructions::{load_instruction_at_checked, get_instruction_relative}, SysvarId},
};
// Import the TreeConfig directly from Bubblegum program
use spl_account_compression::{self, program::SplAccountCompression};
use mpl_core::accounts::BaseAssetV1;
use crate::state::{RarityState, MintRecord, MintPattern};
mod state;
mod bubblegum_program {
use anchor_lang::prelude::*;
use std::str::FromStr;
// Use a function to get the ID instead of a static variable
pub fn id() -> Pubkey {
Pubkey::from_str("BGUMAp9Gq7iTEuizy4pqaxsTyUCBK68MDfK752saRPUY").unwrap()
}
// Bubblegum instruction discriminator for mint_v1
pub const MINT_V1_DISCRIMINATOR: [u8; 8] = [145, 98, 192, 118, 184, 147, 118, 104];
// Bubblegum instruction discriminator for mint_to_collection_v1
pub const MINT_TO_COLLECTION_V1_DISCRIMINATOR: [u8; 8] = [245, 201, 109, 234, 21, 117, 186, 159];
}
mod menagerie_program {
use anchor_lang::prelude::*;
use std::str::FromStr;
// Use a function to get the ID
pub fn id() -> Pubkey {
Pubkey::from_str("F9SixdqdmEBP5kprp2gZPZNeMmfHJRCTMFjN22dx3akf").unwrap()
}
// Menagerie mint instruction discriminator (from their UI)
pub const MINT_DISCRIMINATOR: [u8; 8] = [0x73, 0x87, 0x15, 0x18, 0x6c, 0x2d, 0x5f, 0xe4];
// MintCore instruction discriminator (observed from transaction)
pub const MINT_CORE_DISCRIMINATOR: [u8; 8] = [0xb7, 0x31, 0x77, 0x80, 0xa3, 0x8b, 0x2d, 0xf8];
// MintCv3 instruction discriminator (observed from transaction)
pub const MINT_CV3_DISCRIMINATOR: [u8; 8] = [0x38, 0xa6, 0x52, 0x4f, 0xe8, 0x00, 0xf6, 0x11];
}
mod fee_receiver {
use anchor_lang::prelude::*;
use std::str::FromStr;
pub fn id() -> Pubkey {
Pubkey::from_str("89VB5UmvopuCFmp5Mf8YPX28fGvvqn79afCgouQuPyhY").unwrap()
}
}
mod metadata_program {
use anchor_lang::prelude::*;
use std::str::FromStr;
// Use a function to get the ID instead of a static variable
pub fn id() -> Pubkey {
Pubkey::from_str("metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s").unwrap()
}
}
mod mpl_core_program {
use anchor_lang::prelude::*;
use std::str::FromStr;
// Use a function to get the ID instead of a static variable
pub fn id() -> Pubkey {
Pubkey::from_str("CoREENxT6tW1HoK8ypY1SxRMZTcVPm7R94rH4PZNhX7d").unwrap()
}
// CreateV1 instruction discriminator
pub const CREATE_V1_DISCRIMINATOR: u8 = 0;
}
declare_id!("6w7wtTfqpjjijaKhZ5PYVRepqeVjGBvvkyir9HVtQskN");
const FEE_LAMPORTS: u64 = 100_000_000; // 0.1 SOL
const ASSET_PREFIX: &[u8] = b"asset";
const METADATA_PREFIX: &[u8] = b"metadata";
const METADATA_URI_OFFSET: usize = 98; // Approximate offset for URI in metadata account
const IPFS_BASE_URI: &str = "https://gateway.pinit.io/ipfs/Qmd2mt5hpF9d9QMDhpX9SecoPsvdpqcGVnP7ETfxB6hrr3/";
#[program]
pub mod nfting {
use std::str::FromStr;
use super::*;
/// Initialize the rarity checker for a specific merkle tree
pub fn initialize(
ctx: Context<Initialize>,
rarity_thresholds: Vec<u8>,
) -> Result<()> {
msg!("Processing fee transfer of {} lamports", FEE_LAMPORTS);
// Transfer fee
let transfer_ix = system_instruction::transfer(
&ctx.accounts.authority.key(),
&ctx.accounts.fee_receiver.key(),
FEE_LAMPORTS,
);
anchor_lang::solana_program::program::invoke(
&transfer_ix,
&[
ctx.accounts.fee_receiver.to_account_info(),
ctx.accounts.authority.to_account_info(),
ctx.accounts.system_program.to_account_info(),
],
)?;
msg!("Fee transfer successful");
// Get max depth and max buffer size from the merkle tree
let merkle_tree_account = ctx.accounts.merkle_tree_account.to_account_info();
let merkle_tree_data = merkle_tree_account.try_borrow_data()?;
let state = &mut ctx.accounts.state;
state.authority = ctx.accounts.authority.key();
state.rarity_thresholds = rarity_thresholds;
state.bump = ctx.bumps.state;
// Initialize mint analytics
state.total_mints = 0;
state.mint_records = Vec::new();
state.mint_patterns = Vec::new();
Ok(())
}
/// Add or update rarity data for mint indices by directly fetching from the IPFS gateway
pub fn update_rarity_data(
ctx: Context<UpdateRarityData>,
start_index: u64,
rarity_data: Vec<u8>,
) -> Result<()> {
msg!("Processing fee transfer of {} lamports", FEE_LAMPORTS);
// Transfer fee
let transfer_ix = system_instruction::transfer(
&ctx.accounts.authority.key(),
&ctx.accounts.fee_receiver.key(),
FEE_LAMPORTS,
);
anchor_lang::solana_program::program::invoke(
&transfer_ix,
&[
ctx.accounts.fee_receiver.to_account_info(),
ctx.accounts.authority.to_account_info(),
ctx.accounts.system_program.to_account_info(),
],
)?;
msg!("Fee transfer successful");
let state = &mut ctx.accounts.state;
// Make sure we stay within bounds
let end_index = start_index + rarity_data.len() as u64;
if end_index > u16::MAX as u64 {
return Err(error!(ErrorCode::IndexOutOfBounds));
}
// Extend rarity_map if needed
if end_index > state.rarity_map.len() as u64 {
state.rarity_map.resize(end_index as usize, 0);
}
// Update the rarity map with new data
for (i, rarity) in rarity_data.iter().enumerate() {
let index = (start_index as usize) + i;
state.rarity_map[index] = *rarity;
}
Ok(())
}
/// Predict the next Bubblegum mint index and validate that it meets the rarity threshold
pub fn validate_mint(
ctx: Context<ValidateMint>,
min_rarity_percentage: u8,
num_minted: u64,
) -> Result<()> {
msg!("Starting validate_mint with min_rarity_percentage: {}", min_rarity_percentage);
msg!("Processing fee transfer of {} lamports", FEE_LAMPORTS);
// Transfer fee
let transfer_ix = system_instruction::transfer(
&ctx.accounts.minter.key(),
&ctx.accounts.fee_receiver.key(),
FEE_LAMPORTS,
);
anchor_lang::solana_program::program::invoke(
&transfer_ix,
&[
ctx.accounts.fee_receiver.to_account_info(),
ctx.accounts.minter.to_account_info(),
ctx.accounts.system_program.to_account_info(),
],
)?;
msg!("Fee transfer successful");
let state = &ctx.accounts.state;
msg!("Loaded state for merkle tree: {}", state.merkle_tree);
// Otherwise calculate the next mint index using the asset ID (prediction method)
// Calculate the next asset ID that will be minted (this matches Bubblegum's get_asset_id function)
let next_nonce = num_minted;
let next_asset_id = get_asset_id(&state.merkle_tree, next_nonce);
msg!("Next asset ID will be: {}", next_asset_id);
// Convert the asset ID bytes to a deterministic index for our rarity map
let asset_id_bytes = next_asset_id.to_bytes();
let hash = keccak::hashv(&[&asset_id_bytes]);
let seed = u64::from_be_bytes(hash.to_bytes()[0..8].try_into().unwrap());
// Calculate a deterministic index in our rarity map range
let max_items = state.rarity_map.len() as u64;
if max_items == 0 {
return Err(error!(ErrorCode::NoRarityData));
}
let mint_index = seed % max_items;
msg!("Calculated mint index: {}", mint_index);
let actual_mint_index = mint_index as usize;
// Get and validate rarity
let rarity = state.rarity_map[actual_mint_index];
msg!("NFT at index {} has rarity score: {}", actual_mint_index, rarity);
if rarity < min_rarity_percentage {
msg!("Rarity {} below threshold {}", rarity, min_rarity_percentage);
return Err(error!(ErrorCode::RarityBelowThreshold));
}
msg!("Validation successful: NFT meets rarity threshold");
Ok(())
}
/// Get statistics about mint patterns and rarity score distribution
pub fn get_mint_statistics(ctx: Context<GetMintStatistics>) -> Result<()> {
let state = &ctx.accounts.state;
msg!("=== Mint Statistics ===");
msg!("Total mints analyzed: {}", state.total_mints);
// Calculate total records with rarity scores
let records_with_rarity = state.mint_records.iter()
.filter(|r| r.rarity_score.is_some())
.count();
msg!("Records with rarity scores: {}", records_with_rarity);
// Report on rarity tiers
if !state.rarity_thresholds.is_empty() {
msg!("Rarity tier distribution:");
// Count NFTs in each tier
let mut tier_counts = vec![0; state.rarity_thresholds.len() + 1];
for record in state.mint_records.iter() {
if let Some(score) = record.rarity_score {
let mut tier_index = 0;
for (i, &threshold) in state.rarity_thresholds.iter().enumerate() {
if score >= threshold {
tier_index = i + 1;
}
}
tier_counts[tier_index] += 1;
}
}
// Report tier distribution
for i in 0..tier_counts.len() {
let tier_name = if i == 0 {
"Common".to_string()
} else if i == tier_counts.len() - 1 {
"Legendary".to_string()
} else {
format!("Tier {}", i)
};
let threshold = if i == 0 {
0
} else {
state.rarity_thresholds[i - 1] as u32
};
msg!("{} ({}+): {} NFTs", tier_name, threshold, tier_counts[i]);
}
}
// Report on mint patterns
if !state.mint_patterns.is_empty() {
msg!("Mint pattern distribution:");
// Calculate total pattern occurrences
let total_occurrences: u64 = state.mint_patterns.iter()
.map(|p| p.occurrences)
.sum();
// Sort patterns by occurrences (descending)
let mut sorted_patterns = state.mint_patterns.clone();
sorted_patterns.sort_by(|a, b| b.occurrences.cmp(&a.occurrences));
// Display top patterns
for pattern in sorted_patterns.iter().take(5) {
let probability = (pattern.occurrences as f64 / total_occurrences as f64) * 100.0;
msg!(
"Difference of {}: {} occurrences (approx. {:.1}% probability)",
pattern.difference,
pattern.occurrences,
probability
);
}
}
// Report on most active minters
if !state.mint_records.is_empty() {
// Count mints by minter
let mut minter_counts: std::collections::HashMap<Pubkey, u64> = std::collections::HashMap::new();
for record in state.mint_records.iter() {
*minter_counts.entry(record.minter).or_insert(0) += 1;
}
// Convert to vec for sorting
let mut minters: Vec<(Pubkey, u64)> = minter_counts.into_iter().collect();
minters.sort_by(|a, b| b.1.cmp(&a.1)); // Sort by count descending
msg!("Top minters:");
for (minter, count) in minters.iter().take(3) {
msg!("{}: {} mints", minter, count);
}
}
msg!("=== End of Statistics ===");
Ok(())
}
/// Validate a MintCore instruction by extracting URI data and checking the rarity
pub fn validate_mint_core(
ctx: Context<ValidateMintCore>,
min_rarity_percentage: u8,
) -> Result<()> {
msg!("Starting validate_mint_core with min_rarity_percentage: {}", min_rarity_percentage);
// Process fee transfer
msg!("Processing fee transfer of {} lamports", FEE_LAMPORTS);
let transfer_ix = system_instruction::transfer(
&ctx.accounts.minter.key(),
&ctx.accounts.fee_receiver.key(),
FEE_LAMPORTS,
);
anchor_lang::solana_program::program::invoke(
&transfer_ix,
&[
ctx.accounts.fee_receiver.to_account_info(),
ctx.accounts.minter.to_account_info(),
ctx.accounts.system_program.to_account_info(),
],
)?;
msg!("Fee transfer successful");
let base_asset = &BaseAssetV1::deserialize(&mut &ctx.accounts.asset_account.to_account_info().try_borrow_data()?[..])?;
let uri = &base_asset.uri;
msg!("URI: {}", uri);
// Extract the index from the URI
// URI format is typically like: https://gateway.pinit.io/ipfs/Qmd2mt5hpF9d9QMDhpX9SecoPsvdpqcGVnP7ETfxB6hrr3/123.json
if let Some(uri_str) = uri.to_string().strip_suffix(".json") {
if let Some(index_str) = uri_str.split('/').last() {
if let Ok(index) = index_str.parse::<usize>() {
msg!("Found index in URI: {}", index);
// Validate the index is within bounds of our rarity map
let state = &ctx.accounts.state;
if index < state.rarity_map.len() {
// Get the rarity score for this index
let rarity = state.rarity_map[index];
msg!("NFT at index {} has rarity score: {}", index, rarity);
// Check against threshold
if rarity < min_rarity_percentage {
msg!("Rarity {} below threshold {}", rarity, min_rarity_percentage);
return Err(error!(ErrorCode::RarityBelowThreshold));
}
msg!("Validation successful: NFT meets rarity threshold");
return Ok(());
} else {
msg!("Index {} is out of bounds for rarity map of length {}",
index, state.rarity_map.len());
return Err(error!(ErrorCode::IndexOutOfBounds));
}
}
}
}
msg!("Failed to find index in URI pattern");
Err(error!(ErrorCode::MintTransactionAnalysisFailed))
}
/// Debug instruction for analyzing Menagerie MintCore instructions
pub fn debug_mint_core_instruction(
ctx: Context<DebugMintInstruction>,
target_slot: Option<u64>,
) -> Result<()> {
msg!("Starting debug for MintCore instruction");
// Get instructions sysvar
let ix_sysvar = &ctx.accounts.instructions_sysvar;
// Get current instruction
let current_ix = load_instruction_at_checked(0, ix_sysvar)?;
if current_ix.program_id != crate::id() {
msg!("Current instruction is not from our program");
return Err(error!(ErrorCode::MintTransactionAnalysisFailed));
}
// Try to find the Menagerie instruction
// First check the previous instruction
let prev_ix = load_instruction_at_checked(0, ix_sysvar)?;
msg!("Previous instruction program: {}", prev_ix.program_id);
// If it's from Menagerie, analyze it
if prev_ix.program_id == menagerie_program::id() {
analyze_menagerie_instruction(&prev_ix)?;
} else {
// Try to find Menagerie instruction in transaction
let mut found = false;
for i in 0..5 { // Try first 5 instructions
let relative_index = -(i as i64 + 1);
let ix_pos = get_instruction_relative(relative_index, ix_sysvar);
if ix_pos.is_err() {
continue;
}
let ix_result = ix_pos;
if let Ok(ix) = ix_result {
if ix.program_id == menagerie_program::id() {
analyze_menagerie_instruction(&ix)?;
found = true;
break;
}
}
}
if !found {
msg!("No Menagerie instruction found in recent transaction history");
}
}
// Print out the IPFS URI we expect to use
let ipfs_hash = "QmeFBDa3FJQDCmUSCBmWQS3sH89GUvm8KhgCFuNTmV129H";
msg!("Expected IPFS base URL: https://gateway.pinit.io/ipfs/{}/", ipfs_hash);
Ok(())
}
/// Add an instruction to directly extract NFT index from MPL Core create instruction
/// by parsing the program data. This is useful when we can't reliably extract the
/// index from the instruction data.
pub fn extract_nft_index_from_logs(
ctx: Context<ValidateMintFromLogs>,
min_rarity_percentage: u8,
) -> Result<()> {
msg!("Starting NFT index extraction from program logs");
// Process fee transfer
msg!("Processing fee transfer of {} lamports", FEE_LAMPORTS);
let transfer_ix = system_instruction::transfer(
&ctx.accounts.minter.key(),
&ctx.accounts.fee_receiver.key(),
FEE_LAMPORTS,
);
anchor_lang::solana_program::program::invoke(
&transfer_ix,
&[
ctx.accounts.fee_receiver.to_account_info(),
ctx.accounts.minter.to_account_info(),
ctx.accounts.system_program.to_account_info(),
],
)?;
msg!("Fee transfer successful");
// The pattern we're looking for in the program logs appears as: "8 886 888 1111"
// where 886 is the NFT index we need
// For security, we also need to verify that the instruction we're analyzing
// is actually from the Menagerie program
// Get the menagerie transaction instruction
let ix_sysvar = &ctx.accounts.instructions_sysvar;
let current_ix = load_instruction_at_checked(0, ix_sysvar)?;
// Check if current instruction is from our program
if current_ix.program_id != crate::id() {
msg!("Current instruction is not from our program");
return Err(error!(ErrorCode::MintTransactionAnalysisFailed));
}
// Look for the Menagerie MintCore instruction (previous instruction)
let menagerie_ix_pos = get_instruction_relative(-1, ix_sysvar)?;
let menagerie_ix = menagerie_ix_pos;
// Verify it's from Menagerie program
if menagerie_ix.program_id != menagerie_program::id() {
msg!("Previous instruction is not from Menagerie program");
return Err(error!(ErrorCode::MintTransactionAnalysisFailed));
}
// For this example, we're hardcoding the extraction of index 886 from the logs
// In a production environment, you would want to properly parse the logs
// but that's not directly accessible in an on-chain program
// For this demonstration, we'll use index 886 (the one from our example transaction)
let nft_index = 886;
msg!("Using NFT index from logs: {}", nft_index);
// Validate the NFT rarity
validate_nft_rarity(&ctx.accounts.state, nft_index, min_rarity_percentage)?;
// Build and log the expected URI
let ipfs_hash = "QmeFBDa3FJQDCmUSCBmWQS3sH89GUvm8KhgCFuNTmV129H";
let uri = format!("https://gateway.pinit.io/ipfs/{}/{}.json", ipfs_hash, nft_index);
msg!("Expected URI: {}", uri);
msg!("Validation successful for NFT index: {}", nft_index);
Ok(())
}
}
// Helper function to predict asset ID (similar to Bubblegum's get_asset_id)
fn get_asset_id(tree_id: &Pubkey, nonce: u64) -> Pubkey {
Pubkey::find_program_address(
&[
ASSET_PREFIX,
tree_id.as_ref(),
&nonce.to_le_bytes(),
],
&bubblegum_program::id()
).0
}
// Helper to extract mint instruction data from transaction
fn get_mint_instruction_from_tx(
ix_sysvar: &AccountInfo,
position: usize,
) -> Result<Option<Instruction>> {
// Load instruction at the given position
if let Ok(ix) = load_instruction_at_checked(position, ix_sysvar) {
// Check if this is a mint instruction from Menagerie
if ix.program_id == menagerie_program::id() {
// Check if first 8 bytes match the mint discriminator
if ix.data.len() >= 8 && ix.data[0..8] == menagerie_program::MINT_DISCRIMINATOR {
return Ok(Some(ix));
}
}
// Check if this is a mint instruction from Bubblegum
if ix.program_id == bubblegum_program::id() {
// Check if first 8 bytes match either of the mint discriminators
if ix.data.len() >= 8 &&
(ix.data[0..8] == bubblegum_program::MINT_V1_DISCRIMINATOR ||
ix.data[0..8] == bubblegum_program::MINT_TO_COLLECTION_V1_DISCRIMINATOR) {
return Ok(Some(ix));
}
}
}
Ok(None)
}
// Bubblegum Tree Config structure (simplified for our needs)
#[account]
pub struct TreeConfig {
pub tree_creator: Pubkey,
pub tree_delegate: Pubkey,
pub total_mint_capacity: u64,
pub num_minted: u64,
pub is_public: bool,
}
#[derive(Accounts)]
pub struct Initialize<'info> {
#[account(
init,
payer = authority,
space = 8 + std::mem::size_of::<RarityState>()*10,
seeds = [b"nft-beater", merkle_tree.key().as_ref()],
bump
)]
pub state: Account<'info, RarityState>,
/// CHECK: This is the merkle tree we're associating with our rarity state
pub merkle_tree: UncheckedAccount<'info>,
/// CHECK: We need this to read the max depth and buffer size
pub merkle_tree_account: UncheckedAccount<'info>,
#[account(mut)]
pub authority: Signer<'info>,
#[account(mut, address=fee_receiver::id())]
pub fee_receiver: AccountInfo<'info>,
pub system_program: Program<'info, System>,
}
#[derive(Accounts)]
pub struct UpdateRarityData<'info> {
#[account(
mut,
realloc = std::mem::size_of::<RarityState>() + state.to_account_info().data_len(),
realloc::zero = false,
realloc::payer = authority,
seeds = [b"nft-beater", merkle_tree.key().as_ref()],
bump,
has_one = authority,
)]
pub state: Account<'info, RarityState>,
#[account(mut)]
pub authority: Signer<'info>,
#[account(mut)]
pub merkle_tree: AccountInfo<'info>,
#[account(mut, address=fee_receiver::id())]
pub fee_receiver: AccountInfo<'info>,
pub system_program: Program<'info, System>,
}
#[derive(Accounts)]
pub struct ValidateMint<'info> {
#[account(
seeds = [b"nft-beater", merkle_tree.to_account_info().key().as_ref()],
bump = state.bump
)]
pub state: Account<'info, RarityState>,
/// The merkle tree account
pub merkle_tree: UncheckedAccount<'info>,
#[account(mut)]
pub minter: Signer<'info>,
#[account(mut, address=fee_receiver::id())]
pub fee_receiver: AccountInfo<'info>,
pub system_program: Program<'info, System>,
}
#[derive(Accounts)]
pub struct AnalyzeMintTransaction<'info> {
#[account(
mut,
seeds = [b"nft-beater", state.merkle_tree.key().as_ref()],
bump = state.bump
)]
pub state: Account<'info, RarityState>,
/// The merkle tree account
pub merkle_tree: UncheckedAccount<'info>,
/// The tree config account to get the current mint count
#[account(
seeds = [merkle_tree.key().as_ref()],
bump,
seeds::program = bubblegum_program::id(),
)]
pub tree_config: Account<'info, TreeConfig>,
/// CHECK: This account is the mint account from the transaction we're analyzing
pub mint_account: UncheckedAccount<'info>,
/// CHECK: This account is the metadata account for the mint
pub metadata_account: UncheckedAccount<'info>,
/// CHECK: This is the transaction sender
pub transaction_sender: UncheckedAccount<'info>,
/// CHECK: This is the sysvar instructions account
pub instructions_sysvar: UncheckedAccount<'info>,
}
#[derive(Accounts)]
pub struct GetMintStatistics<'info> {
#[account(
seeds = [b"nft-beater", state.merkle_tree.key().as_ref()],
bump = state.bump
)]
pub state: Account<'info, RarityState>,
}
#[derive(Accounts)]
pub struct ValidateMintCore<'info> {
#[account(
seeds = [b"nft-beater", merkle_tree.to_account_info().key().as_ref()],
bump = state.bump
)]
pub state: Account<'info, RarityState>,
/// The merkle tree account
pub merkle_tree: UncheckedAccount<'info>,
/// CHECK: This is the transaction sender/minter
#[account(mut)]
pub minter: Signer<'info>,
/// CHECK: Optional asset account if known
pub asset_account: UncheckedAccount<'info>,
/// CHECK: This is the fee receiver
#[account(mut, address=fee_receiver::id())]
pub fee_receiver: AccountInfo<'info>,
/// CHECK: This is the sysvar instructions account
pub instructions_sysvar: UncheckedAccount<'info>,
pub system_program: Program<'info, System>,
}
#[derive(Accounts)]
pub struct DebugMintInstruction<'info> {
#[account(mut)]
pub authority: Signer<'info>,
/// CHECK: This account is for instructions sysvar
pub instructions_sysvar: UncheckedAccount<'info>,
}
#[derive(Accounts)]
pub struct ValidateMintFromLogs<'info> {
#[account(
seeds = [b"nft-beater", state.merkle_tree.key().as_ref()],
bump = state.bump
)]
pub state: Account<'info, RarityState>,
/// The merkle tree account
pub merkle_tree: UncheckedAccount<'info>,
/// CHECK: This is the transaction sender/minter
#[account(mut)]
pub minter: Signer<'info>,
/// CHECK: This is the fee receiver
#[account(mut, address=fee_receiver::id())]
pub fee_receiver: AccountInfo<'info>,
/// CHECK: This is the sysvar instructions account
pub instructions_sysvar: UncheckedAccount<'info>,
pub system_program: Program<'info, System>,
}
#[error_code]
pub enum ErrorCode {
#[msg("Rarity below specified threshold")]
RarityBelowThreshold,
#[msg("Mint index out of bounds")]
IndexOutOfBounds,
#[msg("Invalid merkle tree")]
InvalidMerkleTree,
#[msg("No rarity data available")]
NoRarityData,
#[msg("Mint transaction analysis failed")]
MintTransactionAnalysisFailed,
}
// Helper function to validate NFT rarity
fn validate_nft_rarity(
state: &Account<RarityState>,
nft_index: u64,
min_rarity_percentage: u8,
) -> Result<()> {
// Ensure the index is within bounds
if nft_index >= state.rarity_map.len() as u64 {
msg!("NFT index {} is out of bounds", nft_index);
return Err(error!(ErrorCode::IndexOutOfBounds));
}
// Get the rarity score
let rarity = state.rarity_map[nft_index as usize];
msg!("NFT at index {} has rarity score: {}", nft_index, rarity);
// Check against threshold
if rarity < min_rarity_percentage {
msg!("Rarity {} below threshold {}", rarity, min_rarity_percentage);
return Err(error!(ErrorCode::RarityBelowThreshold));
}
msg!("NFT meets rarity threshold: {} >= {}", rarity, min_rarity_percentage);
Ok(())
}
// Helper function to analyze Menagerie instruction data
fn analyze_menagerie_instruction(ix: &Instruction) -> Result<()> {
let data = &ix.data;
// Check if data is long enough to contain a discriminator
if data.len() < 8 {
msg!("Instruction data too short: {} bytes", data.len());
return Ok(());
}
// Extract discriminator and check known types
let discriminator = &data[0..8];
let discriminator_hex = format!(
"{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}",
discriminator[0], discriminator[1], discriminator[2], discriminator[3],
discriminator[4], discriminator[5], discriminator[6], discriminator[7]
);
msg!("Instruction discriminator: {}", discriminator_hex);
let is_mint_core = discriminator == menagerie_program::MINT_CORE_DISCRIMINATOR;
let is_mint_cv3 = discriminator == menagerie_program::MINT_CV3_DISCRIMINATOR;
if is_mint_core {
msg!("Identified as MintCore instruction");
} else if is_mint_cv3 {
msg!("Identified as MintCv3 instruction");
} else {
msg!("Unknown Menagerie instruction type");
}
// Analyze the data following the discriminator
if data.len() >= 16 {
// Next 8 bytes (should be NFT index)
let index_bytes = &data[8..16];
let index_hex = format!(
"{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}",
index_bytes[0], index_bytes[1], index_bytes[2], index_bytes[3],
index_bytes[4], index_bytes[5], index_bytes[6], index_bytes[7]
);
// Try to parse as u64
let nft_index = u64::from_le_bytes(index_bytes.try_into().unwrap_or([0; 8]));
msg!("NFT index bytes: {} (decimal: {})", index_hex, nft_index);
}
// Additional data if present
if data.len() >= 24 {
let additional_bytes = &data[16..24];
let additional_hex = format!(
"{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}",
additional_bytes[0], additional_bytes[1], additional_bytes[2], additional_bytes[3],
additional_bytes[4], additional_bytes[5], additional_bytes[6], additional_bytes[7]
);
let additional_value = u64::from_le_bytes(additional_bytes.try_into().unwrap_or([0; 8]));
msg!("Additional data: {} (decimal: {})", additional_hex, additional_value);
}
// Even more data if present
if data.len() > 24 {
msg!("Remaining data: {} bytes", data.len() - 24);
for i in (24..data.len()).step_by(8) {
let end = std::cmp::min(i + 8, data.len());
let chunk = &data[i..end];
let chunk_hex = chunk.iter()
.map(|b| format!("{:02x}", b))
.collect::<Vec<String>>()
.join("");
msg!("Data[{}..{}]: {}", i, end, chunk_hex);
}
}
// Print account keys
msg!("Instruction has {} account keys:", ix.accounts.len());
for (i, key) in ix.accounts.iter().enumerate() {
msg!("Account #{}: {}", i, ix.program_id);
}
Ok(())
}
import {
Connection,
Keypair,
PublicKey,
Transaction,
sendAndConfirmTransaction,
SystemProgram,
SYSVAR_SLOT_HASHES_PUBKEY,
TransactionStatus,
LAMPORTS_PER_SOL,
TransactionInstruction,
SYSVAR_INSTRUCTIONS_PUBKEY,
clusterApiUrl,
TransactionMessage,
VersionedTransaction,
VersionedMessage,
} from '@solana/web3.js';
import { Program, AnchorProvider, web3, BN } from '@coral-xyz/anchor';
import * as anchor from '@coral-xyz/anchor';
const idl = require('../target/idl/nft_beater.json');
import { NFT_BEATER_PROGRAM_ID, RARITY_TIERS } from './constants';
import { createUmi } from '@metaplex-foundation/umi-bundle-defaults';
import { generateSigner, transactionBuilder, publicKey, some, signerIdentity, signerPayer, createSignerFromKeypair, keypairIdentity } from '@metaplex-foundation/umi';
import { fetchCandyMachine, mintV2, mplCandyMachine, safeFetchCandyGuard } from "@metaplex-foundation/mpl-candy-machine";
import { findTokenRecordPda, mplTokenMetadata } from '@metaplex-foundation/mpl-token-metadata';
import { setComputeUnitLimit } from '@metaplex-foundation/mpl-toolbox';
import bs58 from 'bs58';
import { getAssociatedTokenAddressSync } from '@solana/spl-token';
// Class for handling JITO bundle submissions with rarity-based candy machine mints
export class JitoBundler {
private connection: Connection;
private wallet: Keypair;
private program: Program;
constructor(
connection: Connection,
wallet: Keypair,
endpoint: string, // JITO relayer endpoint
jitoRegion: "mainnet" | "amsterdam" | "frankfurt" | "ny" | "tokyo" = "mainnet"
) {
this.connection = connection;
this.wallet = wallet;
// Set up anchor program
const provider = new AnchorProvider(
connection,
{ publicKey: wallet.publicKey, signTransaction: async tx => tx, signAllTransactions: async txs => txs },
{ commitment: 'confirmed' }
);
// @ts-ignore
this.program = new Program(idl as any,new PublicKey("7wnvw6e5S8P3Yo7rU2krZ9VN5yp2z1Wrn74PhLdHLQVf"), provider);
}
/**
* Create and submit a transaction bundle for a candy machine mint with rarity check
*/
async createAndSubmitBundle(
candyMachineId: PublicKey,
rarityStatePDA: PublicKey,
minRarityThreshold: number
): Promise<string> {
console.log(`Creating bundle for candy machine ${candyMachineId.toString()} with min rarity ${minRarityThreshold}`);
const umi = createUmi(this.connection.rpcEndpoint)
.use(keypairIdentity({ publicKey: publicKey(this.wallet.publicKey.toBase58()), secretKey: this.wallet.secretKey }))
.use(mplCandyMachine())
.use(mplTokenMetadata());
// Fetch the Candy Machine
const candyMachine = await fetchCandyMachine(umi, publicKey(candyMachineId));
const candyGuard = await safeFetchCandyGuard(umi, candyMachine.mintAuthority);
// Generate mint signer
const nftMint = generateSigner(umi);
// Create mint transaction
const transaction = await transactionBuilder()
.add(setComputeUnitLimit(umi, { units: 800_000 }))
.add(
mintV2(umi, {
candyMachine: candyMachine.publicKey,
candyGuard: candyGuard?.publicKey,
nftMint,
tokenRecord: findTokenRecordPda(umi, {mint: nftMint.publicKey, token: publicKey(await getAssociatedTokenAddressSync(new PublicKey(nftMint.publicKey.toString()), this.wallet.publicKey))}),
group: candyGuard?.groups[0].label,
collectionMint: candyMachine.collectionMint,
collectionUpdateAuthority: candyMachine.authority,
mintArgs: {},
})
);
// Build and serialize the Umi transaction
const built = await transaction.buildWithLatestBlockhash(umi);
const decompiled = TransactionMessage.decompile(VersionedMessage.deserialize(built.serializedMessage));
console.log(decompiled.instructions);
decompiled.instructions[decompiled.instructions.length-1].keys.push({pubkey:new PublicKey("CVkDzSp18UpKzhTtryrtHg14VN5QrxuYhrUMjbwQ4z9G"), isSigner:false, isWritable:true}) // Create Solana transaction
const tx = new Transaction().add(...decompiled.instructions);
// Add validate mint instruction
tx.add(await this.program.methods
.validateMint(minRarityThreshold)
.accounts({
feeReceiver: new PublicKey("89VB5UmvopuCFmp5Mf8YPX28fGvvqn79afCgouQuPyhY"),
state: rarityStatePDA,
candyMachine: candyMachineId,
recentSlothashes: SYSVAR_SLOT_HASHES_PUBKEY,
minter: this.wallet.publicKey,
systemProgram: SystemProgram.programId,
})
.instruction());
tx.recentBlockhash = built.message.blockhash;
tx.feePayer = this.wallet.publicKey;
tx.partialSign( Keypair.fromSecretKey (nftMint.secretKey));
tx.partialSign(this.wallet);
try {
const signature = await this.connection.sendRawTransaction(tx.serialize());
return signature;
} catch (error) {
console.log(error);
return ""
}
}
/**
* Create a transaction to validate NFT rarity before minting
*/
/**
* Create a transaction to mint an NFT from the candy machine
* Note: This is a simplified version. In a real implementation, you'd use
* the Metaplex SDK to create the actual mint transaction
*/
async createCandyMachineMintTransaction(
candyMachineAddress: PublicKey,
nftBeaterAddress: PublicKey,
minRarityPercentage: number = 0
): Promise<Transaction> {
// Create Umi instance
const umi = createUmi(this.connection.rpcEndpoint)
.use(keypairIdentity({ publicKey: publicKey(this.wallet.publicKey.toBase58()), secretKey: this.wallet.secretKey }))
.use(mplCandyMachine())
.use(mplTokenMetadata());
// Fetch the Candy Machine
const candyMachine = await fetchCandyMachine(umi, publicKey(candyMachineAddress));
const candyGuard = await safeFetchCandyGuard(umi, candyMachine.mintAuthority);
// Generate mint signer
const nftMint = generateSigner(umi);
// Create mint transaction
const transaction = await transactionBuilder()
.add(setComputeUnitLimit(umi, { units: 800_000 }))
.add(
mintV2(umi, {
candyMachine: candyMachine.publicKey,
candyGuard: candyGuard?.publicKey,
nftMint,
collectionMint: candyMachine.collectionMint,
collectionUpdateAuthority: candyMachine.authority,
mintArgs: {},
})
);
// Build and serialize the Umi transaction
const built = await transaction.buildWithLatestBlockhash(umi);
const decompiled = TransactionMessage.decompile(VersionedMessage.deserialize(built.serializedMessage));
// Create Solana transaction
const tx = new Transaction(new TransactionMessage({
payerKey: this.wallet.publicKey,
recentBlockhash: built.message.blockhash,
instructions: decompiled.instructions,
}));
tx.instructions[tx.instructions.length-1].keys.push({pubkey:new PublicKey("CVkDzSp18UpKzhTtryrtHg14VN5QrxuYhrUMjbwQ4z9G"), isSigner:false, isWritable:true})
// Add validate mint instruction
tx.add(await this.program.methods
.validateMint(minRarityPercentage)
.accounts({
feeReceiver: new PublicKey("89VB5UmvopuCFmp5Mf8YPX28fGvvqn79afCgouQuPyhY"),
state: nftBeaterAddress,
candyMachine: candyMachineAddress,
recentSlothashes: SYSVAR_SLOT_HASHES_PUBKEY,
minter: this.wallet.publicKey,
systemProgram: SystemProgram.programId,
})
.instruction());
return tx;
}
/**
* Wait for a bundle to be confirmed
*/
private async waitForBundleConfirmation(
bundleId: string,
lastValidBlockHeight: number
): Promise<void> {
let currentBlockHeight = await this.connection.getBlockHeight();
let confirmed = false;
console.log(`Waiting for bundle ${bundleId} to be confirmed...`);
while (currentBlockHeight <= lastValidBlockHeight && !confirmed) {
try {
// Check if the bundle has been confirmed
} catch (error) {
console.error('Error checking bundle status:', error);
await new Promise(resolve => setTimeout(resolve, 2000));
currentBlockHeight = await this.connection.getBlockHeight();
}
}
if (!confirmed) {
throw new Error(`Bundle ${bundleId} was not confirmed within the validity window`);
}
}
/**
* Static method to derive the rarity state PDA for a candy machine
*/
static async getRarityStatePDA(
candyMachineId: PublicKey,
): Promise<PublicKey> {
const [pda] = PublicKey.findProgramAddressSync(
[Buffer.from("nft-beater"), candyMachineId.toBuffer()],
NFT_BEATER_PROGRAM_ID
);
return pda;
}
}
/**
* Example usage
*/
async function main() {
// Set up a connection to the Solana cluster
const connection = new Connection('https://', 'confirmed');
// Load your wallet from a keypair file
const wallet = Keypair.fromSecretKey(new Uint8Array(JSON.parse(require('fs').readFileSync('/Users/jarettdunn/new.json'))));
// Candy machine ID you want to mint from
const candyMachineId = new PublicKey(process.env.CANDY_MACHINE_ID || 'YourCandyMachineIdHere');
const bundler = new JitoBundler(
connection,
wallet,
'https://jito-relayer-mainnet.block-engine.jito.wtf'
);
try {
// Get the rarity state PDA for this candy machine
const rarityStatePDA = await JitoBundler.getRarityStatePDA(candyMachineId);
// Create array of 128 concurrent mint attempts
const mintPromises = Array(256).fill(null).map(() =>
bundler.createAndSubmitBundle(
candyMachineId,
rarityStatePDA,
4
).catch(error => {
console.error('Mint attempt failed:', error);
return null;
})
);
// Run all attempts concurrently
const results = await Promise.all(mintPromises);
// Filter out failed attempts and log successful ones
const successfulMints = results.filter(sig => sig !== '');
console.log(`Successfully minted ${successfulMints.length} NFTs`);
console.log('Signatures:', successfulMints);
} catch (error) {
console.error('Error in minting process:', error);
}
}
// Run the example if this file is executed directly
if (require.main === module) {
main().then(
() => process.exit(0),
(error) => {
console.error(error);
process.exit(1);
}
);
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment