👛

XRP Ledger XRPの簡易的なWalletを作成

2022/09/18に公開

今回はWalletの仕組みを知りたいという好奇心で作成しました。

概要

XRP Ledger Test Netに接続してアカウントの作成とXRPの送金を体験できるアプリケーションです。
このアプリケーションはNext.js,TypeScript,axios,xrpl,Tailwindcssを使用しています。
Next.jsのAPIルートを利用して、XRP Ledgerに接続し処理を行うAPIを作成。
そのAPIにaxiosでリクエストを送信して、アカウントの作成やXRPの送金を行います。

XRP Ledgerとは

分散型のパブリックブロックチェーンでリップル社の決済ネットワークです。
今後はスマートコントラクトを実装する案もあります。
トランザクションの処理速度も速く、手数料も安い。

Next.js,TypeScript開発環境の構築

## Next.jsのプロジェクトを作成
yarn create next-app --typescript

## 作成したプロジェクトへ移動
cd [プロジェクト名]

## ローカルサーバー起動
yarn dev

https://nextjs.org/docs/api-reference/create-next-app

Tailwindcssのインストール

下記を参照してください。
https://tailwindcss.com/docs/guides/nextjs

Axiosをインストール

APIにリクエストはAxiosを使います。

yarn add axios

https://axios-http.com/docs/intro

xrpl.jsをインストール

XRP Ledgerへの接続を行うため、クライアントライブラリをインストールします。

yarn add xrpl

https://js.xrpl.org/?_ga=2.166873022.1681270215.1662900366-141466114.1662900366

APIの実装

始めに下記のAPIを実装します。

  • アカウントを作成
  • シードフレーズを使ってアカウントの情報を取得
  • XRPを送金

今回はNext.sjのAPIルートを使用してAPIを実装します。
pages/apiディレクトリの中に新しく各ファイルを作成して記述しました。

https://nextjs.org/docs/api-routes/introduction

アカウントを作成するAPI

pages/api/account.ts
import type { NextApiRequest, NextApiResponse } from "next";
import { Client } from "xrpl";

const handler = async (req: NextApiRequest, res: NextApiResponse) => {
  // XRP Ledger Test Net に接続
  const client = new Client("wss://s.altnet.rippletest.net:51233");
  await client.connect();

  // 新しいウォレットを作成
  const wallet = (await client.fundWallet()).wallet;

  // 残高を取得
  const balance = await client.getXrpBalance(wallet.address);

  // 表示するウォレットのデータを定義
  const newWallet = {
    address: wallet.address,
    publicKey: wallet.publicKey,
    privateKey: wallet.privateKey,
    seed: wallet.seed,
    balance,
  };

  // XRP Ledger Test Net との接続を解除
  await client.disconnect();

  // レスポンスでデータを返す
  res.status(200).json(newWallet);
};

export default handler;

シードフレーズを使ってアカウントの情報を取得するAPI

pages/api/getWallet.ts
import type { NextApiRequest, NextApiResponse } from "next";
import { Client, Wallet } from "xrpl";

const handler = async (req: NextApiRequest, res: NextApiResponse) => {
  // XRP Ledger Test Net に接続
  const client = new Client("wss://s.altnet.rippletest.net:51233");
  await client.connect();

  // seedで自身のアカウントを取得
  const wallet = Wallet.fromSeed(req.body.seed);

  // 残高を取得
  const balance = await client.getXrpBalance(wallet.address);

  // 表示するウォレットのデータを定義
  const currentWallet = {
    address: wallet.address,
    publicKey: wallet.publicKey,
    privateKey: wallet.privateKey,
    seed: req.body.seed,
    balance,
  };

  // XRP Ledger Test Net との接続を解除
  await client.disconnect();

  // レスポンスでデータを返す
  res.status(200).json(currentWallet);
};

export default handler;

XRPを送金するAPI

XRPを送金するAPIの作成を実装
pages/apiディレクトリの中に新しくファイルを作成

pages/api/sendXRP.ts
import type { NextApiRequest, NextApiResponse } from "next";
import { Client, Wallet, xrpToDrops, getBalanceChanges } from "xrpl";

type transferData = {
  seed: string;
  amount: number;
  destination: string;
};

