Etherscan APIを使って新規発行トークンを探索できるのか？

仮想通貨について、以前からやりたかったこと。

- ブロックチェーンは透明性が高いので、デプロイされたばかりの新規トークンを発見することはできるのか？

これができると、色々と夢がありそうです。
実際の売買やエアドロップは`Telegram`や`Discord`で情報を得る必要があると思いますが、できれば自分で発見したいです。

この記事では、`Etherscan API`を利用してEthereumネットワーク上に新しくデプロイされたトークンを探索してみます。

# 要件

Etherscan APIを使用して、以下の手順で新規トークンを探索します。

**1. 最新のブロック番号を取得**
Etherscan APIを使用して最新のブロック番号を取得。

**2. 最後に処理したブロック番号を読み込む**
last_block.txtファイルから最後に処理したブロック番号を読み込む。
ファイルが存在しない場合は、Noneを返す。

**3. 最新のブロック番号との差分を計算し、未処理のブロックを処理対象にする**
初回実行時（last_blockがNoneの場合）は、最新ブロック番号の10ブロック前から処理を開始する。

**4. 各未処理ブロックのトランザクションを取得**
各未処理のブロック番号に対し、Etherscan APIを使用してトランザクションデータを取得する。

**5. コントラクト作成トランザクションを検出する**
各トランザクションのtoアドレスがNoneの場合、そのトランザクションがコントラクト作成であると判断する。

**6. コントラクトアドレスに対してtotalSupplyを取得する**
検出したコントラクトアドレスについて、Etherscan APIを使用してtotalSupplyメソッドを呼び出す。

**7. トークンコントラクトである可能性を判定する**
totalSupplyが0より大きい場合、新しいトークンコントラクトである可能性が高いと判断し、コントラクトアドレスやtotalSupplyを出力する。

**8. 処理したブロック番号を保存する**
処理が完了したブロック番号をlast_block.txtファイルに保存する。

**9. 一定時間待機して再実行する**
指定された間隔（INTERVAL秒）で待機し、手順1から繰り返す。


# スクリプト
**必要なライブラリ:**

```bash
pip install requests
```

**コード:**

