Symbol × SSS Extension で作る dApps 入門 ~Reactを添えて~

# はじめに

GW明けましたね！5月病になっちゃダメですよ！もう一歩踏み出してつよつよ(?)になっちゃいましょう！

GW明けのコンテンツのお時間です。

どうも、いなたつです。SSS Extension作ってる人です。Symbol Draw作ってる人です。

簡単にdAppsが作れると巷で噂のSymbolを使って[前回の記事](https://qiita.com/inatatsu_csg/items/191edf3bcd1f0acf15c1)よりもうすこしナウい感じに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>

完成図

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/269859/58cd9a53-e26b-6b12-a135-b0b98b5d4ae0.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/269859/7f04bf23-5ba3-4e18-d5a4-50832030c263.png)



# 前準備
まずはプロジェクトのセットアップをしていきましょう。
今回は作業ディレクトリは `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`に変更してください

```diff_json 
{
  "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さんにこんにちはです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/269859/88532fd4-1feb-2945-ecd0-6a86e7badaab.png)

いろいろ使うパッケージをインストールしていきます

まずはSymbol関係
```
$ npm i symbol-sdk rxjs
```

UIコンポーネント
```
$ npm i @mui/material @emotion/react @emotion/styled
```

# Hello Symbol

今日もSymbolさんにごあいさつしましょう。`localhost:3000`を開いた際に表示されている内容はApp.tsに記述されているので、ここを変更します。

```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
```App.css
body {
  background: rgb(242, 242, 242);
}
```

こんな感じになります
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/269859/d75fd917-abdf-ab57-8e3b-7c1061950fb9.png)


プログラム解説

前回の記事とほとんど同じですが、解説しときます。

変数addrにSymbolSDKを用いてアドレスの文字列を読み込んでいます。

```js
  const addr = Address.createFromRawAddress(
    'TAD7Q3FEN5CZRZFE3WX6TWEESATVTMJDS2ETVTY'
  )
```

Reactのレンダー部分です。一つのHTML要素を返すことができます。

「{」と「}」で囲まれている部分にはJavaScript(TypeScript)のプログラムが入ります。
今回は`addr.pretty()`が入っているので先程変数に入れたアドレスをハイフンで区切った物が表示されます。

```tsx
  return (
    <div>
      <h1>Hello Symbol 「{addr.pretty()}」</h1>
    </div>
  )

```

# SSS Extension と連携する

今回はさっさとSSSと連携しちゃいます。

SSSと連携するためにReactの機能useStateとuseEffectを使用するのでその説明を簡単に

## useState

React上で状態を管理するために使用する機能で、変数とそれを設定・変更する関数で構成されるReactフックと呼ばれるものの一つです。

以下のような形で使用されます。
```tsx
const [addr, setAddr] = useState('')
```

参考 <https://qiita.com/seira/items/f063e262b1d57d7e78b4>

## useEffect
副作用を扱う際に～～～みたいな難しい説明は置いといて、とりあえず画面描画後に値を変更したり・外部からデータを取得するときに使うやつです

以下のような形で使用されます。
```tsx
  useEffect(() => {
    // プログラム
  }, [])
```

[]の中には値が入る場合と入らない場合があります。値が入らない場合、初回の画面描画時のみこの中のプログラムが実行されます。
値が入っている場合は、指定した値が変更された時に中のプログラムが実行されます。

参考 <https://qiita.com/seira/items/e62890f11e91f6b9653f>

## SSS Extensionからデータ取得

まずSSS Extensionと連携しましょう。やり方を忘れた方は前回の記事を見てきてください。

ではSSS Extensionからデータを取得していきます。

```App.tsx
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>
```tsx
interface SSSWindow extends Window {
  SSS: any
}
declare const window: SSSWindow

```

先程登場したuseStateです、なんか`Address | null`が括弧に囲われていますね、TypeScriptの場合ここにはAddressが入るか何も入らないか(null)のどっちかですよ～って教えてあげる必要があります。
```tsx
const [addr, setAddr] = useState<Address | null>(null)
```

続いてuseEffectです。前回記事で解説したようにsetTimeoutでSSSが読み込まれるのを待ってからSSSからデータを取得します。
取得した`activeAddress`を`setAddr`します。こうすることで、SSSから取得したアドレスをaddr変数に入れることができました。
```tsx
  useEffect(() => {
    setTimeout(() => {
      const activeAddress = window.SSS.activeAddress
      setAddr(Address.createFromRawAddress(activeAddress))
    }, 500)
  }, [])