const handler = async (req: NextApiRequest, res: NextApiResponse) => {
  // 受け取った値を定義
  const transferData: transferData = req.body;

  // XRP Ledger Test Net に接続
  const client = new Client("wss://s.altnet.rippletest.net:51233");
  await client.connect();

  // seedで自身のアカウントを取得
  const wallet = Wallet.fromSeed(transferData.seed);

  // トランザクションを準備
  const prepared = await client.autofill({
    TransactionType: "Payment",
    Account: wallet.address, // 送金するの自身のウォレットアドレス
    Amount: xrpToDrops(transferData.amount), // 送金金額
    Destination: transferData.destination, // 送信先のウォレットアドレス
  });

  // 準備されたトランザクションに署名。
  const signed = wallet.sign(prepared);

  //トランザクションを送信し、結果を待ちます。
  const tx = await client.submitAndWait(signed.tx_blob);

  // トランザクションによって生じた変更を取得
  let mateData;
  if (tx.result.meta !== undefined && typeof tx.result.meta !== "string") {
    mateData = JSON.stringify(getBalanceChanges(tx.result.meta), null, 2);
  }

  // 送金後残高
  const balance = await client.getXrpBalance(wallet.address);

  // XRP Ledger Test Net との接続を解除
  await client.disconnect();

  // レスポンスでデータを返す
  res.status(200).json({ balance, mateData });
};

export default handler;

データを取得して表示

次にクライアント側を実装します。
APIで取得した情報をReact HookのuseStateで変更して表示するように実装しています。

pages/index.tsx
import type { NextPage } from "next";
import { useState } from "react";

type Wallet = {
  address: string;
  publicKey: string;
  privateKey: string;
  seed: string;
  balance: string;
};

const Home: NextPage = () => {
  // 初期値
  const initialWallet = {
    address: "",
    publicKey: "",
    privateKey: "",
    seed: "",
    balance: "",
  };

  // ウォレットのデータを入れるstateを作成
  const [Wallet, setWallet] = useState<Wallet>(initialWallet);

  return (
    <main>
      表示内容を記述
    </main>
  );
};

export default Home;


各機能の作成

下記の機能を実装します。

  • アカウントを作成
  • シードフレーズを使ってアカウントの情報を取得
  • XRPを送金

アカウントを作成

pages/index.tsx
  const createAccount: ComponentProps<"form">["onSubmit"] = async (event) => {
    event.preventDefault();

    // リクエストの定義
    const options: AxiosRequestConfig = {
      url: "/api/account",
      method: "POST",
    };

    // 新しいウォレットのデータを取得しstateを変更
    const { data } = await axios(options);
    setWallet(data);
  };

シードフレーズを使ってアカウントの情報を取得

pages/index.tsx
  // seedをを使い既存のウォレットを取得する関数
  const getWallet: ComponentProps<"form">["onSubmit"] = async (event) => {
    event.preventDefault();

    // 入力されたseedの文字列を使ってリクエストを送る
    const seed: string = event.currentTarget.seed.value;
    const options: AxiosRequestConfig = {
      url: "/api/getWallet",
      method: "POST",
      data: {
        seed,
      },
    };

    // 取得したウォレットのデータでstateを変更
    const { data } = await axios(options);
    setWallet(data);
  };

XRPを送金

pages/index.tsx
  // XRPを送金する関数
  const transfer: ComponentProps<"form">["onSubmit"] = async (event) => {
    event.preventDefault();

    // 送金金額を定義
    const amount = event.currentTarget.amount.value;

    // 送金先のアドレスを定義
    const destination = event.currentTarget.destination.value;

    // 自身のseed,金額,送金先のアドレスをリクエストで送信
    const options: AxiosRequestConfig = {
      url: "/api/sendXRP",
      method: "POST",
      data: {
        seed: Wallet?.seed,
        destination,
        amount,
      },
    };

    const { data } = await axios(options);

    // 送金後の残高を取得してstateを更新
    setWallet({ ...Wallet, balance: data.balance });
  };

取得したデータを表示する

