🧀

【XRPL】DEXでBotを動かす 第2回

2024/04/29に公開

概要

Botを動かすために必要なテストアカウントの作成や設定などを行い、Botを実際に動かしてみる。

第2回の目的

Botを動かす上で必要な設定や操作方法を理解する。

事前準備

開発にはVisual Studio Codeなどのエディタを利用します。自分が好きなエディタで構いませんが、何もエディタがない人はVisual Studio Codeをインストールしてください。

ディレクトリとファイルの作成

botを作成するbotディレクトリを作成してください。そのフォルダの中に下記4ファイルを中身は空で良いので作成してください。

.env.development
.env.production
main.js
XRPLClient.js

ディレクトリ構成

下記のような構成になっていることを確認してください。package.jsonpackage-lock.jsonnode_modulesはまだ作成されていないと思いますが、そのままで問題ありません。

bot
 ├─.env.development ← テスト用の設定ファイルです
 ├─.env.production ← 本番用の設定ファイルです
 ├─main.js ← 基本的な処理はこのファイルに記載します
 ├─XRPLClient.js ← Public APIをコールする処理をこのファイルに記載します
 ├─package.json ← 自動で作成されますが、修正も行います
 ├─package-lock.json ← 自動で作成されます
 └─node_modules ← 自動で作成されます

テストアカウントの発行

XRPLにはテスト用のアカウントが発行できるようになっています。
まずこちらからTestnet用のアカウントを作成しアドレスとシークレットの情報を取得しましょう。このアカウントを使用してDEX上で売買を行います。
※必ずアドレスとシークレット情報はメモ帳などに保存しておいてください

またこちらでテストアカウントのアドレスを入力しアカウントを有効化しておきましょう。
※現在はテストアカウントの発行時に100XRPが付与されるため自動的に有効化がされているようです。botの売買でXRPを利用するのでbithompのfaucetで1000XRPを入手してください。

必要なライブラリのインストール

Node.jsのインストール

xrpl.jsを動かす上でNode.jsの推奨バーションがv14とのことなので公式サイトからVersionを指定してDLする。下の表はあくまで参考程度でお願いします。自分の環境に合わせて選択してください。

x64 x86 ARM64
Windows 32bit 64bit -
Mac - Intel Apple

xrpl.jsのインストール

Visual Studio Codeでbotのフォルダを開き、TERMINALを開きます。

TERMINAL内で下記コマンドを実行する。

npm install xrpl

完了すると、package.jsonpackage-lock.jsonのファイルとnode_modulesのフォルダが作成されていると思います。

dotenvのインストール(本番環境とテスト環境を分けるため)

今回テスト環境でしか動作確認はしませんが、今後本番環境でも動かすことを想定してdotenvというライブラリをインストールします。xrpl.js同様下記コマンドを実行してください。

npm install dotenv

インストールが完了すると、package.jsonにdotenvxrplの情報が記載されているはずです。

コードの作成

事前に作成したファイルに設定内容やコードをコピペしてください。詳細に関しては後程解説します。

package.jsonに追加

package.jsonにプログラムを実行するためのscriptsを追加します。

展開する(Windows)
{
  "scripts": {
    "start:dev": "set NODE_ENV=development && node main.js",
    "start": "set NODE_ENV=production && node main.js"
  },
  "dependencies": {
    "dotenv": "^16.4.5",
    "xrpl": "^3.0.0"
  }
}
展開する(Mac)
{
  "scripts": {
    "start:dev": "NODE_ENV=development node main.js",
    "start": "NODE_ENV=production node main.js"
  },
  "dependencies": {
    "dotenv": "^16.4.5",
    "xrpl": "^3.0.0"
  }
}

.env.developmentに追加

.env.developmentに下記内容をコピペしてください。このファイルはコード内の環境変数になります。

