エアドロタスクの自動化を考える

この記事は 仮想通貨botter Advent Calendar 2024 12日目の掲載記事です。

はじめに

皆様こんにちは、楓♡Cryptoです🍁

私は2023年のQ1ごろまではCEXで裁量トレードする程度だったのですが、Arbitrumのエアドロに関するポストを目にした事をきかっけに活動を始めてから沢山のdAppsやBCGに触れるようになりました。

エアドロを獲得するためにこなす課題(エアドロタスク)の中には、毎日同じことを繰り返すといった類のものがあり、「これって自動化できないかなー」と考える人も多いのかなと思いこの記事を執筆するに至りました。
Botterというとアビトラを思い浮かべる方が殆どかもしれませんが、ジャンルとしては問題ないかと思いました。

そこで今回は自動化に使えるライブラリの紹介と自動操縦ブラウザへのウォレットの適用方法までを解説していきます。
実行環境はNode.jsとします。

※以下の注意書きにご理解いただける方のみ読み進めてください。

注意書き

  1. この記事は「エアドロタスクの自動化を考える」だけの学習を目的とした記事であり推奨はしていません。
    記事内では起動時にウォレット適用するところまでやりたいと思います。

  2. 紹介するライブラリやツールを使用する場合は、サイト規約で禁止されていないか確認してください。書かれていない場合は使ってもいいですか?と連絡を入れましょう。

  3. サーバに過剰な負荷を与える使用方法は絶対にやめましょう。
    1秒に1回のアクセスでアウトだった事例があり、明確な線引きがないため完全自己責任でお願いします。

  4. 記事内のコード利用による損害の一切の責任を負いません。

使用するライブラリ

自動化といえば、少し前は Puppeteer / Node.js や Selenium / Python が有名でしたが、最近ではどちらの実行環境においても Playwright の利用が増えてきているようです。
Puppeteerを開発していた元GoogleチームがMicrosoftに移りPlaywrightを開発しているという流れです。

画像
npmパッケージの利用状況

という事で、Playwrightを使用した方法で解説していきます。

※執筆時当方環境
Node.js:v16.16.0
npm:v9.6.3

環境構築

Node.jsインストール

Node.jsをインストールし、npmコマンドを使用できる状態としてください。

プロジェクトの作成とファイル作成

# プロジェクトフォルダの作成
mkdir crypto_rabbby_test

# フォルダに移動
cd crypto_rabbby_test

# プロジェクトの初期化
npm init -y

# 必要なパッケージのインストール
npm install playwright dotenv adm-zip

# 必要なディレクトリの作成
mkdir rabby_extension
mkdir user_data_dir

# スクリプトファイルの作成
type nul > rabby_dl.js
type nul > rabby_set.js
type nul > rabby_use.js

ファイル構成

今回は分かりやすく
1.rabby_dl.js(Rabby WalletのDL)
2.rabby_set.js(秘密鍵のインポートと設定保存)
3.rabby_use.js(インポートしたウォレットの適用)
の3ファイルに分けて実行します。

crypto_rabbby_test/
  ├── rabby_dl.js            # Rabby Walletダウンロード用スクリプト
  ├── rabby_set.js           # セットアップ用スクリプト
  ├── rabby_use.js           # 使用用スクリプト
  ├── rabby_extension/       # 拡張機能ファイル格納ディレクトリ
  ├── user_data_dir/         # ウォレットインポート後の状態保存ディレクトリ
  ├── node_modules/          # npmパッケージのインストール先
  ├── package.json           # プロジェクトの設定ファイル
  └── package-lock.json      # パッケージのバージョン管理ファイル

Github等での管理を想定していないため、gitignoreは置いてません。
もし使用する場合は下記を記載して user_data_dir 配下は必ず除外してください。

user_data_dir/
node_modules/

Playwrightで使用するブラウザをインストール

npx playwright install chromium

以上でコーディングに移る準備が整いました。

Rabby WalletをDLしよう

ではウォレットをDLするコードを rabby_dl.js に記述しましょう。
実行するとgithubリポジトリからDLし、rabby_extensionに解凍するという事をやっています。

const https = require('https');
const fs = require('fs');
const path = require('path');

function downloadFile(url, filePath) {
    return new Promise((resolve, reject) => {
        const file = fs.createWriteStream(filePath);

        const request = https.get(url, {
            headers: {
                'User-Agent': 'Node.js',
                'Accept': 'application/octet-stream'
            }
        }, (response) => {
            // リダイレクトの処理
            if (response.statusCode === 302 || response.statusCode === 301) {
                file.close();
                fs.unlink(filePath, () => { });
                downloadFile(response.headers.location, filePath).then(resolve).catch(reject);
                return;
            }

            // エラーステータスの処理
            if (response.statusCode !== 200) {
                file.close();
                fs.unlink(filePath, () => { });
                reject(new Error(`Download failed with status code: ${response.statusCode}`));
                return;
            }

            response.pipe(file);

            file.on('finish', () => {
                file.close();
                resolve();
            });
        });

        request.on('error', (err) => {
            file.close();
            fs.unlink(filePath, () => { });
            reject(err);
        });

        file.on('error', (err) => {
            file.close();
            fs.unlink(filePath, () => { });
            reject(err);
        });
    });
}

