├── .vscode └── extensions.json ├── README.md └── app ├── .eslintrc.json ├── .gitignore ├── README.md ├── components └── CandyMachine │ ├── connection.js │ ├── helpers.js │ └── index.js ├── next.config.js ├── package.json ├── pages ├── _app.js ├── api │ └── hello.js └── index.js ├── public └── twitter-logo.svg └── styles ├── App.css ├── CandyMachine.css └── globals.css /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "esbenp.prettier-vscode", 4 | "dbaeumer.vscode-eslint", 5 | "2gua.rainbow-brackets", 6 | "usernamehw.errorlens", 7 | "yzhang.markdown-all-in-one" 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # buildspace Solana NFT Drop Project 2 | ### Welcome 👋 3 | To get started with this course, clone this repo and follow these commands: 4 | 5 | 1. cd into the `app` folder 6 | 2. Run `npm install` at the root of your directory 7 | 3. Run `npm run start` to start the project 8 | 4. Start coding! 9 | 10 | ### What is the .vscode Folder? 11 | If you use VSCode to build your app, we included a list of suggested extensions that will help you build this project! Once you open this project in VSCode, you will see a popup asking if you want to download the recommended extensions :). 12 | 13 | ### Questions? 14 | Have some questions make sure you head over to your [buildspace Dashboard](https://app.buildspace.so/projects/CO77556be5-25e9-49dd-a799-91a2fc29520e) and link your Discord account so you can get access to helpful channels and your instructor! 15 | 16 | -------------------------------------------------------------------------------- /app/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals" 3 | } 4 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | *.pem 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | .pnpm-debug.log* 27 | 28 | # local env files 29 | .env*.local 30 | 31 | # vercel 32 | .vercel 33 | 34 | # typescript 35 | *.tsbuildinfo 36 | next-env.d.ts 37 | -------------------------------------------------------------------------------- /app/README.md: -------------------------------------------------------------------------------- 1 | This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app). 2 | 3 | ## Getting Started 4 | 5 | First, run the development server: 6 | 7 | ```bash 8 | npm run dev 9 | # or 10 | yarn dev 11 | ``` 12 | 13 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. 14 | 15 | You can start editing the page by modifying `pages/index.js`. The page auto-updates as you edit the file. 16 | 17 | [API routes](https://nextjs.org/docs/api-routes/introduction) can be accessed on [http://localhost:3000/api/hello](http://localhost:3000/api/hello). This endpoint can be edited in `pages/api/hello.js`. 18 | 19 | The `pages/api` directory is mapped to `/api/*`. Files in this directory are treated as [API routes](https://nextjs.org/docs/api-routes/introduction) instead of React pages. 20 | 21 | ## Learn More 22 | 23 | To learn more about Next.js, take a look at the following resources: 24 | 25 | - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. 26 | - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. 27 | 28 | You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome! 29 | 30 | ## Deploy on Vercel 31 | 32 | The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. 33 | 34 | Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details. 35 | -------------------------------------------------------------------------------- /app/components/CandyMachine/connection.js: -------------------------------------------------------------------------------- 1 | import { Transaction } from "@solana/web3.js"; 2 | 3 | import { WalletNotConnectedError } from "@solana/wallet-adapter-base"; 4 | 5 | export const getErrorForTransaction = async (connection, txid) => { 6 | // wait for all confirmation before geting transaction 7 | await connection.confirmTransaction(txid, "max"); 8 | 9 | const tx = await connection.getParsedConfirmedTransaction(txid); 10 | 11 | const errors = []; 12 | if (tx?.meta && tx.meta.logMessages) { 13 | tx.meta.logMessages.forEach((log) => { 14 | const regex = /Error: (.*)/gm; 15 | let m; 16 | while ((m = regex.exec(log)) !== null) { 17 | // This is necessary to avoid infinite loops with zero-width matches 18 | if (m.index === regex.lastIndex) { 19 | regex.lastIndex++; 20 | } 21 | 22 | if (m.length > 1) { 23 | errors.push(m[1]); 24 | } 25 | } 26 | }); 27 | } 28 | 29 | return errors; 30 | }; 31 | 32 | export async function sendTransactionsWithManualRetry(connection, wallet, instructions, signers) { 33 | let stopPoint = 0; 34 | let tries = 0; 35 | let lastInstructionsLength = null; 36 | let toRemoveSigners = {}; 37 | instructions = instructions.filter((instr, i) => { 38 | if (instr.length > 0) { 39 | return true; 40 | } else { 41 | toRemoveSigners[i] = true; 42 | return false; 43 | } 44 | }); 45 | let ids = []; 46 | let filteredSigners = signers.filter((_, i) => !toRemoveSigners[i]); 47 | 48 | while (stopPoint < instructions.length && tries < 3) { 49 | instructions = instructions.slice(stopPoint, instructions.length); 50 | filteredSigners = filteredSigners.slice(stopPoint, filteredSigners.length); 51 | 52 | if (instructions.length === lastInstructionsLength) tries = tries + 1; 53 | else tries = 0; 54 | 55 | try { 56 | if (instructions.length === 1) { 57 | const id = await sendTransactionWithRetry(connection, wallet, instructions[0], filteredSigners[0], "single"); 58 | ids.push(id.txid); 59 | stopPoint = 1; 60 | } else { 61 | const { txs } = await sendTransactions(connection, wallet, instructions, filteredSigners, "StopOnFailure", "single"); 62 | ids = ids.concat(txs.map((t) => t.txid)); 63 | } 64 | } catch (e) { 65 | console.error(e); 66 | } 67 | console.log( 68 | "Died on ", 69 | stopPoint, 70 | "retrying from instruction", 71 | instructions[stopPoint], 72 | "instructions length is", 73 | instructions.length 74 | ); 75 | lastInstructionsLength = instructions.length; 76 | } 77 | 78 | return ids; 79 | } 80 | 81 | export const sendTransactions = async ( 82 | connection, 83 | wallet, 84 | instructionSet, 85 | signersSet, 86 | sequenceType = "Parallel", 87 | commitment = "singleGossip", 88 | successCallback = (txid, ind) => {}, 89 | failCallback = (txid, ind) => false, 90 | block 91 | ) => { 92 | if (!wallet.publicKey) throw new WalletNotConnectedError(); 93 | 94 | const unsignedTxns = []; 95 | 96 | if (!block) { 97 | block = await connection.getRecentBlockhash(commitment); 98 | } 99 | 100 | for (let i = 0; i < instructionSet.length; i++) { 101 | const instructions = instructionSet[i]; 102 | const signers = signersSet[i]; 103 | 104 | if (instructions.length === 0) { 105 | continue; 106 | } 107 | 108 | let transaction = new Transaction(); 109 | instructions.forEach((instruction) => transaction.add(instruction)); 110 | transaction.recentBlockhash = block.blockhash; 111 | transaction.setSigners( 112 | // fee payed by the wallet owner 113 | wallet.publicKey, 114 | ...signers.map((s) => s.publicKey) 115 | ); 116 | 117 | if (signers.length > 0) { 118 | transaction.partialSign(...signers); 119 | } 120 | 121 | unsignedTxns.push(transaction); 122 | } 123 | 124 | const signedTxns = await wallet.signAllTransactions(unsignedTxns); 125 | 126 | const pendingTxns = []; 127 | 128 | let breakEarlyObject = { breakEarly: false, i: 0 }; 129 | console.log("Signed txns length", signedTxns.length, "vs handed in length", instructionSet.length); 130 | for (let i = 0; i < signedTxns.length; i++) { 131 | const signedTxnPromise = sendSignedTransaction({ 132 | connection, 133 | signedTransaction: signedTxns[i], 134 | }); 135 | 136 | signedTxnPromise 137 | .then(({ txid, slot }) => { 138 | successCallback(txid, i); 139 | }) 140 | .catch((reason) => { 141 | failCallback(signedTxns[i], i); 142 | if (sequenceType === "StopOnFailure") { 143 | breakEarlyObject.breakEarly = true; 144 | breakEarlyObject.i = i; 145 | } 146 | }); 147 | 148 | if (sequenceType !== "Parallel") { 149 | try { 150 | await signedTxnPromise; 151 | } catch (e) { 152 | console.log("Caught failure", e); 153 | if (breakEarlyObject.breakEarly) { 154 | console.log("Died on ", breakEarlyObject.i); 155 | // Return the txn we failed on by index 156 | return { 157 | number: breakEarlyObject.i, 158 | txs: await Promise.all(pendingTxns), 159 | }; 160 | } 161 | } 162 | } else { 163 | pendingTxns.push(signedTxnPromise); 164 | } 165 | } 166 | 167 | if (sequenceType !== "Parallel") { 168 | await Promise.all(pendingTxns); 169 | } 170 | 171 | return { number: signedTxns.length, txs: await Promise.all(pendingTxns) }; 172 | }; 173 | 174 | export const sendTransaction = async ( 175 | connection, 176 | wallet, 177 | instructions, 178 | signers, 179 | awaitConfirmation = true, 180 | commitment = "singleGossip", 181 | includesFeePayer = false, 182 | block 183 | ) => { 184 | if (!wallet.publicKey) throw new WalletNotConnectedError(); 185 | 186 | let transaction = new Transaction(); 187 | instructions.forEach((instruction) => transaction.add(instruction)); 188 | transaction.recentBlockhash = (block || (await connection.getRecentBlockhash(commitment))).blockhash; 189 | 190 | if (includesFeePayer) { 191 | transaction.setSigners(...signers.map((s) => s.publicKey)); 192 | } else { 193 | transaction.setSigners( 194 | // fee payed by the wallet owner 195 | wallet.publicKey, 196 | ...signers.map((s) => s.publicKey) 197 | ); 198 | } 199 | 200 | if (signers.length > 0) { 201 | transaction.partialSign(...signers); 202 | } 203 | if (!includesFeePayer) { 204 | transaction = await wallet.signTransaction(transaction); 205 | } 206 | 207 | const rawTransaction = transaction.serialize(); 208 | let options = { 209 | skipPreflight: true, 210 | commitment, 211 | }; 212 | 213 | const txid = await connection.sendRawTransaction(rawTransaction, options); 214 | let slot = 0; 215 | 216 | if (awaitConfirmation) { 217 | const confirmation = await awaitTransactionSignatureConfirmation(txid, DEFAULT_TIMEOUT, connection, commitment); 218 | 219 | if (!confirmation) throw new Error("Timed out awaiting confirmation on transaction"); 220 | slot = confirmation?.slot || 0; 221 | 222 | if (confirmation?.err) { 223 | const errors = await getErrorForTransaction(connection, txid); 224 | 225 | console.log(errors); 226 | throw new Error(`Raw transaction ${txid} failed`); 227 | } 228 | } 229 | 230 | return { txid, slot }; 231 | }; 232 | 233 | export const sendTransactionWithRetry = async ( 234 | connection, 235 | wallet, 236 | instructions, 237 | signers, 238 | commitment = "singleGossip", 239 | includesFeePayer = false, 240 | block, 241 | beforeSend 242 | ) => { 243 | if (!wallet.publicKey) throw new WalletNotConnectedError(); 244 | 245 | let transaction = new Transaction(); 246 | instructions.forEach((instruction) => transaction.add(instruction)); 247 | transaction.recentBlockhash = (block || (await connection.getRecentBlockhash(commitment))).blockhash; 248 | 249 | if (includesFeePayer) { 250 | transaction.setSigners(...signers.map((s) => s.publicKey)); 251 | } else { 252 | transaction.setSigners( 253 | // fee payed by the wallet owner 254 | wallet.publicKey, 255 | ...signers.map((s) => s.publicKey) 256 | ); 257 | } 258 | 259 | if (signers.length > 0) { 260 | transaction.partialSign(...signers); 261 | } 262 | if (!includesFeePayer) { 263 | transaction = await wallet.signTransaction(transaction); 264 | } 265 | 266 | if (beforeSend) { 267 | beforeSend(); 268 | } 269 | 270 | const { txid, slot } = await sendSignedTransaction({ 271 | connection, 272 | signedTransaction: transaction, 273 | }); 274 | 275 | return { txid, slot }; 276 | }; 277 | 278 | export const getUnixTs = () => { 279 | return new Date().getTime() / 1000; 280 | }; 281 | 282 | const DEFAULT_TIMEOUT = 15000; 283 | 284 | export async function sendSignedTransaction({ signedTransaction, connection, timeout = DEFAULT_TIMEOUT }) { 285 | const rawTransaction = signedTransaction.serialize(); 286 | const startTime = getUnixTs(); 287 | let slot = 0; 288 | const txid = await connection.sendRawTransaction(rawTransaction, { 289 | skipPreflight: true, 290 | }); 291 | 292 | console.log("Started awaiting confirmation for", txid); 293 | 294 | let done = false; 295 | (async () => { 296 | while (!done && getUnixTs() - startTime < timeout) { 297 | connection.sendRawTransaction(rawTransaction, { 298 | skipPreflight: true, 299 | }); 300 | await sleep(500); 301 | } 302 | })(); 303 | try { 304 | const confirmation = await awaitTransactionSignatureConfirmation(txid, timeout, connection, "recent", true); 305 | 306 | if (!confirmation) throw new Error("Timed out awaiting confirmation on transaction"); 307 | 308 | if (confirmation.err) { 309 | console.error(confirmation.err); 310 | throw new Error("Transaction failed: Custom instruction error"); 311 | } 312 | 313 | slot = confirmation?.slot || 0; 314 | } catch (err) { 315 | console.error("Timeout Error caught", err); 316 | if (err.timeout) { 317 | throw new Error("Timed out awaiting confirmation on transaction"); 318 | } 319 | let simulateResult = null; 320 | try { 321 | simulateResult = (await simulateTransaction(connection, signedTransaction, "single")).value; 322 | } catch (e) {} 323 | if (simulateResult && simulateResult.err) { 324 | if (simulateResult.logs) { 325 | for (let i = simulateResult.logs.length - 1; i >= 0; --i) { 326 | const line = simulateResult.logs[i]; 327 | if (line.startsWith("Program log: ")) { 328 | throw new Error("Transaction failed: " + line.slice("Program log: ".length)); 329 | } 330 | } 331 | } 332 | throw new Error(JSON.stringify(simulateResult.err)); 333 | } 334 | // throw new Error('Transaction failed'); 335 | } finally { 336 | done = true; 337 | } 338 | 339 | console.log("Latency", txid, getUnixTs() - startTime); 340 | return { txid, slot }; 341 | } 342 | 343 | async function simulateTransaction(connection, transaction, commitment) { 344 | // @ts-ignore 345 | transaction.recentBlockhash = await connection._recentBlockhash( 346 | // @ts-ignore 347 | connection._disableBlockhashCaching 348 | ); 349 | 350 | const signData = transaction.serializeMessage(); 351 | // @ts-ignore 352 | const wireTransaction = transaction._serialize(signData); 353 | const encodedTransaction = wireTransaction.toString("base64"); 354 | const config = { encoding: "base64", commitment }; 355 | const args = [encodedTransaction, config]; 356 | 357 | // @ts-ignore 358 | const res = await connection._rpcRequest("simulateTransaction", args); 359 | if (res.error) { 360 | throw new Error("failed to simulate transaction: " + res.error.message); 361 | } 362 | return res.result; 363 | } 364 | 365 | async function awaitTransactionSignatureConfirmation(txid, timeout, connection, commitment = "recent", queryStatus = false) { 366 | let done = false; 367 | let status = { 368 | slot: 0, 369 | confirmations: 0, 370 | err: null, 371 | }; 372 | let subId = 0; 373 | status = await new Promise(async (resolve, reject) => { 374 | setTimeout(() => { 375 | if (done) { 376 | return; 377 | } 378 | done = true; 379 | console.log("Rejecting for timeout..."); 380 | reject({ timeout: true }); 381 | }, timeout); 382 | try { 383 | subId = connection.onSignature( 384 | txid, 385 | (result, context) => { 386 | done = true; 387 | status = { 388 | err: result.err, 389 | slot: context.slot, 390 | confirmations: 0, 391 | }; 392 | if (result.err) { 393 | console.log("Rejected via websocket", result.err); 394 | reject(status); 395 | } else { 396 | console.log("Resolved via websocket", result); 397 | resolve(status); 398 | } 399 | }, 400 | commitment 401 | ); 402 | } catch (e) { 403 | done = true; 404 | console.error("WS error in setup", txid, e); 405 | } 406 | while (!done && queryStatus) { 407 | // eslint-disable-next-line no-loop-func 408 | (async () => { 409 | try { 410 | const signatureStatuses = await connection.getSignatureStatuses([txid]); 411 | status = signatureStatuses && signatureStatuses.value[0]; 412 | if (!done) { 413 | if (!status) { 414 | console.log("REST null result for", txid, status); 415 | } else if (status.err) { 416 | console.log("REST error for", txid, status); 417 | done = true; 418 | reject(status.err); 419 | } else if (!status.confirmations) { 420 | console.log("REST no confirmations for", txid, status); 421 | } else { 422 | console.log("REST confirmation for", txid, status); 423 | done = true; 424 | resolve(status); 425 | } 426 | } 427 | } catch (e) { 428 | if (!done) { 429 | console.log("REST connection error: txid", txid, e); 430 | } 431 | } 432 | })(); 433 | await sleep(2000); 434 | } 435 | }); 436 | 437 | //@ts-ignore 438 | if (connection._signatureSubscriptions[subId]) connection.removeSignatureListener(subId); 439 | done = true; 440 | console.log("Returning status", status); 441 | return status; 442 | } 443 | export function sleep(ms) { 444 | return new Promise((resolve) => setTimeout(resolve, ms)); 445 | } 446 | -------------------------------------------------------------------------------- /app/components/CandyMachine/helpers.js: -------------------------------------------------------------------------------- 1 | import { web3 } from "@project-serum/anchor"; 2 | import * as anchor from "@project-serum/anchor"; 3 | import { TOKEN_PROGRAM_ID } from "@solana/spl-token"; 4 | import { SystemProgram } from "@solana/web3.js"; 5 | import { LAMPORTS_PER_SOL, SYSVAR_RENT_PUBKEY, TransactionInstruction } from "@solana/web3.js"; 6 | 7 | // CLI Properties Given to us 8 | const candyMachineProgram = new web3.PublicKey("cndy3Z4yapfJBmL3ShUp5exZKqR3z33thTzeNMm2gRZ"); 9 | 10 | const TOKEN_METADATA_PROGRAM_ID = new web3.PublicKey("metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s"); 11 | 12 | const SPL_ASSOCIATED_TOKEN_ACCOUNT_PROGRAM_ID = new web3.PublicKey("ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL"); 13 | 14 | const CIVIC = new anchor.web3.PublicKey("gatem74V238djXdzWnJf94Wo1DcnuGkfijbf3AuBhfs"); 15 | 16 | const toDate = (value) => { 17 | if (!value) { 18 | return; 19 | } 20 | 21 | return new Date(value.toNumber() * 1000); 22 | }; 23 | 24 | const numberFormater = new Intl.NumberFormat("en-US", { 25 | style: "decimal", 26 | minimumFractionDigits: 2, 27 | maximumFractionDigits: 2, 28 | }); 29 | 30 | const formatNumber = { 31 | format: (val) => { 32 | if (!val) { 33 | return "--"; 34 | } 35 | 36 | return numberFormater.format(val); 37 | }, 38 | asNumber: (val) => { 39 | if (!val) { 40 | return undefined; 41 | } 42 | 43 | return val.toNumber() / LAMPORTS_PER_SOL; 44 | }, 45 | }; 46 | 47 | const getAtaForMint = async (mint, buyer) => { 48 | return await anchor.web3.PublicKey.findProgramAddress( 49 | [buyer.toBuffer(), TOKEN_PROGRAM_ID.toBuffer(), mint.toBuffer()], 50 | SPL_ASSOCIATED_TOKEN_ACCOUNT_PROGRAM_ID 51 | ); 52 | }; 53 | 54 | const getNetworkExpire = async (gatekeeperNetwork) => { 55 | return await anchor.web3.PublicKey.findProgramAddress([gatekeeperNetwork.toBuffer(), Buffer.from("expire")], CIVIC); 56 | }; 57 | 58 | const getNetworkToken = async (wallet, gatekeeperNetwork) => { 59 | return await anchor.web3.PublicKey.findProgramAddress( 60 | [wallet.toBuffer(), Buffer.from("gateway"), Buffer.from([0, 0, 0, 0, 0, 0, 0, 0]), gatekeeperNetwork.toBuffer()], 61 | CIVIC 62 | ); 63 | }; 64 | 65 | function createAssociatedTokenAccountInstruction(associatedTokenAddress, payer, walletAddress, splTokenMintAddress) { 66 | const keys = [ 67 | { 68 | pubkey: payer, 69 | isSigner: true, 70 | isWritable: true, 71 | }, 72 | { 73 | pubkey: associatedTokenAddress, 74 | isSigner: false, 75 | isWritable: true, 76 | }, 77 | { 78 | pubkey: walletAddress, 79 | isSigner: false, 80 | isWritable: false, 81 | }, 82 | { 83 | pubkey: splTokenMintAddress, 84 | isSigner: false, 85 | isWritable: false, 86 | }, 87 | { 88 | pubkey: SystemProgram.programId, 89 | isSigner: false, 90 | isWritable: false, 91 | }, 92 | { 93 | pubkey: TOKEN_PROGRAM_ID, 94 | isSigner: false, 95 | isWritable: false, 96 | }, 97 | { 98 | pubkey: SYSVAR_RENT_PUBKEY, 99 | isSigner: false, 100 | isWritable: false, 101 | }, 102 | ]; 103 | return new TransactionInstruction({ 104 | keys, 105 | programId: SPL_ASSOCIATED_TOKEN_ACCOUNT_PROGRAM_ID, 106 | data: Buffer.from([]), 107 | }); 108 | } 109 | 110 | export { 111 | candyMachineProgram, 112 | TOKEN_METADATA_PROGRAM_ID, 113 | SPL_ASSOCIATED_TOKEN_ACCOUNT_PROGRAM_ID, 114 | CIVIC, 115 | toDate, 116 | formatNumber, 117 | getAtaForMint, 118 | getNetworkExpire, 119 | getNetworkToken, 120 | createAssociatedTokenAccountInstruction, 121 | }; 122 | -------------------------------------------------------------------------------- /app/components/CandyMachine/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Connection, PublicKey } from "@solana/web3.js"; 3 | import { Program, AnchorProvider, web3 } from "@project-serum/anchor"; 4 | import { MintLayout, TOKEN_PROGRAM_ID, Token } from "@solana/spl-token"; 5 | import { sendTransactions } from "./connection"; 6 | import "./CandyMachine.css"; 7 | import { 8 | candyMachineProgram, 9 | TOKEN_METADATA_PROGRAM_ID, 10 | SPL_ASSOCIATED_TOKEN_ACCOUNT_PROGRAM_ID, 11 | getAtaForMint, 12 | getNetworkExpire, 13 | getNetworkToken, 14 | CIVIC, 15 | } from "./helpers"; 16 | 17 | const { SystemProgram } = web3; 18 | const opts = { 19 | preflightCommitment: "processed", 20 | }; 21 | 22 | const CandyMachine = ({ walletAddress }) => { 23 | const getCandyMachineCreator = async (candyMachine) => { 24 | const candyMachineID = new PublicKey(candyMachine); 25 | return await web3.PublicKey.findProgramAddress([Buffer.from("candy_machine"), candyMachineID.toBuffer()], candyMachineProgram); 26 | }; 27 | 28 | const getMetadata = async (mint) => { 29 | return ( 30 | await PublicKey.findProgramAddress( 31 | [Buffer.from("metadata"), TOKEN_METADATA_PROGRAM_ID.toBuffer(), mint.toBuffer()], 32 | TOKEN_METADATA_PROGRAM_ID 33 | ) 34 | )[0]; 35 | }; 36 | 37 | const getMasterEdition = async (mint) => { 38 | return ( 39 | await PublicKey.findProgramAddress( 40 | [Buffer.from("metadata"), TOKEN_METADATA_PROGRAM_ID.toBuffer(), mint.toBuffer(), Buffer.from("edition")], 41 | TOKEN_METADATA_PROGRAM_ID 42 | ) 43 | )[0]; 44 | }; 45 | 46 | const createAssociatedTokenAccountInstruction = (associatedTokenAddress, payer, walletAddress, splTokenMintAddress) => { 47 | const keys = [ 48 | { pubkey: payer, isSigner: true, isWritable: true }, 49 | { pubkey: associatedTokenAddress, isSigner: false, isWritable: true }, 50 | { pubkey: walletAddress, isSigner: false, isWritable: false }, 51 | { pubkey: splTokenMintAddress, isSigner: false, isWritable: false }, 52 | { 53 | pubkey: web3.SystemProgram.programId, 54 | isSigner: false, 55 | isWritable: false, 56 | }, 57 | { pubkey: TOKEN_PROGRAM_ID, isSigner: false, isWritable: false }, 58 | { 59 | pubkey: web3.SYSVAR_RENT_PUBKEY, 60 | isSigner: false, 61 | isWritable: false, 62 | }, 63 | ]; 64 | return new web3.TransactionInstruction({ 65 | keys, 66 | programId: SPL_ASSOCIATED_TOKEN_ACCOUNT_PROGRAM_ID, 67 | data: Buffer.from([]), 68 | }); 69 | }; 70 | 71 | const mintToken = async () => { 72 | const mint = web3.Keypair.generate(); 73 | 74 | const userTokenAccountAddress = (await getAtaForMint(mint.publicKey, walletAddress.publicKey))[0]; 75 | 76 | const userPayingAccountAddress = candyMachine.state.tokenMint 77 | ? (await getAtaForMint(candyMachine.state.tokenMint, walletAddress.publicKey))[0] 78 | : walletAddress.publicKey; 79 | 80 | const candyMachineAddress = candyMachine.id; 81 | const remainingAccounts = []; 82 | const signers = [mint]; 83 | const cleanupInstructions = []; 84 | const instructions = [ 85 | web3.SystemProgram.createAccount({ 86 | fromPubkey: walletAddress.publicKey, 87 | newAccountPubkey: mint.publicKey, 88 | space: MintLayout.span, 89 | lamports: await candyMachine.program.provider.connection.getMinimumBalanceForRentExemption(MintLayout.span), 90 | programId: TOKEN_PROGRAM_ID, 91 | }), 92 | Token.createInitMintInstruction(TOKEN_PROGRAM_ID, mint.publicKey, 0, walletAddress.publicKey, walletAddress.publicKey), 93 | createAssociatedTokenAccountInstruction( 94 | userTokenAccountAddress, 95 | walletAddress.publicKey, 96 | walletAddress.publicKey, 97 | mint.publicKey 98 | ), 99 | Token.createMintToInstruction(TOKEN_PROGRAM_ID, mint.publicKey, userTokenAccountAddress, walletAddress.publicKey, [], 1), 100 | ]; 101 | 102 | if (candyMachine.state.gatekeeper) { 103 | remainingAccounts.push({ 104 | pubkey: (await getNetworkToken(walletAddress.publicKey, candyMachine.state.gatekeeper.gatekeeperNetwork))[0], 105 | isWritable: true, 106 | isSigner: false, 107 | }); 108 | if (candyMachine.state.gatekeeper.expireOnUse) { 109 | remainingAccounts.push({ 110 | pubkey: CIVIC, 111 | isWritable: false, 112 | isSigner: false, 113 | }); 114 | remainingAccounts.push({ 115 | pubkey: (await getNetworkExpire(candyMachine.state.gatekeeper.gatekeeperNetwork))[0], 116 | isWritable: false, 117 | isSigner: false, 118 | }); 119 | } 120 | } 121 | if (candyMachine.state.whitelistMintSettings) { 122 | const mint = new web3.PublicKey(candyMachine.state.whitelistMintSettings.mint); 123 | 124 | const whitelistToken = (await getAtaForMint(mint, walletAddress.publicKey))[0]; 125 | remainingAccounts.push({ 126 | pubkey: whitelistToken, 127 | isWritable: true, 128 | isSigner: false, 129 | }); 130 | 131 | if (candyMachine.state.whitelistMintSettings.mode.burnEveryTime) { 132 | const whitelistBurnAuthority = web3.Keypair.generate(); 133 | 134 | remainingAccounts.push({ 135 | pubkey: mint, 136 | isWritable: true, 137 | isSigner: false, 138 | }); 139 | remainingAccounts.push({ 140 | pubkey: whitelistBurnAuthority.publicKey, 141 | isWritable: false, 142 | isSigner: true, 143 | }); 144 | signers.push(whitelistBurnAuthority); 145 | const exists = await candyMachine.program.provider.connection.getAccountInfo(whitelistToken); 146 | if (exists) { 147 | instructions.push( 148 | Token.createApproveInstruction( 149 | TOKEN_PROGRAM_ID, 150 | whitelistToken, 151 | whitelistBurnAuthority.publicKey, 152 | walletAddress.publicKey, 153 | [], 154 | 1 155 | ) 156 | ); 157 | cleanupInstructions.push(Token.createRevokeInstruction(TOKEN_PROGRAM_ID, whitelistToken, walletAddress.publicKey, [])); 158 | } 159 | } 160 | } 161 | 162 | if (candyMachine.state.tokenMint) { 163 | const transferAuthority = web3.Keypair.generate(); 164 | 165 | signers.push(transferAuthority); 166 | remainingAccounts.push({ 167 | pubkey: userPayingAccountAddress, 168 | isWritable: true, 169 | isSigner: false, 170 | }); 171 | remainingAccounts.push({ 172 | pubkey: transferAuthority.publicKey, 173 | isWritable: false, 174 | isSigner: true, 175 | }); 176 | 177 | instructions.push( 178 | Token.createApproveInstruction( 179 | TOKEN_PROGRAM_ID, 180 | userPayingAccountAddress, 181 | transferAuthority.publicKey, 182 | walletAddress.publicKey, 183 | [], 184 | candyMachine.state.price.toNumber() 185 | ) 186 | ); 187 | cleanupInstructions.push( 188 | Token.createRevokeInstruction(TOKEN_PROGRAM_ID, userPayingAccountAddress, walletAddress.publicKey, []) 189 | ); 190 | } 191 | const metadataAddress = await getMetadata(mint.publicKey); 192 | const masterEdition = await getMasterEdition(mint.publicKey); 193 | 194 | const [candyMachineCreator, creatorBump] = await getCandyMachineCreator(candyMachineAddress); 195 | 196 | instructions.push( 197 | await candyMachine.program.instruction.mintNft(creatorBump, { 198 | accounts: { 199 | candyMachine: candyMachineAddress, 200 | candyMachineCreator, 201 | payer: walletAddress.publicKey, 202 | wallet: candyMachine.state.treasury, 203 | mint: mint.publicKey, 204 | metadata: metadataAddress, 205 | masterEdition, 206 | mintAuthority: walletAddress.publicKey, 207 | updateAuthority: walletAddress.publicKey, 208 | tokenMetadataProgram: TOKEN_METADATA_PROGRAM_ID, 209 | tokenProgram: TOKEN_PROGRAM_ID, 210 | systemProgram: SystemProgram.programId, 211 | rent: web3.SYSVAR_RENT_PUBKEY, 212 | clock: web3.SYSVAR_CLOCK_PUBKEY, 213 | recentBlockhashes: web3.SYSVAR_RECENT_BLOCKHASHES_PUBKEY, 214 | instructionSysvarAccount: web3.SYSVAR_INSTRUCTIONS_PUBKEY, 215 | }, 216 | remainingAccounts: remainingAccounts.length > 0 ? remainingAccounts : undefined, 217 | }) 218 | ); 219 | 220 | try { 221 | return ( 222 | await sendTransactions( 223 | candyMachine.program.provider.connection, 224 | candyMachine.program.provider.wallet, 225 | [instructions, cleanupInstructions], 226 | [signers, []] 227 | ) 228 | ).txs.map((t) => t.txid); 229 | } catch (e) { 230 | console.log(e); 231 | } 232 | return []; 233 | }; 234 | 235 | return ( 236 |
237 |

