Skip to content
This repository has been archived by the owner on Nov 26, 2024. It is now read-only.

Commit

Permalink
add deserialization
Browse files Browse the repository at this point in the history
  • Loading branch information
URANI committed Mar 28, 2024
1 parent 9b73843 commit fb4c79a
Show file tree
Hide file tree
Showing 11 changed files with 104 additions and 644 deletions.
34 changes: 27 additions & 7 deletions chapters/06_frontend.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,10 @@

<br>

### Instruction Serialization

<br>

* Instruction data must be serialized into a byte buffer to send to clients.

* Every transaction contains:
Expand All @@ -84,29 +88,45 @@
- an array listing every account that will be read from or written to during execution
- a byte buffer of instruction data

<br>

---

### Libraries

* [@solana/web3.js](https://solana-labs.github.io/solana-web3.js/) simplifies this process, so developers can focus on adding instructions and signatures.
- 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)](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.

* Finally, programs store data in PDAs (Program Derived Address):
- PDAs can be thought as a key value store, where the address is the key, and the data inside the account is the value (like records in a database, with the address being the primary key used to look up the values inside).
- PDAs do not have a corresponding secret key
- To store and locate data, we derive a PDA using the `findProgramAddress(seeds, programid)` method
- The accounts belonging to a program can be retrieved with `getProgramAccounts(programId)`
<br>

---

### PDA

<br>

* Programs store data in PDAs (Program Derived Address), which can be thought as a key value store, where the address is the key, and the data inside the account is the value.
- Like records in a database, with the address being the primary key used to look up the values inside.

* PDAs do not have a corresponding secret key.
- To store and locate data, we derive a PDA using the `findProgramAddress(seeds, programid)` method.

* The accounts belonging to a program can be retrieved with `getProgramAccounts(programId)`.
- Account data needs to be deserialized using the same layout used to store it in the first place.
- Accounts created by a program can be fetched with `connection.getProgramAccounts(programId)`.

*


<br>


---

### Frontend demos
### Demos

<br>

Expand Down
82 changes: 29 additions & 53 deletions demos/frontend/05_serialize_custom_data/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -227,30 +227,30 @@ export const Form: FC = () => {
<FormControl isRequired>
<FormLabel color='gray.200'>
Movie Title
</FormLabel>
<Input
id='title'
color='gray.400'
onChange={event => setTitle(event.currentTarget.value)}
/>
</FormLabel>
<Input
id='title'
color='gray.400'
onChange={event => setTitle(event.currentTarget.value)}
/>
</FormControl>
<FormControl isRequired>
<FormLabel color='gray.200'>
Add your review
</FormLabel>
<Textarea
id='review'
</FormLabel>
<Textarea
id='review'
color='gray.400'
onChange={event => setDescription(event.currentTarget.value)}
/>
</FormControl>
<FormControl isRequired>
<FormLabel color='gray.200'>
Rating
</FormLabel>
<NumberInput
max={5}
min={1}
</FormLabel>
<NumberInput
max={5}
min={1}
onChange={(valueString) => setRating(parseInt(valueString))}
>
<NumberInputField id='amount' color='gray.400' />
Expand Down Expand Up @@ -359,7 +359,7 @@ export class MovieCoordinator {
<br>
* Finally, we add the component to create the movie cards:
* Finally, we add the component to create the movie cards, under `components/Cards.tsx`:
<br>
Expand Down Expand Up @@ -413,8 +413,6 @@ export const Card: FC<CardProps> = (props) => {
</Box>
)
}


```
<br>
Expand All @@ -425,60 +423,38 @@ export const Card: FC<CardProps> = (props) => {
```javascript
import { Card } from './Card'
import { FC, useEffect, useMemo, useState } from 'react'
import { FC, useEffect, useState } from 'react'
import { Movie } from '../models/Movie'
import * as web3 from '@solana/web3.js'
import { MovieCoordinator } from '../coordinators/MovieCoordinator'
import { Button, Center, HStack, Input, Spacer } from '@chakra-ui/react'

const MOVIE_REVIEW_PROGRAM_ID = 'CenYq6bDRB7p73EjsPEpiYN7uveyPUTdXkDkgUduboaN'

export const MovieList: FC = () => {
const connection = new web3.Connection(web3.clusterApiUrl('devnet'))
const [movies, setMovies] = useState<Movie[]>([])
const [page, setPage] = useState(1)
const [search, setSearch] = useState('')

useEffect(() => {
MovieCoordinator.fetchPage(
connection,
page,
5,
search,
search !== ''
).then(setMovies)
}, [page, search])
connection.getProgramAccounts(new web3.PublicKey(MOVIE_REVIEW_PROGRAM_ID)).then(async (accounts) => {
const movies: Movie[] = accounts.reduce((accum: Movie[], { pubkey, account }) => {
const movie = Movie.deserialize(account.data)
if (!movie) {
return accum
}

return [...accum, movie]
}, [])
setMovies(movies)
})
}, [])

return (
<div>
<Center>
<Input
id='search'
color='gray.400'
onChange={event => setSearch(event.currentTarget.value)}
placeholder='Search'
w='97%'
mt={2}
mb={2}
/>
</Center>
{
movies.map((movie, i) => <Card key={i} movie={movie} /> )
}
<Center>
<HStack w='full' mt={2} mb={8} ml={4} mr={4}>
{
page > 1 && <Button onClick={() => setPage(page - 1)}>Previous</Button>
}
<Spacer />
{
MovieCoordinator.accounts.length > page * 5 &&
<Button onClick={() => setPage(page + 1)}>Next</Button>
}
</HStack>
</Center>
</div>
)
}

