├── .gitignore ├── README.md ├── components ├── BoilerPlate.jsx ├── Navbar.jsx ├── ProjectCard.jsx ├── RenderedComponent.tsx ├── Requirements.jsx └── tokens │ ├── CreateAccount.tsx │ └── CreateMint.tsx ├── contexts ├── TransitionContextProvider.jsx └── WalletContextProvider.jsx ├── interfaces └── tokens.ts ├── models └── serialize │ ├── StudentIntro.ts │ └── StudentIntroReference.ts ├── next-env.d.ts ├── next.config.js ├── package.json ├── pages ├── _app.tsx ├── faucet │ ├── finished.tsx │ └── starter.tsx ├── index.jsx ├── minter │ ├── finished.tsx │ └── starter.tsx ├── movies │ ├── finished.tsx │ └── starter.tsx ├── sendsol │ ├── finished.jsx │ └── starter.jsx ├── serialize │ ├── finished.tsx │ └── starter.tsx ├── tokens │ ├── finished.tsx │ └── starter.tsx ├── viewer │ ├── finished.tsx │ └── starter.tsx └── wallets │ ├── finished.tsx │ └── starter.tsx ├── postcss.config.js ├── projects └── projects.ts ├── public ├── favicon.ico ├── helius-orange.png └── helius-white.png ├── scripts ├── serialize │ ├── StudentIntroCoordinator.ts │ └── StudentIntroCoordinatorReference.ts └── tokens │ └── InitializeKeypair.ts ├── styles └── globals.css ├── tailwind.config.js ├── tsconfig.json └── yarn.lock /.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 30 | .env.local 31 | .env.development.local 32 | .env.test.local 33 | .env.production.local 34 | 35 | # vercel 36 | .vercel 37 | 38 | # typescript 39 | *.tsbuildinfo 40 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # A Solana Frontend Development Course by Helius 2 | Hey there! This repository was 3 | Hey Baylor Bears! This repository was designed by [Nathan Galindo](https://twitter.com/_zebedee_) on behalf of [Helius](https://www.helius.dev/). It is a course that is made up of a series of projects to help you get started developing frontend applications on [Solana](https://solana.com/). Why Solana? Because Solana is a third-generation protocol with incredibly high throughput and astonishingly low transaction fees, making it an ideal blockchain for builders to hack out the next generation of the internet! 4 | 5 | Screenshot 2024-01-04 at 9 24 40 AM 6 | 7 | # Video Series 8 | This application is paired with a video series what walks you through each project! [Click this link](https://www.youtube.com/watch?v=8azRe9PRLg0&list=PLMZny7wGLM6w4t7pMGATxFTjjMduTsEiF) to follow along. 9 | 10 | ![image](https://github.com/masterzorgon/solana-frontend-development-course/assets/155211932/5f3bd590-c19d-4d1f-954e-6bb0aa4fd767) 11 | 12 | 13 | # Tech Stack 14 | This course focuses on front-end development, and challenges you to build small applications using industry standard front-end technologies as well as some web3 frameworks to interact with the [Solana JSON RPC API](https://docs.solana.com/developing/clients/jsonrpc-api?gclid=Cj0KCQjwxIOXBhCrARIsAL1QFCZyftNFV4i4Sygxkr6LdPazw2sLMPyhQbVqFID-yy8QSqf81dxJHUoaAk2ZEALw_wcB). Listed below are the main frameworks and tools you will be working with: 15 | 16 | - [Next.js](https://nextjs.org/docs) 17 | - [TypeScript](https://www.typescriptlang.org/docs/) 18 | - [Tailwind CSS](https://tailwindcss.com/docs/installation) 19 | - [solana/web3.js](https://solana-labs.github.io/solana-web3.js/) 20 | - [solana/spl-token](https://solana-labs.github.io/solana-program-library/token/js/index.html) 21 | - [solana/wallet-adapter-react](https://solana-labs.github.io/wallet-adapter/) 22 | 23 | The frameworks and tools used to build out these projects were carefully selected because they are the tools real companies are using in the blockchain industry to build out production applications. Even though they may be new to you, becoming familiar with them will greatly benefit your hireability as a software engineer (both within, and beyond the blockchain industry)! 24 | 25 | # Getting Started 26 | Navigate to a folder in your local environment and copy/paste the following commands into your terminal: 27 | ``` 28 | git clone git@github.com:masterzorgon/solana-frontend-development-course.git 29 | cd solana-frontend-development-course 30 | nvm install 20 31 | nvm use 20 32 | yarn 33 | yarn dev 34 | ``` 35 | At this point, the repository should be installed to your computer and hosted on your browser at http://localhost:3000. 36 | 37 | # Workflow 38 | For each project, there is a card which contains a brief description of the project, as well as two buttons: "preview" and "starter". 39 | * The "preview" button, when clicked, will direct you to a finished version of the project. This is for your reference as you build the project on your own. 40 | * The "starter" button, when clicked, will take you to a blank page. The pages labeled "starter.jsx" within your repository are blank files that you will use to build out your own version of the given project. 41 | 42 | # Credits 43 | If you find these learning materials helpful, you can help me out in the following ways: 44 | * By giving this repository a star 🌟 45 | * By giving me a follow on X: [@_zebedee_](https://twitter.com/_zebedee_) 46 | * By learning more about Helius via our website: [helius.dev](https://www.helius.dev/) 47 | -------------------------------------------------------------------------------- /components/BoilerPlate.jsx: -------------------------------------------------------------------------------- 1 | const BoilerPlate = () => { 2 | return ( 3 |
4 |
5 |
6 |

7 | your turn to build! 8 |