Drop Date:

238 |

Items Minted:

239 | 242 |
243 | ); 244 | }; 245 | 246 | export default CandyMachine; 247 | -------------------------------------------------------------------------------- /app/next.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = { 3 | reactStrictMode: true, 4 | swcMinify: true, 5 | }; 6 | 7 | module.exports = nextConfig; 8 | -------------------------------------------------------------------------------- /app/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nextjsapp", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "next lint" 10 | }, 11 | "dependencies": { 12 | "@project-serum/anchor": "^0.25.0", 13 | "@solana/spl-token": "0.1.8", 14 | "@solana/wallet-adapter-base": "^0.9.17", 15 | "@solana/wallet-adapter-react": "^0.15.19", 16 | "@solana/wallet-adapter-react-ui": "^0.9.17", 17 | "@solana/wallet-adapter-wallets": "^0.18.10", 18 | "@solana/web3.js": "^1.62.1", 19 | "next": "12.3.1", 20 | "react": "18.2.0", 21 | "react-dom": "18.2.0" 22 | }, 23 | "devDependencies": { 24 | "eslint": "8.23.1", 25 | "eslint-config-next": "12.3.1" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /app/pages/_app.js: -------------------------------------------------------------------------------- 1 | import { useMemo } from "react"; 2 | import { clusterApiUrl } from "@solana/web3.js"; 3 | import { WalletAdapterNetwork } from "@solana/wallet-adapter-base"; 4 | import { WalletModalProvider } from "@solana/wallet-adapter-react-ui"; 5 | import { PhantomWalletAdapter } from "@solana/wallet-adapter-wallets"; 6 | import { ConnectionProvider, WalletProvider } from "@solana/wallet-adapter-react"; 7 | 8 | import "../styles/App.css"; 9 | import "../styles/globals.css"; 10 | import "../styles/CandyMachine.css"; 11 | import "@solana/wallet-adapter-react-ui/styles.css"; 12 | 13 | const App = ({ Component, pageProps }) => { 14 | const network = WalletAdapterNetwork.Devnet; 15 | const endpoint = useMemo(() => clusterApiUrl(network), [network]); 16 | const wallets = useMemo(() => [new PhantomWalletAdapter()], [network]); 17 | 18 | return ( 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | ); 27 | }; 28 | 29 | export default App; 30 | -------------------------------------------------------------------------------- /app/pages/api/hello.js: -------------------------------------------------------------------------------- 1 | // Next.js API route support: https://nextjs.org/docs/api-routes/introduction 2 | 3 | export default function handler(req, res) { 4 | res.status(200).json({ name: 'John Doe' }) 5 | } 6 | -------------------------------------------------------------------------------- /app/pages/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | // Constants 4 | const TWITTER_HANDLE = "_buildspace"; 5 | const TWITTER_LINK = `https://twitter.com/${TWITTER_HANDLE}`; 6 | 7 | const Home = () => { 8 | return ( 9 |
10 |
11 |
12 |

