5
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?

「KeyStone」をTypeScript/Reactで快適に使う型定義&サンプル

Last updated at Posted at 2025-12-19

「KeyStone」は現在テスト中であり、公式ドキュメントはまだ公開されていません。そのため、TypeScriptから利用する際に型定義がなく、補完や型安全性に課題がありました。この記事では、KeyStoneの型定義を自作し、Reactでの連携サンプルを紹介します。Symbol SDK v3を利用した実装例も掲載しています。

KeyStoneとは?

型定義(KeyStone.ts)

まだ型定義がなくてTypeScriptから使用しづらいので、作ってみました。なお、私はKeyStoneの開発者ではありませんので、以下の定義は私自身がスクリプトを埋め込んで実際に動作検証・調査した内容をまとめたものです。requestPermissionsの用途はまだ公式ドキュメントが存在しないため不明です。分かり次第追記します。

keyStone.ts
/** KeyStone API定義 */
interface KeyStoneAPI {
  getActiveAccount: () => KeyStoneAccountInfo;
  getCurrentNetwork: () => KeyStoneNetworkInfo;
  requestSignTransaction: (transaction: string) => Promise<string>;
  requestSignData: (data: string, options: { description: string }) => Promise<KeyStoneSignedData>;
  refreshActiveAccount: () => KeyStoneAccountInfo;
  refreshNetwork: () => KeyStoneNetworkInfo;
  requestPermissions(permissions: unknown): Promise<void>;
}

/**
 * KeyStone APIへの型安全なアクセス
 */
const getKeyStoneAPI = (): KeyStoneAPI | undefined => {
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
  return (window as any).catapult as KeyStoneAPI | undefined;
};

/** KeyStoneアカウント情報 */
export interface KeyStoneAccountInfo {
  name: string;
  address: string;
  publicKey: string;
  networkType: number;
}

/** KeyStoneネットワーク情報 */
export interface KeyStoneNetworkInfo {
  name: string;
  type: number;
  nodeUrl: string;
  generationHash: string;
  epochAdjustment: number;
}

/** KeyStone署名済データ */
export interface KeyStoneSignedData {
  originalData: string;
  signature: string;
  signerPublicKey: string;
  prefix: string;
}

/**
 * KeyStoneが利用可能かどうか
 */
export const isKeyStoneAvailable = () => getKeyStoneAPI() !== undefined;

/**
 * アクティブなアカウント情報を取得する
 *
 * @returns アカウント情報
 */
export function getActiveAccount(): KeyStoneAccountInfo {
  const api = getKeyStoneAPI();
  if (!api) throw new Error('KeyStone API is not available');
  return api.getActiveAccount();
}

/**
 * ネットワーク情報を取得する
 *
 * @returns ネットワーク情報
 */
export function getCurrentNetwork(): KeyStoneNetworkInfo {
  const api = getKeyStoneAPI();
  if (!api) throw new Error('KeyStone API is not available');
  return api.getCurrentNetwork();
}

/**
 * トランザクションの署名をリクエストする
 *
 * @param transaction トランザクション
 * @returns 署名済みトランザクションHex文字列
 */
export function requestSignTransaction(transaction: string): Promise<string> {
  const api = getKeyStoneAPI();
  if (!api) return Promise.reject(new Error('KeyStone API is not available'));
  return api.requestSignTransaction(transaction);
}

/**
 * データの署名をリクエストする
 *
 * @param data 署名対象データ
 * @param options 署名オプション
 * @returns 署名済データ
 */
export function requestSignData(data: string, options: { description: string }): Promise<KeyStoneSignedData> {
  const api = getKeyStoneAPI();
  if (!api) return Promise.reject(new Error('KeyStone API is not available'));
  return api.requestSignData(data, options);
}

/**
 * アクティブなアカウント情報を更新する
 *
 * @returns アカウント情報
 */
export function refreshActiveAccount(): KeyStoneAccountInfo {
  const api = getKeyStoneAPI();
  if (!api) throw new Error('KeyStone API is not available');
  return api.refreshActiveAccount();
}

/**
 * ネットワーク情報を更新する
 *
 * @returns ネットワーク情報
 */
export function refreshNetwork(): KeyStoneNetworkInfo {
  const api = getKeyStoneAPI();
  if (!api) throw new Error('KeyStone API is not available');
  return api.refreshNetwork();
}

Reactで使う場合のサンプル

App.tsx
import { useEffect, useState } from 'react';
import { PublicKey, Signature, utils } from 'symbol-sdk';
import {
  SymbolFacade,
  SymbolPublicAccount,
  Verifier,
  descriptors,
  generateMosaicAliasId,
  models,
} from 'symbol-sdk/symbol';

import './App.css';
import {
  getActiveAccount,
  getCurrentNetwork,
  isKeyStoneAvailable,
  requestSignData,
  requestSignTransaction,
} from './KeyStone';