9 | 10 | return home 11 | 12 |
13 |
14 |
15 | ) 16 | }; 17 | 18 | export default BoilerPlate; -------------------------------------------------------------------------------- /components/Navbar.jsx: -------------------------------------------------------------------------------- 1 | import { WalletMultiButton } from '@solana/wallet-adapter-react-ui'; 2 | 3 | const Navbar = () => { 4 | return ( 5 | 15 | ); 16 | }; 17 | 18 | export default Navbar; -------------------------------------------------------------------------------- /components/ProjectCard.jsx: -------------------------------------------------------------------------------- 1 | import { CodeIcon } from '@heroicons/react/outline'; 2 | 3 | const ProjectCard = ({ project, displayRequirements, setDisplayRequirements, index }) => ( 4 |
5 |
6 |
7 |
8 |

9 | {index < 10 ? `0${index + 1}` : index + 1} 10 |

11 |
12 | {project.title} 13 |
14 |

{project.description}

15 |
16 |
17 | 18 | preview 19 | 20 | 21 | starter 22 | 23 |
24 |
25 | 32 |
33 |
34 |
35 |
36 | ); 37 | 38 | export default ProjectCard; 39 | -------------------------------------------------------------------------------- /components/RenderedComponent.tsx: -------------------------------------------------------------------------------- 1 | import { ExternalLinkIcon } from '@heroicons/react/outline'; 2 | import { RenderedComponentProps } from '../interfaces/tokens'; 3 | 4 | const RenderedComponent = (props: RenderedComponentProps) => { 5 | return ( 6 |
props.method(event)} className='rounded-lg min-h-content bg-[#2a302f] p-4 sm:col-span-6 lg:col-start-2 lg:col-end-6'> 7 |
8 |

9 | {props.title} 10 |

11 | 18 |
19 |
20 | 39 |
40 |
41 | ); 42 | }; 43 | 44 | export default RenderedComponent; -------------------------------------------------------------------------------- /components/Requirements.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Dialog, Transition } from '@headlessui/react' 3 | import { CodeIcon, LinkIcon } from '@heroicons/react/outline' 4 | 5 | const Requirements = ({ displayRequirements, setDisplayRequirements }) => { 6 | 7 | const requirements = [ 8 | { 9 | title: 'Phantom', 10 | url: 'https://phantom.app/', 11 | description: 'A Solana wallet browser extension' 12 | }, 13 | { 14 | title: 'SOL', 15 | url: 'https://solfaucet.com/', 16 | description: 'Cryptocurrency native to the Solana blockchain' 17 | }, 18 | { 19 | title: 'Node Version Manager', 20 | url: 'https://github.com/nvm-sh/nvm', 21 | description: 'A CLI tool that lets you manage multiple versions of Node.js' 22 | }, 23 | { 24 | title: 'GitHub', 25 | url: 'https://github.com/', 26 | description: 'Software version control platform' 27 | }, 28 | { 29 | title: 'VSCode IDE', 30 | url: 'https://code.visualstudio.com/', 31 | description: 'A free and open source code editor' 32 | }, { 33 | title: 'Yarn', 34 | url: 'https://classic.yarnpkg.com/lang/en/docs/install/#mac-stable', 35 | description: 'A package manager for JavaScript' 36 | }, 37 | { 38 | title: 'Git', 39 | url: 'https://git-scm.com/', 40 | description: 'A version control system' 41 | }, 42 | { 43 | title: 'Solana CLI', 44 | url: 'https://docs.solana.com/cli/install-solana-cli-tools', 45 | description: 'A command line interface for the Solana blockchain' 46 | }, 47 | { 48 | title: 'Anchor CLI', 49 | url: 'https://www.anchor-lang.com/docs/installation', 50 | description: 'A command line interface for developing Anchor projects' 51 | } 52 | ]; 53 | 54 | return ( 55 | 56 | 57 | 66 |
67 | 68 | 69 |
70 |
71 | 80 | 81 |
82 |
83 |
85 |
86 | 87 | Scroll to view dependencies 88 | 89 | 118 |
119 |
120 |
121 | 128 |
129 |
130 |
131 |
132 |
133 |
134 |
135 | ); 136 | } 137 | 138 | export default Requirements; -------------------------------------------------------------------------------- /components/tokens/CreateAccount.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import * as web3 from '@solana/web3.js'; 3 | import * as token from '@solana/spl-token'; 4 | import { toast } from 'react-toastify'; 5 | 6 | import { CreateAccountProps } from '../../interfaces/tokens'; 7 | import RenderedComponent from '../RenderedComponent'; 8 | 9 | const CreateAccount = (props: CreateAccountProps) => { 10 | 11 | const createAccount = async (event: { preventDefault: () => void }) => { 12 | event.preventDefault(); 13 | 14 | if (props.connectionErr()) { return; } 15 | 16 | try { 17 | // Token Accounts are accounts which hold tokens of a given mint. 18 | const tokenAccount = web3.Keypair.generate(); 19 | const space = token.ACCOUNT_SIZE; 20 | // amount of SOL required for the account to not be deallocated 21 | const lamports = await props.connection.getMinimumBalanceForRentExemption(space); 22 | const programId = token.TOKEN_PROGRAM_ID; 23 | 24 | const transaction = new web3.Transaction().add( 25 | // creates a new account 26 | web3.SystemProgram.createAccount({ 27 | fromPubkey: props.publicKey!, 28 | newAccountPubkey: tokenAccount.publicKey, 29 | space, 30 | lamports, 31 | programId 32 | }), 33 | // initializes the new account as a Token Account account 34 | token.createInitializeAccountInstruction( 35 | tokenAccount.publicKey, // account to initialize 36 | props.mintAddr!, // token mint address 37 | props.publicKey!, // owner of new account 38 | token.TOKEN_PROGRAM_ID // spl token program account 39 | ) 40 | ); 41 | 42 | // prompts the user to sign the transaction and submit it to the network 43 | const signature = await props.sendTransaction(transaction, props.connection, { signers: [tokenAccount] }); 44 | props.setAccTx(signature); 45 | props.setAccAddr(tokenAccount.publicKey); 46 | } catch (err) { 47 | toast.error("Error creating Token Account"); 48 | console.log('error', err); 49 | } 50 | }; 51 | 52 | const outputs = [ 53 | { 54 | title: "Token Account Address...", 55 | dependency: props.accAddr!, 56 | href: `https://explorer.solana.com/address/${props.accAddr}?cluster=devnet`, 57 | }, 58 | { 59 | title: "Transaction Signature...", 60 | dependency: props.accTx, 61 | href: `https://explorer.solana.com/tx/${props.accTx}?cluster=devnet`, 62 | } 63 | ]; 64 | 65 | return ( 66 | 74 | ); 75 | }; 76 | 77 | export default CreateAccount; -------------------------------------------------------------------------------- /components/tokens/CreateMint.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import * as web3 from '@solana/web3.js'; 3 | import * as token from '@solana/spl-token'; 4 | import { toast } from 'react-toastify'; 5 | import { CreateMintProps } from '../../interfaces/tokens'; 6 | import RenderedComponent from '../RenderedComponent'; 7 | 8 | const CreateMint = (props: CreateMintProps) => { 9 | 10 | const createMint = async (event: { preventDefault: () => void; }) => { 11 | // prevents page from refreshing 12 | event.preventDefault(); 13 | 14 | // checks if wallet is connected 15 | if (props.connectionErr()) { return; } 16 | 17 | try { 18 | // Token Mints are accounts which hold data ABOUT a specific token. 19 | // Token Mints DO NOT hold tokens themselves. 20 | const tokenMint = web3.Keypair.generate(); 21 | // amount of SOL required for the account to not be deallocated 22 | const lamports = await token.getMinimumBalanceForRentExemptMint(props.connection); 23 | // `token.createMint` function creates a transaction with the following two instruction: `createAccount` and `createInitializeMintInstruction`. 24 | const transaction = new web3.Transaction().add( 25 | // creates a new account 26 | web3.SystemProgram.createAccount({ 27 | fromPubkey: props.publicKey!, 28 | newAccountPubkey: tokenMint.publicKey, 29 | space: token.MINT_SIZE, 30 | lamports, 31 | programId: token.TOKEN_PROGRAM_ID 32 | }), 33 | // initializes the new account as a Token Mint account 34 | token.createInitializeMintInstruction( 35 | tokenMint.publicKey, 36 | 0, 37 | props.publicKey!, 38 | token.TOKEN_PROGRAM_ID 39 | ) 40 | ); 41 | 42 | // prompts the user to sign the transaction and submit it to the network 43 | const signature = await props.sendTransaction(transaction, props.connection, { signers: [tokenMint] }); 44 | props.setMintTx(signature); 45 | props.setMintAddr(tokenMint.publicKey); 46 | } catch (err) { 47 | toast.error('Error creating Token Mint'); 48 | console.log('error', err); 49 | } 50 | }; 51 | 52 | const outputs = [ 53 | { 54 | title: 'Token Mint Address...', 55 | dependency: props.mintAddr!, 56 | href: `https://explorer.solana.com/address/${props.mintAddr}?cluster=devnet`, 57 | }, 58 | { 59 | title: 'Transaction Signature...', 60 | dependency: props.mintTx, 61 | href: `https://explorer.solana.com/tx/${props.mintTx}?cluster=devnet`, 62 | } 63 | ]; 64 | 65 | return ( 66 | 74 | ); 75 | }; 76 | 77 | export default CreateMint; -------------------------------------------------------------------------------- /contexts/TransitionContextProvider.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Transition } from '@headlessui/react' 3 | 4 | 5 | const TransitionContextProvider = ({ children }) => { 6 | 7 | const [isShowing, setIsShowing] = React.useState(false); 8 | 9 | React.useEffect(() => { 10 | setTimeout(() => { 11 | setIsShowing(true); 12 | }, 500); 13 | }, []); 14 | 15 | return ( 16 | 25 | {children} 26 | 27 | ); 28 | }; 29 | 30 | export default TransitionContextProvider; -------------------------------------------------------------------------------- /contexts/WalletContextProvider.jsx: -------------------------------------------------------------------------------- 1 | import { ConnectionProvider, WalletProvider } from '@solana/wallet-adapter-react'; 2 | import { WalletModalProvider } from "@solana/wallet-adapter-react-ui" 3 | import * as walletAdapterWallets from '@solana/wallet-adapter-wallets' 4 | import * as web3 from '@solana/web3.js'; 5 | require('@solana/wallet-adapter-react-ui/styles.css'); 6 | 7 | const WalletContextProvider = ({ children }) => { 8 | 9 | const endpoint = web3.clusterApiUrl('devnet'); 10 | const wallets = [ 11 | new walletAdapterWallets.PhantomWalletAdapter() 12 | ]; 13 | 14 | return ( 15 | 16 | 17 | 18 | {children} 19 | 20 | 21 | 22 | ); 23 | }; 24 | 25 | export default WalletContextProvider; -------------------------------------------------------------------------------- /interfaces/tokens.ts: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import * as web3 from "@solana/web3.js"; 3 | import { SendTransactionOptions } from "@solana/wallet-adapter-base"; 4 | 5 | export interface CreateMintProps { 6 | mintTx: string; 7 | mintAddr: web3.PublicKey | undefined; 8 | 9 | connection: web3.Connection; 10 | publicKey: web3.PublicKey | null; 11 | 12 | setMintTx: (transaction: string) => void; 13 | setMintAddr: (address: web3.PublicKey) => void; 14 | 15 | connectionErr: () => boolean | undefined; 16 | sendTransaction: ( 17 | transaction: web3.Transaction, 18 | connection: web3.Connection, 19 | options?: SendTransactionOptions, 20 | ) => Promise; 21 | }; 22 | 23 | export interface CreateAccountProps { 24 | accTx: string; 25 | accAddr: web3.PublicKey | undefined; 26 | mintAddr: web3.PublicKey | undefined; 27 | 28 | connection: web3.Connection; 29 | publicKey: web3.PublicKey | null; 30 | 31 | setAccTx: (transaction: string) => void; 32 | setAccAddr: (address: web3.PublicKey) => void; 33 | 34 | connectionErr: () => boolean | undefined; 35 | sendTransaction: ( 36 | transaction: web3.Transaction, 37 | connection: web3.Connection, 38 | options?: SendTransactionOptions, 39 | ) => Promise; 40 | }; 41 | 42 | interface OutputItem { 43 | title: string; 44 | dependency: web3.PublicKey | string; 45 | href: string; 46 | } 47 | 48 | export interface RenderedComponentProps { 49 | title: string; 50 | buttonText: string; 51 | 52 | method: (event: React.FormEvent) => void; 53 | validation: web3.PublicKey | undefined | null; 54 | outputs: OutputItem[]; 55 | } -------------------------------------------------------------------------------- /models/serialize/StudentIntro.ts: -------------------------------------------------------------------------------- 1 | 2 | export class StudentIntro { 3 | 4 | } -------------------------------------------------------------------------------- /models/serialize/StudentIntroReference.ts: -------------------------------------------------------------------------------- 1 | import * as borsh from '@project-serum/borsh' 2 | 3 | /* 4 | Class Structure: It includes two properties, name and message, 5 | which are strings. The constructor initializes these properties. 6 | 7 | Borsh Schemas: Two Borsh schemas are defined - one for instructions 8 | (borshInstructionSchema) and one for accounts (borshAccountSchema). These schemas 9 | specify the data structure for serialization/deserialization. 10 | 11 | Serialization: The serialize method converts the instance data into a 12 | binary format (buffer) that can be sent to the blockchain. It uses the borshInstructionSchema. 13 | 14 | Deserialization: The deserialize static method converts binary data from 15 | the blockchain back into an instance of StudentIntroReference. It uses the borshAccountSchema. 16 | */ 17 | 18 | export class StudentIntroReference { 19 | name: string; 20 | message: string; 21 | 22 | constructor(name: string, message: string) { 23 | this.name = name; 24 | this.message = message; 25 | }; 26 | 27 | borshInstructionSchema = borsh.struct([ 28 | borsh.u8('variant'), 29 | borsh.str('name'), 30 | borsh.str('message') 31 | ]); 32 | 33 | static borshAccountSchema = borsh.struct([ 34 | borsh.u8('initialized'), 35 | borsh.str('name'), 36 | borsh.str('message'), 37 | ]); 38 | 39 | // serialize info when we send it to the blockchain 40 | serialize(): Buffer { 41 | const buffer = Buffer.alloc(1000); 42 | this.borshInstructionSchema.encode({ ...this, variant: 0 }, buffer); 43 | return buffer.slice(0, this.borshInstructionSchema.getSpan(buffer)); 44 | }; 45 | 46 | // deserialize info when we retrieve it from the blockchain 47 | static deserialize(buffer?: Buffer): StudentIntroReference | null { 48 | if (!buffer) { 49 | return null; 50 | } 51 | 52 | try { 53 | const { name, message } = this.borshAccountSchema.decode(buffer); 54 | return new StudentIntroReference(name, message); 55 | } catch (error) { 56 | console.log('Deserialization error:', error); 57 | return null; 58 | } 59 | }; 60 | }; -------------------------------------------------------------------------------- /next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | 4 | // NOTE: This file should not be edited 5 | // see https://nextjs.org/docs/basic-features/typescript for more information. 6 | -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | module.exports = { 3 | reactStrictMode: true, 4 | } 5 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "scripts": { 4 | "dev": "next dev", 5 | "build": "next build", 6 | "start": "next start", 7 | "ping": "node scripts/ping.js", 8 | "send": "node scripts/send.js", 9 | "server": "nodemon pages/api/index.js" 10 | }, 11 | "dependencies": { 12 | "@headlessui/react": "^1.6.6", 13 | "@heroicons/react": "^1.0.6", 14 | "@project-serum/borsh": "^0.2.5", 15 | "@solana/spl-token": "^0.2.0", 16 | "@solana/wallet-adapter-base": "^0.9.7", 17 | "@solana/wallet-adapter-react": "^0.15.6", 18 | "@solana/wallet-adapter-react-ui": "^0.9.8", 19 | "@solana/wallet-adapter-wallets": "^0.16.6", 20 | "@solana/web3.js": "^1.47.3", 21 | "axios": "^1.6.7", 22 | "bs58": "^5.0.0", 23 | "dotenv": "^16.0.1", 24 | "next": "latest", 25 | "react": "17.0.2", 26 | "react-dom": "17.0.2", 27 | "react-toastify": "^9.0.5" 28 | }, 29 | "devDependencies": { 30 | "@types/node": "17.0.35", 31 | "@types/react": "18.0.9", 32 | "@types/react-dom": "18.0.5", 33 | "autoprefixer": "^10.4.7", 34 | "postcss": "^8.4.14", 35 | "tailwindcss": "^3.1.2", 36 | "typescript": "4.7.2" 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /pages/_app.tsx: -------------------------------------------------------------------------------- 1 | import '../styles/globals.css' 2 | import type { AppProps } from 'next/app' 3 | import { ToastContainer } from 'react-toastify'; 4 | import 'react-toastify/dist/ReactToastify.css'; 5 | 6 | import Navbar from '../components/Navbar'; 7 | 8 | import WalletContextProvider from '../contexts/WalletContextProvider'; 9 | import TransitionContextProvider from '../contexts/TransitionContextProvider'; 10 | import Head from 'next/head'; 11 | 12 | function MyApp({ Component, pageProps }: AppProps) { 13 | return ( 14 | <> 15 | 16 | Helius 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | ); 28 | } 29 | 30 | export default MyApp 31 | -------------------------------------------------------------------------------- /pages/faucet/finished.tsx: -------------------------------------------------------------------------------- 1 | // imports methods relevant to the react framework 2 | import * as React from 'react'; 3 | // throws notifications for user friendly error handling 4 | import { toast } from 'react-toastify'; 5 | // imports methods for deriving data from the wallet's data store 6 | import { useConnection, useWallet } from '@solana/wallet-adapter-react' 7 | // imports icons 8 | import { ExternalLinkIcon } from '@heroicons/react/outline'; 9 | // library we use to interact with the solana json rpc api 10 | import * as web3 from '@solana/web3.js'; 11 | 12 | const Finished = () => { 13 | // allocate state to hold transaction signature 14 | const [txSig, setTxSig] = React.useState(''); 15 | 16 | // get user info from wallet provider 17 | const { connection } = useConnection(); 18 | const { publicKey, sendTransaction } = useWallet(); 19 | 20 | // function to send sol 21 | const fundWallet = async (event: { preventDefault: () => void }) => { 22 | // prevent page from refreshing when this function runs 23 | event.preventDefault(); 24 | 25 | // if user is not connected, throw an error 26 | if (!publicKey || !connection) { 27 | toast.error('Please connect your wallet'); 28 | throw 'Please connect your wallet'; 29 | } 30 | 31 | // generate a new keypair 32 | const sender = web3.Keypair.generate(); 33 | 34 | // check the balance of the keypair and send funds if needed 35 | const balance = await connection.getBalance(sender.publicKey); 36 | if (balance < web3.LAMPORTS_PER_SOL) { 37 | await connection.requestAirdrop(sender.publicKey, web3.LAMPORTS_PER_SOL * 1); 38 | } 39 | 40 | // create a new transaction and add the instruction to transfer tokens 41 | const transaction = new web3.Transaction().add( 42 | web3.SystemProgram.transfer({ 43 | fromPubkey: sender.publicKey, 44 | toPubkey: publicKey, 45 | lamports: web3.LAMPORTS_PER_SOL * 1 46 | }), 47 | ); 48 | 49 | // send the transaction to the network 50 | try { 51 | const signature = await sendTransaction(transaction, connection, { 52 | signers: [sender] 53 | }); 54 | setTxSig(signature); // if tx lands, set state w/ tx signature 55 | } catch (error) { 56 | toast.error('Error funding wallet'); // if tx fails, throw error notification 57 | throw error; 58 | } 59 | }; 60 | 61 | // format for outputs we want to render 62 | const outputs = [ 63 | { 64 | title: 'Transaction Signature...', 65 | dependency: txSig, 66 | href: `https://explorer.solana.com/tx/${txSig}?cluster=devnet`, 67 | } 68 | ]; 69 | 70 | return ( 71 |
72 |
fundWallet(event)} className='rounded-lg min-h-content bg-[#2a302f] p-4 sm:col-span-6 lg:col-start-2 lg:col-end-6'> 73 |
74 |

75 | Faucet 🚰 76 |

77 | 83 |
84 | 85 |
86 | 105 |
106 |
107 |
108 | ); 109 | }; 110 | 111 | export default Finished; -------------------------------------------------------------------------------- /pages/faucet/starter.tsx: -------------------------------------------------------------------------------- 1 | import BoilerPlate from '../../components/BoilerPlate'; 2 | 3 | const Starter = () => ( 4 | 5 | ) 6 | 7 | export default Starter; -------------------------------------------------------------------------------- /pages/index.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Requirements from '../components/Requirements'; 3 | import ProjectCard from '../components/ProjectCard'; 4 | import { projects } from '../projects/projects'; 5 | 6 | const Index = () => { 7 | 8 | const [displayRequirements, setDisplayRequirements] = React.useState(false); 9 | 10 | return ( 11 |
12 | 16 |
17 |
18 |

Helius Frontend Development Bootcamp

19 | by @_zebedee_ 20 |

This is a collection of projects that will help you learn the basics of frontend development for solana applications. The projects are ordered by difficulty, with the easiest projects at the top and the hardest projects at the bottom. The projects are also ordered by the order in which you should complete them. You should start with the first project and work your way down the list.

21 |
22 |
23 | {projects.map((project, index) => ( 24 | 31 | ))} 32 |
33 |
34 |
35 | ); 36 | }; 37 | 38 | export default Index; 39 | -------------------------------------------------------------------------------- /pages/minter/finished.tsx: -------------------------------------------------------------------------------- 1 | // imports methods relevant to the react framework 2 | import * as React from 'react'; 3 | // throws notifications for user friendly error handling 4 | import { toast } from 'react-toastify'; 5 | // imports methods for deriving data from the wallet's data store 6 | import { useConnection, useWallet } from '@solana/wallet-adapter-react' 7 | // imports icons 8 | import { ExternalLinkIcon } from '@heroicons/react/outline'; 9 | 10 | const nftImageUrl = "https://nathan-galindo.vercel.app/_next/image?url=%2F_next%2Fstatic%2Fmedia%2Fimage-2.614ae0c9.jpg&w=640&q=75"; 11 | const nftExternalUrl = "https://nathan-galindo.vercel.app/"; 12 | 13 | const Finished = () => { 14 | const [apiUrl, setApiUrl] = React.useState(""); 15 | const [nft, setNft] = React.useState(""); 16 | const [nftImage, setNftImage] = React.useState(""); 17 | 18 | // get user info from wallet provider 19 | const { connection } = useConnection(); 20 | const { publicKey } = useWallet(); 21 | 22 | // create compressed nft 23 | const mintCompressedNft = async (event: { preventDefault: () => void }) => { 24 | // prevent react app from resetting 25 | event.preventDefault(); 26 | 27 | // make api call to create cNFT 28 | const response = await fetch(apiUrl, { 29 | method: 'POST', 30 | headers: { 31 | 'Content-Type': 'application/json', 32 | }, 33 | body: JSON.stringify({ 34 | jsonrpc: '2.0', 35 | id: 'helius-fe-course', 36 | method: 'mintCompressedNft', 37 | params: { 38 | name: "Nathan's Second cNFT", 39 | symbol: 'NNFT', 40 | owner: publicKey, 41 | description: 42 | "Nathan's Super cool NFT", 43 | attributes: [ 44 | { 45 | trait_type: 'Cool Factor', 46 | value: 'Super', 47 | }, 48 | ], 49 | imageUrl: nftImageUrl, 50 | externalUrl: nftExternalUrl, 51 | sellerFeeBasisPoints: 6900, 52 | }, 53 | }) 54 | }); 55 | 56 | const { result } = await response.json(); 57 | console.log("RESULT", result); 58 | 59 | if (!result) { 60 | toast.error("Request failed") 61 | throw "Request failed" 62 | } 63 | 64 | setNft(result.assetId); 65 | 66 | fetchNFT(result.assetId, event); 67 | }; 68 | 69 | // fetch nft after it's minted 70 | const fetchNFT = async (assetId: string, event: { preventDefault: () => void }) => { 71 | // prevent app from reloading 72 | event.preventDefault(); 73 | 74 | // api call to fetch nft 75 | const response = await fetch(apiUrl, { 76 | method: 'POST', 77 | headers: { 78 | 'Content-Type': 'applicaiton/json', 79 | }, 80 | body: JSON.stringify({ 81 | jsonrpc: '2.0', 82 | id: 'my-id', 83 | method: 'getAsset', 84 | params: { 85 | id: assetId 86 | } 87 | }) 88 | }); 89 | 90 | // extrapolate api response 91 | const { result } = await response.json(); 92 | 93 | // set nft image in state variable 94 | setNftImage(result.content.links.image); 95 | 96 | // return api result 97 | return { result }; 98 | }; 99 | 100 | // display function outputs to ui 101 | const outputs = [ 102 | { 103 | title: 'Asset ID...', 104 | dependency: nft, 105 | href: `https://xray.helius.xyz/token/${nft}?network=devnet`, 106 | } 107 | ]; 108 | 109 | // set api url onload 110 | React.useEffect(() => { 111 | setApiUrl( 112 | connection.rpcEndpoint.includes("devnet") 113 | ? "https://devnet.helius-rpc.com/?api-key=23aabe59-1cbe-4b31-91da-0ae23a590bdc" 114 | : "https://mainnet.helius-rpc.com/?api-key=23aabe59-1cbe-4b31-91da-0ae23a590bdc" 115 | ); 116 | }, [connection]); 117 | 118 | return ( 119 |
120 |
mintCompressedNft(event)} className='rounded-lg min-h-content bg-[#2a302f] p-4 sm:col-span-6 lg:col-start-2 lg:col-end-6'> 121 |
122 |

