Last active
March 19, 2025 14:49
-
-
Save staccDOTsol/556e30a74d554092128d32861beb68a7 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 }; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | |
} | |
); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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, | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(()) | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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