\src\utils\candy-machine.ts
import * as anchor from "@project-serum/anchor";
import {
MintLayout,
TOKEN_PROGRAM_ID,
Token,
} from "@solana/spl-token";
import { LAMPORTS_PER_SOL } from "@solana/web3.js";
export const CANDY_MACHINE_PROGRAM = new anchor.web3.PublicKey(
"cndyAnrLdpjq1Ssp1z8xxDsB8dxe7u4HL5Nxi2K5WXZ"
);
const SPL_ASSOCIATED_TOKEN_ACCOUNT_PROGRAM_ID = new anchor.web3.PublicKey(
"ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL"
);
const TOKEN_METADATA_PROGRAM_ID = new anchor.web3.PublicKey(
"metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s"
);
export interface CandyMachine {
id: anchor.web3.PublicKey,
connection: anchor.web3.Connection;
program: anchor.Program;
}
interface CandyMachineState {
candyMachine: CandyMachine;
itemsAvailable: number;
itemsRedeemed: number;
itemsRemaining: number;
goLiveDate: Date,
price: number,
}
export const awaitTransactionSignatureConfirmation = async (
txid: anchor.web3.TransactionSignature,
timeout: number,
connection: anchor.web3.Connection,
commitment: anchor.web3.Commitment = "recent",
queryStatus = false
): Promise<anchor.web3.SignatureStatus | null | void> => {
let done = false;
let status: anchor.web3.SignatureStatus | null | void = {
slot: 0,
confirmations: 0,
err: null,
};
let subId = 0;
status = await new Promise(async (resolve, reject) => {
setTimeout(() => {
if (done) {
return;
}
done = true;
console.log("Rejecting for timeout...");
reject({ timeout: true });
}, timeout);
try {
subId = connection.onSignature(
txid,
(result: any, context: any) => {
done = true;
status = {
err: result.err,
slot: context.slot,
confirmations: 0,
};
if (result.err) {
console.log("Rejected via websocket", result.err);
reject(status);
} else {
console.log("Resolved via websocket", result);
resolve(status);
}
},
commitment
);
} catch (e) {
done = true;
console.error("WS error in setup", txid, e);
}
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
if (connection._signatureSubscriptions[subId]) {
connection.removeSignatureListener(subId);
}
done = true;
console.log("Returning status", status);
return status;
}
/* export */ 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<CandyMachineState> => {
const provider = new anchor.Provider(connection, anchorWallet, {
preflightCommitment: "recent",
});
const idl = await anchor.Program.fetchIdl(
CANDY_MACHINE_PROGRAM,
provider
);
const program = new anchor.Program(idl, CANDY_MACHINE_PROGRAM, provider);
const candyMachine = {
id: candyMachineId,
connection,
program,
}
const state: any = await program.account.candyMachine.fetch(candyMachineId);
console.log('state.data', state.data);
const itemsAvailable = state.data.itemsAvailable.toNumber();
const itemsRedeemed = state.itemsRedeemed.toNumber();
const itemsRemaining = itemsAvailable - itemsRedeemed;
const price = state.data.price.toNumber() * 0.000000001;
let goLiveDate = state.data.goLiveDate.toNumber();
goLiveDate = new Date(goLiveDate * 1000);
return {
candyMachine,
itemsAvailable,
itemsRedeemed,
itemsRemaining,
goLiveDate,
price,
};
}
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];
};
const getMetadata = 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];
};
const getTokenWallet = async (
wallet: anchor.web3.PublicKey,
mint: anchor.web3.PublicKey
) => {
return (
await anchor.web3.PublicKey.findProgramAddress(
[wallet.toBuffer(), TOKEN_PROGRAM_ID.toBuffer(), mint.toBuffer()],
SPL_ASSOCIATED_TOKEN_ACCOUNT_PROGRAM_ID
)
)[0];
};
export const mintOneToken = async (
candyMachine: CandyMachine,
config: anchor.web3.PublicKey,
payer: anchor.web3.PublicKey,
treasury: anchor.web3.PublicKey,
): Promise<string> => {
const mint = anchor.web3.Keypair.generate();
const token = await getTokenWallet(payer, mint.publicKey);
const { connection, program } = candyMachine;
const metadata = await getMetadata(mint.publicKey);
const masterEdition = await getMasterEdition(mint.publicKey);
const rent = await connection.getMinimumBalanceForRentExemption(
MintLayout.span
);
return await program.rpc.mintNft({
accounts: {
config,
candyMachine: candyMachine.id,
payer: payer,
wallet: treasury,
mint: mint.publicKey,
metadata,
masterEdition,
mintAuthority: payer,
updateAuthority: payer,
tokenMetadataProgram: TOKEN_METADATA_PROGRAM_ID,
tokenProgram: TOKEN_PROGRAM_ID,
systemProgram: anchor.web3.SystemProgram.programId,
rent: anchor.web3.SYSVAR_RENT_PUBKEY,
clock: anchor.web3.SYSVAR_CLOCK_PUBKEY,
},
signers: [mint],
instructions: [
anchor.web3.SystemProgram.createAccount({
fromPubkey: payer,
newAccountPubkey: mint.publicKey,
space: MintLayout.span,
lamports: rent,
programId: TOKEN_PROGRAM_ID,
}),
Token.createInitMintInstruction(
TOKEN_PROGRAM_ID,
mint.publicKey,
0,
payer,
payer
),
createAssociatedTokenAccountInstruction(
token,
payer,
payer,
mint.publicKey
),
Token.createMintToInstruction(
TOKEN_PROGRAM_ID,
mint.publicKey,
token,
payer,
[],
1
),
],
});
}
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));
}`
`
\src\hooks\use-candy-machine.ts
import { useEffect, useState } from "react";
import * as anchor from "@project-serum/anchor";
import { awaitTransactionSignatureConfirmation, CandyMachine, getCandyMachineState, mintOneToken } from "../utils/candy-machine";
import { useWallet } from "@solana/wallet-adapter-react";
import toast from 'react-hot-toast';
import useWalletBalance from "./use-wallet-balance";
import { LAMPORTS_PER_SOL } from "@solana/web3.js";
const treasury = new anchor.web3.PublicKey(
process.env.NEXT_PUBLIC_TREASURY_ADDRESS!
);
const config = new anchor.web3.PublicKey(
process.env.NEXT_PUBLIC_CANDY_MACHINE_CONFIG!
);
const candyMachineId = new anchor.web3.PublicKey(
process.env.NEXT_PUBLIC_CANDY_MACHINE_ID!
);
const rpcHost = process.env.NEXT_PUBLIC_SOLANA_RPC_HOST!;
const connection = new anchor.web3.Connection(rpcHost);
const txTimeout = 30000;
export default function useCandyMachine() {
const [, setBalance] = useWalletBalance()
const [candyMachine, setCandyMachine] = useState<CandyMachine>();
const wallet = useWallet();
const [isMinting, setIsMinting] = useState(false); // true when user got to press MINT
const [isSoldOut, setIsSoldOut] = useState(false); // true when items remaining is zero
const [counter, setCounter] = useState<any>({});
const [price, setPrice] = useState<number | null>(null);
const [mintStartDate, setMintStartDate] = useState(new Date(parseInt(process.env.NEXT_PUBLIC_CANDY_START_DATE!, 10)));
useEffect(() => {
(async () => {
if (
!wallet ||
!wallet.publicKey ||
!wallet.signAllTransactions ||
!wallet.signTransaction
) {
return;
}
const anchorWallet = {
publicKey: wallet.publicKey,
signAllTransactions: wallet.signAllTransactions,
signTransaction: wallet.signTransaction,
} as anchor.Wallet;
const { candyMachine, goLiveDate, itemsRemaining, itemsAvailable, price } =
await getCandyMachineState(
anchorWallet,
candyMachineId,
connection
);
setIsSoldOut(itemsRemaining === 0);
setMintStartDate(goLiveDate);
setCandyMachine(candyMachine);
setCounter({
itemsRemaining,
itemsAvailable
});
setPrice(price);
})();
}, [wallet, candyMachineId, connection]);
const onMint = async () => {
try {
setIsMinting(true);
if (wallet.connected && candyMachine?.program && wallet.publicKey) {
const mintTxId = await mintOneToken(
candyMachine,
config,
wallet.publicKey,
treasury
);
const status = await awaitTransactionSignatureConfirmation(
mintTxId,
txTimeout,
connection,
"singleGossip",
false
);
if (!status?.err) {
toast.success("Congratulations! Mint succeeded!")
} else {
toast.error("Mint failed! Please try again!")
}
}
} catch (error: any) {
let message = error.msg || "Minting failed! Please try again!";
if (!error.msg) {
if (error.message.indexOf("0x138")) {
} else if (error.message.indexOf("0x137")) {
message = `SOLD OUT!`;
} else if (error.message.indexOf("0x135")) {
message = `Insufficient funds to mint. Please fund your wallet.`;
}
} else {
if (error.code === 311) {
message = `SOLD OUT!`;
setIsSoldOut(true);
} else if (error.code === 312) {
message = `Minting period hasn't started yet.`;
}
}
toast.error(message)
} finally {
if (wallet?.publicKey) {
const balance = await connection.getBalance(wallet?.publicKey);
setBalance(balance / LAMPORTS_PER_SOL);
}
setIsMinting(false);
}
};
return { isSoldOut, mintStartDate, isMinting, onMint }
}`
`
\src\pages\mint.tsx
import Head from 'next/head'
import { Navbar } from '../components/Navbar';
import { useState } from "react";
import { Toaster } from 'react-hot-toast';
import { useWallet } from "@solana/wallet-adapter-react";
import {
shortenAddress,
} from "../utils/candy-machine";
import useCandyMachine from '../hooks/use-candy-machine';
import useWalletBalance from '../hooks/use-wallet-balance';
import Countdown from 'react-countdown';
import { WalletMultiButton } from '@solana/wallet-adapter-react-ui';
const Mint = () => {
const [balance] = useWalletBalance()
const [isActive, setIsActive] = useState(false); // true when countdown completes
const wallet = useWallet();
const [counter, setCounter] = useState<any>({});
const [price, setPrice] = useState<number | null>(null);
const { isSoldOut, mintStartDate, isMinting, onMint } = useCandyMachine()
return (
<main className="">
<Toaster />
<Head>
<title>Solana Candy Machine</title>
<meta name="description" content="Solana Candy Machine is an open-source project using NextJS,
Metaplex protocol which serve as an example app for a NFT candy machine app." />
<link rel="icon" href="/favicon.ico" />
</Head>
<Navbar />
<div>
<br />
{!!counter && (
<>
Items available: {counter.itemsRemaining} / {counter.itemsAvailable}
<br />
<br />
<br />
<br />
</>
)}
</div>
<div className="flex justify-center">
{wallet.connected &&
<button className="px-8 py-1.5 rounded bg-blue-400 text-white font-bold hover:bg-blue-600 hover:text-white"
disabled={isSoldOut || isMinting || !isActive}
onClick={onMint}
>
{isSoldOut ? (
"SOLD OUT"
) : isActive ?
<span>MINT {price} Sol {isMinting && ""}</span> :
<Countdown
date={mintStartDate}
onMount={({ completed }) => completed && setIsActive(true)}
onComplete={() => setIsActive(true)}
renderer={renderCounter}
/>
}
</button>
}
</div>
</main>
);
};
const renderCounter = ({ days, hours, minutes, seconds, completed }: any) => {
return (
<span>
{hours} hours, {minutes} minutes, {seconds} seconds
</span>
);
};
export default Mint;
`
but not work...