123 | cNFT Minter 🖼️ 124 |

125 | 132 |
133 | 134 |
135 | 154 |
155 | 156 |
157 | { 158 | nftImage // if nftImage exists, render image, otherwise render text 159 | ? 160 | 166 | : 167 |

NFT Image Goes Here

168 | } 169 |
170 |
171 |
172 | ); 173 | }; 174 | 175 | export default Finished; -------------------------------------------------------------------------------- /pages/minter/starter.tsx: -------------------------------------------------------------------------------- 1 | import BoilerPlate from '../../components/BoilerPlate'; 2 | 3 | const Starter = () => ( 4 | 5 | ) 6 | 7 | export default Starter; -------------------------------------------------------------------------------- /pages/movies/finished.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import * as web3 from '@solana/web3.js'; 3 | import * as borsh from '@project-serum/borsh'; 4 | import { toast } from 'react-toastify'; 5 | import { useConnection, useWallet } from '@solana/wallet-adapter-react'; 6 | import { ExternalLinkIcon } from '@heroicons/react/outline'; 7 | 8 | const Finished = () => { 9 | // react state variables 10 | const [rating, setRating] = React.useState(0); 11 | const [description, setDescription] = React.useState(""); 12 | const [title, setTitle] = React.useState(""); 13 | const [txSig, setTxSig] = React.useState(""); 14 | 15 | // custom on-chain program we are interacting with 16 | const programId = new web3.PublicKey('GWenWxNqXEEM4Cue4jRoYrGuyGb3FTAGu4fZGSwpMU5P'); 17 | 18 | // grab user's wallet details 19 | const { connection } = useConnection(); 20 | const { publicKey, sendTransaction } = useWallet(); 21 | 22 | // define schema of data we're serializing and sending to the blockchain 23 | const movieReviewLayout = borsh.struct([ 24 | borsh.u8('variant'), 25 | borsh.str('title'), 26 | borsh.u8('rating'), 27 | borsh.str('description') 28 | ]); 29 | 30 | // function to send our input to the on-chain program 31 | const sendMovieReview = async (event: { preventDefault: () => void; }) => { 32 | // prevent app refresh when this function runs 33 | event.preventDefault(); 34 | 35 | // check if user's wallet is connected 36 | if (!publicKey || !connection) { 37 | toast.error('Please connect your wallet'); 38 | throw 'Please connect your wallet'; 39 | }; 40 | 41 | // create a buffer to store user input | note: a solana account can store up to 10MB (10,485,760 bytes) 42 | let buffer = Buffer.alloc(1000); 43 | 44 | // create unique movie title 45 | const movieTitle = `${title} - (${(Math.random() * 1000000).toString().slice(0, 4)})`; 46 | 47 | // encodes the provided data into a binary format according to the movieInstructionLayout's schema and writes the encoded data into the buffer. 48 | movieReviewLayout.encode({ 49 | variant: 0, 50 | title: movieTitle, 51 | rating: rating, 52 | description: description 53 | }, buffer); 54 | 55 | // adjust the buffer size in case our data has any unused space (to avoid paying hire rent / bloating blockchain space) 56 | buffer = buffer.slice(0, movieReviewLayout.getSpan(buffer)); 57 | 58 | // derive the address of the account we will store this info in on-chain 59 | const [pda] = await web3.PublicKey.findProgramAddress( 60 | [publicKey!.toBuffer(), Buffer.from(movieTitle)], 61 | programId 62 | ); 63 | 64 | // initialize a transaction object (empty) 65 | const transaction = new web3.Transaction(); 66 | 67 | // create the instruction (we will add to the transaction object) 68 | const instruction = new web3.TransactionInstruction({ 69 | programId: programId, 70 | data: buffer, 71 | keys: [ 72 | { 73 | pubkey: publicKey!, 74 | isSigner: true, 75 | isWritable: false, 76 | }, 77 | { 78 | pubkey: pda, 79 | isSigner: false, 80 | isWritable: true 81 | }, 82 | { 83 | pubkey: web3.SystemProgram.programId, 84 | isSigner: false, 85 | isWritable: false 86 | } 87 | ] 88 | }); 89 | 90 | // add instruction to transaction object 91 | transaction.add(instruction); 92 | 93 | 94 | try { // send transaction to blockchain 95 | const signature = await sendTransaction(transaction, connection); 96 | setTxSig(signature); 97 | toast.success("Movie review sent to blockchain!") 98 | console.log(`https://explorer.solana.com/tx/${signature}?cluster=devnet`); 99 | } catch (error) { // throw error messages if request fails 100 | console.error(error); 101 | } finally { // reset state variables in any case 102 | setDescription(""); 103 | setRating(0); 104 | setTitle(""); 105 | } 106 | }; 107 | 108 | // define repetitive ui elements 109 | const outputs = [ 110 | { 111 | title: 'Transaction Signature...', 112 | dependency: txSig, 113 | href: `https://explorer.solana.com/tx/${txSig}?cluster=devnet` 114 | } 115 | ]; 116 | 117 | return ( 118 |
119 |
120 |
121 |
122 |

123 | Movie Review 🎬 124 |

125 | 132 |
133 | 134 |
135 |

136 | What is the name of the movie? 137 |

138 | setTitle(event.target.value)} 144 | value={title} 145 | /> 146 |
147 | 148 |
149 |

