From 07d6c5ebf16a8098088e1a876c56c19131e93faa Mon Sep 17 00:00:00 2001 From: "Dev Rel @ Urani" <162904807+urani-engineering@users.noreply.github.com> Date: Thu, 28 Mar 2024 10:05:14 -0700 Subject: [PATCH] Serialize + PDA II (#11) --- README.md | 1 + chapters/04_pda.md | 4 +- chapters/06_frontend.md | 6 +- .../05_serialize_custom_data/README.md | 405 +- .../06_serialize_custom_data_II/.gitignore | 9 + .../06_serialize_custom_data_II/LICENSE | 373 + .../06_serialize_custom_data_II/README.md | 108 + .../components/AppBar.tsx | 14 + .../components/Card.tsx | 42 + .../components/Form.tsx | 107 + .../components/StudentIntroList.tsx | 54 + .../components/WalletContextProvider.tsx | 26 + .../coordinators/StudentIntroCoordinator.ts | 66 + .../models/StudentIntro.ts | 49 + .../06_serialize_custom_data_II/next-env.d.ts | 5 + .../next.config.js | 6 + .../package-lock.json | 6588 +++++++++++++++++ .../06_serialize_custom_data_II/package.json | 34 + .../pages/_app.tsx | 16 + .../pages/index.tsx | 33 + .../public/favicon.ico | Bin 0 -> 25931 bytes .../public/solanaLogo.png | Bin 0 -> 128737 bytes .../public/vercel.svg | 4 + .../styles/Home.module.css | 33 + .../styles/globals.css | 16 + .../06_serialize_custom_data_II/tsconfig.json | 20 + 26 files changed, 7657 insertions(+), 362 deletions(-) create mode 100644 demos/frontend/06_serialize_custom_data_II/.gitignore create mode 100644 demos/frontend/06_serialize_custom_data_II/LICENSE create mode 100644 demos/frontend/06_serialize_custom_data_II/README.md create mode 100644 demos/frontend/06_serialize_custom_data_II/components/AppBar.tsx create mode 100644 demos/frontend/06_serialize_custom_data_II/components/Card.tsx create mode 100644 demos/frontend/06_serialize_custom_data_II/components/Form.tsx create mode 100644 demos/frontend/06_serialize_custom_data_II/components/StudentIntroList.tsx create mode 100644 demos/frontend/06_serialize_custom_data_II/components/WalletContextProvider.tsx create mode 100644 demos/frontend/06_serialize_custom_data_II/coordinators/StudentIntroCoordinator.ts create mode 100644 demos/frontend/06_serialize_custom_data_II/models/StudentIntro.ts create mode 100644 demos/frontend/06_serialize_custom_data_II/next-env.d.ts create mode 100644 demos/frontend/06_serialize_custom_data_II/next.config.js create mode 100644 demos/frontend/06_serialize_custom_data_II/package-lock.json create mode 100644 demos/frontend/06_serialize_custom_data_II/package.json create mode 100644 demos/frontend/06_serialize_custom_data_II/pages/_app.tsx create mode 100644 demos/frontend/06_serialize_custom_data_II/pages/index.tsx create mode 100644 demos/frontend/06_serialize_custom_data_II/public/favicon.ico create mode 100644 demos/frontend/06_serialize_custom_data_II/public/solanaLogo.png create mode 100644 demos/frontend/06_serialize_custom_data_II/public/vercel.svg create mode 100644 demos/frontend/06_serialize_custom_data_II/styles/Home.module.css create mode 100644 demos/frontend/06_serialize_custom_data_II/styles/globals.css create mode 100644 demos/frontend/06_serialize_custom_data_II/tsconfig.json diff --git a/README.md b/README.md index c444b06a..32383d97 100644 --- a/README.md +++ b/README.md @@ -67,5 +67,6 @@ Compared to older platforms like Bitcoin and EVM-based protocols, Solana is: * **[Demo 3: Interacting with Wallets](demos/frontend/03_wallets_ping)** * **[Demo 4: Sending Transactions with Wallets](demos/frontend/04_wallets_tx)** * **[Demo 5: Serializing Custom Data with PDA](demos/frontend/05_serialize_custom_data)** +* **[Demo 6: Serializing Custom Data with PDA II](demos/frontend/06_serialize_custom_data_II)** diff --git a/chapters/04_pda.md b/chapters/04_pda.md index 330fe026..89b07e7d 100644 --- a/chapters/04_pda.md +++ b/chapters/04_pda.md @@ -113,7 +113,9 @@ pub struct UserStats { * Learn how PDA and CPI work on Anchor through [backend's demo 4](https://github.com/urani-labs/solana-dev-onboarding-rs/tree/main/demos/backend/04_pda_and_cpi). -* Build a frontend dApp that leverages PDA through [frontend's demo 5](https://github.com/urani-labs/solana-dev-onboarding-rs/tree/main/demos/frontend/05_serialize_custom_data). +* Build a frontend dApp that leverages PDA through [frontend's demo 5](https://github.com/urani-labs/solana-dev-onboarding-rs/tree/main/demos/frontend/05_serialize_custom_data) and +[frontend's demo 6](https://github.com/urani-labs/solana-dev-onboarding-rs/tree/main/demos/frontend/06_serialize_custom_data_II). +
diff --git a/chapters/06_frontend.md b/chapters/06_frontend.md index b92e695c..939d6bb3 100644 --- a/chapters/06_frontend.md +++ b/chapters/06_frontend.md @@ -88,7 +88,7 @@ - The library builds the array of accounts based on that information and handles the logic for including a recent blockhash. -* To facilitate this process of serialization, we can use [Binary Object Representation Serializer for Hashin (Borsh)]() and the library [@coral-xyz/borsh](https://github.com/coral-xyz). +* To facilitate this process of serialization, we can use [Binary Object Representation Serializer for Hashin (Borsh)](https://borsh.io/) and the library [@coral-xyz/borsh](https://github.com/coral-xyz). - Borsh can be used in security-critical projects as it prioritizes consistency, safety, speed; and comes with a strict specification. @@ -97,8 +97,10 @@ --- -### Demos +### Frontend demos
* [Demo 5: Serializing Custom Data with PDA](https://github.com/urani-labs/solana-dev-onboarding-rs/tree/main/demos/frontend/05_serialize_custom_data) + +* [Demo 6: Serializing Custom Data with PDA II](https://github.com/urani-labs/solana-dev-onboarding-rs/tree/main/demos/frontend/06_serialize_custom_data_II) \ No newline at end of file diff --git a/demos/frontend/05_serialize_custom_data/README.md b/demos/frontend/05_serialize_custom_data/README.md index 771cf411..c467b47b 100644 --- a/demos/frontend/05_serialize_custom_data/README.md +++ b/demos/frontend/05_serialize_custom_data/README.md @@ -424,374 +424,61 @@ export const Card: FC = (props) => {
```javascript -🛹 Demo 5: Serializing Custom Data with PDA - -tl; dr - -In this demo we build a Movie Review dApp that lets users submit a movie review and have it stored on Solana’s network. - -Setup - -Run npm install from the root of the project. -Install Phantom Wallet. - -The Buffer Layout - -The Movie Review program is expecting instruction data to contain: - -variant as an unsigned, 8-bit integer representing which instruction should be executed -title as a string representing the title of the movie that you are reviewing -rating as an unsigned, 8-bit integer representing the rating out of 5 that you are giving to the movie you are reviewing -description as a string representing the written portion of the review you are leaving for the movie -To configure a borsh layout in the Movie class, we create a borshInstructionSchema property and set it to the appropriate borsh struct containing the properties listed above. - - -import * as borsh from '@coral-xyz/borsh' - -export class Movie { - title: string; - rating: number; - description: string; - - ... - - borshInstructionSchema = borsh.struct([ - borsh.u8('variant'), - borsh.str('title'), - borsh.u8('rating'), - borsh.str('description'), - ]) -} - -The serialize() method - -We create a method that returns a Buffer with a Movie object’s properties encoded into the appropriate layout, under models/Movie.ts: - -import * as borsh from '@project-serum/borsh' - -export class Movie { - title: string; - rating: number; - description: string; - - constructor(title: string, rating: number, description: string) { - this.title = title; - this.rating = rating; - this.description = description; - } - - static mocks: Movie[] = [ - new Movie('The Shawshank Redemption', 5, `For a movie shot entirely in prison where there is no hope at all, shawshank redemption's main massage and purpose is to remind us of hope, that even in the darkest places hope exists, and only needs someone to find it. Combine this message with a brilliant screenplay, lovely characters and Martin freeman, and you get a movie that can teach you a lesson everytime you watch it. An all time Classic!!!`), - new Movie('The Godfather', 5, `One of Hollywood's greatest critical and commercial successes, The Godfather gets everything right; not only did the movie transcend expectations, it established new benchmarks for American cinema.`), - new Movie('The Godfather: Part II', 4, `The Godfather: Part II is a continuation of the saga of the late Italian-American crime boss, Francis Ford Coppola, and his son, Vito Corleone. The story follows the continuing saga of the Corleone family as they attempt to successfully start a new life for themselves after years of crime and corruption.`), - new Movie('The Dark Knight', 5, `The Dark Knight is a 2008 superhero film directed, produced, and co-written by Christopher Nolan. Batman, in his darkest hour, faces his greatest challenge yet: he must become the symbol of the opposite of the Batmanian order, the League of Shadows.`), - ] - - borshInstructionSchema = borsh.struct([ - borsh.u8('variant'), - borsh.str('title'), - borsh.u8('rating'), - borsh.str('description'), - ]) - - static borshAccountSchema = borsh.struct([ - borsh.bool('initialized'), - borsh.u8('rating'), - borsh.str('title'), - borsh.str('description'), - ]) - - serialize(): Buffer { - const buffer = Buffer.alloc(1000) - this.borshInstructionSchema.encode({ ...this, variant: 0 }, buffer) - return buffer.slice(0, this.borshInstructionSchema.getSpan(buffer)) - } - - static deserialize(buffer?: Buffer): Movie | null { - if (!buffer) { - return null - } - - try { - const { title, rating, description } = this.borshAccountSchema.decode(buffer) - return new Movie(title, rating, description) - } catch (e) { - console.log('Deserialization error:', e) - console.log(buffer) - return null - } - } -} - -Sending Transactions - -We create and send the transaction when a user submits the form, under components/Form.tsx: - -import { FC } from 'react' +import { Card } from './Card' +import { FC, useEffect, useMemo, useState } from 'react' import { Movie } from '../models/Movie' -import { useState } from 'react' -import { Box, Button, FormControl, FormLabel, Input, NumberDecrementStepper, NumberIncrementStepper, NumberInput, NumberInputField, NumberInputStepper, Textarea } from '@chakra-ui/react' import * as web3 from '@solana/web3.js' -import { useConnection, useWallet } from '@solana/wallet-adapter-react' - -const MOVIE_REVIEW_PROGRAM_ID = 'CenYq6bDRB7p73EjsPEpiYN7uveyPUTdXkDkgUduboaN' - -export const Form: FC = () => { - const [title, setTitle] = useState('') - const [rating, setRating] = useState(0) - const [description, setDescription] = useState('') - - const { connection } = useConnection(); - const { publicKey, sendTransaction } = useWallet(); - - const handleSubmit = (event: any) => { - event.preventDefault() - const movie = new Movie(title, rating, description) - handleTransactionSubmit(movie) - } - - const handleTransactionSubmit = async (movie: Movie) => { - if (!publicKey) { - alert('Please connect your wallet!') - return - } - - const buffer = movie.serialize() - const transaction = new web3.Transaction() - - const [pda] = await web3.PublicKey.findProgramAddress( - [publicKey.toBuffer(), Buffer.from(movie.title)],// new TextEncoder().encode(movie.title)], - new web3.PublicKey(MOVIE_REVIEW_PROGRAM_ID) - ) - - const instruction = new web3.TransactionInstruction({ - keys: [ - { - pubkey: publicKey, - isSigner: true, - isWritable: false, - }, - { - pubkey: pda, - isSigner: false, - isWritable: true - }, - { - pubkey: web3.SystemProgram.programId, - isSigner: false, - isWritable: false - } - ], - data: buffer, - programId: new web3.PublicKey(MOVIE_REVIEW_PROGRAM_ID) - }) - - transaction.add(instruction) - - try { - let txid = await sendTransaction(transaction, connection) - alert(`Transaction submitted: https://explorer.solana.com/tx/${txid}?cluster=devnet`) - console.log(`Transaction submitted: https://explorer.solana.com/tx/${txid}?cluster=devnet`) - } catch (e) { - console.log(JSON.stringify(e)) - alert(JSON.stringify(e)) - } - } - +import { MovieCoordinator } from '../coordinators/MovieCoordinator' +import { Button, Center, HStack, Input, Spacer } from '@chakra-ui/react' + +export const MovieList: FC = () => { + const connection = new web3.Connection(web3.clusterApiUrl('devnet')) + const [movies, setMovies] = useState([]) + const [page, setPage] = useState(1) + const [search, setSearch] = useState('') + + useEffect(() => { + MovieCoordinator.fetchPage( + connection, + page, + 5, + search, + search !== '' + ).then(setMovies) + }, [page, search]) + return ( - -
- - - Movie Title - - setTitle(event.currentTarget.value)} - /> - - - - Add your review - -