lesson-1_ウォレットを作成しよう
👛 HD ウォレットについて
このセクションでは、HDウォレットと呼ばれる種類のウォレットを構築します。HDとは階層的決定性(Hierarchy Deterministic)の略です。
これは簡単に言うと、1つのシードからマスタキーとなる秘密鍵を生成し、そこから木構造のような階層的に複数の派生秘密鍵と派生公開鍵及びアドレスを生成します。
最初の秘密鍵は、シードと呼ばれるランダムな文字列から作ります。その後は作った秘密鍵をシードとして新たな秘密鍵を階層的に作ることができます。
シードは覚えにくい文字列ですので、シードから「シードフレーズ」や「リカバリーフレーズ」などと呼ばれる、12個から24個の単語に変換して記録しておく方法が使われています。
参考: 暗号資産におけるウォレットとは ② 〜HD ウォレット編〜
⏬ BIP39 ライブラリを追加する
ここからは、components/GenerateWallet/index.js
ファイルを更新してGenerateWalletコンポーネントを作成していきます。
フレーズを生成するには、決定論的なキーのフレーズ生成の標準を設定したBIP39仕様
を満たす外部ライブラリを活用する必要があります。
JavaScriptには BIP39
と呼ばれるライブラリがあるのでこれを利用していきましょう。
BIP39
ライブラリは、フレーズを生成し、それをSolanaウォレットキーの生成に必要なシードに変換するために必要な機能を提供してくれます。
BIP39
ライブラリをnpm install
する
npm install bip39@^3.1.0
- importする
ライブラリのインストールが完了したら、 ファイルの先頭でライブラリを読み込みましょう。
import * as bip39 from "bip39";
🏭 ニーモニックフレーズを生成する
BIP39
にはニーモニックフレーズを生成するためのメソッドgenerateMnemonic
があります。これを呼び出し、変数に格納してみましょう。
const generatedMnemonic = bip39.generateMnemonic();
これにより、ユーザーがメモして安全に保管できるように、ニーモニックフレーズを設定し、表示することができます。フレーズそれ自体で、その所持者はそのフレーズに一致するアカウントにアクセスすることが可能になります。
🔐 キーペアとシード
ブロックチェーン上のアカウントに接続する前に、このニーモニックフレーズをブロックチェーンが理解できる形に変換する必要があります。ニーモニックフレーズは、長くて古風な数字を、より人間に近い形に変換する抽象的なものなのです。
@solana/web3.js
ライブラリがキーペアオブジェクトを生成するためにこのフレーズを使用できるように、フレーズをバイトに変換する必要があります。キーペアは、データを暗号化できる公開キーとデータを復号化できる秘密キーからなるウォレットアカウントとなります。
@solana/web3.js
ドキュメントを確認すると、Keypair
クラスが "An account keypair used for signing transactions." として定義されていることがわかります。これはまさに、ニーモニックフレーズを使用 して生成する必要があるものです。
またドキュメントを読むと、 Keypair
クラスには、32バイトのシードからKeypair
を生成するfromSeed
メソッドがあることがわかります。そして、シードはUint8Array
である必要があります。つまり、ニーモニックフレーズをUint8Array
に変換する方法が必要だということです。
BIP39
ライブラリに戻ると、mnemonicToSeedSync(mnemonic)
というメソッドがあり、16進数のリストのようなBuffer
オブジェクトが返されます。このメソッドを実行し、生成したニーモニックを渡すことで、テストすることができます。
const seed = bip39.mnemonicToSeedSync(generatedMnemonic);
console.log(seed);
// > Uint8Array(64)
ゴールまでもう少しです!🥭
Keypair
クラスは32バイトのUint8Array
を必要としますが、現在は64バイトのUint8Array
を取得しています。シードをsliceして、最初の32バイトだけを保持するようにしましょう。
const seed = bip39.mnemonicToSeedSync(generatedMnemonic).slice(0, 32);
console.log(seed);
// > Uint8Array(32)
正しい形式のシードがあれば、Keypair
のfromSeed
メソッドを使って、アカウントのキーペアを生成することができます。
const newAccount = Keypair.fromSeed(new Uint8Array(seed));
console.log("newAccount", newAccount.publicKey.toString());
// > ランダムな文字列
👛 ウォレット生成関数を定義する
これまでの説明を踏まえて、ウォレットを生成するための関数generateWallet
を定義します。それでは、components/GenerateWallet/index.js
を更新していきましょう。
まずは、下記のインポート文を追加します。
import { Keypair } from "@solana/web3.js";
import { useState } from "react";
次に、export default function GenerateWallet() {
の下に下記のコードを追加します。
const generateWallet = () => {
const generatedMnemonic = bip39.generateMnemonic();
// ニーモニックフレーズを使用して、シードを生成します。
const seed = bip39.mnemonicToSeedSync(generatedMnemonic).slice(0, 32);
// シードを使用して、アカウントを生成します。
const newAccount = Keypair.fromSeed(new Uint8Array(seed));
setMnemonic(generatedMnemonic);
setAccount(newAccount);
};
generateWallet
関数では、ニーモニックフレーズとアカウントの生成を行ってます。
また、生成したニーモニックフレーズとアカウントは、useState
を用いて値を保持します。ニーモニックフレーズは、GenerateWallet
コンポーネント内でのみ表示するため、mnemonic
という状態変数に格納します。アカウントは、複数のコンポーネント間で共有したいので、Home
コンポーネント内で状態変数を定義し、各コンポーネントに必要なものを引数で渡す形にしましょう。
GenerateWalletコンポーネントの引数にsetAccount
を記述し、export default function GenerateWallet() {
の直下に、mnemonic
を保持する状態変数を定義しましょう。
// `{ setAccount }`を引数に追加
export default function GenerateWallet({ setAccount }) {
// 下記を追加
const [mnemonic, setMnemonic] = useState(null);
🎨 ウォレット生成ボタンをレンダリングする
さきほど定義したgenerateWallet
関数を呼び出すためのボタンを用意しましょう。return文を下記のコードで更新してください。
return (
<>
<button
className="p-2 my-6 text-white bg-indigo-500 focus:ring focus:ring-indigo-300 rounded-lg cursor-pointer"
onClick={generateWallet}
>
ウォレットを生成
</button>
{mnemonic && (
<>
<div className="mt-1 p-4 border border-gray-300 bg-gray-200">
{mnemonic}
</div>
<strong className="text-xs">
このフレーズは秘密にして、安全に保管してください。このフレーズが漏洩すると、誰でもあなたの資産にアクセスできてしまいます。
<br />
オンライン銀行口座のパスワードのようなものだと考えてください。
</strong>
</>
)}
</>
);
✅ コンポーネントの動作確認
GenerateWallet
コンポーネントの実装が完了したので、テストスクリプトを実行してみましょう。
簡単にテスト内容を説明します。components/GenerateWallet/index.test.js
では、期待するボタンがレンダリングされるか、ボタンを押したときに適切な関数が実行されるかをテストしています。
- 期待するボタンがレンダリングされるか
/** テスト内容 */
it("should exist generate wallet button", () => {
/** 準備 */
/** コンポーネントをレンダリングする */
render(<GenerateWallet />);
/** 実行 */
/** 「ウォレットを生成」ボタン要素を取得する */
const btnElement = screen.getByRole("button", {
name: /ウォレットを生成/i,
});
/** 確認 */
/** ボタン要素がドキュメントのボディに存在するかどうか(レンダリングされたか)を確認する */
expect(btnElement).toBeInTheDocument();
});
- ボタンを押したときに適切な関数が実行されるか
it("should implement generate wallet flow", async () => {
/** 準備 */
/** GenerateWalletコンポーネントに渡すモック関数を作成する */
const mockedSetAccount = jest.fn();
/** 「ウォレットを生成」ボタンを押したときに実行される関数の戻り値にダミーの値を設定する */
bip39.generateMnemonic.mockImplementation(() => dummyMnemonic);
bip39.mnemonicToSeedSync.mockImplementation(() => dummySeed);
jest.spyOn(Keypair, "fromSeed").mockImplementation(() => dummyAccount);
render(<GenerateWallet setAccount={mockedSetAccount} />);
const btnElement = screen.getByRole("button", {
name: /ウォレットを生成/i,
});
/** 実行 */
/**「ウォレットを生成」ボタンをクリックする*/
await userEvent.click(btnElement);
/** 確認 */
/** bip39.generateMnemonic関数が呼ばれたか */
expect(bip39.generateMnemonic).toBeCalled();
/** 期待する値がドキュメントのボディに存在するか */
expect(await screen.findByText(dummyMnemonic)).toBeInTheDocument();
/** Keypair.fromSeed関数が、引数にdummyUint8ArraySeedを渡されて呼ばれたか*/
expect(Keypair.fromSeed).toBeCalledWith(dummyUint8ArraySeed);
});
モック(Mock)という言葉は、実際のものや状況を「模倣」するものを指します。
テストにおいては、実際のオブジェクトや関数の代わりに使用される模擬的なオブジェクトや関数を指します。上記のテストスクリプトでは、コンポーネントに渡す引数・Bip39モジュールの関数をモックしています。これにより、テスト対象のコードとそれ以外の部分(コンポーネントの外から渡されるデータや外部モジュールなど)を分離し、テスト対象のコードのみを独立してテストできるようになります。
例えば、BIP39ライブラリのgenerateMnemonicは毎回ランダムなニーモニックフレーズを返しますが、これではテスト時にどのような値が返ってくるかわからないため確認が困難となります。そこで、関数をモックしてテスト時の戻り値を設定することで動作確認が容易となります。関数の動作自体がテスト結果に影響を与えることを回避できるためです。
それでは、テストスクリプトを実行してみましょう。ターミナル上で下記を実行します。
npm run test
components/GenerateWallet/index.test.jsがPASS
していることを確認できたらOKです!
🖥 生成したウォレットアドレスを表示する
それでは、作成したGenerateWallet
コンポーネントをHome
コンポーネントに組み込みましょう。ここからは、pages/index.js
ファイルの編集になります。
まずは、GenerateWallet
コンポーネントをインポートしましょう。
import GenerateWallet from "../components/GenerateWallet";