見出し画像

ウォレット1億個つくって億ウォレになろう

アドカレ「仮想通貨botter Advent Calendar 2024」9日目の記事です。

ほとんどの方、はじめまして。ロイケみたいな名前で活動してます(Twitter)。アイコンは裁量高値掴みしたNFTを自戒のため使ってます。

2017冬から細々CEXアビトラとかBCGゲームの攻略とかしてましたが、
最近もっぱらWebページ/API解析とか署名突破とかブラウザ操作自動化とか
そんな感じのやってます(最近ではないですがYuliverse箱とか)。
QASH鯖closedにハンター試験で入れていただき、最近QASHさんのアドカレ
でも紹介いただきました。ありがとうございます!

生きてるエッジは書けなかったので、ウォレット沢山つくって管理しようぜ
的な内容です。強強botterの皆様は御存知な気がしますが一応かきました。署名突破の話でなくて申し訳ありません。
たぶん間違ってたり雑な理解なので、強い方はマサカリ投げて下さい。

公開前(というか完成前)なのに、Kudasai忘年会@東京のジャンケン大会で優勝したら「複垢ですか?」と聞かれましたw


※当記事ではウォレット、アカウント、アドレスとか用語適当に使いますが
基本的に秘密鍵ベースのEOA(EVMのExternally Owned Account)のことです。

そもそもウォレットたくさん必要?

単純にアドレス毎に回数制限あるFaucetやMint等をbot化する場合、明らかにウォレットたくさん必要です(色んな問題は考えないものとすれば)。
以上です。

残りの理由は頑張って無理に考えただけなので適当に読み飛ばして下さい。

他については、たぶん諸説あり状況も限定的ですが、
誰でも儲かる(ホワイトリスト等でアドレスが限定されていない)場合に
沢山Tx(トランザクション)を送りたい時、沢山ウォレット(アドレス)
があると便利です。
秒〜分の短時間な歪みに対するアビトラTxというよりは、分〜時間くらいの
の少し長い時間持続して1Txで解消できない大きな歪みに対して、出来るだけ多くのTxを送りたいみたいなのを想定しています。

同じアドレスで同じチェーンだと複数のbotを運用しづらい

EVM(Ethereum Virtual Machine)系チェーンのTx(トランザクション)は
nonce(number used once)が連番である必要があります。
そのためbotが複数存在する場合に同一のアドレスを使っているとTxのnonceが衝突して一方のbotのTxが通らなかったり、それどころか自分のbot同士で
Tx通そうと無駄にガス戦争になる可能性があります(自分の実装による)。

概念図をmermaidで作りました。コードっぽい表記で図を作れます。
ブラウザで動くplaygroundあり、png等で出力できます。便利。https://mermaid.live/
sequenceDiagram
    participant bot_A
    participant bot_B
    participant TxPool
    participant Chain
    Note over bot_A,Chain: ボット間でのnonce競合のケース    
    bot_A->>TxPool: Tx(nonce:10, gas:20Gwei)
    bot_B->>TxPool: Tx(nonce:10, gas:22Gwei)
    Note over TxPool: 同じnonceで高いガス価格のTxを優先
    TxPool->>Chain: Tx from bot_B (nonce:10)
    Chain-->>bot_B: success
    TxPool-->>bot_A: failed (nonce has already been used)

特に高頻度なbotが複数だと問題になるので分けている方が多いと思います。ウォレットストーキング時にTx混ざると分かりづらいのでbot毎に分けて。

下図のようなガス代が上下する環境でもnonceから開放されるだけでなく、
時間あたり通せるTx数が単独より複数ウォレットの方が多くなりそうです。

パワポは手作業だけど、好きな図は作れますよね

とはいえTx数を気にするならそもそも単一のプロセスでnonceを順番に1個
ずつ送らない
と思います。下図のように並列でTx送信する場合、中央やや下
のようにTxを全て新ウォレットから送ればnonceを考える必要がないです。

そもそも同時に別プロセスや非同期で実行すれば時間あたりのTx数は増える
その場合にnonceを全く気にせずTx送れるのは全て新ウォレットを使う方法(だがトレードオフ)