```

では表示です。

なんかふえました。

もし、アドレスがなんにも入ってない(null)場合はアドレスがないよ～って表示してください。ってはじめに書いてます。

アドレスを読み込めた場合は`addr === null`が`false`なのでこれは実行されずに下に進みます。

ここは先程と同じでアドレスをハイフン区切りで表示ですね。

```tsx
  if (addr === null) {
    return (
      <div>
        <h1>Hello Symbol not set address</h1>
      </div>
    )
  }
  return (
    <div>
      <h1>Hello Symbol 「{addr.pretty()}」</h1>
    </div>
  )
```

アドレスを取得する部分を消して画面を開くとaddrがnullなので下図のような画面になります
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/269859/0236d7e2-184a-cc90-a1e9-ebf1d3c752e3.png)

とりあえずこれで、SSS Extensionからデータ取得ができましたね！

# SSS Extensionで管理者ログイン
SSS ExtensionのactiveAddressで得られるアドレスは開いているユーザーが誰であるかを示します。

つまり、サイトの所有者であるか、訪問者であるかを判別できるということです。

`OWNER_ADDR`を定義します。このアドレスでログインしている場合、サイトオーナーということを判別できます。
```tsx
const OWNER_ADDR = Address.createFromRawAddress(
  'TAD7Q3FEN5CZRZFE3WX6TWEESATVTMJDS2ETVTY'
)
```
そして、オーナーアドレスでログインした場合の描画を定義しましょう

```tsx
  if (OWNER_ADDR.plain() === addr.plain()) {
    return (
      <div>
        <h1>Hello Symbol 管理者ログイン</h1>
      </div>
    )
  }
```

オーナーアドレスでログインするとこんな感じですね

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/269859/55d99db5-6021-ad77-2c03-b82ed065d229.png)

SSSのアクティブアカウントを変更するとさっきと同じようにアドレスが表示されます
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/269859/e11c149b-0461-4a61-3c90-2bde37f16bee.png)

# コンポーネントを分割する

全部1ファイルに書くととても長くなるのでコンポーネント(部品)に分けます。

srcディレクトリ以下に`OwnerPage.tsx`と`GuestPage.tsx`を作成してください。

```App.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
```

```OwnerPage.tsx
function OwnerPage() {
  return (
    <div>
      <h1>Hello Symbol 管理者ログイン</h1>
    </div>
  )
}

export default OwnerPage

```

```GuestPage.tsx
type Props = {
  address: string
}

function GuestPage(props: Props) {
  return (
    <div>
      <h1>Hello Symbol 「{props.address}」</h1>
    </div>
  )
}

export default GuestPage


```

propsくんの登場です。

propsくんは、作ったコンポーネントに値を渡す架け橋になってくれる存在です

```tsx
<GuestPage address={addr.plain()} />
```

こんな感じで作成したコンポーネントに値を渡します。

すると、コンポーネント側で値を扱うことができるようになります。

# 管理者ページを作る
管理者ページではメッセージを投稿することができます。そして、他のユーザーからの応援メッセージを見ることができます。

こんなページを作っていきます
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/269859/58cd9a53-e26b-6b12-a135-b0b98b5d4ae0.png)

本ページは大きく3つの要素(コンポーネント)で構成されています。

- Create Component
- PostList Component
- CheerList Component

## Create Component
本コンポーネントは投稿を作成するための要素になります。
まず、Create.tsxを作成してください。

プログラムは以下の様になります。
```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',
})
```

本コンポーネントの引数の型です。
アドレスを受け取ります。
```tsx
type Props = {
  address: string
}
```

コンポーネントの状態を示します。投稿する内容である`contents`と、署名要求を行う`isRequest`があります。
```tsx
const [contents, setContents] = useState('')
const [isRequest, setIsRequest] = useState(false)
```

引数(props)のaddressは文字列(string型)なのでSymbol SDKで扱うために`Address.createFromAddress`でAddressにします。
```tsx
const address = Address.createFromRawAddress(props.address)
```

isRequestが変更され、trueになった場合、SSSのrequestSignを実行します。
```tsx
  useEffect(() => {
    if (isRequest) {
      window.SSS.requestSign().then((signedTx: SignedTransaction) => {
        new TransactionHttp(NODE_URL).announce(signedTx)
      })
    }
  }, [isRequest])