150 | A brief description of the movie? 151 |

152 | setDescription(event.target.value)} 158 | value={description} 159 | /> 160 |
161 | 162 |
163 |

164 | What is the movie rating? 165 |

166 | setRating(parseInt(event.target.value))} 174 | value={rating} 175 | /> 176 |
177 |
178 | 197 |
198 |
199 |
200 |
201 | ); 202 | }; 203 | 204 | export default Finished; -------------------------------------------------------------------------------- /pages/movies/starter.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import * as web3 from "@solana/web3.js"; 3 | import * as borsh from "@project-serum/borsh"; 4 | import { toast } from "react-toastify"; 5 | import { useConnection, useWallet } from "@solana/wallet-adapter-react"; 6 | import { ExternalLinkIcon } from "@heroicons/react/outline"; 7 | import { transcode } from "buffer"; 8 | 9 | const Starter = () => { 10 | const [rating, setRating] = React.useState(0); 11 | const [description, setDescription] = React.useState(""); 12 | const [title, setTitle] = React.useState(""); 13 | const [txSig, setTxSig] = React.useState(""); 14 | 15 | const programId = new web3.PublicKey("GWenWxNqXEEM4Cue4jRoYrGuyGb3FTAGu4fZGSwpMU5P"); 16 | 17 | const { connection } = useConnection(); 18 | const { publicKey, sendTransaction } = useWallet(); 19 | 20 | const movieReviewLayout = borsh.struct([ 21 | borsh.u8("variant"), 22 | borsh.str("title"), 23 | borsh.u8("rating"), 24 | borsh.str("description") 25 | ]); 26 | 27 | const sendMovieReview = async (event: { preventDefault: () => void; }) => { 28 | event.preventDefault(); 29 | 30 | if (!publicKey || !connection) { 31 | toast.error("Please connect to the app"); 32 | throw "Please connect wallet"; 33 | } 34 | 35 | // creating a buffer 36 | let buffer = Buffer.alloc(1000); 37 | 38 | const movieTitle = `${title} - (${(Math.random() * 1000000).toString().slice(0, 4)})` 39 | 40 | movieReviewLayout.encode({ 41 | variant: 0, 42 | title: movieTitle, 43 | rating: rating, 44 | description: description 45 | }, buffer); 46 | 47 | buffer = buffer.slice(0, movieReviewLayout.getSpan(buffer)) 48 | 49 | const [pda] = await web3.PublicKey.findProgramAddressSync( 50 | [ 51 | publicKey!.toBuffer(), 52 | Buffer.from(movieTitle), 53 | ], 54 | programId 55 | ); 56 | 57 | const transaction = new web3.Transaction(); 58 | 59 | const instruction = new web3.TransactionInstruction({ 60 | programId: programId, 61 | data: buffer, 62 | keys: [ 63 | { 64 | pubkey: publicKey!, 65 | isSigner: true, 66 | isWritable: false 67 | }, 68 | { 69 | pubkey: pda, 70 | isSigner: false, 71 | isWritable: true, 72 | }, 73 | { 74 | pubkey: web3.SystemProgram.programId, 75 | isSigner: false, 76 | isWritable: false 77 | } 78 | ] 79 | }); 80 | 81 | transaction.add(instruction); 82 | 83 | // async logic 84 | try { 85 | const signature = await sendTransaction(transaction, connection); 86 | setTxSig(signature); 87 | toast.success("Succesful!"); 88 | console.log(`https://explorer.solana.com/tx/${signature}?cluster=devnet`); 89 | } catch (error) { 90 | console.error("error", error); 91 | toast.error("Request failed!"); 92 | } finally { 93 | setDescription(""); 94 | setRating(0); 95 | setTitle(""); 96 | } 97 | }; 98 | 99 | const outputs = [ 100 | { 101 | title: "Transaction Signature...", 102 | dependency: txSig, 103 | href: `https://explorer.solana.com/tx/${txSig}?cluster=devnet` 104 | }, 105 | ]; 106 | 107 | return ( 108 |
109 |
110 |
sendMovieReview(event)} 112 | className="rounded-lg min-h-content p-4 bg-[#2a302f] sm:col-span-6 lg:col-start-2 lg:col-end-6" 113 | > 114 |
115 |

Movie Review

116 | 123 |
124 | 125 |
126 |

What is the name of the movie?

127 | setTitle(event.target.value)} 133 | value={title} 134 | /> 135 |
136 | 137 |
138 |

