デジタル貯金箱の作り方

# 趣旨
ライトニング上でPiggyBank（貯金箱）を作ってる有志の方を見かけて、自分の持ってるハードウェアでも似たようなの実装してみたくなったのでコードを書き始めました。

長期保管用に子供の貯金箱として機能させたほうが活用しやすいと考え、手数料も基本的にはそんなに高くないのでオンチェーン用の貯金箱を作ります。

ハードウェアはドライバが必要になるので指定のハードウェアを買わない限りは各自で準備するしかないんですけど、僕がたまたま持ってるというだけのハードウェアのドライバをわざわざ公開しても誰も買わないでしょうし、実践もしないでしょうから、ハードウェアのシミュレーション時点でのコードを公開します。なので、E-Inkなどを使って実装したい場合はご自身でドライバなどをセットしてください。

# コード
主にXpubからアドレスを生成し、中にsatoshiが入っているかどうかを確認して合計を求め、まだ使用済みでないアドレスをインデックス順に表示します。5分ごとに更新されるよう設定してますけど、blockstream社のAPIの遅延が僕のテスト段階では発生していて着金額はだいぶあとになってから更新されるみたいです。もし即時に数字を確認したいだとか、自分のノードと繋ぎたいという場合はコードの一部を書き換える必要があります。

まずはライブラリをインストールします。
```
pip install bip_utils
pip install requests
pip install pillow
pip install matplotlib
```

僕はJupyter Notebookにて動かしていたので、ターミナル上で起動するなら画像をどこかに保管する仕様に変えた方がいいかもです。画像生成時に入れる文言なんかも変え放題ですし、画像サイズも別に僕のハードウェアに合わせる必要はないので、いくらでも好きなように書き換えればよいと思います。