function App() {
  const [networkType, setNetworkType] = useState<number | undefined>(undefined);
  const [accountName, setAccountName] = useState<string | undefined>(undefined);
  const [accountAddress, setAccountAddress] = useState<string | undefined>(undefined);
  const [accountPublicKey, setAccountPublicKey] = useState<string | undefined>(undefined);

  const [networkName, setNetworkName] = useState<string | undefined>(undefined);
  const [type, setType] = useState<number | undefined>(undefined);
  const [nodeUrl, setNodeUrl] = useState<string | undefined>(undefined);
  const [generationHash, setGenerationHash] = useState<string | undefined>(undefined);
  const [epochAdjustment, setEpochAdjustment] = useState<number | undefined>(undefined);

  const [signTransactionResult, setSignTransactionResult] = useState<string | undefined>(undefined);

  const [signDataInput, setSignDataInput] = useState<string>('テストデータ');
  const [signDataDescription, setSignDataDescription] = useState<string>('テストデータへの署名');
  const [signDataResult, setSignDataResult] = useState<string | undefined>(undefined);
  const [signDataVerified, setSignDataVerified] = useState<boolean | undefined>(undefined);

  useEffect(() => {
    if (!isKeyStoneAvailable()) return;

    const network = getCurrentNetwork();
    if (network) {
      setType(network.type);
      setNetworkName(network.name);
      setNodeUrl(network.nodeUrl);
      setGenerationHash(network.generationHash);
      setEpochAdjustment(network.epochAdjustment);
    }

    const accountInfo = getActiveAccount();
    if (accountInfo) {
      setAccountName(accountInfo.name);
      setNetworkType(accountInfo.networkType);
      setAccountAddress(accountInfo.address);
      setAccountPublicKey(accountInfo.publicKey);
    }
  }, []);

  const handleSignTransaction = async () => {
    // ネットワーク解決
    const networkName = models.NetworkType.valueToKey(networkType!).toLowerCase();
    const facade = new SymbolFacade(networkName);

    // アクティブアカウントを送信先アカウントとして生成
    const account = new SymbolPublicAccount(facade, new PublicKey(accountPublicKey!));

    // 転送モザイク設定(ネームスペースからモザイクIDを解決)
    const mosaicId = generateMosaicAliasId('symbol.xym');
    const mosaics = [
      new descriptors.UnresolvedMosaicDescriptor(
        new models.UnresolvedMosaicId(mosaicId),
        new models.Amount(10_000000n)
      ),
    ];

    // 平文メッセージ
    const message = new TextEncoder().encode('\0Hello, Symbol!! Hello, KeyStone!!!');

    // 転送トランザクションディスクリプタ生成
    const transferTxDescriptor = new descriptors.TransferTransactionV1Descriptor(
      account.address, // 受信者アドレス
      mosaics, // 転送モザイク
      message // メッセージ
    );
    // 転送トランザクション生成
    const transferTx = facade.createTransactionFromTypedDescriptor(
      transferTxDescriptor, // トランザクションディスクリプタ
      account.publicKey, // 送信者公開鍵
      100, // 手数料係数
      60 * 60 * 2 // 有効期限(秒)
    );

    // KeyStoneへ署名リクエスト
    const result = await requestSignTransaction(utils.uint8ToHex(transferTx.serialize()));
    setSignTransactionResult(result);
  };

  const handleSignData = async () => {
    // KeyStoneへ署名リクエスト
    const result = await requestSignData(signDataInput, {
      description: signDataDescription,
    });
    setSignDataResult(JSON.stringify(result, null, 2));

    // 署名検証
    const verified = new Verifier(new PublicKey(accountPublicKey!)).verify(
      new TextEncoder().encode(result!.prefix + result!.originalData),
      new Signature(result!.signature)
    );
    setSignDataVerified(verified);
  };

  return (
    <>
      <h1>KeyStone React App</h1>
      {isKeyStoneAvailable() ? 'KeyStone Available' : 'KeyStone Not Available'}

      <h2>ネットワーク情報</h2>
      <p>Network Name: {networkName}</p>
      <p>Type: {type}</p>
      <p>Node URL: {nodeUrl}</p>
      <p>Generation Hash: {generationHash}</p>
      <p>Epoch Adjustment: {epochAdjustment}</p>

      <h2>アクティブアカウント情報</h2>
      <p>Network Type: {networkType}</p>
      <p>Account Name: {accountName}</p>
      <p>Account Address: {accountAddress}</p>
      <p>Account Public Key: {accountPublicKey}</p>

      <h2>トランザクション署名リクエスト</h2>
      <button onClick={handleSignTransaction}>署名トランザクション送信</button>
      <br />
      {signTransactionResult && (
        <div
          style={{
            background: '#222',
            color: '#f8f8f2',
            padding: 8,
            borderRadius: 4,
            margin: '8px 0',
          }}
        >
          <h3>署名結果</h3>
          <pre style={{ fontSize: '12px', overflowX: 'auto' }}>{signTransactionResult}</pre>
        </div>
      )}

      <h2>データ署名リクエスト</h2>
      <form
        onSubmit={(e) => {
          e.preventDefault();
          handleSignData();
        }}
        style={{ marginBottom: 8 }}
      >
        <div>
          <label>
            署名データ:
            <input
              type="text"
              value={signDataInput}
              onChange={(e) => setSignDataInput(e.target.value)}
              style={{ width: 300, marginLeft: 8 }}
              placeholder="署名するデータ"
              required
            />
          </label>
        </div>
        <div style={{ marginTop: 4 }}>
          <label>
            説明:
            <input
              type="text"
              value={signDataDescription}
              onChange={(e) => setSignDataDescription(e.target.value)}
              style={{ width: 300, marginLeft: 8 }}
              placeholder="説明"
            />
          </label>
        </div>
        <button type="submit" style={{ marginTop: 8 }}>
          署名データ送信
        </button>
      </form>
      {signDataResult && (
        <div
          style={{
            background: '#222',
            color: '#f8f8f2',
            padding: 8,
            borderRadius: 4,
            margin: '8px 0',
          }}
        >
          <h3>署名結果</h3>
          <pre style={{ fontSize: '12px', overflowX: 'auto' }}>{signDataResult}</pre>
          {signDataVerified !== undefined && <p>署名検証結果: {signDataVerified ? '有効' : '無効'}</p>}
        </div>
      )}
    </>
  );
}

export default App;
5
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
5
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?