```python:token_scan.py
import requests
import time
import os

# Etherscan APIキー（取得用のリンクは後述）
ETHERSCAN_API_KEY = "xxxxxxxxxx"

# 監視間隔 (秒) - 10秒ごとにブロックをチェック
INTERVAL = 10

# 最後に確認したブロック番号を保存するファイル
LAST_BLOCK_FILE = "last_block.txt"


def get_latest_block():
    """
    最新のブロック番号を取得します。

    Returns:
        int: 最新のブロック番号 (10進数)。エラーが発生した場合は None。
    """
    url = f"https://api.etherscan.io/api?module=proxy&action=eth_blockNumber&apikey={ETHERSCAN_API_KEY}"
    try:
        # Etherscan APIにリクエストを送信
        response = requests.get(url)  
        response.raise_for_status()
        data = response.json()
        # レート制限対策 - Etherscan APIへのリクエスト間隔を1秒空ける
        time.sleep(1)

        # レスポンスが有効な形式であることを確認
        if data["jsonrpc"] == "2.0" and data["id"] is not None and data["result"]:
            # 16進数のブロック番号を10進数に変換して返す
            return int(data["result"], 16)
        else:
            print(f"Error getting latest block: Unexpected response format: {data}")
            return None
    except requests.exceptions.RequestException as e:
        # リクエストエラーが発生した場合のエラーメッセージ
        print(f"Error getting latest block: {e}")
        return None


def get_block_transactions(block_number):
    """
    指定されたブロックに含まれるトランザクションを取得します。

    Args:
        block_number (int): ブロック番号 (10進数)。

    Returns:
        list: トランザクションのリスト。エラーが発生した場合は None。
    """
    url = f"https://api.etherscan.io/api?module=proxy&action=eth_getBlockByNumber&tag=0x{hex(block_number)[2:]}&boolean=true&apikey={ETHERSCAN_API_KEY}"
    try:
        # Etherscan APIにリクエストを送信
        response = requests.get(url)
        response.raise_for_status()
        data = response.json()
        # レート制限対策 - Etherscan APIへのリクエスト間隔を1秒空ける
        time.sleep(1)

        # レスポンスが有効な形式であることを確認
        if data["jsonrpc"] == "2.0" and data["id"] is not None and data["result"]:
            return data["result"]["transactions"]
        else:
            print(f"Error getting block transactions: Unexpected response format: {data}")
            return None
    except requests.exceptions.RequestException as e:
        print(f"Error getting block transactions: {e}")
        return None


def check_for_new_tokens(transactions, block_number):
    """
    トランザクションリストから新しいトークンコントラクトを探します。

    Args:
        transactions (list): トランザクションのリスト。
        block_number (int):  チェック対象のブロック番号。
    """
    # 処理中のブロック番号を表示
    print(f"Checking block {block_number}")
    from_to_count = 0

    for tx in transactions:
        # 数が多いので、5件以上出力したらループを抜ける
        if from_to_count >= 5:
            break

        from_addr = None
        to_addr = None

        # fromアドレスとtoアドレスを最大5つまで表示
        if "from" in tx:
            from_addr = tx['from']

        if "to" in tx and tx["to"] is not None:
            to_addr = tx['to']

        # 新しいコントラクトが作成されたトランザクション(toアドレスがNone)を検出
        if tx["to"] is None and "contractAddress" in tx:
            # 新しいコントラクトのアドレスを取得
            contract_address = tx["contractAddress"]

            try:
                totalSupply_url = f"https://api.etherscan.io/api?module=proxy&action=eth_call&to={contract_address}&data=0x18160ddd&apikey={ETHERSCAN_API_KEY}"
                #totalSupplyの取得をEtherscan APIで行う
                totalSupply_response = requests.get(totalSupply_url)
                totalSupply_data = totalSupply_response.json()

                # API呼び出しが成功したことを確認
                if totalSupply_data.get("status") == "1":
                    total_supply_hex = totalSupply_data["result"]
                    # totalSupplyが0x(空)の場合のエラー処理
                    if total_supply_hex == "0x":
                        total_supply = 0
                    else:
                        total_supply = int(total_supply_hex, 16)

                    # totalSupplyが0より大きい場合、新しいトークンとして表示
                    if total_supply > 0:
                        print(
                            f"Potential new token contract (totalSupply={total_supply}): {contract_address} from: {tx.get('from')}")
                else:
                     print(f"Error getting totalSupply for {contract_address}: {totalSupply_data.get('message', 'Unknown error')}")
            # 予期せぬエラーが発生した場合の処理
            except Exception as e:
                print(f"Error checking totalSupply for {contract_address}: {e}")

        # コントラクト作成トランザクションだがcontractAddressがない場合
        elif tx["to"] is None:
            # print(f"Contract creation transaction without contractAddress: {tx['hash']} from: {tx.get('from')}")
            pass

        # 通常のトランザクションの場合、fromとtoアドレスを表示
        elif from_addr is not None and to_addr is not None:
            print(f"Regular transaction: from: {from_addr} to: {to_addr}")
            from_to_count += 1


def load_last_block():
    """最後に処理したブロック番号を読み込みます。"""
    try:
        with open(LAST_BLOCK_FILE, "r") as f:
            return int(f.read())
    except FileNotFoundError:
        return None


def save_last_block(block_number):
    """最後に処理したブロック番号を保存します。"""
    with open(LAST_BLOCK_FILE, "w") as f:
        f.write(str(block_number))


if __name__ == "__main__":
    while True:
        # 最新のブロック番号を取得
        latest_block = get_latest_block()
        # 取得に失敗した場合はcontinue
        if latest_block is None:
            continue

        # 最後に処理したブロック番号を読み込む
        last_block = load_last_block()

        # 初めての実行の場合は、最新のブロック番号から10ブロック前から開始
        if last_block is None:
            last_block = latest_block - 10

        # 最後に処理したブロックの次から最新のブロックまでループ
        for block_number in range(last_block + 1, latest_block + 1):
            # ブロック内のトランザクションを取得
            transactions = get_block_transactions(block_number)
            # 取得に失敗した場合はcontinue
            if transactions is None:
                continue

            # 新しいトークンコントラクトを探す
            check_for_new_tokens(transactions, block_number)
            # 処理したブロック番号を保存
            save_last_block(block_number)

        # 指定された間隔で待機
        time.sleep(INTERVAL)
```