A brief description of the movies?

139 | setDescription(event.target.value)} 145 | value={description} 146 | /> 147 |
148 | 149 |
150 |

151 | What is the movie rating? 152 |

153 | setRating(parseInt(event.target.value))} 161 | value={rating} 162 | /> 163 |
164 | 165 |
166 | 190 |
191 |
192 |
193 |
194 | ); 195 | }; 196 | 197 | export default Starter; -------------------------------------------------------------------------------- /pages/sendsol/finished.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react'; 2 | import * as web3 from '@solana/web3.js'; 3 | import { toast } from 'react-toastify'; 4 | import { useConnection, useWallet } from '@solana/wallet-adapter-react'; 5 | import { ExternalLinkIcon } from '@heroicons/react/outline'; 6 | 7 | const Finished = () => { 8 | 9 | const [account, setAccount] = useState(''); 10 | const [amount, setAmount] = useState(0); 11 | const [balance, setBalance] = useState(0); 12 | const [txSig, setTxSig] = useState(''); 13 | 14 | const { connection } = useConnection(); 15 | const { publicKey, sendTransaction } = useWallet(); 16 | 17 | const handleTransaction = async () => { 18 | if (!connection || !publicKey) { 19 | toast.error('Please connect your wallet.'); 20 | return; 21 | } 22 | 23 | const transaction = new web3.Transaction(); 24 | const instruction = web3.SystemProgram.transfer({ 25 | fromPubkey: publicKey, 26 | lamports: amount * web3.LAMPORTS_PER_SOL, 27 | toPubkey: account, 28 | }); 29 | 30 | transaction.add(instruction); 31 | 32 | try { 33 | const signature = await sendTransaction(transaction, connection); 34 | setTxSig(signature) 35 | 36 | const newBalance = balance - amount; 37 | setBalance(newBalance); 38 | } 39 | catch (error) { 40 | console.log(error); 41 | toast.error('Transaction failed!'); 42 | } 43 | finally { 44 | setAccount(''); 45 | setAmount(0); 46 | document.getElementById('account').value = ''; 47 | document.getElementById('amount').value = ''; 48 | } 49 | }; 50 | 51 | useEffect(() => { 52 | const getInfo = async () => { 53 | if (connection && publicKey) { 54 | const info = await connection.getAccountInfo(publicKey); 55 | setBalance(info.lamports / web3.LAMPORTS_PER_SOL); 56 | } 57 | }; 58 | getInfo(); 59 | }, [connection, publicKey]); 60 | 61 | const outputs = [ 62 | { 63 | title: 'Account Balance...', 64 | dependency: balance, 65 | }, 66 | { 67 | title: 'Transaction Signature...', 68 | dependency: txSig, 69 | href: `https://explorer.solana.com/tx/${txSig}?cluster=devnet` 70 | }, 71 | ]; 72 | 73 | return ( 74 |
75 |
76 |
77 |
78 |

79 | Send Sol 💸 80 |

81 | 88 |
89 |
90 |

91 | Address of receiver 92 |

93 | setAccount(event.target.value)} 99 | /> 100 |
101 |
102 |

103 | Number amount 104 |

