CoinCheck API を TypeScript + Node.js で書いてみる

# About

何故か CoinCheck API の example に Node.js が無いので簡単な API クライアントを TypeScript で書いてみました。

https://coincheck.com/ja/documents/exchange/api

# Main Code

各種モデルとインフラストラクチャ（CoinCheck class）を1コードにした形で示します。

```CoinCheck.ts
/**
 * 相場情報（簡易版）
 */
class Ticker {
    last?: number // 最後の取引価格
    bid?: number  // 現在の買い注文の最高価格
    ask?: number // 現在の売り注文の最安価格
    high?: number // 24時間での最高取引価格
    low?: number //  24時間での最安取引価格
    volume?: number // 24時間での取引量
    timestamp?: number // 現在の時刻
}

/**
 * 残高
 */
class Balance {
    success?: boolean // 成功フラグ
    jpy?: number // 日本円の残高
    btc?: number // ビットコインの残高
    jpy_reserved?: number // 未決済の買い注文に利用している日本円の合計
    btc_reserved?: number // 未決済の売り注文に利用しているビットコインの合計
    jpy_lend_in_use?: number // 貸出申請をしている日本円の合計（現在は日本円貸出の機能を提供していません）
    btc_lend_in_use?: number // 貸出申請をしているビットコインの合計（現在はビットコイン貸出の機能を提供していません）
    jpy_lent?: number // 貸出をしている日本円の合計（現在は日本円貸出の機能を提供していません）
    btc_lent?: number // 貸出をしているビットコインの合計（現在はビットコイン貸出の機能を提供していません）
    jpy_debt?: number // 借りている日本円の合計
    btc_debt?: number // 借りている日本円の合計
}

/**
 * 注文結果
 */
class OrderResponse {
    success?: boolean // 成功フラグ
    id?: number // 新規注文のID
    rate?: number // 注文のレート
    amount?: number // 注文の量
    order_type?: string // 注文方法 "buy", "sell"
    stop_loss_rate?: number // 逆指値レート
    pair?: string // 取引ぺア "btc_jpy"
    created_at?: string // 注文の作成日時
}

/**
 * 各残高の増減分
 */
class Funds {
    btc?: number
    jpy?: number
}

/**
 * 注文履歴
 */
class Transaction {
    id?: number // トランザクションID
    order_id?: number // 注文のID
    created_at?: string // 取引が行われた時間
    funds?: Funds // 各残高の増減分
    pair?: string // 取引ペア
    rate?: number // 約定価格
    fee_currency?: string // 手数料の通貨
    fee?: number // 発生した手数料
    liquidity?: string // "T" ( Taker ) or "M" ( Maker )
    side?: string // "sell" or "buy"
}

/**
 * 出金用に登録された銀行口座の一覧を取得
 */
class BankAccount {
    id?: number // ID
    bank_name?: string // 銀行名
    branch_name?: string // 支店名
    bank_account_type?: string // 銀行口座の種類（futsu : 普通口座, toza : 当座預金口座）
    number?: number // 口座番号
    name?: string // 名義
}

/**
 * エラー情報
 */
class CoinCheckError {
    error?: Error // エラー情報
    statusCode?: number // HTTP ステータスコード
    message?: string // CoinCheck サーバメッセージ

    constructor(error?: Error, statusCode?: number, message?: string) {
        this.error = error
        this.statusCode = statusCode
        this.message = message
    }
}

/**
 * コンストラクタ引数
 */
class ConstructorParams {
    accessKey?: string
    secretKey?: string
}

/**
 * CoinCheck API for Node.js
 */
class CoinCheck {
    private crypto
    private request
    private originUrl: string
    private accessKey?: string
    private secretKey?: string

    constructor(params: ConstructorParams) {
        this.crypto = require('crypto')
        this.request = require('request')
        this.originUrl = 'https://coincheck.com/api'
        this.accessKey = params.accessKey
        this.secretKey = params.secretKey
    }

    private apiRequest(path: string, method = 'GET', json?: any): Promise<any> {
        let _this = this
        return new Promise(function(resolve, reject) {
            let accessNonce = new Date().getTime()
            var signature = `${accessNonce}${_this.originUrl}${path}`
            if (json) signature += JSON.stringify(json)
            let accessSignature = _this.crypto.createHmac('sha256', _this.secretKey).update(signature).digest('hex')
            let options = {
                url: `${_this.originUrl}${path}`,
                method: method,
                headers: {
                    'ACCESS-KEY': _this.accessKey,
                    'ACCESS-NONCE': accessNonce,
                    'ACCESS-SIGNATURE': accessSignature,
                    'Content-type': 'application/json',
                },
                json: json,
                data: undefined,
            }
            _this.request(options, (error: any, response: { statusCode: number }, body: any) => {
                if(!error && response.statusCode == 200) {
                    resolve(JSON.parse(body))
                } else {
                    reject(new CoinCheckError(error, response.statusCode, body ? body.error : undefined))
                }
            })
        })
    }

    /**
     * 相場情報（簡易版）
     * @returns 相場情報（簡易版）
     */
    getTicker(): Promise<Ticker> { return this.apiRequest('/ticker') }

    /**
     * 残高を取得
     * @returns 残高（Balance）
     */
    getBalance(): Promise<Balance> { return this.apiRequest('/accounts/balance') }

    /**
     * 最近の取引履歴を取得
     * @returns 履歴（Array<Transaction>）
     */
    async getTransactions() {
        let result = await this.apiRequest('/exchange/orders/transactions')
        return result.transactions as Array<Transaction>
    }

    /**
     * 銀行口座一覧を取得
     * @returns 履歴（Array<BankAccount>）
     */
     async getBankAccounts() {
        let result = await this.apiRequest('/bank_accounts')
        return result.data as Array<BankAccount>
    }

    /**
     * BTC/JPYのレート取得
     * @returns BTC/JPYのレート
     */
    async getBtcJpyRate() {
        let result = await this.apiRequest('/rate/btc_jpy')
        return result.rate as number
    }

    /**
     ビットコインを成行買い
     * @param jpy 購入価格（日本円）
     * @returns 注文結果（OrderResponse）
     */
    buyBTC(jpy: number): Promise<OrderResponse> {
        return this.apiRequest('/exchange/orders', 'POST', {
            pair: 'btc_jpy',
            order_type: 'market_buy',
            market_buy_amount: jpy
        })
    }
}

export { CoinCheck, CoinCheckError }
```