ただし複数ウォレットへのガス代(ETH等)の配付で余計なガス代と手間は
発生します。ETHメインネット等ガス代が高いチェーンでは現実的ではない
と思います。
ガス代の配付については、botでETH等を単純に1Txずつヒマな時に事前送金するのも悪くないですが、Multisender(Disperse.appみたいなの)等あれば
使用すれば1Txで大量配付できますし、コントラクトが書ける方は自作関数
でも良いかなと思います(ただしセルフGOX注意、緊急脱出withdraw関数
の作成を忘れずに
)。エアドロ狙う場合は配付方法がシビル判定に影響すると思います。最強なのはアカウント事にCEXから出金。

geth --txpool.accountqueue 64 --txpool.accountslots 16

--txpool.accountqueue value (default: 64) ($GETH_TXPOOL_ACCOUNTQUEUE)
Maximum number of non-executable transaction slots permitted per account
--txpool.accountslots value (default: 16) ($GETH_TXPOOL_ACCOUNTSLOTS)
Minimum number of executable transaction slots guaranteed per account

Command-line Options | go-ethereum
https://geth.ethereum.org/docs/fundamentals/command-line-options

チェーンやノード設定によりますが、EVM系でgethデフォルト設定は上記の
通りです(ほか全体制限--txpool.global{queue,slots}もあります)。
具体的には特定のアカウント(ウォレット、アドレス)毎にノードがTx数を制限していて、TxPool(ブロックに取り込まれる候補のトランザクション)が実行可能Txが16、実行不能Txが64に制限されています。ザックリ言うと、
実行可能Txは連番nonceでガス要件もOKなTx、実行不能Txはそれ以外です。
正直自分はこれで困ったことない気がするのですが、これ見てもアカウントを分けた方が有利な気がします。ノード勉強中なので誰か教えて下さい。

ウォレット沢山メリットデメリットまとめ 

飛躍や諸説ありそうですが早く本題に行きたいので、メリットとデメリット
まとめます。

メリット

  • アドレス毎の回数制限を回避できる(メイン?)

  • 並列/非同期的に時間あたり沢山Tx送る際にnonce調整が楽になる(かも)

  • TxPoolに沢山Txを入れれる、低いガス代のTxを放置もできる

  • 複数戦略を同時展開する際にウォレット毎に戦略を分けると集計など楽

  • 中の人としてはストーキングされづらくなる(かも)

  • バグ等でGOXしても全ウォレットには波及しない(かも)

  • 大口より小口が相対的に優遇される場合エアドロたくさん貰える(かも)

デメリット

  • 事前にガス代など配付が必要、資金やホワイトリスト必要な場合は困難

  • 秘密鍵の管理やコードの実装が面倒になる

  • 戦略としては単一ウォレット見やすいのでストーキングされやすいかも

  • エアドロとかの際にシビル判定されるかも(何回か判定された)

さぁウォレットを作ろう

BIP39(Bitcoin Improvement Proposal 39)準拠な英単語12個を使った
ニーモニック(mnemonic、ほぼシードフレーズ)を沢山つくりましょう。
下記コードで生成したウォレットで起こった不具合には責任を負えません。
乱数のランダム性やライブラリのバージョン、出力ファイルの扱いには注意して下さい。基本的に使い捨てウォレットで、メインウォレットとして使用しないで下さい。本番環境での使用は自己責任でお願いします!!!!!!!!

mnemonic使って導出パス(※)だけ変更して秘密鍵を量産する方が生成速度が上がりそうに思いますが、自分が試した範囲では下記すべてで導出パス変更な場合も生成速度は概ね同じだったのと、mnemonicのimportしかできないウォレットがあるので以降すべてmnemonicと標準の導出パスでアドレスを作っています。
(※)m/44'/60'/0'/0/0とか、最後の/0を連番で変更して秘密鍵を生成することでMetaMaskとかは共通シードフレーズでアドレスを増やしてる。
たぶん速度が変わらないのはmnemonicの生成より主に秘密鍵の生成に時間がかかるから?やってることは個人的にはここの解説が分かりやすそう。

MetaMaskとかRabbyでウォレットをポチポチ増やすのは大変ですが、
botterらしくコードを書きましょう。

Python版(シングルプロセス) 100個/秒のオーダー

自分の環境(Mac Studio M2 Ultra, CPU 24コア)だと、概ね275個/秒くらい
作れます。たぶん多くの場合は事足ります。