async function downloadRabbyExtension() {
    console.log('Rabby Wallet拡張機能のダウンロードを開始します...');

    const options = {
        hostname: 'api.github.com',
        path: '/repos/RabbyHub/Rabby/releases/latest',
        headers: {
            'User-Agent': 'Node.js',
            'Accept': 'application/vnd.github.v3+json'
        }
    };

    return new Promise((resolve, reject) => {
        https.get(options, (res) => {
            let data = '';

            res.on('data', (chunk) => data += chunk);
            res.on('end', async () => {
                try {
                    const release = JSON.parse(data);
                    const version = release.tag_name;
                    console.log(`最新バージョン: ${version}`);

                    const rabbyAsset = release.assets.find(asset =>
                        asset.name.toLowerCase().includes('rabby') &&
                        asset.name.toLowerCase().endsWith('.zip')
                    );

                    if (!rabbyAsset) {
                        console.log('\n利用可能なアセット:');
                        release.assets.forEach(asset => {
                            console.log(`- ${asset.name} (${asset.browser_download_url})`);
                        });
                        throw new Error('拡張機能が見つかりませんでした');
                    }

                    const downloadUrl = rabbyAsset.browser_download_url;
                    console.log('\nダウンロードURL:', downloadUrl);

                    // 既存のファイルとディレクトリを削除
                    const zipPath = path.join(process.cwd(), 'rabby.zip');
                    const extensionDir = path.join(process.cwd(), 'rabby_extension');

                    if (fs.existsSync(zipPath)) {
                        fs.unlinkSync(zipPath);
                    }
                    if (fs.existsSync(extensionDir)) {
                        fs.rmSync(extensionDir, { recursive: true, force: true });
                    }
                    fs.mkdirSync(extensionDir, { recursive: true });

                    // ファイルをダウンロード
                    console.log('\nダウンロードを開始します...');
                    await downloadFile(downloadUrl, zipPath);

                    console.log('ダウンロード完了。解凍を開始します...');
                    const AdmZip = require('adm-zip');
                    const zip = new AdmZip(zipPath);
                    zip.extractAllTo(extensionDir, true);

                    // 一時ファイルを削除
                    fs.unlinkSync(zipPath);

                    console.log('Rabby Wallet拡張機能のセットアップが完了しました');
                    resolve();

                } catch (error) {
                    console.error('エラーが発生しました:', error);
                    reject(error);
                }
            });
        }).on('error', (error) => {
            console.error('ネットワークエラーが発生しました:', error);
            reject(error);
        });
    });
}

// スクリプトが直接実行された場合のみ実行
if (require.main === module) {
    downloadRabbyExtension()
        .then(() => process.exit(0))
        .catch(() => process.exit(1));
}

module.exports = downloadRabbyExtension;

書いたらnodeコマンドでファイルを実行してみてください。
エラー無くrabby_extensionディレクトリ内に沢山jsファイルができていれば多分成功してます。

秘密鍵をインポートしよう

.envを使用しなくて良いようあえて途中の入力は手動にしてます。
下記コードを rabby_set.jsに記述します。

const { chromium } = require('playwright');
const path = require('path');

const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms));

async function setupRabbyWallet() {
   const pathToExtension = path.join(process.cwd(), 'rabby_extension');
   const userDataDir = path.join(process.cwd(), 'user_data_dir');

   console.log('ブラウザを起動中...');
   const browser = await chromium.launchPersistentContext(userDataDir, {
       headless: false,
       args: [
           `--disable-extensions-except=${pathToExtension}`,
           `--load-extension=${pathToExtension}`,
       ],
   });

   // 新しいタブが開かれたら閉じる
   browser.on('page', async (newPage) => {
       const pages = await browser.pages();
       if (pages.length > 1) {
           await newPage.close();
       }
   });

   try {
       const pages = await browser.pages();
       const page = pages[0];

       page.setDefaultTimeout(180000);

       // Extension IDの取得
       await page.goto('chrome://extensions/');
       await page.waitForSelector('extensions-manager');

       const rabbyId = await page.evaluate(() => {
           const extensions = document.querySelector('extensions-manager').shadowRoot
               .querySelector('extensions-item-list').shadowRoot
               .querySelectorAll('extensions-item');
           for (const ext of extensions) {
               const name = ext.shadowRoot.querySelector('#name').textContent;
               if (name.includes('Rabby')) {
                   return ext.getAttribute('id');
               }
           }
       });

       if (!rabbyId) {
           throw new Error('Rabby WalletのExtension IDが見つかりませんでした');
       }

       // ウォレットの状態チェック
       console.log('ウォレットの状態を確認中...');
       await page.goto(`chrome-extension://${rabbyId}/index.html`);
       await page.waitForLoadState('domcontentloaded');

       // unlock画面が表示されるかどうかを確認
       const hasUnlockScreen = await page.waitForSelector('text="Unlock"', { timeout: 5000 })
           .then(() => true)
           .catch(() => false);

       if (hasUnlockScreen) {
           console.log('ウォレットは既に設定済みです');
           await page.evaluate(() => {
               alert('ウォレットは既に設定済みです');
           });
           return;
       }

       console.log('新規セットアップを開始します...');
       await page.goto(`chrome-extension://${rabbyId}/index.html#/new-user/guide`);

       // ここから手動で秘密鍵のインポートとパスワードの設定を行います。
       // 設定完了後、Rabby Walletのセットアップが完了しました!というアラートが出るようにしています。

        // "Rabby Wallet is Ready to Use" メッセージの表示を待つ
        await page.waitForSelector('text="Rabby Wallet is Ready to Use"');

        // セットアップ完了のアラートを表示
        await page.evaluate(() => {
            alert('Rabby Walletのセットアップが完了しました!\nブラウザは手動で閉じてください。');
        });

       console.log('セットアップが完了しました。ブラウザは手動で閉じることができます。');

   } catch (error) {
       console.error('エラーが発生しました:', error);
       throw error;
   }
}