EtherScan API は[こちら](https://etherscan.io/apis)

**実行結果:**

```bash
% python token_scan.py
Checking block 21408558
Regular transaction: from: 0x958536d6e8eed9bc64a97d67a2416a8f7d111991 to: 0x95ad61b0a150d79219dcf64e1e6cc01f0b64c4ce
Regular transaction: from: 0xd34d928ee62a44dd21885a75b1a6596fd0f088a0 to: 0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48
Regular transaction: from: 0x21debfa81fc74415383cfbc597f77ccf6b61334b to: 0xcf0c122c6b73ff809c693db761e7baebe62b6a2e
Regular transaction: from: 0xb5a46bc8b76fd2825aeb43db9c9e89e89158ecde to: 0x9f8f72aa9304c8b593d555f12ef6589cc3a579a2
Regular transaction: from: 0x48f18653beae232ac20574247357362dd88f6e00 to: 0x090185f2135308bad17527004364ebcc2d37e5f6
Checking block 21408559
Regular transaction: from: 0x080086911d8c78008800fae75871a657b77d0082 to: 0x0000e0ca771e21bd00057f54a68c30d400000000
Regular transaction: from: 0xfba0014d3a9dbe8a0cda6affd3da7b541a1ec32f to: 0xea8cf32e4ac03acab2babb9028bac5c853e0ce80
Regular transaction: from: 0x4066e9bd5618373d2da7a1cb7bba03ef800875ee to: 0x6719c6ebf80d6499ca9ce170cda72beb3f1d1a54
Regular transaction: from: 0xa40c08732ea74411e404bb108043ccf2992fbf87 to: 0x0000000000007f150bd6f54c40a34d7c3d5e9f56
Regular transaction: from: 0x6000cf9e08c900e2da21f04f00a0430000fad259 to: 0x509fde8900840c1f8900f3325c0000bdaa00b500
Checking block 21408560
Regular transaction: from: 0xffa45a7c2d6c1e73a4306d6448a24768de467232 to: 0x055c48651015cf5b21599a4ded8c402fdc718058
Regular transaction: from: 0x315d2ee4fccda0def532ef4108ff57204f8d9eba to: 0x738e79fbc9010521763944ddf13aad7f61502221
Regular transaction: from: 0xa7b326ad42dfef8507f0e6543afe65e99c3115ef to: 0x4fa98b4640d73991007e109c41814310431ada58
Regular transaction: from: 0x2a0eff97bb68298e36bea8c86852436e96af92b9 to: 0x73a8a6f5d9762ea5f1de193ec19cdf476c7e86b1
Regular transaction: from: 0x5b43453fce04b92e190f391a83136bfbecedefd1 to: 0x68d3a973e7272eb388022a5c6518d9b2a2e66fbf
Checking block 21408561
Regular transaction: from: 0x4066e9bd5618373d2da7a1cb7bba03ef800875ee to: 0x6719c6ebf80d6499ca9ce170cda72beb3f1d1a54
Regular transaction: from: 0xb0fc682a69b37abf85cd519f9e4fd94a8c7c8669 to: 0x1bf621aa9cee3f6154881c25041bb39aed4ca7cc
Regular transaction: from: 0x11235534a66a33c366b84933d5202c841539d1c9 to: 0x22ce84a7f86662b78e49c6ec9e51d60fdde7b70a
Regular transaction: from: 0x1057c855a823203b32f49e1c1741a77fbe13ae31 to: 0x51c72848c68a965f66fa7a88855f9f7784502a7f
Regular transaction: from: 0xc16157e00b1bff1522c6f01246b4fb621da048d0 to: 0x9a15bb3a8fec8d0d810691bafe36f6e5d42360f7
Checking block 21408562
Regular transaction: from: 0x111206594f2fb5927f719b4417ab1da3d41b14b1 to: 0x000000d40b595b94918a28b27d1e2c66f43a51d3
Regular transaction: from: 0x0dfd32320af10f58cd4a5c2b567b7739ea2d691c to: 0x3fc91a3afd70395cd496c647d5a6cc9d4b2b7fad
Regular transaction: from: 0x1114c78d5de672996d812dc2e1a05b5f33eacdfb to: 0x000000d40b595b94918a28b27d1e2c66f43a51d3
Regular transaction: from: 0x93793bd1f3e35a0efd098c30e486a860a0ef7551 to: 0x68d3a973e7272eb388022a5c6518d9b2a2e66fbf
Regular transaction: from: 0x448166a91e7bc50d0ac720c2fbed29e0963f5af8 to: 0x68d3a973e7272eb388022a5c6518d9b2a2e66fbf
```

新規トークンを見つけることはできませんでしたが、トランザクションログを表示することはできましたね。

このスクリプトでは一部のログしか取得できないので、新規トークンを見つけるのは難しいかもしれません。しかし、可能性はゼロではないので、見つけたときは、夢が広がりますね。

今後は、スクリプトの改良も視野に、宝探しを続けたいと思います🙂