```
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ export const AppBar: FC = () => {
return (
<div className={styles.AppHeader}>
<Image src="/solanaLogo.png" height={30} width={200} />
<span>Demo 5: Serialization + PDA</span>
<span>Movie Reviews</span>
<WalletMultiButton />
</div>
)
Expand Down
26 changes: 13 additions & 13 deletions demos/frontend/05_serialize_custom_data/components/Form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -82,30 +82,30 @@ export const Form: FC = () => {
<FormControl isRequired>
<FormLabel color='gray.200'>
Movie Title
</FormLabel>
<Input
id='title'
color='gray.400'
onChange={event => setTitle(event.currentTarget.value)}
/>
</FormLabel>
<Input
id='title'
color='gray.400'
onChange={event => setTitle(event.currentTarget.value)}
/>
</FormControl>
<FormControl isRequired>
<FormLabel color='gray.200'>
Add your review
</FormLabel>
<Textarea
id='review'
</FormLabel>
<Textarea
id='review'
color='gray.400'
onChange={event => setDescription(event.currentTarget.value)}
/>
</FormControl>
<FormControl isRequired>
<FormLabel color='gray.200'>
Rating
</FormLabel>
<NumberInput
max={5}
min={1}
</FormLabel>
<NumberInput
max={5}
min={1}
onChange={(valueString) => setRating(parseInt(valueString))}
>
<NumberInputField id='amount' color='gray.400' />
Expand Down
51 changes: 15 additions & 36 deletions demos/frontend/05_serialize_custom_data/components/MovieList.tsx
Original file line number Diff line number Diff line change
@@ -1,54 +1,33 @@
import { Card } from './Card'
import { FC, useEffect, useMemo, useState } from 'react'
import { FC, useEffect, useState } from 'react'
import { Movie } from '../models/Movie'
import * as web3 from '@solana/web3.js'
import { MovieCoordinator } from '../coordinators/MovieCoordinator'
import { Button, Center, HStack, Input, Spacer } from '@chakra-ui/react'

const MOVIE_REVIEW_PROGRAM_ID = 'CenYq6bDRB7p73EjsPEpiYN7uveyPUTdXkDkgUduboaN'

export const MovieList: FC = () => {
const connection = new web3.Connection(web3.clusterApiUrl('devnet'))
const [movies, setMovies] = useState<Movie[]>([])
const [page, setPage] = useState(1)
const [search, setSearch] = useState('')

useEffect(() => {
MovieCoordinator.fetchPage(
connection,
page,
5,
search,
search !== ''
).then(setMovies)
}, [page, search])
connection.getProgramAccounts(new web3.PublicKey(MOVIE_REVIEW_PROGRAM_ID)).then(async (accounts) => {
const movies: Movie[] = accounts.reduce((accum: Movie[], { pubkey, account }) => {
const movie = Movie.deserialize(account.data)
if (!movie) {
return accum
}

return [...accum, movie]
}, [])
setMovies(movies)
})
}, [])

return (
<div>
<Center>
<Input
id='search'
color='gray.400'
onChange={event => setSearch(event.currentTarget.value)}
placeholder='Search'
w='97%'
mt={2}
mb={2}
/>
</Center>
{
movies.map((movie, i) => <Card key={i} movie={movie} /> )
}
<Center>
<HStack w='full' mt={2} mb={8} ml={4} mr={4}>
{
page > 1 && <Button onClick={() => setPage(page - 1)}>Previous</Button>
}
<Spacer />
{
MovieCoordinator.accounts.length > page * 5 &&
<Button onClick={() => setPage(page + 1)}>Next</Button>
}
</HStack>
</Center>
</div>
)
}
5 changes: 2 additions & 3 deletions demos/frontend/05_serialize_custom_data/models/Movie.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,17 +38,16 @@ export class Movie {
return buffer.slice(0, this.borshInstructionSchema.getSpan(buffer))
}

static deserialize(buffer?: Buffer): Movie | null {
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) {
} catch(e) {
console.log('Deserialization error:', e)
console.log(buffer)
return null
}
}
Expand Down
Loading

0 comments on commit fb4c79a

Please sign in to comment.