if (require.main === module) {
   setupRabbyWallet()
       .then(() => {
           console.log('スクリプトの実行が完了しました。ブラウザは開いたままです。');
       })
       .catch((error) => {
           console.error('セットアップに失敗しました:', error);
       });
}

module.exports = setupRabbyWallet;

書いたら実行してください。
ブラウザが立ち上がってRabby Walletの画面が開いたら秘密鍵やパスワードの入力を行って設定を完了させます。
最後に、「Rabby Walletのセットアップが完了しました!」とアラートが出たら完了なので、ブラウザを閉じます。
この時点でuser_data_dirにcookie情報などを含めたユーザーデータが格納されています。流出しないよう十分注意してください。

自動処理を実行しよう

ここまででウォレットのデータを流用できるようになりましたので、
rabby_use.jsに下記コードを記述します。

const { chromium } = require('playwright');
const path = require('path');

const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms));

async function useRabbyWallet() {
    const pathToExtension = path.join(process.cwd(), 'rabby_extension');
    const userDataDir = path.join(process.cwd(), 'user_data_dir');

    console.log('ブラウザを起動中...');
    const browser = await chromium.launchPersistentContext(userDataDir, {
        headless: false,
        args: [
            `--disable-extensions-except=${pathToExtension}`,
            `--load-extension=${pathToExtension}`,
        ],
    });

    try {
        const pages = await browser.pages();
        const page = pages[0];

        // Extension IDの取得
        await page.goto('chrome://extensions/');
        await page.waitForSelector('extensions-manager');

        const rabbyId = await page.evaluate(() => {
            const extensions = document.querySelector('extensions-manager').shadowRoot
                .querySelector('extensions-item-list').shadowRoot
                .querySelectorAll('extensions-item');
            for (const ext of extensions) {
                const name = ext.shadowRoot.querySelector('#name').textContent;
                if (name.includes('Rabby')) {
                    return ext.getAttribute('id');
                }
            }
        });

        // Rabby Walletを開く
        await page.goto(`chrome-extension://${rabbyId}/index.html`);
        
        // アラートでパスワード入力を促す
        await page.evaluate(() => {
            alert('パスワードを入力してウォレットをアンロックしてください。');
        });

        // アンロック完了(.amount-numberが表示される)を待つ
        await page.waitForSelector('.amount-number');
        console.log('ウォレットのアンロックが完了しました');


        await sleep(100000);
    } catch (error) {
        console.error('エラーが発生しました:', error);
    }
}

if (require.main === module) {
    useRabbyWallet()
        .then(() => {
            console.log('ブラウザを起動しました');
        })
        .catch((error) => {
            console.error('起動に失敗しました:', error);
        });
}

module.exports = useRabbyWallet;

書いたら実行します。
const browser = await chromium.launchPersistentContext(userDataDir, {
の箇所でローカルに保存されたユーザーデータを呼び出しているわけです。
書いてる途中でアンロックのために .env 使わないと完全自動化できない事に気づきましたが公開用としてはこちらの方が良い(コピペ防止)かと思いましたので、そのままにしました。

最後に

わかりづらい個所もあったかと思いますが、ご質問やご感想はXの方にいただけると嬉しいです。お気軽にリプください😊
現在はDEXアビトラを勉強しはじめたところですので、何かネタが生まれれば来年も何か書いてみたいなと思っています。

最後まで読んでいただきありがとうございました。


いいなと思ったら応援しよう!

コメント

ログイン または 会員登録 するとコメントできます。
エアドロタスクの自動化を考える|楓
word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word

mmMwWLliI0fiflO&1
mmMwWLliI0fiflO&1
mmMwWLliI0fiflO&1
mmMwWLliI0fiflO&1
mmMwWLliI0fiflO&1
mmMwWLliI0fiflO&1
mmMwWLliI0fiflO&1