# 常に最新バージョンを使うのは悪意あるコードが混入する可能性あるので避けてる
# 最近もsolana/web3.jsのnpmパッケージに攻撃者が悪意のあるコードを含んだバージョンあったし
# pip install bip-utils==2.9.0 pandas==1.5.3 tqdm==4.66.4 web3==6.6.1
# versionは適当だがweb3は関数名が変わるので注意

import pandas as pd
from datetime import datetime
from bip_utils import Bip39MnemonicGenerator, Bip39MnemonicValidator, Bip39WordsNum
from tqdm.auto import tqdm
from web3 import Web3

N_GENERATE = 10000

web3 = Web3()
web3.eth.account.enable_unaudited_hdwallet_features()

list_wallet = []
for _ in tqdm(range(N_GENERATE)):
    mnemonic = Bip39MnemonicGenerator().FromWordsNumber(Bip39WordsNum.WORDS_NUM_12).ToStr()
    account = web3.eth.account.from_mnemonic(mnemonic)
    list_wallet.append({'address': account.address, 'private_key': account._private_key.hex(), 'mnemonic': mnemonic})

df_wallet = pd.DataFrame(list_wallet, columns=['address', 'private_key', 'mnemonic'])
df_wallet.to_csv(f'wallet_{datetime.now().strftime("%Y%m%d_%H%M%S")}.csv', index=False)

Python版(マルチプロセス) 1000個/秒のオーダー

自分の環境(Mac Studio M2 Ultra, CPU 24コア)だと概ね1600個/秒くらい
作れます。システムが不安定にならないよう、n_jobs=-2でCPUコア1個だけ余らせてます(-1だと全て使う)。急に10万アカとか欲しいときに便利です。

# pip install bip-utils==2.9.0 pandas==1.5.3 tqdm==4.66.4 web3==6.6.1 joblib==1.3.0
import pandas as pd
from bip_utils import Bip39MnemonicGenerator, Bip39MnemonicValidator, Bip39WordsNum
from datetime import datetime
from tqdm.auto import tqdm
from web3 import Web3
from joblib import Parallel, delayed

N_GENERATE = 10000

def generate_wallet():
    web3 = Web3()
    web3.eth.account.enable_unaudited_hdwallet_features()
    mnemonic = Bip39MnemonicGenerator().FromWordsNumber(Bip39WordsNum.WORDS_NUM_12).ToStr()
    account = web3.eth.account.from_mnemonic(mnemonic)
    return {'address': account.address, 'private_key': account._private_key.hex(), 'mnemonic': mnemonic}

list_wallet = Parallel(n_jobs=-2)(delayed(generate_wallet)() for _ in tqdm(range(N_GENERATE)))

df_wallet = pd.DataFrame(list_wallet, columns=['address', 'private_key', 'mnemonic'])
df_wallet.to_csv(f'wallet_{datetime.now().strftime("%Y%m%d_%H%M%S")}.csv', index=False)

Rust版(マルチスレッド) 10000個/秒のオーダー

自分の環境(Mac Studio M2 Ultra, CPU 24コア)でCPUコアを1つ余らせて
概ね12000個/秒くらい作れます。タイトルの1億ウォレットを目指す場合も
2時間半以内には作れる計算です。
全然使わないのでコード紛失してChatGPT o1に作り直させたのは秘密(でもPythonと比べるとRustのコードはバグだらけで動かないので手直し必要)。前はrayon(マルチスレッド)でなくてtokio(非同期)で書いた気がする。

# ./Cargo.toml

[package]
name = "generate_wallets"
version = "0.1.0"
edition = "2021"

[dependencies]
bip39 = "2.0"
ethereum-types = "0.14"
hex = "0.4"
indicatif = "0.17"
rayon = "1.7"
rand = "0.8"
secp256k1 = { version = "0.27", features = ["rand"] }
serde = { version = "1.0", features = ["derive"] }
tiny-keccak = { version = "2.0", features = ["keccak"] }
csv = "1.2"
num_cpus = "1.16"
tiny-hderive = "0.3"

[profile.release]
lto = true
codegen-units = 1
panic = "abort"
// ./src/main.rs
// cargo run --release

use bip39::{Mnemonic, Language};
use ethereum_types::H160;
use hex;
use indicatif::{ProgressBar, ProgressStyle};
use rayon::prelude::*;
use secp256k1::{SecretKey, PublicKey, Secp256k1};
use serde::Serialize;
use std::fs::File;
use std::time::SystemTime;
use csv::Writer;
use tiny_keccak::{Hasher, Keccak};
use std::sync::Arc;
use rand::rngs::OsRng;
use rand::Rng;
use tiny_hderive::bip32::ExtendedPrivKey;
use std::io::BufWriter;