pages/index.tsx
  return (
    <main className="text-gray-600 bg-gray-100">
      <div className="container px-5 py-24 mx-auto">

          {/* // アカウントを作成 /// */}
          <div className="p-4 lg:w-1/2 md:w-full  mx-auto ">
            <div className="border p-3 ">
              <h2 className="title-font mb-4 text-3xl font-medium text-gray-900 sm:text-4xl">
                アカウントの作成
              </h2>
              <form onSubmit={createAccount} className="mt-5">
                <button className="mt-5 block rounded border-0 bg-sky-500 py-2 px-6 text-lg text-white hover:bg-sky-600 focus:outline-none">
                  作成
                </button>
              </form>
            </div>
          </div>
          <div className="p-4 lg:w-1/2 md:w-full mx-auto">
            <div className="border p-3">
              <h2 className="title-font mb-4 text-3xl font-medium text-gray-900 sm:text-4xl">
                アカウント
              </h2>
              <p className="mt-5 leading-relaxed">
                seedを入力して既存のアカウントデータを取得する
              </p>
              <form onSubmit={getWallet} className="mt-5">
                <label
                  htmlFor="seed"
                  className="text-sm leading-7 text-gray-600"
                >
                  seed
                </label>
                <input
                  type="text"
                  name="seed"
                  id="seed"
                  className=" w-full rounded border border-gray-300 bg-white py-1 px-3 text-base leading-8 text-gray-700 outline-none transition-colors duration-200 ease-in-out focus:border-sky-500 focus:ring-2 focus:ring-sky-200"
                />
                <button className="mt-5 block rounded border-0 bg-sky-500 py-2 px-6 text-lg text-white hover:bg-sky-600 focus:outline-none">
                  取得
                </button>
              </form>
            </div>
          </div>

          {/* // アカウントを取得 // */}
          <div className="p-4 lg:w-1/2 md:w-full mx-auto">
            <div className="border p-3 ">
              <h2 className="title-font mb-4 text-3xl font-medium text-gray-900 sm:text-4xl">
                アカウント情報
              </h2>
              <dl>
                <div className="mt-6 flex-grow sm:mt-0">
                  <dt className="title-font mb-1 text-xl font-medium text-gray-900">
                    address
                  </dt>
                  <dd className="leading-relaxed"> {Wallet?.address}</dd>
                </div>
                <div className="mt-6 flex-grow sm:mt-0">
                  <dt className="title-font mb-1 text-xl font-medium text-gray-900">
                    publicKey
                  </dt>
                  <dd className="leading-relaxed">
                    <p>{Wallet?.publicKey}</p>{" "}
                  </dd>
                </div>
                <div className="mt-6 flex-grow sm:mt-0">
                  <dt className="title-font mb-1 text-xl font-medium text-gray-900">
                    privateKey
                  </dt>
                  <dd className="leading-relaxed"> {Wallet?.privateKey}</dd>
                </div>
                <div className="mt-6 flex-grow sm:mt-0">
                  <dt className="title-font mb-1 text-xl font-medium text-gray-900">
                    seed
                  </dt>
                  <dd className="leading-relaxed"> {Wallet?.seed}</dd>
                </div>
                <div className="mt-6 flex-grow sm:mt-0">
                  <dt className="title-font mb-1 text-xl font-medium text-gray-900">
                    balance
                  </dt>
                  <dd className="leading-relaxed"> {Wallet?.balance}</dd>
                </div>
              </dl>
            </div>
          </div>

          {/* // XRPを送金 // */}
          <div className="p-4 lg:w-1/2 md:w-full mx-auto">
            <div className="border p-3 ">
              <h2 className="title-font mb-4 text-3xl font-medium text-gray-900 sm:text-4xl">
                XRPの送金
              </h2>
              {Wallet.address !== "" ? (
                <form onSubmit={transfer}>
                  <div>
                    <label
                      htmlFor="destination"
                      className="text-sm leading-7 text-gray-600"
                    >
                      送信先アドレス
                    </label>
                    <input
                      type="text"
                      name="destination"
                      id="destination"
                      className="w-full rounded border border-gray-300 bg-white py-1 px-3 text-base leading-8 text-gray-700 outline-none transition-colors duration-200 ease-in-out focus:border-sky-500 focus:ring-2 focus:ring-sky-200"
                    />
                  </div>
                  <div>
                    <label
                      htmlFor="amount"
                      className="text-sm leading-7 text-gray-600"
                    >
                      送金金額
                    </label>
                    <input
                      type="number"
                      name="amount"
                      id="amount"
                      className="w-full rounded border border-gray-300 bg-white py-1 px-3 text-base leading-8 text-gray-700 outline-none transition-colors duration-200 ease-in-out focus:border-sky-500 focus:ring-2 focus:ring-sky-200"
                    />
                  </div>
                  <button className="mt-5 block rounded border-0 bg-sky-500 py-2 px-6 text-lg text-white hover:bg-sky-600 focus:outline-none">
                    送金
                  </button>
                </form>
              ) : (
                <p>アカウントを取得してください</p>
              )}
            </div>
          </div>

        </div>
    </main>
  );

