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

Commit

Permalink
Add deserialization (#12)
Browse files Browse the repository at this point in the history
  • Loading branch information
urani-engineering-helper authored Mar 28, 2024
1 parent 07d6c5e commit b0d49b7
Show file tree
Hide file tree
Showing 11 changed files with 149 additions and 638 deletions.
42 changes: 42 additions & 0 deletions chapters/04_pda.md
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,48 @@ pub struct UserStats {

* This means that programs can be given control over assets, which they then manage according to the rules defined in the code.

<br>

----

### Examples of PDAs

<br>

* A program with global state:

```javascript
const [pda, bump] = await findProgramAddress(Buffer.from("GLOBAL_STATE"), programId)
```

<br>

* A program with user-specific data:

```javascript
const [pda, bump] = await web3.PublicKey.findProgramAddress(
[
publicKey.toBuffer()
],
programId
)
```

<br>

* A program with multiple data items per user:

```javascript
const [pda, bump] = await web3.PublicKey.findProgramAddress(
[
publicKey.toBuffer(),
Buffer.from("Shopping list")
],
programId,
);
```


<br>

---
Expand Down
33 changes: 31 additions & 2 deletions chapters/06_frontend.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,11 @@

---

## 🛹 Serializing Data
## 🛹 Serializing Data and PDA

<br>

### Instruction Serialization

<br>

Expand All @@ -84,20 +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.

<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
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 b0d49b7

Please sign in to comment.