「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;