展開する
# testnet環境
XRPL_NETWORK=wss://testnet.xrpl-labs.com
# XRPアドレス
XRPL_ACCOUNT_ADDRESS=ここに作成したテストアカウントのアドレスを入力
# シークレット
XRPL_ACCOUNT_SECRET=ここに作成したテストアカウントのシークレットを入力
# 取引するペア
CURRENCY=USD
# 取引するペアの発行者アドレス(私が発行したIOUのアドレスです)
CURRENCY_ISSUER=rsJpVW1uy9qcv33w69Z3xpV7TttBzTkk5o
# 注文が刺さらない場合に注文をリセットするまでの時間
# (例)「5」と指定した場合は、5分間注文が刺さらなかったら注文をリセットする
# 「0」は注文が刺さるまでリセットされません。「1」は常にリセットされます。
RESET_ORDER_TIME=1

XRPL_ACCOUNT_ADDRESSXRPL_ACCOUNT_SECRETには自分のテストアカウントの情報を入力することを忘れないでください。

main.jsに追加

main.jsに下記コードをコピペしてください。このファイルにはBotの一連の流れが記載されています。

コードを展開する
main.js
const XRPLClient = require('./XRPLClient');
const dotenv = require('dotenv');
const envFile = `.env.${process.env.NODE_ENV.trim()}`;
dotenv.config({ path: envFile });
const client = new XRPLClient();
let isRunning = false;

async function main() {
    try {
        await client.connect();
        await run();
        // 20秒おきにrun関数が実行されます
        setInterval(async function (){
            await run();
        }, 1000 * 20);
    } catch (error) {
        console.error(error);
    }
}

async function run() {

    if (isRunning) {
        return;
    }

    try {
        isRunning = true;

        // 最安値の売り注文を取得
        const askOrderBook = await client.getAskOrderBook();
        const askValue = parseFloat(askOrderBook?.offers[0]?.quality * 1000000).toFixed(12);
        console.log(`Ask Price: ${askValue}`);

        // 最高値の買い注文を取得
        const bitOrderBook = await client.getBitOrderBook();
        const bitValue = parseFloat(1 / bitOrderBook?.offers[0]?.quality * 1000000).toFixed(12);
        console.log(`Bit Price: ${bitValue}`);

        // XRPのレート
        const xrpRate = parseFloat(askValue / 2 + bitValue / 2);
        console.log(`XRP Rate: ${xrpRate}`);

        // XRPの保有量を取得
        const xrpBalance = await client.getXrpBalance();
        console.log(`XRP Balance: ${xrpBalance}`)

        // トークン保有量を取得
        const tokenBalance = parseFloat(await client.getAvailableTokenBalance());
        console.log(`USD Balance: ${tokenBalance}`);

        // 自分の全ての注文を取得
        const myOrder = await client.getOrders();

        // (XRP/USD,USD/XRP)ペアのみを取得
        const offers = currentOffers(myOrder);
        if (offers.length > 0) {
            await cancelOrder(tokenBalance, offers);
        }

        if (await isOrderCreatable()) {
            console.log("注文開始");
            {
                // XRPの売り注文
                let workRate = xrpRate;
                let totalBarance = (xrpBalance * xrpRate) + tokenBalance;
                let xrpDiff = (xrpBalance * xrpRate) - (totalBarance / 1.5);
                const e = 0.6;
                workRate = round(workRate * (1 + e / 100), 6);
                let workDifference = ((((xrpBalance * workRate) + tokenBalance) - totalBarance) / 1.5) + (xrpDiff / 2);
                if (workDifference > 0 && (workDifference / workRate > 0.0001)) {
                    const lot = round(workDifference / workRate, 6);
                    if (lot * workRate > totalBarance * 0.00095) {
                        await client.sellOrder(lot.toString(), round(lot * workRate, 6).toString());
                    }
                }
            }
            {
                // XRPの買い注文
                let workRate = xrpRate;
                let totalBarance = (xrpBalance * xrpRate) + tokenBalance;
                let xrpDiff = (totalBarance / 1.5) - (xrpBalance * xrpRate);
                const e = 0.6;
                workRate = round(workRate / (1 + e / 100), 6);
                let workDifference = ((totalBarance - ((xrpBalance * workRate) + tokenBalance)) / 1.5) + (xrpDiff / 2);
                if (workDifference < tokenBalance && (workDifference / workRate > 0.0001)) {
                    const lot = round(workDifference / workRate, 6);
                    if (lot * workRate > totalBarance * 0.00095) {
                        await client.buyOrder(lot.toString(), round(lot * workRate, 6).toString());
                    }
                }
            }
        }
    } finally {
        isRunning = false;
    }
}