105 | setAmount(event.target.value)} 112 | /> 113 |
114 |
115 | 134 |
135 |
136 |
137 |
138 | ); 139 | }; 140 | 141 | export default Finished; 142 | -------------------------------------------------------------------------------- /pages/sendsol/starter.jsx: -------------------------------------------------------------------------------- 1 | import BoilerPlate from '../../components/BoilerPlate'; 2 | 3 | const Starter = () => ( 4 | 5 | ) 6 | 7 | export default Starter; -------------------------------------------------------------------------------- /pages/serialize/finished.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC } from 'react'; 2 | import * as web3 from '@solana/web3.js'; 3 | import { useConnection, useWallet } from '@solana/wallet-adapter-react'; 4 | import { toast } from 'react-toastify'; 5 | 6 | import { StudentIntroReference } from '../../models/serialize/StudentIntroReference'; 7 | import { StudentIntroCoordinatorReference } from '../../scripts/serialize/StudentIntroCoordinatorReference' 8 | 9 | /* 10 | account data needs to be deserialized using the same 11 | layout used to store it in the first place 12 | 13 | when submitting a transaction to a program, the client needs to 14 | include all addresses for accounts that will be written to or read from. 15 | This means that unlike more traditional client-server architectures, 16 | the client needs to have implementation-specific knowledge about 17 | the Solana program. The client needs to know which program 18 | derived address (PDA) is going to be used to store data 19 | so that it can include that address in the transaction. 20 | 21 | when reading data from a program, the client needs to 22 | know which account(s) to read from 23 | */ 24 | 25 | const Finished: FC = () => { 26 | 27 | // REACT VARIABLES 28 | const [name, setName] = React.useState(''); 29 | const [thoughts, setThoughts] = React.useState(''); 30 | 31 | // INTRO LIST STATE VARIABLES 32 | const [studentIntros, setStudentIntros] = React.useState([]); 33 | const [page, setPage] = React.useState(1) 34 | const [search, setSearch] = React.useState('') 35 | 36 | // SOLANA PROGRAM WE ARE INTERACTING WITH 37 | const TARGET_PROGRAM_ID = 'HdE95RSVsdb315jfJtaykXhXY478h53X6okDupVfY9yf'; 38 | 39 | // WALLET VARIABLES 40 | const { connection } = useConnection(); 41 | const { publicKey, sendTransaction } = useWallet(); 42 | 43 | // SUBMIT A NEW INTRO 44 | const createSubmission = async (event: { preventDefault: () => void }) => { 45 | event.preventDefault(); 46 | const studentIntro = new StudentIntroReference(name, thoughts); 47 | await handleTransactionSubmit(studentIntro); 48 | }; 49 | 50 | // CREATE AND SUBMIT A NEW TRANSACTION 51 | const handleTransactionSubmit = async (studentIntro: StudentIntroReference) => { 52 | // check that the wallet is connected 53 | if (!connection || !publicKey) { 54 | toast.error('Please connect your wallet.'); 55 | return; 56 | } 57 | 58 | // call `serialize` on `StudentIntro` to get buffer byte data 59 | const buffer = studentIntro.serialize(); 60 | 61 | // create a new `Transaction` object 62 | const transaction = new web3.Transaction(); 63 | 64 | // get all accounts that the transaction will interact with 65 | const [ pda ] = web3.PublicKey.findProgramAddressSync( 66 | [ publicKey.toBuffer() ], 67 | new web3.PublicKey(TARGET_PROGRAM_ID) 68 | ); 69 | 70 | // create a new `Instruction` object containing `keys`, `programId`, `buffer byte data` 71 | // `keys` is an array of accounts that the transaction will interact with 72 | // `data` is the buffer byte data 73 | // `programId` is the smart contract that the transaction will interact with 74 | const instruction = new web3.TransactionInstruction({ 75 | // `keys` is an array of objects containing `pubkey`, `isSigner`, and `isWritable` properties 76 | keys: [ 77 | { 78 | pubkey: publicKey, 79 | isSigner: true, 80 | isWritable: false 81 | }, 82 | { 83 | pubkey: pda, 84 | isSigner: false, 85 | isWritable: true 86 | }, 87 | { 88 | pubkey: web3.SystemProgram.programId, 89 | isSigner: false, 90 | isWritable: false 91 | } 92 | ], 93 | // data is stored as BPF encoded byte data on the Solana blockchain 94 | data: buffer, 95 | programId: new web3.PublicKey(TARGET_PROGRAM_ID), 96 | }); 97 | 98 | // add the `Instruction` to the `Transaction` 99 | transaction.add(instruction); 100 | 101 | // use `sendTransaction`, passing in the `Transaction` and `connection` objects 102 | try { 103 | const response = await sendTransaction(transaction, connection); 104 | console.log(`Transaction submitted: https://explorer.solana.com/tx/${response}?cluster=devnet`) 105 | toast.success('Transaction was successful!'); 106 | } catch (error: any) { 107 | toast.error('Transaction failed!'); 108 | console.log('Error:', error); 109 | } finally { 110 | // reset react state variables 111 | setName(''); 112 | setThoughts(''); 113 | }; 114 | }; 115 | 116 | React.useEffect(() => { 117 | StudentIntroCoordinatorReference.fetchPage( 118 | connection, 119 | page, 120 | 5, 121 | search, 122 | search !== '' 123 | ).then(setStudentIntros) 124 | }, [page, search]); 125 | 126 | return ( 127 |
128 | {/* FORM */} 129 |
130 |
131 |
132 |

133 | Introduce yourself ✌️ 134 |

135 | 142 |
143 |
144 |
145 |

146 | What do we call you? 147 |

148 | setName(event.target.value)} 154 | value={name} 155 | /> 156 |
157 |
158 |

159 | What brings you to Solana? 160 |

161 | setThoughts(event.target.value)} 167 | value={thoughts} 168 | /> 169 |
170 |
171 |
172 |
173 | 174 | {/* LIST OF RESPONSES */} 175 |
176 |
177 |

178 | Meet the students 179 |

180 |
181 |
182 | { 183 | studentIntros.map((studentIntro: StudentIntroReference, index: number) => ( 184 | (studentIntro.name && studentIntro.message) && 185 |
189 |

190 | {studentIntro.name} 191 |

192 |

193 | {studentIntro.message} 194 |

195 |
196 | )) 197 | } 198 |
199 |
200 |
201 | { 202 | page > 1 && 203 | 209 | } 210 |
211 |
212 | { 213 | StudentIntroCoordinatorReference.accounts.length > page * 5 && 214 | 221 | } 222 |
223 |
224 |
225 |
226 |
227 |
228 | ); 229 | }; 230 | 231 | export default Finished; 232 | -------------------------------------------------------------------------------- /pages/serialize/starter.tsx: -------------------------------------------------------------------------------- 1 | import BoilerPlate from '../../components/BoilerPlate'; 2 | 3 | const Starter = () => ( 4 | 5 | ) 6 | 7 | export default Starter; -------------------------------------------------------------------------------- /pages/tokens/finished.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import * as web3 from "@solana/web3.js"; 3 | import * as token from '@solana/spl-token'; 4 | import { useConnection, useWallet } from "@solana/wallet-adapter-react"; 5 | 6 | import { toast } from "react-toastify"; 7 | import { ExternalLinkIcon } from '@heroicons/react/outline'; 8 | 9 | const Finished = () => { 10 | // Token Mint 11 | const [mintTx, setMintTx] = React.useState(""); 12 | const [mintAddr, setMintAddr] = React.useState(undefined); 13 | 14 | // Token Account 15 | const [accTx, setAccTx] = React.useState(""); 16 | const [accAddr, setAccAddr] = React.useState(undefined); 17 | 18 | const { connection } = useConnection(); 19 | const { publicKey, sendTransaction } = useWallet(); 20 | 21 | // error handling; is wallet connected? 22 | const connectionErr = () => { 23 | if (!publicKey || !connection) { 24 | toast.error("Please connect your wallet"); 25 | return true; 26 | } else { return false; } 27 | }; 28 | 29 | // create transaction to create a token mint on the blockchain 30 | const createMint = async (event: { preventDefault: () => void; }) => { 31 | // prevents page from refreshing 32 | event.preventDefault(); 33 | 34 | // checks if wallet is connected 35 | if (connectionErr()) { return; } 36 | 37 | try { 38 | // Token Mints are accounts which hold data ABOUT a specific token. 39 | // Token Mints DO NOT hold tokens themselves. 40 | const tokenMint = web3.Keypair.generate(); 41 | // amount of SOL required for the account to not be deallocated 42 | const lamports = await token.getMinimumBalanceForRentExemptMint(connection); 43 | // `token.createMint` function creates a transaction with the following two instruction: `createAccount` and `createInitializeMintInstruction`. 44 | const transaction = new web3.Transaction().add( 45 | // creates a new account 46 | web3.SystemProgram.createAccount({ 47 | fromPubkey: publicKey!, 48 | newAccountPubkey: tokenMint.publicKey, 49 | space: token.MINT_SIZE, 50 | lamports, 51 | programId: token.TOKEN_PROGRAM_ID 52 | }), 53 | // initializes the new account as a Token Mint account 54 | token.createInitializeMintInstruction( 55 | tokenMint.publicKey, 56 | 0, 57 | publicKey!, 58 | token.TOKEN_PROGRAM_ID 59 | ) 60 | ); 61 | 62 | // prompts the user to sign the transaction and submit it to the network 63 | const signature = await sendTransaction(transaction, connection, { signers: [tokenMint] }); 64 | setMintTx(signature); 65 | setMintAddr(tokenMint.publicKey); 66 | } catch (err) { 67 | toast.error('Error creating Token Mint'); 68 | console.log('error', err); 69 | } 70 | }; 71 | 72 | // create transaction to create a token account fo the mint we created on the blockchain 73 | const createAccount = async (event: { preventDefault: () => void }) => { 74 | event.preventDefault(); 75 | 76 | if (connectionErr()) { return; } 77 | 78 | try { 79 | // Token Accounts are accounts which hold tokens of a given mint. 80 | const tokenAccount = web3.Keypair.generate(); 81 | const space = token.ACCOUNT_SIZE; 82 | // amount of SOL required for the account to not be deallocated 83 | const lamports = await connection.getMinimumBalanceForRentExemption(space); 84 | const programId = token.TOKEN_PROGRAM_ID; 85 | 86 | const transaction = new web3.Transaction().add( 87 | // creates a new account 88 | web3.SystemProgram.createAccount({ 89 | fromPubkey: publicKey!, 90 | newAccountPubkey: tokenAccount.publicKey, 91 | space, 92 | lamports, 93 | programId 94 | }), 95 | // initializes the new account as a Token Account account 96 | token.createInitializeAccountInstruction( 97 | tokenAccount.publicKey, // account to initialize 98 | mintAddr!, // token mint address 99 | publicKey!, // owner of new account 100 | token.TOKEN_PROGRAM_ID // spl token program account 101 | ) 102 | ); 103 | 104 | // prompts the user to sign the transaction and submit it to the network 105 | const signature = await sendTransaction(transaction, connection, { signers: [tokenAccount] }); 106 | setAccTx(signature); 107 | setAccAddr(tokenAccount.publicKey); 108 | } catch (err) { 109 | toast.error("Error creating Token Account"); 110 | console.log('error', err); 111 | } 112 | }; 113 | 114 | const createAccountOutputs = [ 115 | { 116 | title: "Token Account Address...", 117 | dependency: accAddr!, 118 | href: `https://explorer.solana.com/address/${accAddr}?cluster=devnet`, 119 | }, 120 | { 121 | title: "Transaction Signature...", 122 | dependency: accTx, 123 | href: `https://explorer.solana.com/tx/${accTx}?cluster=devnet`, 124 | } 125 | ]; 126 | 127 | const createMintOutputs = [ 128 | { 129 | title: 'Token Mint Address...', 130 | dependency: mintAddr!, 131 | href: `https://explorer.solana.com/address/${mintAddr}?cluster=devnet`, 132 | }, 133 | { 134 | title: 'Transaction Signature...', 135 | dependency: mintTx, 136 | href: `https://explorer.solana.com/tx/${mintTx}?cluster=devnet`, 137 | } 138 | ]; 139 | 140 | return ( 141 |
142 |
createMint(event)} className='rounded-lg min-h-content bg-[#2a302f] p-4 sm:col-span-6 lg:col-start-2 lg:col-end-6'> 143 |
144 |

