9
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

nem / symbolAdvent Calendar 2024

Day 10

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

Last updated at Posted at 2024-12-09

はじめに

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

本記事は、nem / symbol Advent Calendar 2024 の10日目の記事となります。

SSS Extensionの開発や株式会社 OpeningLineでSymbolブロックチェーンを用いたWebアプリケーションの開発や、Symbolブロックチェーンを用いたアプリケーション開発に関する書籍 実践Symbolの執筆などを行っています。

今回は、SSS Extensionを使ってWebアプリケーション上で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 公式アカウント

SSS Extension Chrome Web Store

Apostille

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

実装

プログラム全文
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;

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

image.png

内容に分けてプログラムの解説をしていきます。

下準備

まずは、ブロックチェーンノードのURLを用意します。今回は弊社のテストネットノードを利用します。

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

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

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

  • networkType
  • generationHash
  • epochAdjustment
  • feeMultipilier
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を作成します。

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

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

アポスティーユの作成

ここで入力したファイルを確認します。

if (file === null) return;
const reader = new FileReader();
reader.addEventListener('load', async () => {
// do something...
}
reader.readAsText(file);

ファイルの中身の取得ができなかった場合はエラーを表示します。

const data = `${reader.result}`;
if (data === '') {
    alert('error');
    return;
}

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

ユーザーの公開鍵はSSSからgetActivePublicKey()で取得することができます。

const apostille = facade.createApostille(
    data,
    file.name,
    userPublicKey,
    option,
);

アポスティーユからトランザクションと、指定する必要のある連署者のデータを取得します。

const transaction = apostille.createTransaction();
const cosignatories = apostille.getCosignatories();

SSSにセットし、ユーザーに署名を要求します。
署名が行われるとトランザクションハッシュが表示され、ブロックチェーンノードへとアナウンスされます。

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

アポスティーユの検証

入力したトランザクションハッシュからトランザクションのメッセージを取得します。
メッセージの中にはファイルハッシュに関する情報が記録されています。
また、トランザクションから署名者の公開鍵を取得します。

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の検証関数へと渡します。
すると検証に成功したか、失敗したかが真偽値として取得できます。

const isValid = facade.auditApostille(data, message, signerPublicKey);
if (isValid) {
    alert('Valid Success');
} else {
    alert('Invalid');
}

動作確認

ファイルを選択し、Create Apostille ボタンを押下しファイルをアポスティーユします。
image.png

作成されると、トランザクションのハッシュ値が表示されるのでコピーしておきましょう。
image.png

入力欄にトランザクションハッシュを入力してAuditボタンを押下し、アポスティーユを検証します。
image.png

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

image.png

ファイルを別のものに差し替え、再度検証します。
image.png

ファイルとトランザクションハッシュが一致しないので、Invalidと表示されます。
image.png

検証時のトランザクション
https://testnet.symbol.fyi/transactions/F376BE1160B34A3522DB4B3C6BFE03E8F60DF76D2431193C34267375A82E9BDC

おわりに

SSSのApostilleライブラリを活用することで簡単にWebアプリケーション上でApostilleを実現できることが伝わったかと思います。本ライブラリの内部実装の一部解説や、NFTやトレーサビリティでのブロックチェーンのユースケースや組み込み方について実践Symbolで色々解説しておりますのでよろしければお手に取ってみてください。

9
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
9
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?