#[derive(Serialize)]
struct Wallet {
    address: String,
    private_key: String,
    mnemonic: String,
}

fn keccak256(input: &[u8]) -> [u8; 32] {
    let mut hasher = Keccak::v256();
    let mut output = [0u8; 32];
    hasher.update(input);
    hasher.finalize(&mut output);
    output
}

fn to_checksum_address(addr: &str) -> String {
    let addr_lower = addr.to_lowercase();
    let mut hasher = Keccak::v256();
    hasher.update(addr_lower.as_bytes());
    let mut hash = [0u8; 32];
    hasher.finalize(&mut hash);
    let hash_hex = hex::encode(hash);

    let mut checksum_addr = String::with_capacity(2 + addr_lower.len());
    checksum_addr.push_str("0x");

    for (i, c) in addr_lower.chars().enumerate() {
        let val = u8::from_str_radix(&hash_hex[i..i+1], 16).unwrap();
        if val >= 8 {
            checksum_addr.push(c.to_ascii_uppercase());
        } else {
            checksum_addr.push(c);
        }
    }

    checksum_addr
}

fn generate_wallet(secp: &Secp256k1<secp256k1::All>) -> Wallet {
    let mut rng = OsRng;
    let mut entropy = [0u8; 16];
    rng.fill(&mut entropy);

    let mnemonic = Mnemonic::from_entropy_in(Language::English, &entropy).unwrap();

    let seed = mnemonic.to_seed("");

    let xprv = ExtendedPrivKey::derive(seed.as_ref(), "m/44'/60'/0'/0/0")
        .expect("Failed to derive xprv");
    let secret_key = SecretKey::from_slice(&xprv.secret()).expect("Invalid secret key slice");

    let public_key = PublicKey::from_secret_key(secp, &secret_key);
    let public_key_bytes = public_key.serialize_uncompressed();
    let keccak = keccak256(&public_key_bytes[1..]);
    let address = H160::from_slice(&keccak[12..]);

    let words = mnemonic.words();
    let mut phrase = String::with_capacity(12 * 8);
    if let Some(first) = words.clone().next() {
        phrase.push_str(first);
    }
    for w in words.skip(1) {
        phrase.push(' ');
        phrase.push_str(w);
    }

    let address_hex = hex::encode(address.as_bytes());
    let checksum_address = to_checksum_address(&address_hex);

    Wallet {
        address: checksum_address,
        private_key: hex::encode(secret_key.as_ref()),
        mnemonic: phrase,
    }
}

fn main() {
    const N_GENERATE: usize = 100000000; // 1億ウォレット欲しいんじゃい!!

    println!("Generating {} wallets...", N_GENERATE);

    rayon::ThreadPoolBuilder::new()
        .num_threads(num_cpus::get() - 1) // コア数-1
        .build_global()
        .unwrap();

    let pb = ProgressBar::new(N_GENERATE as u64);
    pb.set_style(
        ProgressStyle::default_bar()
            .template("{spinner:.green} [{elapsed_precise}] [{wide_bar:.cyan/blue}] {pos}/{len} ({per_sec}, {eta})")
            .unwrap()
            .progress_chars("#>-")
    );

    let secp = Arc::new(Secp256k1::new());

    let wallets: Vec<Wallet> = (0..N_GENERATE)
        .into_par_iter()
        .map(|_| {
            let wallet = generate_wallet(&secp);
            pb.inc(1);
            wallet
        })
        .collect();

    pb.finish_with_message("Generation completed");

    let timestamp = SystemTime::now()
        .duration_since(SystemTime::UNIX_EPOCH)
        .unwrap()
        .as_secs();
    let filename = format!("wallet_{}.csv", timestamp);

    let file = File::create(&filename).unwrap();
    let mut writer = Writer::from_writer(BufWriter::new(file));

    for wallet in wallets {
        writer.serialize(wallet).unwrap();
    }
    writer.flush().unwrap();

    println!("Wallets saved to {}", filename);
    println!("Run with `cargo run --release` for best performance.");
}

GPU, FPGA, ASICを使えば更に高速化できそうですが、もうええでしょう。

