西出正美です。有益なことや無益なことなどいろいろ書いています。

Typescriptでビットコインアドレスを生成する

Typescriptでビットコインアドレスを生成したいこと、ありますよね。
僕はあるので自分用にメモです。[参考文献]

👇このボタンをクリックするとビットコインアドレスを生成します。

ビットコインのアドレスは、32バイトの秘密鍵を以下のようにしてビットコインアドレスに変換します。

secp256k1
base58変換とチェックサム付与
BASE58変換とチェックサム付与
BASE32変換とチェックサム付与
32バイトの秘密鍵
公開鍵
WIF形式秘密鍵
ビットコインアドレス形式
P2PKH形式ビットコインアドレス
Bech32形式ビットコインアドレス

これはTypescriptでも簡単に計算できます。

それを実際に書いてみたのが ↓ この今回のコードです

import { randomBytes, createHash, BinaryLike } from 'crypto';

import { useState, FC, useMemo, useCallback } from 'react';

import { Button, TextField } from '@mui/material';
import { bech32 } from 'bech32';
import { encode } from 'bs58';
import RIPEMD160 from 'ripemd160';
import { publicKeyCreate, privateKeyVerify } from 'secp256k1';

/** 16進数の文字列かどうかチェックする正規表現 */
const check32BytesHex = /^[0-9a-fA-F]{64}$/;

/** 16進数の文字列をUint8Arrayに変換する */
const hexToUint8Array = (hex: string): Uint8Array => Buffer.from(hex, 'hex');

/** Bufferを16進数の文字列に変換する */
const bufferToHex = (data: Buffer): string => data.toString('hex');

/** sha256でハッシュする */
const sha256 = (data: BinaryLike): Buffer =>
  createHash('sha256').update(data).digest();

/** ripemd160でハッシュする */
const ripemd160 = (data: BinaryLike): Buffer =>
  new RIPEMD160().update(data).digest();

/** BASE58チェックサム生成 */
const calcBase58Checksum = (data: BinaryLike): Buffer =>
  sha256(sha256(data)).subarray(0, 4);

/** BASE58変換+チェックサム */
const base58WithChecksum = (data: Uint8Array): string =>
  encode([...data, ...calcBase58Checksum(data)]);

/** ビットコインアドレス(BASE58(P2PKH)形式)生成 */
const genBitcoinAddressP2PKH = (publicKey: Uint8Array): string =>
  base58WithChecksum(Uint8Array.from([0x00, ...ripemd160(sha256(publicKey))]));

/** BASE32変換+チェックサム */
const bech32WithChecksum = (data: Uint8Array) =>
  bech32.encode('bc', [0x00, ...bech32.toWords(data)]);

/** ビットコインアドレス(BASE32(Bech32)形式)生成 */
const genBitcoinAddressBECH32 = (publicKey: Uint8Array): string =>
  bech32WithChecksum(ripemd160(sha256(publicKey)));

/** 秘密鍵(WIF形式)生成 */
const genWif = (privateKey: Uint8Array): string =>
  base58WithChecksum(Uint8Array.from([0x80, ...privateKey]));

const TypescriptBitcoin: FC = () => {
  // 秘密鍵(32バイトの16進数)
  const [hexPrivateKey, setHexPrivateKey] = useState(
    bufferToHex(randomBytes(32))
  );

  /** 秘密鍵(32バイト) */
  const privateKey: Uint8Array | string = useMemo(() => {
    if (!check32BytesHex.test(hexPrivateKey))
      return '秘密鍵が32バイトの16進数ではない';
    return hexToUint8Array(hexPrivateKey);
  }, [hexPrivateKey]);

  /** 公開鍵 */
  const publicKey: Uint8Array | string = useMemo(() => {
    if (typeof privateKey === 'string') return privateKey;
    if (!privateKeyVerify(privateKey)) return '秘密鍵が不適切';
    return publicKeyCreate(privateKey, false);
  }, [privateKey]);

  /** ビットコインアドレス(BASE32(Bech32)形式) */
  const bitcoinAddressBECH32 = useMemo(() => {
    if (typeof privateKey === 'string') return privateKey;
    if (typeof publicKey === 'string') return publicKey;
    return genBitcoinAddressBECH32(publicKey);
  }, [privateKey, publicKey]);

  /** ビットコインアドレス(BASE58(P2PKH)形式) */
  const bitcoinAddressP2PKH = useMemo(() => {
    if (typeof privateKey === 'string') return privateKey;
    if (typeof publicKey === 'string') return publicKey;
    return genBitcoinAddressP2PKH(publicKey);
  }, [privateKey, publicKey]);

  /** 秘密鍵をランダムに生成 */
  const genRamdamKey = useCallback(
    () => setHexPrivateKey(bufferToHex(randomBytes(32))),
    []
  );

  /** 秘密鍵(WIF形式) */
  const wif = useMemo(
    () => genWif(hexToUint8Array(hexPrivateKey)),
    [hexPrivateKey]
  );

  return (
    <>
      <Button onClick={genRamdamKey} variant="outlined">
        秘密鍵をランダムに生成
      </Button>
      <TextField
        error={!check32BytesHex.test(hexPrivateKey)}
        fullWidth
        label="秘密鍵(32バイトの16進数)"
        margin="dense"
        value={hexPrivateKey}
        onChange={(e) => {
          setHexPrivateKey(e.currentTarget.value);
        }}
      />
      <TextField
        fullWidth
        label="秘密鍵(WIF形式)"
        margin="dense"
        value={wif}
        InputProps={{
          readOnly: true,
        }}
      />
      <TextField
        fullWidth
        label="ビットコインアドレス(BASE58(P2PKH)形式)"
        margin="dense"
        InputProps={{
          readOnly: true,
        }}
        value={bitcoinAddressP2PKH}
      />
      <TextField
        fullWidth
        label="ビットコインアドレス(BASE32(Bech32)形式)"
        margin="dense"
        InputProps={{
          readOnly: true,
        }}
        value={bitcoinAddressBECH32}
      />
    </>
  );
};

export { TypescriptBitcoin };
NISHIDEMASAMI.GITHUB.IO
NISHIDE, Masami

西出正美です。有益なことや無益なことなどいろいろ書いています。

©NISHIDE, Masami Some Rights Reserved