part 2
우리의 고양이 노드와 소통할 수 있는 커스텀 리액트 컴포넌트 만들기
Substrate Front-end Template 설치하기
git clone https://github.com/substrate-developer-hub/substrate-front-end-template.git
cd substrate-front-end-template
yarn install
폴더구조
substrate-front-end-template
|
+-- public
| |
| +-- assets <-- Kitty avatar PNG files
|
+-- src <-- our React components
| |
| +-- tests
| |
| +-- config <-- where to specify our custom types
| |
| +-- substrate-lib <-- lib to give access to PolkadotJS API
| | |
| | +-- components <-- contains TxButton, used throughout our application
| |
| ...
...
다른 터미널을 열어서 Part1에서 만든 노드 시작하기
node-kitties
from its directory.cd kitties/
./target/release/node-kitties --dev --tmp
프론트엔드 디렉토리로 돌아와서, 시작하기
yarn start
무엇을 만들까?
Substrate Front-end template은 폴카닷JS API와 RPC엔드포인트를 사용해 Substrate노드와 소통
그래서 스토리지 아이템도 읽고 dispatchable 함수를 호출해 extrinsic을 만들 수도 있음
** 폴카닷 JS API 기초 공부하기
Basics and Metadata
RPC queries
Storage methods
Extrinsics methods
The Devhub substrate-frontend-template
Kitties.js만들기
가장 top-level에 있는 컴포넌트로, Apps.js에 의해 렌더링될 것
src폴더에 Kitties.js파일 만들고 임포트
import React, { useEffect, useState } from 'react'
import { Form, Grid } from 'semantic-ui-react'
import { useSubstrateState } from './substrate-lib'
import { TxButton } from './substrate-lib/components'
import KittyCards from './KittyCards'
폴카닷 JS API 인스턴스의 래퍼인 substrate-lib를 통해서 폴카닷 JS API를 사용할 것
계정 키를 가져올 수도 있음
const parseKitty = ({ dna, price, gender, owner }) => ({
dna,
price: price.toJSON(),
gender: gender.toJSON(),
owner: owner.toJSON(),
})
export default function Kitties(props) {
const { api, keyring } = useSubstrateState()
const [kittyIds, setKittyIds] = useState([])
const [kitties, setKitties] = useState([])
const [status, setStatus] = useState('')
parseKittiy() : 고양이 데이터를 가지고 있고 오브젝트를 리턴하는 함수
Kitties : 체인의 스토리지 아이템이 실시간으로 변하는 걸 감지하고 useEffect를 써서
우리의 다른 컴포넌트의 상태를 업데이트시켜줌
실시간으로 변화를 감지해야 하는 건
각각 subscription 함수를 만들 것임
// Subscription function for kitty count
const subscribeCount = () => {
let unsub = null
const asyncFetch = async () => {
unsub = await api.query.substrateKitties.countForKitties(
async count => {
// Fetch all kitty keys
const entries = await api.query.substrateKitties.kitties.entries()
const ids = entries.map(entry => entry[1].unwrap().dna)
setKittyIds(ids)
}
)
}
asyncFetch()
return () => {
unsub && unsub()
}
}
// Subscription function to construct all kitty objects
const subscribeKitties = () => {
let unsub = null
const asyncFetch = async () => {
unsub = await api.query.substrateKitties.kitties.multi(
kittyIds,
kitties => {
const kittiesMap = kitties.map(kitty => parseKitty(kitty.unwrap()))
setKitties(kittiesMap)
}
)
}
asyncFetch()
return () => {
// return the unsubscription cleanup function
unsub && unsub()
}
}
asyncFetch에서 고양이 스토리지를 구독했다.
이 컴포넌트가 끝날 때, 구독 취소를 하고 싶다.
그래서 리턴함수로 정리해줌
리액트의 useEffect()로 subscribeCount와 subscribeKitties를 넘겨주면 됨
useEffect(subscribeCount, [api, keyring])
useEffect(subscribeKitties, [api, keyring, kittyIds])
import React from 'react'
// Generate an array [start, start + 1, ..., end] inclusively
const genArray = (start, end) =>
Array.from(Array(end - start + 1).keys()).map(v => v + start)
const IMAGES = {
accessory: genArray(1, 20).map(
n => ${process.env.PUBLIC_URL}/assets/KittyAvatar/accessorie_${n}.png
),
body: genArray(1, 15).map(
n => ${process.env.PUBLIC_URL}/assets/KittyAvatar/body_${n}.png
),
eyes: genArray(1, 15).map(
n => ${process.env.PUBLIC_URL}/assets/KittyAvatar/eyes_${n}.png
),
mouth: genArray(1, 10).map(
n => ${process.env.PUBLIC_URL}/assets/KittyAvatar/mouth_${n}.png
),
fur: genArray(1, 10).map(
n => ${process.env.PUBLIC_URL}/assets/KittyAvatar/fur_${n}.png
),
}
const dnaToAttributes = dna => {
const attribute = (index, type) =>
IMAGES[type]dna[index] % IMAGES[type].length]
return {
body: attribute(0, 'body'),
eyes: attribute(1, 'eyes'),
accessory: attribute(2, 'accessory'),
fur: attribute(3, 'fur'),
mouth: attribute(4, 'mouth'),
}
}
const KittyAvatar = props => {
const outerStyle = { height: '160px', position: 'relative', width: '50%' }
const innerStyle = {
height: '150px',
position: 'absolute',
top: '3%',
left: '50%',
}
const { dna } = props
if (!dna) return null
const cat = dnaToAttributes(dna)
return (
<div style={outerStyle}>
<img alt="body" src={cat.body} style={innerStyle} />
<img alt="fur" src={cat.fur} style={innerStyle} />
<img alt="mouth" src={cat.mouth} style={innerStyle} />
<img alt="eyes" src={cat.eyes} style={innerStyle} />
<img alt="accessory" src={cat.accessory} style={innerStyle} />
</div>
)
}
export default KittyAvatar
KittyCards.js로부터 dna 하나만 전달받음
링크에 가서 PNG다운받고 public/assets/KittyAvatar라는 새 폴더를 만들어서 PNG들 붙여넣기
먼저 src/KittyCards.js를 만들고 임포트
import React from 'react'
import {
Button,
Card,
Grid,
Message,
Modal,
Form,
Label,
} from 'semantic-ui-react'
import KittyAvatar from './KittyAvatar'
import { useSubstrateState } from './substrate-lib'
import { TxButton } from './substrate-lib/components'
먼저 TransferModal을 보자
Substrate Front-end Template이 TxButton이라는 컴포넌트를 갖고 있는데
노드와 상호작용하는 전송 버튼을 포함하는 유용한 방법이다
노드에 트랜잭션을 보내고 고양이 팔레트에 서명된 extrinsic을 유발하도록 해줌
transfer button
modal(kitty id, receiver address)
transfer, cancel button
세 파트로 구성됨
나머지도 비슷한 구성임
리액트 훅으로 props 추출하기
const TransferModal = props => {
const { kitty, accountPair, setStatus } = props;
const [open, setOpen] = React.useState(false);
const [formValue, setFormValue] = React.useState({});
const formChange = key => (ev, el) => {
setFormValue({ ...formValue, [key]: el.value });
};
const confirmAndClose = unsub => {
setOpen(false)
if (unsub && typeof unsub === 'function') unsub()
}
return (
<Modal
onClose={() => setOpen(false)}
onOpen={() => setOpen(true)}
open={open}
trigger={
Transfer
}
)<Modal.Header>Kitty Transfer</Modal.Header> <Modal.Content> <Form> <Form.Input fluid label="Kitty ID" readOnly value={kitty.dna} /> <Form.Input fluid label="Receiver" placeholder="Receiver Address" onChange={formChange('target')} /> </Form> </Modal.Content> <Modal.Actions> <Button basic color="grey" onClick={() => setOpen(false)}> Cancel </Button> <TxButton label="Transfer" type="SIGNED-TX" setStatus={setStatus} onClick={confirmAndClose} attrs={{ palletRpc: 'substrateKitties', callable: 'transfer', inputParams: [formValue.target, kitty.dna], paramFields: [true, true], }} /> </Modal.Actions>
Semantic UI Card컴포넌트 사용할 것임
// Use props
const KittyCard = props => {
const { kitty, setStatus } = props
const { dna = null, owner = null, gender = null, price = null } = kitty
const displayDna = dna && dna.toJSON()
const { currentAccount } = useSubstrateState()
const isSelf = currentAccount.address === kitty.owner
return (
{isSelf && (
Mine
)}
{/ Render the Kitty Avatar /}
<Card.Content>
{/ Display the Kitty DNA /}
<Card.Meta style={{ fontSize: '.9em', overflowWrap: 'break-word' }}>
DNA: {displayDna}
</Card.Meta>
{/ Display the Kitty Gender, Owner, and Price /}
<Card.Description>
<p style={{ overflowWrap: 'break-word' }}>Gender: {gender}</p>
<p style={{ overflowWrap: 'break-word' }}>Owner: {owner}</p>
<p style={{ overflowWrap: 'break-word' }}>
Price: {price || 'Not For Sale'}
</p>
</Card.Description>
</Card.Content>
<Card.Content extra style={{ textAlign: 'center' }}>
{owner === currentAccount.address ? (
<>
</>
) : (
<>
</>
)}
</Card.Content>
}
주인이 옮기는 경우에만 Transfer Modal과 SetPRice 컴포넌트가 뜨도록 해줌
const KittyCards = props => {
const { kitties, setStatus } = props
if (kitties.length === 0) {
return (
<Message.Header>
No Kitty found here... Create one now!
👇
</Message.Header>
)
}
return (
{kitties.map((kitty, i) => (
<Grid.Column key={kitty-${i}
}>
</Grid.Column>
))}
)
}
export default KittyCards
SetPrice와 BuyKitty는 안 다룰것임 솔루션 봐라
return <Grid.Column width={16}>
이거 복붙하고 : Grid안에 KittyCard 컴포넌트 넣음
이거도 복붇하고 : TxButton 컴포넌트 렌더링하기 위해 form 사용
임포트문 추가
import Kitties from './Kitties'
Container에 다음 줄 추가
<Grid.Row>
</Grid.Row>