これでアナタも億ウォレです。
とりあえずウォレット量産できたので、次に使い方を考えましょう。

ここから後はウォレットとかアカウント量産したときに起こりそうな事案に自分はどうやったかを書いてます。ベストプラクティスでないかもですが、
botは動いて稼げるのであれば正義と思います(たまに忘れる)。

秘密鍵の管理(億ウォレだと出力18.5GBですw)

やり方も無数にあるし明かせないパターンも有りそうですが、典型的なのは
下記でしょうか。ここに関しては特に有識者の方、教えて下さい!!

csvそのまま

さっきcsvで出力したばっかりですし、まぁ楽です。
ただし、平文(暗号化されていない文字列)なので要注意です。
正しく管理できる前提ですがバックアップとして保管しておくのもアリとは思います。下記の暗号化は復号化(元に戻す)ができなくならないよう注意して下さい。

パスワード保護されたxlsx等

少なくともWindows環境でExcel入っていれば、Pythonからパスワード保護
.xlsxは生成できます(win32com.client.Dispatch("Excel.Application"))。
読込はmsoffcrypto-toolとかでMacOS/Linux環境でもPythonから可能です。

ほか7zコマンドとかでパスワード付のzipにするのも有効です。GnuPGとかでパスワードとか公開鍵で暗号化ファイルを生成するのも良いかもです。ただ結局毎回復号したファイル生成する気がします。個人的には、結局ローカルなりリモートで復号しますし、そもそも今回のアドレス使い捨てウォレットなので、publicなチャットに貼るとか、publicなリポジトリにpushするとかしなければcsvで良い気がします(あとゴミなウォレット100万個とか渡されても…)。

なおdotenvxが良いというウワサ(かんたさんのアドカレ記事で知りました)ですが試せてません。巨大なウォレット一覧を環境変数に入れるのは不適切な気がしますが、少ないアカウント数ならdotenvx試してみたいです。

DBに入れてDBのアクセス権で制御

後半に関係するのですが、サーバーのアクセス権限を管理する知識ありそうならDBに平文(もしくは暗号化文字列、平文パスワードDB保存は許さん)で入れてしまい、DBのアクセス権限で保護するのも良さそうです。
WebのログインパスワードとかはDBに(多くは)ハッシュ化された文字列で保存されていますが、ハッシュ化は暗号化と違って元に戻せないので注意、秘密鍵は復号可能な暗号化文字列とかで保存しましょう。

秘密鍵以外にも動的なアカウント情報追記したい

アカウントを量産すると、アカウント数 >> 同時にTxとか飛ばせる数となるはずで、アカウント情報(特に処理により変わる残高とか)を元に次に処理するアカウントを選定したり、情報を追記・更新したい欲求が生じます。

このとき、csvのままだと、(多くの実装は)csvの更新は一部のみ変更するのでなく全部を書き換えます。シングルプロセスの場合は毎回すこし無駄に時間がかかる(18.5GBだと地獄)くらいですが並列だと同時に書き込めない(それぞれのプロセスで変更箇所は異なるはずなので衝突して消えないよう最新を読み込んでロックして書き込んでロック解除してを1プロセスずつ…)ので、愚直なcsv実装だと書き込み部分が煩雑かつ律速になって並列処理のメリットが完全に消えてしまいます。

毎回、黄色を書き換える為に緑も全て書き換える、書き換え途中のファイルは赤色
しかも書き換え途中を読み込んで書き込むバグとかあると悲惨な事に(赤以降が消える)
せめて別名で書き出してリネームとか、排他アクセス制御でロックとか使わないと壊れる

ということで、同時に大規模に読み書きが必要そうならDBに保存することを考えましょう。学習コストは辛いですが、GPTさんが助けくれるはず。
DB専門家にはならなくても、そこそこで動けば問題ないはず。
自分は並列処理に強みあるPostgreSQL使ってます。DBeaverとか使用すればGUIで表形式に表示してソートとかフィルターできますし、DB怖くないよ。

ダミーデータつくるの面倒だったのでカラム幅めちゃ狭にして代用(&テストネットのみ垢)

IPアドレスが足りない?