完成したpages/index.tsx

pages/index.tsx
import type { NextPage } from "next";
import { ComponentProps, useState } from "react";
import axios, { AxiosRequestConfig } from "axios";

type Wallet = {
  address: string;
  publicKey: string;
  privateKey: string;
  seed: string;
  balance: string;
};

const Home: NextPage = () => {
  // 初期値
  const initialWallet = {
    address: "",
    publicKey: "",
    privateKey: "",
    seed: "",
    balance: "",
  };

  // ウォレットのデータを入れるstateを作成
  const [Wallet, setWallet] = useState<Wallet>(initialWallet);

  // アカウントを作成する関数
  const createAccount: ComponentProps<"form">["onSubmit"] = async (event) => {
    event.preventDefault();

    // リクエストの定義
    const options: AxiosRequestConfig = {
      url: "/api/account",
      method: "POST",
    };

    // 新しいウォレットのデータを取得しstateを変更
    const { data } = await axios(options);
    setWallet(data);
  };


  // seedをを使い既存のウォレットを取得する関数
  const getWallet: ComponentProps<"form">["onSubmit"] = async (event) => {
    event.preventDefault();

    // 入力されたseedの文字列を使ってリクエストを送る
    const seed: string = event.currentTarget.seed.value;
    const options: AxiosRequestConfig = {
      url: "/api/getWallet",
      method: "POST",
      data: {
        seed,
      },
    };

    // 取得したウォレットのデータでstateを変更
    const { data } = await axios(options);
    setWallet(data);
  };


  // XRPを送金する関数
  const transfer: ComponentProps<"form">["onSubmit"] = async (event) => {
    event.preventDefault();

    // 送金金額を定義
    const amount = event.currentTarget.amount.value;

    // 送金先のアドレスを定義
    const destination = event.currentTarget.destination.value;

    // 自身のseed,金額,送金先のアドレスをリクエストで送信
    const options: AxiosRequestConfig = {
      url: "/api/sendXRP",
      method: "POST",
      data: {
        seed: Wallet?.seed,
        destination,
        amount,
      },
    };

    const { data } = await axios(options);

    // 送金後の残高を取得してstateを更新
    setWallet({ ...Wallet, balance: data.balance });
  };

  return (
    <main className="text-gray-600 bg-gray-100">
      <div className="container px-5 py-24 mx-auto">

          {/* // アカウントを作成 /// */}
          <div className="p-4 lg:w-1/2 md:w-full  mx-auto ">
            <div className="border p-3 ">
              <h2 className="title-font mb-4 text-3xl font-medium text-gray-900 sm:text-4xl">
                アカウントの作成
              </h2>
              <form onSubmit={createAccount} className="mt-5">
                <button className="mt-5 block rounded border-0 bg-sky-500 py-2 px-6 text-lg text-white hover:bg-sky-600 focus:outline-none">
                  作成
                </button>
              </form>
            </div>
          </div>
          <div className="p-4 lg:w-1/2 md:w-full mx-auto">
            <div className="border p-3">
              <h2 className="title-font mb-4 text-3xl font-medium text-gray-900 sm:text-4xl">
                アカウント
              </h2>
              <p className="mt-5 leading-relaxed">
                seedを入力して既存のアカウントデータを取得する
              </p>
              <form onSubmit={getWallet} className="mt-5">
                <label
                  htmlFor="seed"
                  className="text-sm leading-7 text-gray-600"
                >
                  seed
                </label>
                <input
                  type="text"
                  name="seed"
                  id="seed"
                  className=" w-full rounded border border-gray-300 bg-white py-1 px-3 text-base leading-8 text-gray-700 outline-none transition-colors duration-200 ease-in-out focus:border-sky-500 focus:ring-2 focus:ring-sky-200"
                />
                <button className="mt-5 block rounded border-0 bg-sky-500 py-2 px-6 text-lg text-white hover:bg-sky-600 focus:outline-none">
                  取得
                </button>
              </form>
            </div>
          </div>

          {/* // アカウントを取得 // */}
          <div className="p-4 lg:w-1/2 md:w-full mx-auto">
            <div className="border p-3 ">
              <h2 className="title-font mb-4 text-3xl font-medium text-gray-900 sm:text-4xl">
                アカウント情報
              </h2>
              <dl>
                <div className="mt-6 flex-grow sm:mt-0">
                  <dt className="title-font mb-1 text-xl font-medium text-gray-900">
                    address
                  </dt>
                  <dd className="leading-relaxed"> {Wallet?.address}</dd>
                </div>
                <div className="mt-6 flex-grow sm:mt-0">
                  <dt className="title-font mb-1 text-xl font-medium text-gray-900">
                    publicKey
                  </dt>
                  <dd className="leading-relaxed">
                    <p>{Wallet?.publicKey}</p>{" "}
                  </dd>
                </div>
                <div className="mt-6 flex-grow sm:mt-0">
                  <dt className="title-font mb-1 text-xl font-medium text-gray-900">
                    privateKey
                  </dt>
                  <dd className="leading-relaxed"> {Wallet?.privateKey}</dd>
                </div>
                <div className="mt-6 flex-grow sm:mt-0">
                  <dt className="title-font mb-1 text-xl font-medium text-gray-900">
                    seed
                  </dt>
                  <dd className="leading-relaxed"> {Wallet?.seed}</dd>
                </div>
                <div className="mt-6 flex-grow sm:mt-0">
                  <dt className="title-font mb-1 text-xl font-medium text-gray-900">
                    balance
                  </dt>
                  <dd className="leading-relaxed"> {Wallet?.balance}</dd>
                </div>
              </dl>
            </div>
          </div>

          {/* // XRPを送金 // */}
          <div className="p-4 lg:w-1/2 md:w-full mx-auto">
            <div className="border p-3 ">
              <h2 className="title-font mb-4 text-3xl font-medium text-gray-900 sm:text-4xl">
                XRPの送金
              </h2>
              {Wallet.address !== "" ? (
                <form onSubmit={transfer}>
                  <div>
                    <label
                      htmlFor="destination"
                      className="text-sm leading-7 text-gray-600"
                    >
                      送信先アドレス
                    </label>
                    <input
                      type="text"
                      name="destination"
                      id="destination"
                      className="w-full rounded border border-gray-300 bg-white py-1 px-3 text-base leading-8 text-gray-700 outline-none transition-colors duration-200 ease-in-out focus:border-sky-500 focus:ring-2 focus:ring-sky-200"
                    />
                  </div>
                  <div>
                    <label
                      htmlFor="amount"
                      className="text-sm leading-7 text-gray-600"
                    >
                      送金金額
                    </label>
                    <input
                      type="number"
                      name="amount"
                      id="amount"
                      className="w-full rounded border border-gray-300 bg-white py-1 px-3 text-base leading-8 text-gray-700 outline-none transition-colors duration-200 ease-in-out focus:border-sky-500 focus:ring-2 focus:ring-sky-200"
                    />
                  </div>
                  <button className="mt-5 block rounded border-0 bg-sky-500 py-2 px-6 text-lg text-white hover:bg-sky-600 focus:outline-none">
                    送金
                  </button>
                </form>
              ) : (
                <p>アカウントを取得してください</p>
              )}
            </div>
          </div>

        </div>
    </main>
  );
};

export default Home;

アプリケーションの使い方

1.下記のコマンドでローカルサーバーを起動します

yarn dev
  1. 作成ボタンを押してアカウントを作成します。
    作成したアカウントはseedを使って復元できます。
    使い回しが可能なのでseedをメモして使用してください。
    (送金を体験するためは2つアカウントを作成する必要があります。)

3.XRP送金
送金先のアドレスと送金金額を入力して送金できます。
作成したアカウントで試してみてください。

まとめ

暗号資産のウォレットでは、どのようなデータをやり取りするのか理解することができました。

XRPは送金速度が早くSWIFTの代替になり得る可能性は0ではないと思って情報を追っていく予定です。

今回は簡易的なテストネットでのウォレットの作成でしたが、今後はセキュリティについて多くのことを学習して、本番環境で使用できる安全なウォレットを作成したいと思います。

ご一読いただきありがとうございました。

Discussion