├── .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 |
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 | 
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 |
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 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
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 |
24 |
25 | setDisplayRequirements(!displayRequirements)}
28 | >
29 | requirements
30 |
31 |
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 |
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 |
84 |
85 |
86 |
87 | Scroll to view dependencies
88 |
89 |
118 |
119 |
120 |
121 | setDisplayRequirements(false)}
125 | >
126 | Close
127 |
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 |
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 |
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 |
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 |
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 |
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 |
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 | setPage(page - 1)}
205 | className='bg-helius-orange rounded-lg w-24 py-1 font-semibold transition-all duration-200 hover:bg-transparent border-2 border-transparent hover:border-helius-orange'
206 | >
207 | Previous
208 |
209 | }
210 |
211 |
212 | {
213 | StudentIntroCoordinatorReference.accounts.length > page * 5 &&
214 | setPage(page + 1)}
216 | className='bg-helius-orange rounded-lg w-24 py-1 font-semibold transition-all duration-200 hover:bg-transparent border-2 border-transparent hover:border-helius-orange'
217 | disabled={studentIntros.length === 0}
218 | >
219 | Next
220 |
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 |
176 |
177 |
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 |
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 |
135 |
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 | setDisplayDetails(false)}
179 | >
180 | Close
181 |
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 |
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 |
--------------------------------------------------------------------------------