はじめに
GW明けましたね!5月病になっちゃダメですよ!もう一歩踏み出してつよつよ(?)になっちゃいましょう!
GW明けのコンテンツのお時間です。
どうも、いなたつです。SSS Extension作ってる人です。Symbol Draw作ってる人です。
簡単にdAppsが作れると巷で噂のSymbolを使って前回の記事よりもうすこしナウい感じにdApps開発していきましょうか。
普段から僕がdAppsを開発する際の形式に沿ってやっている(少し簡易化はしています)ので、覗き見していってください。
つかうもの
- React
- TypeScript
- Symbol
- SSS Extension
前提知識
- 前回記事の内容 https://qiita.com/inatatsu_csg/items/191edf3bcd1f0acf15c1
- NodeJS / npm のインストール (「npm インストール方法」 とかで検索 macかwindowsかも入れると探しやすい)
推奨知識
以下の知識を持っていると読みやすいです。無くても読めるとは思います。
- Reactの基礎 (書いてるプログラムの解説はします)
- TypeScript (JavaScriptになんか型が表示されてらぁくらいに思っていただければ)
本記事の目標
- ReactでSymbol dAppsを作ってみる
- Symbolを用いた投稿アプリの開発
作成するものの概要
オーナーとゲストがいます。あなたがオーナーでそれ以外の人(アカウント)はゲストですね。
オーナーは投稿を行い、投稿の一覧と応援メッセージの一覧を確認することができます。そして、その投稿に対してゲストは応援メッセージを送ることができます。
完成物 : https://inatatsu-tatsuhiro.github.io/SSS-dApps-React/
リポジトリ : https://github.com/inatatsu-tatsuhiro/SSS-dApps-React
完成図
前準備
まずはプロジェクトのセットアップをしていきましょう。
今回は作業ディレクトリは SSS-React
とします。
SSS-React
内でcreate-react-app (以下CRA)を使ってTypeScriptの雛形を作りましょう。これは結構時間がかかります。
nodeとnpmが入っていない or 古い と実行できないです。
$ npx create-react-app . --template typescript
最近のCRA(v5)はwebpack5とかいうやつをつかってるらしいです。ちょっとこいつがSymbol SDK使う際にめんどくさいのでバージョンを落とします
package.jsonを開いてください。そこにreact-scripts
というのがあると思います。このバージョンを変更します。
赤くハイライトされた行を緑にハイライトされた行のように5.0.1
から4.0.3
に変更してください
{
"name": "app",
"version": "0.1.0",
"private": true,
"dependencies": {
"@testing-library/jest-dom": "^5.16.4",
"@testing-library/react": "^13.1.1",
"@testing-library/user-event": "^13.5.0",
"@types/jest": "^27.4.1",
"@types/node": "^16.11.33",
"@types/react": "^18.0.8",
"@types/react-dom": "^18.0.3",
"react": "^18.1.0",
"react-dom": "^18.1.0",
- "react-scripts": "5.0.1",
+ "react-scripts": "4.0.3",
"typescript": "^4.6.4",
"web-vitals": "^2.1.4"
},
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test",
"eject": "react-scripts eject"
},
"eslintConfig": {
"extends": [
"react-app",
"react-app/jest"
]
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
}
}
これを変更したら一旦パッケージを読み込みましょう
$ npm install
そしたらReactを起動します。
$ npm start
localhost:3000
とブラウザのアドレス欄に入力してみてください。下図のような画面が表示されればReactさんにこんにちはです。
いろいろ使うパッケージをインストールしていきます
まずはSymbol関係
$ npm i symbol-sdk rxjs
UIコンポーネント
$ npm i @mui/material @emotion/react @emotion/styled
Hello Symbol
今日もSymbolさんにごあいさつしましょう。localhost:3000
を開いた際に表示されている内容はApp.tsに記述されているので、ここを変更します。
import React from 'react'
import { Address } from 'symbol-sdk'
import './App.css'
function App() {
const addr = Address.createFromRawAddress(
'TAD7Q3FEN5CZRZFE3WX6TWEESATVTMJDS2ETVTY'
)
return (
<div>
<h1>Hello Symbol 「{addr.pretty()}」</h1>
</div>
)
}
export default App
今回も画像の範囲がわかりやすいように背景色つけたものにしときます。もともとのcssはいらないので消しちゃってOK
body {
background: rgb(242, 242, 242);
}
プログラム解説
前回の記事とほとんど同じですが、解説しときます。
変数addrにSymbolSDKを用いてアドレスの文字列を読み込んでいます。
const addr = Address.createFromRawAddress(
'TAD7Q3FEN5CZRZFE3WX6TWEESATVTMJDS2ETVTY'
)
Reactのレンダー部分です。一つのHTML要素を返すことができます。
「{」と「}」で囲まれている部分にはJavaScript(TypeScript)のプログラムが入ります。
今回はaddr.pretty()
が入っているので先程変数に入れたアドレスをハイフンで区切った物が表示されます。
return (
<div>
<h1>Hello Symbol 「{addr.pretty()}」</h1>
</div>
)
SSS Extension と連携する
今回はさっさとSSSと連携しちゃいます。
SSSと連携するためにReactの機能useStateとuseEffectを使用するのでその説明を簡単に
useState
React上で状態を管理するために使用する機能で、変数とそれを設定・変更する関数で構成されるReactフックと呼ばれるものの一つです。
以下のような形で使用されます。
const [addr, setAddr] = useState('')
参考 https://qiita.com/seira/items/f063e262b1d57d7e78b4
useEffect
副作用を扱う際に~~~みたいな難しい説明は置いといて、とりあえず画面描画後に値を変更したり・外部からデータを取得するときに使うやつです
以下のような形で使用されます。
useEffect(() => {
// プログラム
}, [])
[]の中には値が入る場合と入らない場合があります。値が入らない場合、初回の画面描画時のみこの中のプログラムが実行されます。
値が入っている場合は、指定した値が変更された時に中のプログラムが実行されます。
参考 https://qiita.com/seira/items/e62890f11e91f6b9653f
SSS Extensionからデータ取得
まずSSS Extensionと連携しましょう。やり方を忘れた方は前回の記事を見てきてください。
ではSSS Extensionからデータを取得していきます。
import React, { useEffect, useState } from 'react'
import { Address } from 'symbol-sdk'
import './App.css'
interface SSSWindow extends Window {
SSS: any
}
declare const window: SSSWindow
function App() {
const [addr, setAddr] = useState<Address | null>(null)
useEffect(() => {
setTimeout(() => {
const activeAddress = window.SSS.activeAddress
setAddr(Address.createFromRawAddress(activeAddress))
}, 500)
}, [])
if (addr === null) {
return (
<div>
<h1>Hello Symbol not set address</h1>
</div>
)
}
return (
<div>
<h1>Hello Symbol 「{addr.pretty()}」</h1>
</div>
)
}
export default App
プログラム解説
TypeScriptの場合はこれを書かないとエラーが出ちゃいます。本来windowには無いSSSというエリアにアクセスするため、こんなものがあるよ~って定義してあげます。SSSが提供するisAllowedSSS()
等を使用したい場合はSSSと併記してあげてください
参考 https://inatatsu-tatsuhiro.github.io/SSS-Demo/demo0
interface SSSWindow extends Window {
SSS: any
}
declare const window: SSSWindow
先程登場したuseStateです、なんかAddress | null
が括弧に囲われていますね、TypeScriptの場合ここにはAddressが入るか何も入らないか(null)のどっちかですよ~って教えてあげる必要があります。
const [addr, setAddr] = useState<Address | null>(null)
続いてuseEffectです。前回記事で解説したようにsetTimeoutでSSSが読み込まれるのを待ってからSSSからデータを取得します。
取得したactiveAddress
をsetAddr
します。こうすることで、SSSから取得したアドレスをaddr変数に入れることができました。
useEffect(() => {
setTimeout(() => {
const activeAddress = window.SSS.activeAddress
setAddr(Address.createFromRawAddress(activeAddress))
}, 500)
}, [])
では表示です。
なんかふえました。
もし、アドレスがなんにも入ってない(null)場合はアドレスがないよ~って表示してください。ってはじめに書いてます。
アドレスを読み込めた場合はaddr === null
がfalse
なのでこれは実行されずに下に進みます。
ここは先程と同じでアドレスをハイフン区切りで表示ですね。
if (addr === null) {
return (
<div>
<h1>Hello Symbol not set address</h1>
</div>
)
}
return (
<div>
<h1>Hello Symbol 「{addr.pretty()}」</h1>
</div>
)
アドレスを取得する部分を消して画面を開くとaddrがnullなので下図のような画面になります
とりあえずこれで、SSS Extensionからデータ取得ができましたね!
SSS Extensionで管理者ログイン
SSS ExtensionのactiveAddressで得られるアドレスは開いているユーザーが誰であるかを示します。
つまり、サイトの所有者であるか、訪問者であるかを判別できるということです。
OWNER_ADDR
を定義します。このアドレスでログインしている場合、サイトオーナーということを判別できます。
const OWNER_ADDR = Address.createFromRawAddress(
'TAD7Q3FEN5CZRZFE3WX6TWEESATVTMJDS2ETVTY'
)
そして、オーナーアドレスでログインした場合の描画を定義しましょう
if (OWNER_ADDR.plain() === addr.plain()) {
return (
<div>
<h1>Hello Symbol 管理者ログイン</h1>
</div>
)
}
オーナーアドレスでログインするとこんな感じですね
SSSのアクティブアカウントを変更するとさっきと同じようにアドレスが表示されます
コンポーネントを分割する
全部1ファイルに書くととても長くなるのでコンポーネント(部品)に分けます。
srcディレクトリ以下にOwnerPage.tsx
とGuestPage.tsx
を作成してください。
import React, { useEffect, useState } from 'react'
import { Address } from 'symbol-sdk'
import './App.css'
import GuestPage from './GuestPage'
import OwnerPage from './OwnerPage'
interface SSSWindow extends Window {
SSS: any
}
declare const window: SSSWindow
const OWNER_ADDR = Address.createFromRawAddress(
'TAD7Q3FEN5CZRZFE3WX6TWEESATVTMJDS2ETVTY'
)
function App() {
const [addr, setAddr] = useState<Address | null>(null)
useEffect(() => {
setTimeout(() => {
const activeAddress = window.SSS.activeAddress
setAddr(Address.createFromRawAddress(activeAddress))
}, 500)
}, [])
if (addr === null) {
return (
<div>
<h1>Hello Symbol not set address</h1>
</div>
)
}
if (OWNER_ADDR.plain() === addr.plain()) {
return <OwnerPage />
}
return <GuestPage address={addr.plain()} />
}
export default App
function OwnerPage() {
return (
<div>
<h1>Hello Symbol 管理者ログイン</h1>
</div>
)
}
export default OwnerPage
type Props = {
address: string
}
function GuestPage(props: Props) {
return (
<div>
<h1>Hello Symbol 「{props.address}」</h1>
</div>
)
}
export default GuestPage
propsくんの登場です。
propsくんは、作ったコンポーネントに値を渡す架け橋になってくれる存在です
<GuestPage address={addr.plain()} />
こんな感じで作成したコンポーネントに値を渡します。
すると、コンポーネント側で値を扱うことができるようになります。
管理者ページを作る
管理者ページではメッセージを投稿することができます。そして、他のユーザーからの応援メッセージを見ることができます。
本ページは大きく3つの要素(コンポーネント)で構成されています。
- Create Component
- PostList Component
- CheerList Component
Create Component
本コンポーネントは投稿を作成するための要素になります。
まず、Create.tsxを作成してください。
プログラムは以下の様になります。
import { Button, TextField } from '@mui/material'
import styled from '@emotion/styled'
import { useEffect, useState } from 'react'
import {
Address,
Deadline,
NetworkType,
PlainMessage,
SignedTransaction,
TransactionHttp,
TransferTransaction,
UInt64,
} from 'symbol-sdk'
const EPOCH = 1637848847
const NODE_URL = 'https://sym-test.opening-line.jp:3001'
const NET_TYPE = NetworkType.TEST_NET
type Props = {
address: string
}
interface SSSWindow extends Window {
SSS: any
}
declare const window: SSSWindow
function Create(props: Props) {
const [contents, setContents] = useState('')
const [isRequest, setIsRequest] = useState<boolean>(false)
const address = Address.createFromRawAddress(props.address)
useEffect(() => {
if (isRequest) {
window.SSS.requestSign().then((signedTx: SignedTransaction) => {
new TransactionHttp(NODE_URL).announce(signedTx)
})
}
}, [isRequest])
const submit = () => {
const message = `::CREATE::${contents}`
const tx = TransferTransaction.create(
Deadline.create(EPOCH),
address,
[],
PlainMessage.create(message),
NET_TYPE,
UInt64.fromUint(2000000)
)
window.SSS.setTransaction(tx)
setIsRequest(true)
}
return (
<Wrapper>
<TextField
label="contents"
value={contents}
fullWidth
onChange={(e) => setContents(e.target.value)}
/>
<Button onClick={submit}>ボタン</Button>
</Wrapper>
)
}
export default Create
const Wrapper = styled('div')({
display: 'flex',
justifyContent: 'center',
alignItems: 'flex-end',
flexDirection: 'column',
})
本コンポーネントの引数の型です。
アドレスを受け取ります。
type Props = {
address: string
}
コンポーネントの状態を示します。投稿する内容であるcontents
と、署名要求を行うisRequest
があります。
const [contents, setContents] = useState('')
const [isRequest, setIsRequest] = useState(false)
引数(props)のaddressは文字列(string型)なのでSymbol SDKで扱うためにAddress.createFromAddress
でAddressにします。
const address = Address.createFromRawAddress(props.address)
isRequestが変更され、trueになった場合、SSSのrequestSignを実行します。
useEffect(() => {
if (isRequest) {
window.SSS.requestSign().then((signedTx: SignedTransaction) => {
new TransactionHttp(NODE_URL).announce(signedTx)
})
}
}, [isRequest])
トランザクションの作成になります。
送信先のアドレスは引数で与えたアドレス(自分のアドレス)になります。自分から自分へのトランザクションを発生させています。
また、メッセージは::CREATE::
を頭につけることで、本サービスで作成したトランザクションであるかを判別しやすくしています。
ここで発生させたトランザクションは送信先、送信元がオーナーであり、:::CREATE:::
で始まるトランザクションを探せばいいということになります。
const submit = () => {
const message = `::CREATE::${contents}`
const tx = TransferTransaction.create(
Deadline.create(EPOCH),
address,
[],
PlainMessage.create(message),
NET_TYPE,
UInt64.fromUint(2000000)
)
window.SSS.setTransaction(tx)
setIsRequest(true)
}
muiのテキストフィールドとボタンを配置しています。
テキストフィールドの入力値がコンポーネントの状態contents
とリンクしています。
ボタンを押すとsubmit関数が実行されます。
return (
<Wrapper>
<TextField
label="contents"
value={contents}
fullWidth
onChange={(e) => setContents(e.target.value)}
/>
<Button onClick={submit}>ボタン</Button>
</Wrapper>
)
emotionでスタイリング
const Wrapper = styled('div')({
display: 'flex',
justifyContent: 'center',
alignItems: 'flex-end',
flexDirection: 'column',
})
PostList Component
本コンポーネントは自分の投稿を一覧で表示します。画面左側ですね。
PostList.tsxを作成します。
プログラムは以下の様になります。
/* eslint-disable react-hooks/exhaustive-deps */
import styled from '@emotion/styled'
import { useEffect, useState } from 'react'
import {
Address,
Order,
RepositoryFactoryHttp,
TransactionGroup,
TransactionSearchCriteria,
TransactionType,
TransferTransaction,
} from 'symbol-sdk'
const NODE_URL = 'https://sym-test.opening-line.jp:3001'
type Props = {
address: string
}
const POST_REG = /^::CREATE::/
function PostList(props: Props) {
const [transactions, setTransactions] = useState<TransferTransaction[]>([])
const address = Address.createFromRawAddress(props.address)
useEffect(() => {
const repositoryFactory = new RepositoryFactoryHttp(NODE_URL)
const transactionHttp = repositoryFactory.createTransactionRepository()
const searchCriteria: TransactionSearchCriteria = {
group: TransactionGroup.Confirmed,
recipientAddress: address,
order: Order.Desc,
type: [TransactionType.TRANSFER],
}
transactionHttp
.search(searchCriteria)
.toPromise()
.then((txs) => {
if (txs === undefined) return
setTransactions(getPostTxs(txs.data as TransferTransaction[]))
})
}, [])
const getPostTxs = (txs: TransferTransaction[]): TransferTransaction[] => {
const postTxs: TransferTransaction[] = []
for (const tx of txs) {
if (POST_REG.test(tx.message.payload)) {
postTxs.push(tx)
}
}
return postTxs
}
const getPostMsg = (tx: TransferTransaction): string => {
return tx.message.payload.split(POST_REG)[1]
}
return (
<Root>
<h1>POST</h1>
{transactions.map((tx) => {
const hash = !!tx.transactionInfo ? tx.transactionInfo.hash : ''
return (
<div key={tx.signature}>
<h3>POST : {getPostMsg(tx)}</h3>
<h3>HASH : {hash}</h3>
<hr />
</div>
)
})}
</Root>
)
}
export default PostList
const Root = styled('div')({
margin: '32px',
})
正規表現で::CREATE::
から始まるって指定をしてます。
参考 https://zenn.dev/hinoshin/articles/470ce8e10caccc
const POST_REG = /^::CREATE::/
このコンポーネントでは転送トランザクション(TransferTransaction)のリストを扱います
const [transactions, setTransactions] = useState<TransferTransaction[]>([])
RepositoryFactoryからTransactionRepositoryを作成しトランザクションを検索します。
検索条件は
- group : 承認済みのトランザクション
- recipientAddress : トランザクション受信者が自分であること
- order : 降順にならべる
- type : 転送トランザクションであること
で検索した結果をgetPostTxs
関数に通します。
取得するトランザクションはtypeで転送トランザクションのみを指定しているのですが、transactionHttp.search
はTransaction[]型を返すため、 txs.data as TransferTransaction[]
とし、as
を用いてTransferTransaction[]
に変換しています。
useEffect(() => {
const repositoryFactory = new RepositoryFactoryHttp(NODE_URL)
const transactionHttp = repositoryFactory.createTransactionRepository()
const searchCriteria: TransactionSearchCriteria = {
group: TransactionGroup.Confirmed,
recipientAddress: address,
order: Order.Desc,
type: [TransactionType.TRANSFER],
}
transactionHttp
.search(searchCriteria)
.toPromise()
.then((txs) => {
if (txs === undefined) return
setTransactions(getPostTxs(txs.data as TransferTransaction[]))
})
}, [])
getPostTxs関数は先程定義したPOST_REG
と一致するかをチェックし、一致するもののみを返却します。
const getPostTxs = (txs: TransferTransaction[]): TransferTransaction[] => {
const postTxs: TransferTransaction[] = []
for (const tx of txs) {
if (POST_REG.test(tx.message.payload)) {
postTxs.push(tx)
}
}
return postTxs
}
getPostMsg関数は、split関数を用いてPOST_REG
で転送トランザクションのメッセージを分割します。
分割した、後ろ側が、投稿の内容となるので、それを返却します。
const getPostMsg = (tx: TransferTransaction): string => {
return tx.message.payload.split(POST_REG)[1]
}
配列transactions
をmap関数を用いてHTMLの要素にします。transactions配列のそれぞれの要素をtxとし、getPostMsg関数の結果とトランザクションのハッシュを表示します。
return (
<Root>
<h1>POST</h1>
{transactions.map((tx) => {
const hash = !!tx.transactionInfo ? tx.transactionInfo.hash : ''
return (
<div key={tx.signature}>
<h3>POST : {getPostMsg(tx)}</h3>
<h3>HASH : {hash}</h3>
<hr />
</div>
)
})}
</Root>
)
CheerList Component
本コンポーネントは自分の投稿に対する応援メッセージを確認することができます。画面右側ですね。
CheerList.tsxを作成します。
プログラムは以下の様になります。
/* eslint-disable react-hooks/exhaustive-deps */
import styled from '@emotion/styled'
import { useEffect, useState } from 'react'
import {
Address,
NetworkType,
Order,
RepositoryFactoryHttp,
TransactionGroup,
TransactionSearchCriteria,
TransactionType,
TransferTransaction,
} from 'symbol-sdk'
const NODE_URL = 'https://sym-test.opening-line.jp:3001'
const NET_TYPE = NetworkType.TEST_NET
type Props = {
address: string
pubkey: string
}
const CHEER_REG = /^::CHEER/
function CheerList(props: Props) {
const [transactions, setTransactions] = useState<TransferTransaction[]>([])
const address = Address.createFromRawAddress(props.address)
useEffect(() => {
const repositoryFactory = new RepositoryFactoryHttp(NODE_URL)
const transactionHttp = repositoryFactory.createTransactionRepository()
const searchCriteria: TransactionSearchCriteria = {
group: TransactionGroup.Confirmed,
recipientAddress: address,
order: Order.Desc,
type: [TransactionType.TRANSFER],
}
transactionHttp
.search(searchCriteria)
.toPromise()
.then((txs) => {
if (txs === undefined) return
setTransactions(getCheerTxs(txs.data as TransferTransaction[]))
})
}, [])
const getCheerTxs = (txs: TransferTransaction[]): TransferTransaction[] => {
const cheerTxs: TransferTransaction[] = []
for (const tx of txs) {
if (CHEER_REG.test(tx.message.payload)) {
cheerTxs.push(tx)
}
}
return cheerTxs
}
const getCheerMsg = (tx: TransferTransaction): string => {
const tmp = tx.message.payload.split('|')[1]
return tmp.split('::')[1]
}
const getCheerHash = (tx: TransferTransaction): string => {
const tmp = tx.message.payload.split('|')[1]
return tmp.split('::')[0]
}
return (
<Root>
<h1>CHEER</h1>
{transactions.map((tx) => {
const addr =
tx.signer !== undefined
? Address.createFromPublicKey(
tx.signer?.publicKey,
NET_TYPE
).pretty()
: 'NOT FOUND'
return (
<div key={tx.signature}>
<h3>TO: {getCheerHash(tx)}</h3>
<h3>MSG: {getCheerMsg(tx)}</h3>
<h3>FROM: {addr}</h3>
<hr />
</div>
)
})}
</Root>
)
}
export default CheerList
const Root = styled('div')({
margin: '32px',
})
正規表現で::CHEERから始まるって指定をしてます。
const CHEER_REG = /^::CHEER/
PostListと同様に検索し、応援メッセージのみを取得しています。
useEffect(() => {
const repositoryFactory = new RepositoryFactoryHttp(NODE_URL)
const transactionHttp = repositoryFactory.createTransactionRepository()
const searchCriteria: TransactionSearchCriteria = {
group: TransactionGroup.Confirmed,
recipientAddress: address,
order: Order.Desc,
type: [TransactionType.TRANSFER],
}
transactionHttp
.search(searchCriteria)
.toPromise()
.then((txs) => {
if (txs === undefined) return
setTransactions(getCheerTxs(txs.data as TransferTransaction[]))
})
}, [])
const getCheerTxs = (txs: TransferTransaction[]): TransferTransaction[] => {
const cheerTxs: TransferTransaction[] = []
for (const tx of txs) {
if (CHEER_REG.test(tx.message.payload)) {
cheerTxs.push(tx)
}
}
return cheerTxs
}
getCheerMsgは応援メッセージ(<Cheer message>)、getCheerHashは応援する投稿のハッシュ(<Transaction hash>)を取得します。
以下のような形式で応援メッセージは構成されます。
::CHEER|<Transaction hash>::
<Cheer message>
const tmp = tx.message.payload.split('|')
でCHEERと<Transaction hash>の部分を分割し、その後ろ側を変数tmpに代入しています。
そして、tmpをさらに::
で分割した前側はTransaction hash
後ろ側はCheer message
としてそれぞれ返却しています。
const getCheerMsg = (tx: TransferTransaction): string => {
const tmp = tx.message.payload.split('|')[1]
return tmp.split('::')[1]
}
const getCheerHash = (tx: TransferTransaction): string => {
const tmp = tx.message.payload.split('|')[1]
return tmp.split('::')[0]
}
表示部分ですね。PostListとほとんど同じです。送信者のアドレス、宛先のハッシュ、メッセージを表示しています。
return (
<Root>
<h1>CHEER</h1>
{transactions.map((tx) => {
const addr =
tx.signer !== undefined
? Address.createFromPublicKey(
tx.signer?.publicKey,
NET_TYPE
).pretty()
: 'NOT FOUND'
return (
<div key={tx.signature}>
<h3>TO: {getCheerHash(tx)}</h3>
<h3>MSG: {getCheerMsg(tx)}</h3>
<h3>FROM: {addr}</h3>
<hr />
</div>
)
})}
</Root>
)
OwnerPage Component
作成したコンポーネントを呼び出します。
import styled from '@emotion/styled'
import Create from './Create'
import CheerList from './CheerList'
import PostList from './PostList'
type Props = {
address: string
pubkey: string
}
function OwnerPage(props: Props) {
return (
<Wrapper>
<Create address={props.address} />
<Flex>
<PostList address={props.address} />
<CheerList address={props.address} pubkey={props.pubkey} />
</Flex>
</Wrapper>
)
}
export default OwnerPage
const Wrapper = styled('div')({
display: 'flex',
justifyContent: 'center',
flexDirection: 'column',
width: '80vw',
margin: '10vw',
})
const Flex = styled('div')({
display: 'flex',
justifyContent: 'center',
width: '100%',
})
ゲストページを作る
ゲストページでは、オーナーの投稿の一覧を確認と、応援メッセージを送ることができます。
本ページのコンポーネントは以下の2つです
- PostList Component
- Cheer Component
PostListはオーナーページと同じ物を使うため割愛します。
Cheer Component
import { Button, TextField } from '@mui/material'
import styled from '@emotion/styled'
import { useEffect, useState } from 'react'
import {
Address,
Deadline,
NetworkType,
PlainMessage,
SignedTransaction,
TransactionHttp,
TransferTransaction,
UInt64,
} from 'symbol-sdk'
const EPOCH = 1637848847
const NODE_URL = 'https://sym-test.opening-line.jp:3001'
const NET_TYPE = NetworkType.TEST_NET
type Props = {
address: string
}
interface SSSWindow extends Window {
SSS: any
}
declare const window: SSSWindow
function Cheer(props: Props) {
const [hash, setHash] = useState('')
const [cheer, setCheer] = useState('')
const [isRequest, setIsRequest] = useState<boolean>(false)
const address = Address.createFromRawAddress(props.address)
useEffect(() => {
if (isRequest) {
window.SSS.requestSign().then((signedTx: SignedTransaction) => {
new TransactionHttp(NODE_URL).announce(signedTx)
})
}
}, [isRequest])
const submit = () => {
const message = `::CHEER|${hash}::${cheer}`
const tx = TransferTransaction.create(
Deadline.create(EPOCH),
address,
[],
PlainMessage.create(message),
NET_TYPE,
UInt64.fromUint(2000000)
)
window.SSS.setTransaction(tx)
setIsRequest(true)
}
return (
<Wrapper>
<TextField
label="hash"
value={hash}
fullWidth
onChange={(e) => setHash(e.target.value)}
/>
<TextField
label="cheer"
value={cheer}
fullWidth
onChange={(e) => setCheer(e.target.value)}
/>
<Button onClick={submit}>ボタン</Button>
</Wrapper>
)
}
export default Cheer
const Wrapper = styled('div')({
display: 'flex',
justifyContent: 'center',
alignItems: 'flex-end',
flexDirection: 'column',
})
応援メッセージを送るトランザクションです。
入力された、ハッシュと応援メッセージを転送トランザクションのメッセージに設定しています。
const submit = () => {
const message = `::CHEER|${hash}::${cheer}`
const tx = TransferTransaction.create(
Deadline.create(EPOCH),
address,
[],
PlainMessage.create(message),
NET_TYPE,
UInt64.fromUint(2000000)
)
window.SSS.setTransaction(tx)
setIsRequest(true)
}
GuestPage Component
OwnerPage Componentと同様で並べてるだけですね
import styled from '@emotion/styled'
import { Address } from 'symbol-sdk'
import Cheer from './Cheer'
import PostList from './PostList'
type Props = {
address: string
}
const OWNER_ADDR = Address.createFromRawAddress(
'TDHLRYXKIT4QOEEL3PRBP4PWLJ6NWU3LSGB56BY'
)
function GuestPage(props: Props) {
return (
<Wrapper>
<h1>Hello Symbol 「{props.address}」</h1>
<Cheer address={OWNER_ADDR.plain()} />
<PostList address={OWNER_ADDR.plain()} />
</Wrapper>
)
}
export default GuestPage
const Wrapper = styled('div')({
display: 'flex',
justifyContent: 'center',
flexDirection: 'column',
width: '80vw',
margin: '10vw',
})
ユーザーによってページを変える
activeAddressとactivePublicKeyを取得してコンポーネントに流すだけですね
import React, { useEffect, useState } from 'react'
import { Address } from 'symbol-sdk'
import './App.css'
import GuestPage from './GuestPage'
import OwnerPage from './OwnerPage'
interface SSSWindow extends Window {
SSS: any
}
declare const window: SSSWindow
const OWNER_ADDR = Address.createFromRawAddress(
'TDHLRYXKIT4QOEEL3PRBP4PWLJ6NWU3LSGB56BY'
)
function App() {
const [addr, setAddr] = useState<Address | null>(null)
const [pubkey, setPubkey] = useState<string>('')
console.log('render')
useEffect(() => {
console.log('hello')
setTimeout(() => {
const activeAddress = window.SSS.activeAddress
const activePublicKey = window.SSS.activePublicKey
setAddr(Address.createFromRawAddress(activeAddress))
setPubkey(activePublicKey)
}, 500)
}, [])
if (addr === null) {
return (
<div>
<h1>Hello Symbol not set address</h1>
</div>
)
}
if (OWNER_ADDR.plain() === addr.plain()) {
return <OwnerPage address={addr.plain()} pubkey={pubkey} />
}
return <GuestPage address={addr.plain()} />
}
export default App
dAppsを進化させる
ここまできたら、あとはあなたのアイデア次第でブログとコメントにもなりますし、ツイートとリプライにもなりますし、Todoと投げ銭にもなります。あしらいの工夫次第ですね。
最後に進化させるためのアイデアを共有して記事を終わりたいと思います。
- URLにアドレスを入れるとそのユーザーのオーナーページになるようにする
- 応援メッセージのハッシュを入力ではなく選択するできるようにする
- オーナーページのUIで投稿と応援メッセージを紐付けるようにする
- 投稿内容をリッチにして長いコンテンツに対応させる
- トランザクションのメッセージは1024バイト程なのでアグリゲートトランザクションを使うことになると思う
- 投稿の詳細を見れるページを作る
- 投げ戦機能
お疲れさまでした〜〜〜