SSSとReactでアポスティーユする

# はじめに
こんにちは、いなたつ @inatatsu_csg です。

本記事は、[nem / symbol Advent Calendar 2024](https://qiita.com/advent-calendar/2024/nem) の10日目の記事となります。

SSS Extensionの開発や[株式会社 OpeningLine](https://www.opening-line.co.jp/)でSymbolブロックチェーンを用いたWebアプリケーションの開発や、Symbolブロックチェーンを用いたアプリケーション開発に関する書籍 [実践Symbol](https://amzn.asia/d/g8H4Wmp)の執筆などを行っています。

今回は、SSS Extensionを使ってWebアプリケーション上で[Apostilleを行うライブラリ](https://www.npmjs.com/package/@sss-symbol/apostille)について解説いたします。

本記事で取り扱う内容についての詳しい解説は上記書籍6,7章に記載されていますので、ご興味いただけましたらご一読の方よろしくお願いします。

# 本記事のゴール
Viteで生成したReactプロジェクトにSSS Extensionで署名可能なアポスティーユアプリケーションを作る

# 使用技術
- React
- TypeSciript
- Symbol
- SSS Extension
- SSS Apostille Library

# 本記事で説明しないこと
- Reactの基本
- HTML/CSS部分の詳細な説明

# 前提知識
## Symbolブロックチェーン
Symbolブロックチェーンは2021年3月17日に、ブロックチェーンシステムNEMの次世代版としてローンチされました。
Symbolは、スマートコントラクトを持たないブロックチェーンシステムです。
豊富なAPIやSDKを持ち、アプリケーションロジック上に直接コントラクトを組み込むことができます。
Symbolを用いてブロックチェーンアプリケーション (dApps) を開発することにより、すべての実装をJavaScriptやTypeScriptで完結させることが可能です。
これにより、コントラクトを実装するために別の言語(Solidityなど)を習得する必要がないといったメリットがあります。

## SSS Extension
Web アプリケーションと連携しWebアプリケーション上で秘密鍵を扱うことなくトランザクションへと署名することができるブラウザ拡張機能です。

これにより、
- コントラクトの内容を署名前に確認する
- Webアプリケーションに秘密鍵を知らせることなくトランザクションに署名する
 
ことができるようになります。

Ethereumを扱ったことがある人向けに一言でいうとSymbol版のMetaMaskのようなものです。

2022 年 3 月 17 日 (Symbol一周年) に正式リリースされました。

[SSS Extension 公式アカウント](https://twitter.com/SSS_Symbol)

[SSS Extension Chrome Web Store](https://chrome.google.com/webstore/detail/sss-extension/llildiojemakefgnhhkmiiffonembcan)

## Apostille
Apostilleは公証を意味し、ブロックチェーン技術を用いてそのデータがその時点で存在したことを証明するプロトコルです。
ApostilleではSymbolブロックチェーン上でアカウントを生成し、アカウントのメタデータにファイルの情報を記録し最終的に秘密鍵を破棄することでファイルの不変性を担保します。


# 実装

<details><summary>プログラム全文</summary>

```tsx
import { ApostilleFacade, HashType } from '@sss-symbol/apostille';
import { useState } from 'react';
import {
  getActivePublicKey,
  requestSignWithCosignatories,
  setTransaction,
} from 'sss-module';
import { Convert, RepositoryFactoryHttp } from 'symbol-sdk';

const NODE = 'https://sym-test-03.opening-line.jp:3001';

const repFac = new RepositoryFactoryHttp(NODE);
const txRep = repFac.createTransactionRepository();

const info = await ApostilleFacade.getNetworkInfomation(NODE);

const option = {
  isOwner: true,
};
const facade = new ApostilleFacade(HashType.SHA256, info);

function Apostille() {
  const [file, setFile] = useState<File | null>(null);
  const [hash, setHash] = useState('');
  const [filePreview, setFilePreview] = useState<string | null>(null);
  
  const userPublicKey = getActivePublicKey();
  
  const create = () => {
    if (file === null) return;

    
    const reader = new FileReader();
    reader.addEventListener('load', async () => {
      const data = `${reader.result}`;
      if (data === '') {
        alert('error');
        return;
      }

      const apostille = facade.createApostille(
        data,
        file.name,
        userPublicKey,
        option,
      );
      const transaction = apostille.createTransaction();
      const cosignatories = apostille.getCosignatories();

      setTransaction(transaction);
      const signedTx = await requestSignWithCosignatories(cosignatories);
      alert(signedTx.hash)
      txRep.announce(signedTx);
    });

    reader.readAsText(file);
  };

  const audit = () => {
    if (file === null) return;
    const reader = new FileReader();
    reader.addEventListener('load', async () => {
      const data = `${reader.result}`;

      if (data === '') {
        alert('error');
        return;
      }

      const result = await fetch(`${NODE}/transactions/confirmed/${hash}`).then(
        (data) => data.json(),
      );
      const rowMessage = result.transaction.transactions[0].transaction
      .message as string;
      const message = Convert.decodeHex(rowMessage.replace('00', ''));
      const signerPublicKey = result.transaction.cosignatures[0]
        .signerPublicKey as string;

      const isValid = facade.auditApostille(data, message, signerPublicKey);

      if (isValid) {
        alert('Valid Success');
      } else {
        alert('Invalid');
      }
    });

    reader.readAsText(file);
  };
  const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    const selectedFile = e.target.files ? e.target.files[0] : null;
    setFile(selectedFile);
    if (selectedFile) {
      const reader = new FileReader();
      reader.onload = () => {
        setFilePreview(reader.result as string);
      };
      reader.readAsDataURL(selectedFile);
    } else {
      setFilePreview(null);
    }
  };


  return (
    <div style={{ padding: '20px', fontFamily: 'Arial, sans-serif', maxWidth: '600px', margin: '0 auto' }}>
      <h1>Apostille Creator & Auditor</h1>
      <input
        type="file"
        onChange={handleFileChange}
        style={{ marginBottom: '10px', display: 'block' }}
      />
      {file && (
        <div style={{ marginBottom: '20px' }}>
          <p>Selected file: {file.name}</p>
          {file.type.startsWith('image/') && (
            <img src={filePreview!} alt="Preview" style={{ maxWidth: '100%', maxHeight: '200px' }} />
          )}
        </div>
      )}
      <button
        onClick={create}
        style={{
          padding: '10px 20px',
          backgroundColor: '#007BFF',
          color: 'white',
          border: 'none',
          borderRadius: '5px',
          cursor: 'pointer',
          marginBottom: '20px',
        }}
      >
        Create Apostille
      </button>

      <div style={{ marginBottom: '20px' }}>
        <label htmlFor="auditHashInput" style={{ display: 'block', marginBottom: '5px' }}>Enter Hash to Audit:</label>
        <input
          id="auditHashInput"
          type="text"
          value={hash}
          onChange={(e) => setHash(e.target.value)}
          style={{
            width: '100%',
            padding: '10px',
            borderRadius: '5px',
            border: '1px solid #ced4da',
            marginBottom: '10px',
          }}
        />
      </div>
      <button
        onClick={audit}
        style={{
          padding: '10px 20px',
          backgroundColor: '#28a745',
          color: 'white',
          border: 'none',
          borderRadius: '5px',
          cursor: 'pointer',
        }}
      >
        Audit
      </button>
    </div>
  );
}

export default Apostille;
```

</details>

今回作成するページは、ファイルを選択するエリア、アポスティーユを作成するボタン、トランザクションハッシュの入力欄、アポスティーユを検証するボタンだけのシンプルな画面構成になります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/269859/d04c7674-ba61-44af-8f9f-535476779bc0.png)



内容に分けてプログラムの解説をしていきます。
## 下準備
まずは、ブロックチェーンノードのURLを用意します。今回は弊社のテストネットノードを利用します。

そしてSymbolSDK(v2)でRepositoryFactoryを作成し、TransactionRepositoryを作成します。

TransactionRepositoryはブロックチェーンへとトランザクションをアナウンスする際に使用します。

ApostilleFacade.getNetworkInfomationでは、SSS Apostille Libraryを用いてブロックチェーンノードから以下の情報を取得します。
- networkType
- generationHash
- epochAdjustment
- feeMultipilier


```ts
const NODE = 'https://sym-test-03.opening-line.jp:3001';

const repFac = new RepositoryFactoryHttp(NODE);
const txRep = repFac.createTransactionRepository();

const info = await ApostilleFacade.getNetworkInfomation(NODE);
```

そしてアポスティーユのオプションの定義と、先ほど取得したブロックチェーンの情報からアポスティーユを行うためのfacadeを作成します。

:::note info
SSS Apostille Libraryは Symbol SDK V3に近い操作感を意識して、facadeパターンを採用しています。
:::

```ts
const option = {
  isOwner: true,
};
const facade = new ApostilleFacade(HashType.SHA256, info);
```

## アポスティーユの作成
ここで入力したファイルを確認します。
```ts
if (file === null) return;
const reader = new FileReader();
reader.addEventListener('load', async () => {
// do something...
}
reader.readAsText(file);
```

ファイルの中身の取得ができなかった場合はエラーを表示します。
```ts
const data = `${reader.result}`;
if (data === '') {
    alert('error');
    return;
}
```

facadeを利用して、アポスティーユを生成します。
引数には、データ、ファイル名、ユーザーの公開鍵、オプションを設定します。
データとファイル名は入力したファイルから取得します。

ユーザーの公開鍵はSSSから`getActivePublicKey()`で取得することができます。
```ts
const apostille = facade.createApostille(
    data,
    file.name,
    userPublicKey,
    option,
);
```

アポスティーユからトランザクションと、指定する必要のある連署者のデータを取得します。
```ts
const transaction = apostille.createTransaction();
const cosignatories = apostille.getCosignatories();
```

SSSにセットし、ユーザーに署名を要求します。
署名が行われるとトランザクションハッシュが表示され、ブロックチェーンノードへとアナウンスされます。
```ts
setTransaction(transaction);
const signedTx = await requestSignWithCosignatories(cosignatories);
alert(signedTx.hash)
txRep.announce(signedTx);
```


### アポスティーユの検証
入力したトランザクションハッシュからトランザクションのメッセージを取得します。
メッセージの中にはファイルハッシュに関する情報が記録されています。
また、トランザクションから署名者の公開鍵を取得します。
```ts
const result = await fetch(`${NODE}/transactions/confirmed/${hash}`).then(
    (data) => data.json(),
);
const rowMessage = result.transaction.transactions[0].transaction.message as string;
const message = Convert.decodeHex(rowMessage.replace('00', ''));
const signerPublicKey = result.transaction.cosignatures[0].signerPublicKey as string;
```

これらの情報をfacadeの検証関数へと渡します。
すると検証に成功したか、失敗したかが真偽値として取得できます。
```ts
const isValid = facade.auditApostille(data, message, signerPublicKey);
if (isValid) {
    alert('Valid Success');
} else {
    alert('Invalid');
}
```

# 動作確認

ファイルを選択し、Create Apostille ボタンを押下しファイルをアポスティーユします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/269859/ef11aa4f-ef6c-7cc3-c8c6-223b85b36bfd.png)

作成されると、トランザクションのハッシュ値が表示されるのでコピーしておきましょう。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/269859/5711f50e-2679-e972-c071-ab7bfbd28aee.png)

入力欄にトランザクションハッシュを入力してAuditボタンを押下し、アポスティーユを検証します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/269859/32af33a7-1594-5648-6e6b-0f9c2abb166a.png)

ファイルとトランザクションハッシュが一致した場合はValid Successと表示されます。


![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/269859/b1ad9fde-ee29-cdce-0e6c-549b78837bc3.png)




ファイルを別のものに差し替え、再度検証します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/269859/d883e9ef-aa87-2e39-dd8d-0e7140089ad0.png)

ファイルとトランザクションハッシュが一致しないので、Invalidと表示されます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/269859/faf92f49-8a99-d9fc-8444-91a9a3a375d4.png)

検証時のトランザクション
https://testnet.symbol.fyi/transactions/F376BE1160B34A3522DB4B3C6BFE03E8F60DF76D2431193C34267375A82E9BDC
# おわりに
SSSのApostilleライブラリを活用することで簡単にWebアプリケーション上でApostilleを実現できることが伝わったかと思います。本ライブラリの内部実装の一部解説や、NFTやトレーサビリティでのブロックチェーンのユースケースや組み込み方について[実践Symbol](https://amzn.asia/d/g8H4Wmp)で色々解説しておりますのでよろしければお手に取ってみてください。