🍭 Candy Drop

13 |

NFT drop machine with fair mint

14 |
15 |
16 | Twitter Logo 17 | {`built on @${TWITTER_HANDLE}`} 18 |
19 |
20 |
21 | ); 22 | }; 23 | 24 | export default Home; 25 | -------------------------------------------------------------------------------- /app/public/twitter-logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /app/styles/App.css: -------------------------------------------------------------------------------- 1 | .App { 2 | height: 100vh; 3 | background-color: rgb(20, 20, 20); 4 | overflow: scroll; 5 | text-align: center; 6 | overflow: hidden; 7 | } 8 | 9 | .container { 10 | height: 100%; 11 | display: flex; 12 | flex-direction: column; 13 | justify-content: center; 14 | padding: 0 30px 0 30px; 15 | color: white; 16 | position: relative; 17 | /* background-color: #35aee2; */ 18 | } 19 | 20 | .authed-container { 21 | height: 100%; 22 | display: flex; 23 | flex-direction: column; 24 | padding: 30px; 25 | } 26 | 27 | .header-container { 28 | /* background-color: #60c657; */ 29 | } 30 | 31 | .header { 32 | margin: 0; 33 | font-size: 50px; 34 | font-weight: bold; 35 | } 36 | 37 | .sub-text { 38 | font-size: 25px; 39 | } 40 | 41 | .gradient-text { 42 | background: -webkit-linear-gradient(left, #60c657 30%, #35aee2 60%); 43 | background-clip: text; 44 | -webkit-background-clip: text; 45 | -webkit-text-fill-color: transparent; 46 | } 47 | 48 | .cta-button { 49 | height: 45px; 50 | border: 0; 51 | width: auto; 52 | padding-left: 40px; 53 | padding-right: 40px; 54 | border-radius: 10px; 55 | cursor: pointer; 56 | font-size: 16px; 57 | font-weight: bold; 58 | } 59 | 60 | .connect-wallet-button { 61 | color: white; 62 | background: -webkit-linear-gradient(left, #ff8867, #ff52ff); 63 | background-size: 200% 200%; 64 | animation: gradient-animation 4s ease infinite; 65 | } 66 | 67 | .mint-button { 68 | background: -webkit-linear-gradient(left, #4e44ce, #35aee2); 69 | background-size: 200% 200%; 70 | animation: gradient-animation 4s ease infinite; 71 | margin-left: 10px; 72 | } 73 | 74 | .footer-container { 75 | display: flex; 76 | justify-content: center; 77 | align-items: center; 78 | position: absolute; 79 | width: 100%; 80 | bottom: 0; 81 | left: 0; 82 | /* padding-bottom: 45px; */ 83 | } 84 | 85 | .twitter-logo { 86 | width: 35px; 87 | height: 35px; 88 | } 89 | 90 | .footer-text { 91 | font-size: 16px; 92 | font-weight: bold; 93 | color: white; 94 | } 95 | 96 | .connected-container input[type="text"] { 97 | display: inline-block; 98 | padding: 10px; 99 | width: 50%; 100 | height: 60px; 101 | font-size: 16px; 102 | box-sizing: border-box; 103 | background-color: rgba(0, 0, 0, 0.25); 104 | border: none; 105 | border-radius: 10px; 106 | margin: 50px auto; 107 | } 108 | 109 | .connected-container button { 110 | height: 50px; 111 | } 112 | 113 | .button-container { 114 | display: flex; 115 | padding: 30px 0; 116 | justify-content: center; 117 | } 118 | 119 | /* KeyFrames */ 120 | @-webkit-keyframes gradient-animation { 121 | 0% { 122 | background-position: 0% 50%; 123 | } 124 | 50% { 125 | background-position: 100% 50%; 126 | } 127 | 100% { 128 | background-position: 0% 50%; 129 | } 130 | } 131 | @-moz-keyframes gradient-animation { 132 | 0% { 133 | background-position: 0% 50%; 134 | } 135 | 50% { 136 | background-position: 100% 50%; 137 | } 138 | 100% { 139 | background-position: 0% 50%; 140 | } 141 | } 142 | @keyframes gradient-animation { 143 | 0% { 144 | background-position: 0% 50%; 145 | } 146 | 50% { 147 | background-position: 100% 50%; 148 | } 149 | 100% { 150 | background-position: 0% 50%; 151 | } 152 | } 153 | -------------------------------------------------------------------------------- /app/styles/CandyMachine.css: -------------------------------------------------------------------------------- 1 | .machine-container { 2 | display: flex; 3 | flex-direction: column; 4 | } 5 | 6 | .gif-container { 7 | display: flex; 8 | flex-direction: column; 9 | } 10 | 11 | .gif-grid { 12 | display: grid; 13 | grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); 14 | grid-gap: 1.5rem; 15 | justify-items: center; 16 | margin: 0; 17 | padding: 20px 0 20px 0; 18 | } 19 | 20 | .gif-grid .gif-item { 21 | display: flex; 22 | flex-direction: column; 23 | position: relative; 24 | justify-self: center; 25 | align-self: center; 26 | } 27 | 28 | .gif-item img { 29 | width: 100%; 30 | height: 200px; 31 | border-radius: 10px; 32 | object-fit: cover; 33 | } 34 | 35 | .timer-container .timer-header { 36 | font-size: 20px; 37 | font-weight: bold; 38 | } 39 | 40 | .timer-container .timer-value { 41 | font-size: 18px; 42 | } 43 | -------------------------------------------------------------------------------- /app/styles/globals.css: -------------------------------------------------------------------------------- 1 | html, 2 | body { 3 | padding: 0; 4 | margin: 0; 5 | font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen, Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, 6 | sans-serif; 7 | -webkit-font-smoothing: antialiased; 8 | -moz-osx-font-smoothing: grayscale; 9 | } 10 | 11 | a { 12 | color: inherit; 13 | text-decoration: none; 14 | } 15 | 16 | * { 17 | box-sizing: border-box; 18 | } 19 | 20 | code { 21 | font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New", monospace; 22 | } 23 | 24 | @media (prefers-color-scheme: dark) { 25 | html { 26 | color-scheme: dark; 27 | } 28 | body { 29 | color: white; 30 | background: black; 31 | } 32 | } 33 | --------------------------------------------------------------------------------