```

トランザクションの作成になります。

送信先のアドレスは引数で与えたアドレス(自分のアドレス)になります。自分から自分へのトランザクションを発生させています。

また、メッセージは`::CREATE::`を頭につけることで、本サービスで作成したトランザクションであるかを判別しやすくしています。

ここで発生させたトランザクションは送信先、送信元がオーナーであり、`:::CREATE:::`で始まるトランザクションを探せばいいということになります。
```tsx
  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関数が実行されます。
```tsx
  return (
    <Wrapper>
      <TextField
        label="contents"
        value={contents}
        fullWidth
        onChange={(e) => setContents(e.target.value)}
      />
      <Button onClick={submit}>ボタン</Button>
    </Wrapper>
  )
```

emotionでスタイリング
```tsx
const Wrapper = styled('div')({
  display: 'flex',
  justifyContent: 'center',
  alignItems: 'flex-end',
  flexDirection: 'column',
})

```

## PostList Component
本コンポーネントは自分の投稿を一覧で表示します。画面左側ですね。
PostList.tsxを作成します。
プログラムは以下の様になります。

```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>
```tsx
const POST_REG = /^::CREATE::/
```


このコンポーネントでは転送トランザクション(TransferTransaction)のリストを扱います
```tsx
const [transactions, setTransactions] = useState<TransferTransaction[]>([])
```

RepositoryFactoryからTransactionRepositoryを作成しトランザクションを検索します。

検索条件は
- group : 承認済みのトランザクション
- recipientAddress : トランザクション受信者が自分であること
- order : 降順にならべる
- type : 転送トランザクションであること

で検索した結果を`getPostTxs`関数に通します。

取得するトランザクションはtypeで転送トランザクションのみを指定しているのですが、`transactionHttp.search`はTransaction[]型を返すため、 `txs.data as TransferTransaction[]`とし、`as`を用いて`TransferTransaction[]`に変換しています。

```tsx
  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`と一致するかをチェックし、一致するもののみを返却します。

```tsx
  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`で転送トランザクションのメッセージを分割します。
分割した、後ろ側が、投稿の内容となるので、それを返却します。

```tsx
  const getPostMsg = (tx: TransferTransaction): string => {
    return tx.message.payload.split(POST_REG)[1]
  }
```

配列`transactions`をmap関数を用いてHTMLの要素にします。transactions配列のそれぞれの要素をtxとし、getPostMsg関数の結果とトランザクションのハッシュを表示します。

```tsx
  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を作成します。
プログラムは以下の様になります。

```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から始まるって指定をしてます。
```tsx
const CHEER_REG = /^::CHEER/
```

PostListと同様に検索し、応援メッセージのみを取得しています。
```tsx
  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`としてそれぞれ返却しています。

```tsx
  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とほとんど同じです。送信者のアドレス、宛先のハッシュ、メッセージを表示しています。

```tsx
  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
作成したコンポーネントを呼び出します。

```OwnerPage.tsx
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%',
})

```

# ゲストページを作る

ゲストページでは、オーナーの投稿の一覧を確認と、応援メッセージを送ることができます。

こんな画面になります
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/269859/7f04bf23-5ba3-4e18-d5a4-50832030c263.png)


本ページのコンポーネントは以下の2つです

- PostList Component
- Cheer Component

PostListはオーナーページと同じ物を使うため割愛します。

## Cheer Component

```Cheer.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 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',
})
```

応援メッセージを送るトランザクションです。

入力された、ハッシュと応援メッセージを転送トランザクションのメッセージに設定しています。
```tsx
  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と同様で並べてるだけですね
```OwnerPage.tsx
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を取得してコンポーネントに流すだけですね

```App.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(
  '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バイト程なのでアグリゲートトランザクションを使うことになると思う
- 投稿の詳細を見れるページを作る
- 投げ戦機能

お疲れさまでした〜〜〜