```python e-ink.py
from bip_utils import Bip84, Bip84Coins, Bip44Changes
import requests
import json
import os
import time
from PIL import Image, ImageDraw, ImageFont
import matplotlib.pyplot as plt
import qrcode

# File to store the zpub
zpub_file = "zpub.json"

# ==========================
# Load zpub from file
# ==========================
def load_zpub():
    if os.path.exists(zpub_file):
        with open(zpub_file, 'r') as f:
            data = json.load(f)
            return data.get("zpub")
    else:
        raise FileNotFoundError("zpub.json not found. Please make sure the file exists.")

zpub = load_zpub()

# ==========================
# Bitcoin Address Generation using BIP84
# ==========================
# Initialize from xpub (use Bip84 class to derive public key from zpub)
bip84_ctx = Bip84.FromExtendedKey(zpub, Bip84Coins.BITCOIN)

# ==========================
# Fetching Bitcoin Balance using Blockstream API
# ==========================
def get_balance_blockstream(address):
    url = f"https://blockstream.info/api/address/{address}"
    response = requests.get(url)
    if response.status_code == 200:
        data = response.json()
        # Combine confirmed and pending balances
        confirmed_balance_satoshis = data.get('chain_stats', {}).get('funded_txo_sum', 0) - \
                                     data.get('chain_stats', {}).get('spent_txo_sum', 0)
        pending_balance_satoshis = data.get('mempool_stats', {}).get('funded_txo_sum', 0) - \
                                   data.get('mempool_stats', {}).get('spent_txo_sum', 0)
        total_balance_satoshis = confirmed_balance_satoshis + pending_balance_satoshis
        return total_balance_satoshis
    else:
        print(f"Error fetching balance for {address}. HTTP Status: {response.status_code}")
        return None


# ==========================
# Simulating E-Ink Display (250x122 pixels) with Custom Text and QR Code
# ==========================
def display_on_eink_simulation(total_savings_count, total_balance_satoshis, addr):
    eink_display = Image.new('1', (250, 122), 255)  # 1-bit color (Black & White)
    draw = ImageDraw.Draw(eink_display)

    # Use a font you have on your system, adjust the path to the font file if necessary
    font = ImageFont.truetype('/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf', 12)

    # Generate a QR code for the current unused address
    qr = qrcode.QRCode(
        version=1,
        error_correction=qrcode.constants.ERROR_CORRECT_L,
        box_size=3,
        border=1,
    )
    qr.add_data(addr)
    qr.make(fit=True)

    qr_img = qr.make_image(fill="black", back_color="white").resize((100, 100))  # Resize QR to fit
    eink_display.paste(qr_img, (5, 10))  # Paste QR code onto the display

    # Draw custom text on the E-Ink display
    draw.text((110, 10), f"PiggyBank", font=font, fill=0)
    draw.text((110, 40), f"Total Balance:", font=font, fill=0)
    draw.text((110, 60), f"{total_balance_satoshis} sats", font=font, fill=0)
    draw.text((110, 90), f"Saved {total_savings_count} times!", font=font, fill=0)

    # Simulate the display using matplotlib
    plt.figure(figsize=(6, 3))
    plt.imshow(eink_display, cmap='gray')
    plt.axis("off")
    # plt.title(f"Simulated 2.13-inch E-Ink Display")
    plt.show()

# ==========================
# Main loop to check addresses and display unused ones
# ==========================
current_index = 0  # Start from index 0
total_savings_count = 0  # Track the number of savings in satoshis

while True:
    # Calculate total balance from all used addresses
    total_balance_satoshis = 0
    unused_address_found = False
    unused_address = None

    # Loop through addresses until an unused address is found
    i = 0
    while not unused_address_found:
        addr_ctx = bip84_ctx.Change(Bip44Changes.CHAIN_EXT).AddressIndex(i)
        addr = addr_ctx.PublicKey().ToAddress()

        print(f"Checking address {i}: {addr}")  # Logging address being checked

        balance_satoshis = get_balance_blockstream(addr)
        
        # Skip fetching if rate-limited (HTTP 429)
        if balance_satoshis is None:
            print(f"Skipping address {i} due to rate limit.")
            continue
        
        # Log fetched balance
        print(f"Address {i}: {addr}, Balance: {balance_satoshis} sats")
        
        # If the balance is 0, it's an unused address
        if balance_satoshis == 0 and not unused_address_found:
            unused_address_found = True
            unused_address = addr
            current_index = i  # Update current index when unused address is found

        # Accumulate balance for used addresses
        if balance_satoshis > 0:
            total_balance_satoshis += balance_satoshis
            total_savings_count += 1  # Increment savings count for each address with a balance

        i += 1  # Move to the next index only for balance checking

    # Display the new unused address and the total balance on the simulated E-Ink display
    display_on_eink_simulation(total_savings_count, total_balance_satoshis, unused_address)

    # Print for debugging purposes
    print(f"Unused address: {unused_address} (Index: {current_index})")
    print(f"Total Balance: {total_balance_satoshis} sats")

    # Wait for 5 minutes (300 seconds) before refreshing
    time.sleep(300)
```

# 結果
テスト用に生成したニーモニックフレーズで生成したXpubを使用してみました。
```
Checking address 0: bc1q0dcudrlwpqphpvqgfdyjw0p99m76rvqe5tr2tg
Address 0: bc1q0dcudrlwpqphpvqgfdyjw0p99m76rvqe5tr2tg, Balance: 0 sats
```
![ダウンロード (40).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3670541/2a6e0cca-4483-a132-0e66-2e636a390b86.png)
```
Unused address: bc1q0dcudrlwpqphpvqgfdyjw0p99m76rvqe5tr2tg (Index: 0)
Total Balance: 0 sats
```

この画像だけがハードウェアに表示されるようにコードを準備しているんですが、MicroSDカードが自宅に届くのを待っているのでまだ実装できません。E-Inkを利用する予定なので電源を入れずとも最後に電源を落とした時のQRコードが残りますし、新しいQRコードが欲しければ電源を入れればいいので、家族に対するプレゼントにはちょうどいいかなくらいに思ってます。