RPC叩きすぎるとIP BANされて困ります。
PCが余っていて電気代とか騒音とか気にしないならProxy、そうでないならクラウドVPS等が良い気がします。
スペック最小限なクラウドならContabo, ConoHaあたりが安い気がします。ノードでなく小規模なbotだけなら最小構成でも結構いけます(botによる)。用途によってはGoogle Cloud Run Functions, AWS Lambda等も良いかもですがIPアドレスについての制御は難しめです。
ProxyはWebShare(リファ)が無料で1GB/月のHTTPproxy 10個つかえます。データセンターのIPみたいなプランだと結構お安め(自己リファ未検証)。
実は自分は無料枠しか使ってない、自己リファとかすれば更に安い??

IPの数でも課金、帯域(使用容量)でも課金
(普段と違って)普通の使い方してみたら、なんか無料枠1GBより多く使えた…?
普段は下記の認証操作などバイパスに一時的にだけ使ってる

定期的に認証操作が必要だけど、帯域無制限?な無料Proxyあるけどエッジ
なのと教えてもらったやつなので秘密です。今のところ、たまに自宅IP BANされるけど、1日くらいで許してくれる優しいサービスです

画面が足りない

直コンとかCLIでないbot、例えばブラウザ操作をSeleniumとかPlaywright
(Playwrightずっと入門中です)で自動化したbotはHeadlessでも良いですが諸々の事情で画面やウインドウの座標でクリックしたい場合もあります。
普段使いのPCで定期的に別のマウス操作されるのは困るのと、マウス操作がbot間で競合しそうな場合も沢山PCがあれば解決です。

クラウドVPS立てまくるのは結構お高いので、最近自分はProxmoxっていうオープンソース(有料版もある)の自宅クラウドできるOSを複数PCに入れて簡易・自宅データセンターを作ってます。逸般の誤家庭になりつつある。
シビル的にUserAgent変更だけで信用できるか不明なのでLinux系のDesktopよりWindowsかMacで実行されたブラウザが良い気がしてる(検証不能)。
そのうちM4 Mac miniに浮気してそう(年末のコンビニApple課金還元まち、
毎年ではない)。

PCとネットワーク機器と自前保守のコストあわせるとクラウドお勧めします!これは趣味です。
(Windows Insider Preview動くけど)Windowsのライセンスちゃんと守ろう!

うわっ…私のbot、止まってる…?

ウォレットが増えて、アカウントが増えて、botが増えて、となると止まってないかの死活管理も大変になってきます。
自分は時系列DBのInfluxDBv2(version変わると結構ちがうので注意)とGrafanaでダッシュボードとアラートルールを作成しています。インフラ系で主に使われてる?クラウド版もあるけど自分は自分のサーバーに建ててる。

左が死活管理。botの処理の最後に時系列DBへの書き込み追加する場合もあれば、
OS側のcronとかで定期的にプロセスが死んでないか監視してる場合もある。
右は死んだ鞘が生きてる頃の監視ダッシュボード。Grafana上ででアラートルール作成も可能。
個人的にはGrafana自体が落ちる可能性もあるので別のアラート方法も併用必要と思う。

基本は手動でダッシュボードとかアラート作成するインターフェースですが、GrafanaのダッシュボードとPythonだとgrafanalibで作成可能、アラートはGrafana自体がAPI持ってるのでAPI経由で作成してます。これならbotが200個とかでも大丈夫です。
アラートの通知先はE-mailとかWebHookとか色々デフォルトであるのでDiscord, Telegram, LINE, IFTTTあたりに通知してApple Watchで受け取るのが良いと思います。我が家はスピーカー再生と照明点灯も追加されてます。
睡眠を生け贄にして鞘(とbot死活)を通知。

最後に

後半はアカウント量産した後に自分がやってること書いてみました。
総じて自分はこうしてるってだけでベストプラクティスかは分かりません。
botter界隈では上記の仮想基盤とか監視基盤やってる人は多くないイメージなので、もし参考になってたら嬉しいです。そして、もっと良い方法あれば差し支えなければ共有いただけると幸いです。

あと自動化できてない手動で取れる鞘あるけど四六時中は張り付けない状況の方、bot共同開発ご相談ください(できるかは見るまで分かりませんが、できなかったら知らなかった鞘は知らないフリします)。

本記事で紹介した方法やコードは、あくまで技術的参考例であり、これらを利用した結果生じた損失、トラブル、第三者からのシビル判定等について、筆者は一切の責任を負いません。関連する法令・規制や各種ライセンス、
利用規約を適正に遵守してください。


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