let tmpTokenBalance = 0;
async function cancelOrder(tokenBalance, offers) {
    // USDの保有量が変わった場合か、指定された時間(分)に注文をリセット
    if (tmpTokenBalance != tokenBalance || isResetOrder()) {
        for (const offer of offers) {
            await client.cancelOrder(offer);
        }
        tmpTokenBalance = tokenBalance;
    }
}

async function isOrderCreatable() {
    const myOrder = await client.getOrders();
    const offers = currentOffers(myOrder);
    return offers.length == 0;
}

function currentOffers(order) {
    let offerReturnObj = [];
    if (order.offers != undefined) {
        for (let j = 0; j < order.offers.length; j++) {
            if (typeof order.offers[j].taker_gets == "object") {
                if (order.offers[j].taker_gets.currency == process.env.CURRENCY &&
                    order.offers[j].taker_gets.issuer == process.env.CURRENCY_ISSUER) {
                    offerReturnObj.push(order.offers[j])
                }
            } else if (typeof order.offers[j].taker_pays == "object") {
                if (order.offers[j].taker_pays.currency == process.env.CURRENCY &&
                    order.offers[j].taker_pays.issuer == process.env.CURRENCY_ISSUER) {
                    offerReturnObj.push(order.offers[j])
                }
            }
        }
    }
    return offerReturnObj;
}

function isResetOrder() {
    const now = new Date();
    const minutes = now.getMinutes();
    return minutes % process.env.RESET_ORDER_TIME === 0;
}

function round(num, del) {
    if (num == 0) {
        return 0
    } else {
        return Math.round(num * Math.pow(10, del)) / Math.pow(10, del)
    }
}

main();

XRPLClient.jsに追加

XRPLClient.jsに下記コードをコピペしてください。このクラスファイルはクライアントライブラリのPublic APIを利用し、Botで動かす上で必要なメソッドとしてまとめています。

コードを展開する
XRPLClient.js
const xrpl = require('xrpl');