145 | Create Mint 🦄 146 |

147 | 153 |
154 |
155 | 174 |
175 |
176 | 177 |
createAccount(event)} className='rounded-lg min-h-content bg-[#2a302f] p-4 sm:col-span-6 lg:col-start-2 lg:col-end-6'> 178 |
179 |

180 | Create Account ✨ 181 |

182 | 188 |
189 |
190 | 209 |
210 |
211 |
212 | ); 213 | }; 214 | 215 | export default Finished; 216 | -------------------------------------------------------------------------------- /pages/tokens/starter.tsx: -------------------------------------------------------------------------------- 1 | import BoilerPlate from '../../components/BoilerPlate'; 2 | 3 | const Starter = () => ( 4 | 5 | ); 6 | 7 | export default Starter; -------------------------------------------------------------------------------- /pages/viewer/finished.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { Dialog, Transition } from '@headlessui/react' 3 | import { toast } from "react-toastify"; 4 | import { useConnection, useWallet } from '@solana/wallet-adapter-react'; 5 | import { DocumentTextIcon, CubeTransparentIcon } from '@heroicons/react/outline'; 6 | 7 | // although it's better practice to import your API key as an environment variable, i've instantiated the API key as a constant variable for demo purposes 8 | const HELIUS_API_KEY = ""; 9 | 10 | const Finished = () => { 11 | // react state variables 12 | const [parseHistoryUrl, setParseHistoryUrl] = React.useState(""); 13 | const [listOfTxs, setListOfTxs] = React.useState([]); 14 | const [displayDetails, setDisplayDetails] = React.useState(false); 15 | const [transactionDetails, setTransactionDetails] = React.useState<{}>({}); 16 | 17 | const { connection } = useConnection(); // grab wallet connection string 18 | const { publicKey } = useWallet(); // grab wallet pubkey 19 | 20 | // only parses NFT, Jupiter, and SPL related transactions so far 21 | const parseTransactionHistory = async (event: { preventDefault: () => void }) => { 22 | // prevent app from reloading 23 | event.preventDefault(); 24 | 25 | // make sure user's wallet is connected 26 | if (!publicKey || !connection) { 27 | toast.error("Please connect wallet!"); 28 | throw "Wallet not connected"; 29 | } 30 | 31 | // api call to get tx history for wallet 32 | const response = await fetch(parseHistoryUrl); 33 | const data = await response.json(); 34 | 35 | // set state of tx sigs 36 | setListOfTxs(data); 37 | 38 | console.log("parsed transaction history", data); 39 | }; 40 | 41 | // retrieve the specific transaction user wants to view 42 | const handleTransactionDetails = (signature: string) => { 43 | const transaction = listOfTxs.find(tx => tx.signature === signature); 44 | setTransactionDetails(transaction); 45 | setDisplayDetails(true); 46 | }; 47 | 48 | // update url endpoints whenever wallet changes / set cluster of tx url 49 | React.useEffect(() => { 50 | setParseHistoryUrl(`https://api.helius.xyz/v0/addresses/${publicKey}/transactions?api-key=${HELIUS_API_KEY}`) 51 | }, [connection, publicKey]); 52 | 53 | return ( 54 |
55 | 60 |
parseTransactionHistory(event)} className='rounded-lg min-h-content bg-[#2a302f] p-4 sm:col-span-6 lg:col-start-2 lg:col-end-6'> 61 |
62 |

63 | Transaction Viewer 👀 64 |

65 | 71 |
72 | 73 | {listOfTxs.length > 0 &&
74 |
    75 | {listOfTxs.map(({ signature }, index) => ( 76 |
  • 77 |

    Transaction:

    78 | 85 |
  • 86 | ))} 87 |
88 |
} 89 |
90 |
91 | ); 92 | }; 93 | 94 | type TransactionDetailsProps = { 95 | transactionDetails: any; // Consider defining a more specific type 96 | displayDetails: boolean; 97 | setDisplayDetails: (displayDetails: boolean) => void; // Adjust as needed 98 | }; 99 | 100 | const TransactionDetails: React.FC = ({ 101 | transactionDetails, 102 | displayDetails, 103 | setDisplayDetails 104 | }) => { 105 | return ( 106 | 107 | 108 | 117 |
118 | 119 | 120 |
121 |
122 | 131 | 132 |
133 |
134 |
136 |
137 | 138 | Scroll to view transaction details 139 | 140 |
141 | {Object.entries(transactionDetails).map(([key, value], index) => ( 142 | (value !== null && typeof value !== 'object' || !Array.isArray(value)) && ( 143 |

144 |

145 |
148 |

{key}:

149 |
150 | 153 | {( 154 | value !== null 155 | ? (typeof value === 'string' || typeof value === 'number') 156 | ? ( 157 | value.toString().length > 10 158 | ? `${value.toString().slice(0, 10)}...` 159 | : value 160 | ) 161 | : 'Not Available' 162 | : 'Not Available' 163 | )} 164 | 165 |
166 |
167 |
168 |

169 | ) 170 | ))} 171 |
172 |
173 |
174 |
175 | 182 |
183 |
184 |
185 |
186 |
187 |
188 |
189 | ); 190 | }; 191 | 192 | export default Finished; 193 | -------------------------------------------------------------------------------- /pages/viewer/starter.tsx: -------------------------------------------------------------------------------- 1 | import BoilerPlate from '../../components/BoilerPlate'; 2 | 3 | const Starter = () => ( 4 | 5 | ) 6 | 7 | export default Starter; -------------------------------------------------------------------------------- /pages/wallets/finished.tsx: -------------------------------------------------------------------------------- 1 | // imports methods relevant to the react framework 2 | import * as React from 'react'; 3 | // library we use to interact with the solana json rpc api 4 | import * as web3 from '@solana/web3.js'; 5 | // allows us access to methods and components which give us access to the solana json rpc api and user's wallet data 6 | import * as walletAdapterReact from '@solana/wallet-adapter-react'; 7 | // allows us to choose from the available wallets supported by the wallet adapter 8 | import * as walletAdapterWallets from '@solana/wallet-adapter-wallets'; 9 | // imports a component which can be rendered in the browser 10 | import { WalletModalProvider, WalletMultiButton } from '@solana/wallet-adapter-react-ui'; 11 | // applies the styling to the components which are rendered on the browser 12 | require('@solana/wallet-adapter-react-ui/styles.css'); 13 | // imports methods for deriving data from the wallet's data store 14 | import { useConnection, useWallet } from '@solana/wallet-adapter-react'; 15 | 16 | const Finished = () => { 17 | // allows us to add the wallet account balance to our react function component 18 | const [balance, setBalance] = React.useState(0); 19 | // we specify which network we want to connect to 20 | const endpoint = web3.clusterApiUrl('devnet'); 21 | // we specify which wallets we want our wallet adapter to support 22 | const wallets = [ 23 | new walletAdapterWallets.PhantomWalletAdapter() 24 | ]; 25 | 26 | // connection context object that is injected into the browser by the wallet 27 | const { connection } = useConnection(); 28 | // user's public key of the wallet they connected to our application 29 | const { publicKey } = useWallet(); 30 | 31 | // when the status of `connection` or `publicKey` changes, we execute the code block below 32 | React.useEffect(() => { 33 | const getInfo = async () => { 34 | if (connection && publicKey) { 35 | // we get the account info for the user's wallet data store and set the balance in our application's state 36 | const info = await connection.getAccountInfo(publicKey); 37 | setBalance(info!.lamports / web3.LAMPORTS_PER_SOL); 38 | } 39 | } 40 | getInfo(); 41 | // the code above will execute whenever these variables change in any way 42 | }, [connection, publicKey]); 43 | 44 | return ( 45 | <> 46 | {/* provides a connection to the solana json rpc api */} 47 | 48 | {/* makes the wallet adapter available to the entirety of our application (wrapped in this component) */} 49 | 50 | {/* provides components to the wrapped application */} 51 | 52 |
53 |
54 |
55 |
56 |

57 | account info ✨ 58 |