# How to use

```package.json
{
  "name": "cc-test",
  "version": "1.0.0",
  "description": "CoinCheck API Client Test Module",
  "main": "index.js",
  "scripts": {
    "start": "ts-node test.ts",
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "author": "Yoji Suzuki",
  "license": "MIT",
  "dependencies": {
    "request": "^2.88.2",
    "ts-node": "^10.9.1",
    "typescript": "^4.7.4"
  },
  "devDependencies": {
    "@types/node": "^18.7.5"
  }
}
```

```tsconfig.json
{
  "compilerOptions": {
    "module": "CommonJS",
    "strict": true,
  }
}
```

```test.ts
import { CoinCheck, CoinCheckError } from './CoinCheck'

(async() => {
    const cc = new CoinCheck({
        accessKey: "アクセスキー",
        secretKey: "シークレットアクセスキー"
    })
    console.log(`ticker: ${JSON.stringify(await cc.getTicker())}`)
    console.log(`balance: ${JSON.stringify(await cc.getBalance())}`)
    console.log(`transactions: ${JSON.stringify(await cc.getTransactions())}`)
    console.log(`bankAccounts: ${JSON.stringify(await cc.getBankAccounts())}`)
    const btcJpy = await cc.getBtcJpyRate()
    console.log(`BTC/JPY Rate: 1BTC = ¥${btcJpy}, 0.005BTC = ¥${btcJpy * 0.005}`)
    console.log(`order: ${JSON.stringify(await cc.buyBTC(500))}`)
})().catch((reason) => {
    console.error(reason)
})
```

# Setup

`package.json`, `CoinCheck.ts`, `test.ts` を同じディレクトリに入れて `npm install`

# Result

test.ts の accessKey, secretKey にテストしたい口座のものを入力して `npm start` すれば以下のように出力されます。

```
% npm start

> cc-test@1.0.0 start
> ts-node test.ts

ticker: {"last":3217416,"bid":3217561,"ask":3218554,"high":3250000,"low":3168720,"volume":2615.46643772,"timestamp":1660637503}
balance: {"success":true,"jpy":"日本円残高","btc":"ビットコイン残高",...}
transactions: []
bankAccounts: []
BTC/JPY Rate: 1BTC = ¥3218057.5, 0.005BTC = ¥16090.2875
CoinCheckError {
  error: null,
  statusCode: 400,
  message: 'Amount 量が最低量(0.005 BTC)を下回っています'
}
```

`test.ts` では `buyBTC(500)` で ¥500 分の BitCoin を購入しようとしていますが、最低購入価格は 0.005BTC 以上の時価（上記のテスト実行時なら ¥16091 以上）にする必要があるので `400 Bad Request` エラーで失敗して、CoinCheckのサーバから `"Amount 量が最低量(0.005 BTC)を下回っています"` というメッセージが返されました。

# Motivation

Node.js での CoinCheck API の使い方について、幾つか紹介しているサイトがあったのですが、GET のパターンしか書かれていなかったり、パラメタを何故か query-params にしていたりなど、色々謎が多い実装が多く見受けられたので、勉強がてら _「ちゃんと実行できるシンプルな例を書いてみよう」_ と思い立ちコードを書いてみました。

ただし、BTCの最低購入価格が ¥16,000 ぐらいと思っていたよりも高く、 `buyBTC` の正常系や `getTransactions` や `getBankAccounts` のデータ有りのケースはまだ叩けていません。なので、バグっていたらゴメンナサイ。お小遣いに余裕がある時に気が向いたらテストしてみます。

BTC（に限らず暗号通貨全般）は、数年間程度の短い期間で100倍以上になった程度のボラティリティの高さが魅力？（毎日が世界恐慌?）なので、1/100以下に下がることも十分にあり得ると思います。なので、¥500で買えるようになる（1BTC = 10万円ぐらいに暴落する）のを待ってみても良いかもしれません。

個人的に暗号通貨自体の実用性は（ボラティリティが高すぎて）イマイチだと思っているのですが、B2Cの送金とかで銀行APIを使うのと比べればかなり敷居が低いので、その点は良い感じかもしれない...と思っているところです。ですが、そのためにはもっと少額で現物取引できることが望ましいので、1BTC = 1万円ぐらいで安定してくれると良いなぁ。価格変動し難いアルトコインなんかもあるんでしょうけど、アルトコインが無限増殖してしまうと暗号通貨を「通貨」として使う上でネックが生じるので微妙だと考えています。 _（EUR導入以前のヨーロッパのように不便になってしまいそう）_

余談ですが、信用貨幣の価値は発行元（通常は国家）の信用価値により変動しますが、主体が存在しないBTCの場合、価値のボラリティティが無限に増大するのはある意味当然の結果だと思います。暗号通貨を現行の非暗号通貨と同様、先ずは「本位貨幣」（ex: 1BTC = 米1合と交換できる）として流通させ、流通しきったタイミングで「信用貨幣」に切り替える戦略 _（要は米ドルなどと同じ流通戦略）_ なら、もっと安定するのかな？