class XRPLClient {
  // インスタンス生成時にネットワーククライアントを定義し、シークレット情報からウォレット情報を取得します
  constructor() {
    this.client = new xrpl.Client(process.env.XRPL_NETWORK);
    this.wallet = xrpl.Wallet.fromSecret(process.env.XRPL_ACCOUNT_SECRET)
    this.isConnected = false;
  }
  // XRPLに接続
  async connect() {
    try {
      await this.client.connect();
      this.isConnected = true;
      console.log('Connected to XRPL');
    } catch (error) {
      console.error('Failed to connect to XRPL:', error);
      throw error;
    }
  }
  // XRPLから切断
  async disconnect() {
    try {
      await this.client.disconnect();
      this.isConnected = false;
      console.log('Disconnected from XRPL');
    } catch (error) {
      console.error('Failed to disconnect from XRPL:', error);
      throw error;
    }
  }
  // 自分がオーダーしたすべての注文を取得
  async getOrders() {
    const response = await this.client.request({
      command: 'account_offers',
      account: process.env.XRPL_ACCOUNT_ADDRESS
    });
    return response.result;
  }
  // 売り注文のオーダーブックを取得
  async getAskOrderBook() {
    try {
      const response = await this.client.request({
        command: 'book_offers',
        taker_gets: {
          currency: 'XRP'
        },
        taker_pays: {
          currency: process.env.CURRENCY,
          issuer: process.env.CURRENCY_ISSUER
        },
        limit: 1
      })
      return response.result
    } catch (error) {
      console.error('Failed to request ask order book:', error);
      return [];
    }
  }
  // 買い注文のオーダーブックを取得
  async getBitOrderBook() {
    try {
      const response = await this.client.request({
        command: 'book_offers',
        taker_gets: {
          currency: process.env.CURRENCY,
          issuer: process.env.CURRENCY_ISSUER
        },
        taker_pays: {
          currency: 'XRP'
        },
        limit: 1
      })
      return response.result
    } catch (error) {
      console.error('Failed to request bit order book:', error);
      return [];
    }
  }
  // 自分のアカウントのXRP残高を取得
  async getXrpBalance() {
    const response = await this.client.getXrpBalance(
      process.env.XRPL_ACCOUNT_ADDRESS,
      { ledger_index: "validated"},
    );
    return response - 10;
  }
  // 自分のアカウントでトラストラインを引いたトークンの中から取引するトークンの残高を取得
  async getAvailableTokenBalance() {
    const response = await this.client.request({
      command: 'account_lines',
      account: process.env.XRPL_ACCOUNT_ADDRESS
    });
    if (response.result.lines.length > 0) {
      for (let i = 0; i < response.result.lines.length; i++) {
        if (response.result.lines[i].currency == process.env.CURRENCY &&
          response.result.lines[i].account == process.env.CURRENCY_ISSUER) {
          return response.result.lines[i].balance;
        }
      }
    }
    return 0;
  }
  // 注文をキャンセルする
  async cancelOrder(offer) {
    try {
      await this.client.submit({
        TransactionType: 'OfferCancel',
        Account: process.env.XRPL_ACCOUNT_ADDRESS,
        OfferSequence: offer.seq
      }, { wallet: this.wallet })
      console.log(`Succcess cancel order seq: ${offer.seq}`);
    } catch (error) {
      console.error('Failed to request cancel order:', error);
      // throw error;
    }
  }
  // 買い注文をオーダーする
  async buyOrder(xrpAmount, currencyAmount) {
    try {
      await this.client.submit({
        TransactionType: "OfferCreate",
        Account: process.env.XRPL_ACCOUNT_ADDRESS,
        Flags: 0,
        TakerGets: {
          currency: process.env.CURRENCY,
          issuer: process.env.CURRENCY_ISSUER,
          value: currencyAmount
        },
        TakerPays: xrpl.xrpToDrops(xrpAmount)
      }, { wallet: this.wallet })
      console.log(`Succcess buy order. xrpAmount: ${xrpAmount}, usdAmount: ${currencyAmount}`);
    } catch (error) {
      console.error('Failed to request buy order:', error);
      // throw error;
    }
  }
  // 売り注文をオーダーする
  async sellOrder(xrpAmount, currencyAmount) {
    try {
      await this.client.submit({
        TransactionType: "OfferCreate",
        Account: process.env.XRPL_ACCOUNT_ADDRESS,
        Flags: 0,
        TakerGets: xrpl.xrpToDrops(xrpAmount),
        TakerPays: {
          currency: process.env.CURRENCY,
          issuer: process.env.CURRENCY_ISSUER,
          value: currencyAmount
        }
      }, { wallet: this.wallet })
      console.log(`Succcess sell order. xrpAmount: ${xrpAmount}, usdAmount: ${currencyAmount}`);
    } catch (error) {
      console.error('Failed to request sell order:', error);
      // throw error;
    }
  }
}
module.exports = XRPLClient;

トラストラインを引く

実際にBotを動かしたいところですが、このBotはXRPと私がテストネットで発行したIOU(USD)を売買するBotになります。なのでテストアカウントでIOUのトラストラインを引かなければ売買を行うことができません。トラストラインの引き方はこちらをご確認ください。
※トラストラインを引くアドレスは下記になります。

rsJpVW1uy9qcv33w69Z3xpV7TttBzTkk5o

Botを動かしてみよう

準備が整ったので実際にBotを動かしてみましょう。下記コマンドをTERMINALに打つと20秒おきにDEX上の価格を取得し売買のオファーを出します。

npm run start:dev

TERMINALを見ると下記のような情報が流れてきたら成功です。おめでとうございます!
XRPは1000XRP以上テストアカウントに存在しますが、USDは保有していません。そのためXRPの売り注文のみがオファーとして出されています。
プログラムを止める場合はTERMINAL上でCtrl+cを押してください。

最後に

次回の第3回はBotで利用したxrpl.jsのクライアントライブラリのPublic APIに関しての解説になります。

Discussion