59 | {/* button component for connecting to solana wallet */} 60 | 63 |
64 | 65 |
66 |
    67 |
  • 68 |

    Wallet is connected...

    69 |

    70 | {publicKey ? 'yes' : 'no'} 71 |

    72 |
  • 73 | 74 |
  • 75 |

    Balance...

    76 |

    77 | {balance} 78 |

    79 |
  • 80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 | 89 | ); 90 | }; 91 | 92 | export default Finished; 93 | -------------------------------------------------------------------------------- /pages/wallets/starter.tsx: -------------------------------------------------------------------------------- 1 | import BoilerPlate from '../../components/BoilerPlate'; 2 | 3 | const Starter = () => ( 4 | 5 | ) 6 | 7 | export default Starter; -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /projects/projects.ts: -------------------------------------------------------------------------------- 1 | interface Href { 2 | finished: string; 3 | starter: string; 4 | } 5 | 6 | interface Project { 7 | title: string; 8 | description: string; 9 | href: Href 10 | } 11 | 12 | export const projects: Project[] = [ 13 | { 14 | title: 'wallets', 15 | description: 'You will use solana/web3.js to implement the Solana wallet adapter in an application.', 16 | href: { 17 | finished: '/wallets/finished', 18 | starter: '/wallets/starter', 19 | } 20 | }, 21 | { 22 | title: 'faucet', 23 | description: 'You will build a SOL faucet that you will use to fund your Phantom browser wallet.', 24 | href: { 25 | finished: '/faucet/finished', 26 | starter: '/faucet/starter', 27 | } 28 | }, 29 | { 30 | title: 'send sol', 31 | description: 'You will create an application that allows you to send SOL to another wallet on the Solana devnet.', 32 | href: { 33 | finished: '/sendsol/finished', 34 | starter: '/sendsol/starter', 35 | } 36 | }, 37 | { 38 | title: 'serialize', 39 | description: 'Using Borsh, you will serialize custom instruction data to interact with an existing Solana smart contract.', 40 | href: { 41 | finished: '/serialize/finished', 42 | starter: '/serialize/starter', 43 | } 44 | }, 45 | { 46 | title: 'tokens', 47 | description: 'You will use the @solana/spl-token library to create Token Mints, create SPL-tokens, and burn tokens.', 48 | href: { 49 | finished: '/tokens/finished', 50 | starter: '/tokens/starter', 51 | } 52 | }, 53 | { 54 | title: 'nft minter', 55 | description: 'Create an compressed NFT minting machine using Helius APIs.', 56 | href: { 57 | finished: '/minter/finished', 58 | starter: '/minter/starter', 59 | } 60 | }, 61 | { 62 | title: 'movies', 63 | description: 'You will write and deploy a Solana program which buffer byte data. You will then create a UI to interact with it.', 64 | href: { 65 | finished: '/movies/finished', 66 | starter: '/movies/starter', 67 | } 68 | }, 69 | { 70 | title: 'transaction viewer', 71 | description: 'You will use the Helius transaction parser API to view the transaction history of your wallet.', 72 | href: { 73 | finished: '/viewer/finished', 74 | starter: '/viewer/starter', 75 | } 76 | }, 77 | ]; 78 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/masterzorgon/solana-frontend-development-course/f1f719dbd1ff8a5c9947f8977491eb56193df9e2/public/favicon.ico -------------------------------------------------------------------------------- /public/helius-orange.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/masterzorgon/solana-frontend-development-course/f1f719dbd1ff8a5c9947f8977491eb56193df9e2/public/helius-orange.png -------------------------------------------------------------------------------- /public/helius-white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/masterzorgon/solana-frontend-development-course/f1f719dbd1ff8a5c9947f8977491eb56193df9e2/public/helius-white.png -------------------------------------------------------------------------------- /scripts/serialize/StudentIntroCoordinator.ts: -------------------------------------------------------------------------------- 1 | 2 | export class StudentIntroCoordinator { 3 | 4 | }; -------------------------------------------------------------------------------- /scripts/serialize/StudentIntroCoordinatorReference.ts: -------------------------------------------------------------------------------- 1 | import bs58 from 'bs58' 2 | import * as web3 from '@solana/web3.js' 3 | 4 | import { StudentIntroReference } from '../../models/serialize/StudentIntroReference' 5 | 6 | /* 7 | Prefetching Accounts: The prefetchAccounts static method retrieves accounts associated 8 | with a specific Solana program ID (STUDENT_INTRO_PROGRAM_ID). It uses filters based on 9 | a search string and sorts the accounts based on a data slice logic. This method updates 10 | the accounts static property with a list of public keys corresponding to the fetched accounts. 11 | 12 | Fetching Page: The fetchPage static method is used for pagination. 13 | It fetches a specific page of "StudentIntro" data from the Solana blockchain. 14 | This method checks if a reload is necessary or if the accounts array is empty, and 15 | then it calls prefetchAccounts. It slices the accounts array to get a subset 16 | corresponding to the requested page and fetches detailed account information, 17 | which is then deserialized into "StudentIntro" objects and returned. 18 | */ 19 | 20 | const STUDENT_INTRO_PROGRAM_ID = 'HdE95RSVsdb315jfJtaykXhXY478h53X6okDupVfY9yf' 21 | 22 | export class StudentIntroCoordinatorReference { 23 | static accounts: web3.PublicKey[] = [] 24 | 25 | static async prefetchAccounts(connection: web3.Connection, search: string) { 26 | const accounts = await connection.getProgramAccounts( 27 | new web3.PublicKey(STUDENT_INTRO_PROGRAM_ID), 28 | { 29 | dataSlice: { offset: 1, length: 12 }, 30 | filters: search === '' ? [] : [ 31 | { 32 | memcmp: 33 | { 34 | offset: 5, 35 | bytes: bs58.encode(Buffer.from(search)) 36 | } 37 | } 38 | ] 39 | } 40 | ); 41 | 42 | accounts.sort((a, b) => { 43 | const lengthA = a.account.data.readUInt32LE(0) 44 | const lengthB = b.account.data.readUInt32LE(0) 45 | 46 | // borsh adds u32 int at the beginning of the string. 47 | // we must create a data slice with a 4 byte offset 48 | const dataA = a.account.data.slice(4, 4 + lengthA) 49 | const dataB = b.account.data.slice(4, 4 + lengthB) 50 | 51 | return dataA.compare(dataB) 52 | }); 53 | 54 | this.accounts = accounts.map(account => account.pubkey); 55 | }; 56 | 57 | static async fetchPage(connection: web3.Connection, page: number, perPage: number, search: string, reload: boolean = false): Promise { 58 | if (this.accounts.length === 0 || reload) { 59 | await this.prefetchAccounts(connection, search) 60 | } 61 | 62 | const paginatedPublicKeys = this.accounts.slice( 63 | (page - 1) * perPage, 64 | page * perPage, 65 | ); 66 | 67 | if (paginatedPublicKeys.length === 0) { 68 | return [] 69 | } 70 | 71 | const accounts = await connection.getMultipleAccountsInfo(paginatedPublicKeys); 72 | 73 | const studentIntros = accounts.reduce((accum: StudentIntroReference[], account) => { 74 | const studentIntro = StudentIntroReference.deserialize(account?.data); 75 | 76 | if (!studentIntro) { 77 | return accum 78 | } 79 | 80 | return [...accum, studentIntro]; 81 | }, []); 82 | 83 | return studentIntros; 84 | }; 85 | }; -------------------------------------------------------------------------------- /scripts/tokens/InitializeKeypair.ts: -------------------------------------------------------------------------------- 1 | import * as web3 from '@solana/web3.js'; 2 | 3 | export const initializeKeypair = async (connection: web3.Connection): Promise => { 4 | const signer = web3.Keypair.generate(); 5 | await airdropSolIfNeeded(signer, connection); 6 | 7 | return signer; 8 | }; 9 | 10 | const airdropSolIfNeeded = async (signer: web3.Keypair, connection: web3.Connection) => { 11 | const balance = await connection.getBalance(signer.publicKey); 12 | 13 | if (balance < web3.LAMPORTS_PER_SOL * 1) { 14 | console.log('Airdropping 1 SOL...') 15 | await connection.requestAirdrop(signer.publicKey, web3.LAMPORTS_PER_SOL * 1) 16 | }; 17 | }; 18 | -------------------------------------------------------------------------------- /styles/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | html { 6 | background-color: #161b19; 7 | } -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | module.exports = { 3 | content: [ 4 | './pages/**/*.{js,ts,jsx,tsx}', 5 | './components/**/*.{js,ts,jsx,tsx}', 6 | ], 7 | theme: { 8 | extend: { 9 | keyframes: { 10 | wiggle: { 11 | '0%, 100%': { transform: 'rotate(-3deg)' }, 12 | '50%': { transform: 'rotate(3deg)' }, 13 | } 14 | }, 15 | animation: { 16 | 'spin': 'spin 3s linear infinite', 17 | }, 18 | colors: { 19 | "helius-orange": "#e44a2a", 20 | }, 21 | }, 22 | }, 23 | plugins: [], 24 | } 25 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": true, 8 | "forceConsistentCasingInFileNames": true, 9 | "noEmit": true, 10 | "esModuleInterop": true, 11 | "module": "esnext", 12 | "moduleResolution": "node", 13 | "resolveJsonModule": true, 14 | "isolatedModules": true, 15 | "jsx": "preserve", 16 | "incremental": true 17 | }, 18 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", "scripts/ping.js", "pages/index.jsx", "pages/api/index.js", "pages/sendsol/sendsol.jsx", "pages/_app.jsx"], 19 | "exclude": ["node_modules"] 20 | } 21 | --------------------------------------------------------------------------------