UNPKG

candy-machine-assistant

Version:

A tool to assist in the connecting to candy machines, mint accounts and confirm NFT transactions based on Solana's Metaplex NFTs.

526 lines (482 loc) 15.4 kB
/* eslint-disable */ import * as anchor from "@project-serum/anchor"; import { MintLayout, TOKEN_PROGRAM_ID, Token } from "@solana/spl-token"; import { SystemProgram, Transaction, SYSVAR_SLOT_HASHES_PUBKEY, TransactionInstruction } from "@solana/web3.js"; import { sendTransactions, SequenceType } from "./connection"; import { CIVIC, getAtaForMint, getNetworkExpire, getNetworkToken, SPL_ASSOCIATED_TOKEN_ACCOUNT_PROGRAM_ID, } from "./utils"; export const CANDY_MACHINE_PROGRAM = new anchor.web3.PublicKey("cndy3Z4yapfJBmL3ShUp5exZKqR3z33thTzeNMm2gRZ"); export const TOKEN_METADATA_PROGRAM_ID = new anchor.web3.PublicKey("metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s"); interface CandyMachineState { authority: anchor.web3.PublicKey; itemsAvailable: number; itemsRedeemed: number; itemsRemaining: number; treasury: anchor.web3.PublicKey; tokenMint: null | anchor.web3.PublicKey; isSoldOut: boolean; isActive: boolean; isPresale: boolean; isWhitelistOnly: boolean; goLiveDate: null | anchor.BN; price: anchor.BN; gatekeeper: null | { expireOnUse: boolean; gatekeeperNetwork: anchor.web3.PublicKey; }; endSettings: null | { number: anchor.BN; endSettingType: any; }; whitelistMintSettings: null | { mode: any; mint: anchor.web3.PublicKey; presale: boolean; discountPrice: null | anchor.BN; }; hiddenSettings: null | { name: string; uri: string; hash: Uint8Array; }; retainAuthority: boolean; } export interface CandyMachineAccount { id: anchor.web3.PublicKey; program: anchor.Program; state: CandyMachineState; } interface Remain { pubkey: anchor.web3.PublicKey; isWritable: boolean; isSigner: boolean; } export const awaitTransactionSignatureConfirmation = async ( txid: anchor.web3.TransactionSignature, timeout: number, connection: anchor.web3.Connection, queryStatus = false, ): Promise<anchor.web3.SignatureStatus | null | void> => { let done = false; let status: anchor.web3.SignatureStatus | null | void = { slot: 0, confirmations: 0, err: null, }; const subId = 0; status = await new Promise(async (resolve, reject) => { setTimeout(() => { if (done) { return; } done = true; console.log("Rejecting for timeout..."); reject({ timeout: true }); }, timeout); while (!done && queryStatus) { // eslint-disable-next-line no-loop-func (async () => { try { const signatureStatuses = await connection.getSignatureStatuses([txid]); status = signatureStatuses && signatureStatuses.value[0]; if (!done) { if (!status) { console.log("REST null result for", txid, status); } else if (status.err) { console.log("REST error for", txid, status); done = true; reject(status.err); } else if (!status.confirmations) { console.log("REST no confirmations for", txid, status); } else { console.log("REST confirmation for", txid, status); done = true; resolve(status); } } } catch (e) { if (!done) { console.log("REST connection error: txid", txid, e); } } })(); await sleep(2000); } }); //@ts-ignore try { connection.removeSignatureListener(subId); } catch (e) { // ignore } done = true; return status; }; const createAssociatedTokenAccountInstruction = ( associatedTokenAddress: anchor.web3.PublicKey, payer: anchor.web3.PublicKey, walletAddress: anchor.web3.PublicKey, splTokenMintAddress: anchor.web3.PublicKey, ) => { const keys = [ { pubkey: payer, isSigner: true, isWritable: true }, { pubkey: associatedTokenAddress, isSigner: false, isWritable: true }, { pubkey: walletAddress, isSigner: false, isWritable: false }, { pubkey: splTokenMintAddress, isSigner: false, isWritable: false }, { pubkey: anchor.web3.SystemProgram.programId, isSigner: false, isWritable: false, }, { pubkey: TOKEN_PROGRAM_ID, isSigner: false, isWritable: false }, { pubkey: anchor.web3.SYSVAR_RENT_PUBKEY, isSigner: false, isWritable: false, }, ]; return new anchor.web3.TransactionInstruction({ keys, programId: SPL_ASSOCIATED_TOKEN_ACCOUNT_PROGRAM_ID, data: Buffer.from([]), }); }; export const getCandyMachineState = async ( anchorWallet: anchor.Wallet, candyMachineId: anchor.web3.PublicKey, connection: anchor.web3.Connection, ): Promise<CandyMachineAccount> => { const provider = new anchor.Provider(connection, anchorWallet, { preflightCommitment: "processed", }); const getProgramState = async (): Promise<[anchor.Program, any]> => { const idl = await anchor.Program.fetchIdl(CANDY_MACHINE_PROGRAM, provider); const program = new anchor.Program(idl!, CANDY_MACHINE_PROGRAM, provider); const state: any = await program.account.candyMachine.fetch(candyMachineId); return [program, state]; }; const getCurrentBlockTime = async (): Promise<number> => { const slot = await connection.getSlot(); return (await connection.getBlockTime(slot)) ?? new Date().getTime() / 1000; }; const [[program, state], currentBlockTime] = await Promise.all([getProgramState(), getCurrentBlockTime()]); const itemsAvailable = state.data.itemsAvailable.toNumber(); const itemsRedeemed = state.itemsRedeemed.toNumber(); const itemsRemaining = itemsAvailable - itemsRedeemed; const timeDiff = new Date().getTime() / 1000 - currentBlockTime; const goLiveDate = state.data.goLiveDate !== null ? state.data.goLiveDate + timeDiff : null; return { id: candyMachineId, program, state: { authority: state.authority, itemsAvailable, itemsRedeemed, itemsRemaining, isSoldOut: itemsRemaining === 0, isActive: false, isPresale: false, isWhitelistOnly: false, goLiveDate: state.data.goLiveDate, treasury: state.wallet, tokenMint: state.tokenMint, gatekeeper: state.data.gatekeeper, endSettings: state.data.endSettings, whitelistMintSettings: state.data.whitelistMintSettings, hiddenSettings: state.data.hiddenSettings, price: state.data.price, retainAuthority: state.data.retainAuthority, }, }; }; const getMasterEdition = async (mint: anchor.web3.PublicKey): Promise<anchor.web3.PublicKey> => { return ( await anchor.web3.PublicKey.findProgramAddress( [Buffer.from("metadata"), TOKEN_METADATA_PROGRAM_ID.toBuffer(), mint.toBuffer(), Buffer.from("edition")], TOKEN_METADATA_PROGRAM_ID, ) )[0]; }; export const getMetadataPDA = async (mint: anchor.web3.PublicKey): Promise<anchor.web3.PublicKey> => { return ( await anchor.web3.PublicKey.findProgramAddress( [Buffer.from("metadata"), TOKEN_METADATA_PROGRAM_ID.toBuffer(), mint.toBuffer()], TOKEN_METADATA_PROGRAM_ID, ) )[0]; }; export const getCandyMachineCreator = async ( candyMachine: anchor.web3.PublicKey, ): Promise<[anchor.web3.PublicKey, number]> => { return await anchor.web3.PublicKey.findProgramAddress( [Buffer.from("candy_machine"), candyMachine.toBuffer()], CANDY_MACHINE_PROGRAM, ); }; export const getCollectionPDA = async ( candyMachineAddress: anchor.web3.PublicKey, ): Promise<[anchor.web3.PublicKey, number]> => { return await anchor.web3.PublicKey.findProgramAddress( [Buffer.from("collection"), candyMachineAddress.toBuffer()], CANDY_MACHINE_PROGRAM, ); }; export interface CollectionData { mint: anchor.web3.PublicKey; candyMachine: anchor.web3.PublicKey; } export const getCollectionAuthorityRecordPDA = async ( mint: anchor.web3.PublicKey, newAuthority: anchor.web3.PublicKey, ): Promise<anchor.web3.PublicKey> => { return ( await anchor.web3.PublicKey.findProgramAddress( [ Buffer.from("metadata"), TOKEN_METADATA_PROGRAM_ID.toBuffer(), mint.toBuffer(), Buffer.from("collection_authority"), newAuthority.toBuffer(), ], TOKEN_METADATA_PROGRAM_ID, ) )[0]; }; export type SetupState = { mint: anchor.web3.Keypair; userTokenAccount: anchor.web3.PublicKey; transaction: string; }; export const createAccountsForMint = async ( candyMachine: CandyMachineAccount, payer: anchor.web3.PublicKey, ): Promise<SetupState> => { const mint = anchor.web3.Keypair.generate(); const userTokenAccountAddress = (await getAtaForMint(mint.publicKey, payer))[0]; const signers: anchor.web3.Keypair[] = [mint]; const instructions: any[] = [ anchor.web3.SystemProgram.createAccount({ fromPubkey: payer, newAccountPubkey: mint.publicKey, space: MintLayout.span, lamports: await candyMachine.program.provider.connection.getMinimumBalanceForRentExemption(MintLayout.span), programId: TOKEN_PROGRAM_ID, }), Token.createInitMintInstruction(TOKEN_PROGRAM_ID, mint.publicKey, 0, payer, payer), createAssociatedTokenAccountInstruction(userTokenAccountAddress, payer, payer, mint.publicKey), Token.createMintToInstruction(TOKEN_PROGRAM_ID, mint.publicKey, userTokenAccountAddress, payer, [], 1), ]; return { mint: mint, userTokenAccount: userTokenAccountAddress, transaction: ( await sendTransactions( candyMachine.program.provider.connection, candyMachine.program.provider.wallet, [instructions], [signers], SequenceType.StopOnFailure, "singleGossip", () => {}, () => false, undefined, [], [], ) ).txs[0].txid, }; }; type MintResult = { mintTxId: string; metadataKey: anchor.web3.PublicKey; }; export const mintOneToken = async ( candyMachine: CandyMachineAccount, payer: anchor.web3.PublicKey, beforeTransactions: Transaction[] = [], afterTransactions: Transaction[] = [], setupState?: SetupState, ): Promise<MintResult | null> => { const mint = setupState?.mint ?? anchor.web3.Keypair.generate(); const userTokenAccountAddress = (await getAtaForMint(mint.publicKey, payer))[0]; const userPayingAccountAddress = candyMachine.state.tokenMint ? (await getAtaForMint(candyMachine.state.tokenMint, payer))[0] : payer; const candyMachineAddress = candyMachine.id; const remainingAccounts: Remain[] = []; const instructions: TransactionInstruction[] = []; const signers: anchor.web3.Keypair[] = []; console.log("SetupState: ", setupState); if (!setupState) { signers.push(mint); instructions.push( ...[ anchor.web3.SystemProgram.createAccount({ fromPubkey: payer, newAccountPubkey: mint.publicKey, space: MintLayout.span, lamports: await candyMachine.program.provider.connection.getMinimumBalanceForRentExemption( MintLayout.span, ), programId: TOKEN_PROGRAM_ID, }), Token.createInitMintInstruction(TOKEN_PROGRAM_ID, mint.publicKey, 0, payer, payer), createAssociatedTokenAccountInstruction(userTokenAccountAddress, payer, payer, mint.publicKey), Token.createMintToInstruction(TOKEN_PROGRAM_ID, mint.publicKey, userTokenAccountAddress, payer, [], 1), ], ); } if (candyMachine.state.gatekeeper) { remainingAccounts.push({ pubkey: (await getNetworkToken(payer, candyMachine.state.gatekeeper.gatekeeperNetwork))[0], isWritable: true, isSigner: false, }); if (candyMachine.state.gatekeeper.expireOnUse) { remainingAccounts.push({ pubkey: CIVIC, isWritable: false, isSigner: false, }); remainingAccounts.push({ pubkey: (await getNetworkExpire(candyMachine.state.gatekeeper.gatekeeperNetwork))[0], isWritable: false, isSigner: false, }); } } if (candyMachine.state.whitelistMintSettings) { const mint = new anchor.web3.PublicKey(candyMachine.state.whitelistMintSettings.mint); const whitelistToken = (await getAtaForMint(mint, payer))[0]; remainingAccounts.push({ pubkey: whitelistToken, isWritable: true, isSigner: false, }); if (candyMachine.state.whitelistMintSettings.mode.burnEveryTime) { remainingAccounts.push({ pubkey: mint, isWritable: true, isSigner: false, }); remainingAccounts.push({ pubkey: payer, isWritable: false, isSigner: true, }); } } if (candyMachine.state.tokenMint) { remainingAccounts.push({ pubkey: userPayingAccountAddress, isWritable: true, isSigner: false, }); remainingAccounts.push({ pubkey: payer, isWritable: false, isSigner: true, }); } const metadataAddress = await getMetadataPDA(mint.publicKey); const masterEdition = await getMasterEdition(mint.publicKey); const [candyMachineCreator, creatorBump] = await getCandyMachineCreator(candyMachineAddress); console.log(remainingAccounts.map(rm => rm.pubkey.toBase58())); instructions.push( await candyMachine.program.instruction.mintNft(creatorBump, { accounts: { candyMachine: candyMachineAddress, candyMachineCreator, payer: payer, wallet: candyMachine.state.treasury, mint: mint.publicKey, metadata: metadataAddress, masterEdition, mintAuthority: payer, updateAuthority: payer, tokenMetadataProgram: TOKEN_METADATA_PROGRAM_ID, tokenProgram: TOKEN_PROGRAM_ID, systemProgram: SystemProgram.programId, rent: anchor.web3.SYSVAR_RENT_PUBKEY, clock: anchor.web3.SYSVAR_CLOCK_PUBKEY, recentBlockhashes: SYSVAR_SLOT_HASHES_PUBKEY, instructionSysvarAccount: anchor.web3.SYSVAR_INSTRUCTIONS_PUBKEY, }, remainingAccounts: remainingAccounts.length > 0 ? remainingAccounts : undefined, }), ); const [collectionPDA] = await getCollectionPDA(candyMachineAddress); const collectionPDAAccount = await candyMachine.program.provider.connection.getAccountInfo(collectionPDA); if (collectionPDAAccount && candyMachine.state.retainAuthority) { try { const collectionData = (await candyMachine.program.account.collectionPda.fetch( collectionPDA, )) as CollectionData; const collectionMint = collectionData.mint; const collectionAuthorityRecord = await getCollectionAuthorityRecordPDA(collectionMint, collectionPDA); if (collectionMint) { const collectionMetadata = await getMetadataPDA(collectionMint); const collectionMasterEdition = await getMasterEdition(collectionMint); console.log("Collection PDA: ", collectionPDA.toBase58()); console.log("Authority: ", candyMachine.state.authority.toBase58()); instructions.push( await candyMachine.program.instruction.setCollectionDuringMint({ accounts: { candyMachine: candyMachineAddress, metadata: metadataAddress, payer: payer, collectionPda: collectionPDA, tokenMetadataProgram: TOKEN_METADATA_PROGRAM_ID, instructions: anchor.web3.SYSVAR_INSTRUCTIONS_PUBKEY, collectionMint, collectionMetadata, collectionMasterEdition, authority: candyMachine.state.authority, collectionAuthorityRecord, }, }), ); } } catch (error) { console.error(error); } } const instructionsMatrix = [instructions]; const signersMatrix = [signers]; try { const txns = ( await sendTransactions( candyMachine.program.provider.connection, candyMachine.program.provider.wallet, instructionsMatrix, signersMatrix, SequenceType.StopOnFailure, "singleGossip", () => {}, () => false, undefined, beforeTransactions, afterTransactions, ) ).txs.map(t => t.txid); const mintTxn = txns[0]; return { mintTxId: mintTxn, metadataKey: metadataAddress, }; } catch (e) { console.log(e); } return null; }; export const shortenAddress = (address: string, chars = 4): string => { return `${address.slice(0, chars)}...${address.slice(-chars)}`; }; const sleep = (ms: number): Promise<void> => { return new Promise(resolve => setTimeout(resolve, ms)); };