【Python3】Web3.pyでEVM系のウォレット残高を確認してみる!

どーも!

たかぽんです!

最近はちょっと仮想通貨周りの技術を色々と触っているのですが、最近やっとこさ思い腰を上げてウォレット周りの自動化にチャレンジしています。

そのついでに、基本的なところを記事にできたらなぁ...ということで、書いてみようと思います!

Web3.pyって何?

ざっくりとした説明にはなってしまいますが、ブロックチェーン上にはアプリケーション(いろんなプロジェクトのサービスやゲームなど...)があります。

基本的にブロックチェーン上でサービスやゲームをする場合、Walletが必要になるのですが、そのWalletを用いた操作等をするためのpythonのライブラリになります。

より厳密にはなんか難しい定義がありそうですが、ブロックチェーン上でのいろんなサービス等を総称してWeb3と捉えていいのかなと思います。

Web3.pyのインストール

では前準備として、Web3ライブラリをインストールしていきましょう!

前提になりますが、Macでの環境であること、python3がinstall済みであることを仮定して進めていきますね。

python3がインストールされていれば以下のコマンドでインストールされると思います。

pip install web3

何も問題なければこれでライブラリのinstallはOKです!

プログラムの実行について

web3のライブラリのインストールが完了したら、簡単な実行方法を確認しておきます。

適当なディレクトリにて、適当にファイルを作ります。

(base) taka@Taka test % touch test.py
(base) taka@Taka test % ls
test.py

筆者はtouchコマンドで作成しましたが、もちろん、エディタ等で新規作成でも大丈夫です。

作成したら、以下のコードをtest.pyのファイルの中に貼り付けて保存しましょう!

print('Hello World')

保存ができたら、同じ階層にて、以下のコマンドを叩いてみてください。

(base) taka@Taka test % python3 test.py
Hello World

Hello Worldと表示されました!

最低限実行する場合はこのような形で実行が可能です。

python、楽ちんですね!

では次からライブラリをつかってweb3に触れていきましょう!

チェーンへ接続(BSC)

まず、最初にチェーンへ接続する必要があります。

そのため、その準備をします。

ちなみに、今回はEVM互換のチェーン(イーサリアムチェーンと互換性のあるチェーン)全てにおいて大体似たような形で実装ができると思います。

例えばBSCやCRO,Harmonyチェーンなども同様に実装が可能です。

今回はその一例として、BSCで試していきますね。

では、まず最初にBSCへ接続していきます。

接続するには、そのチェーンごとのRPCが必要になります。

おそらく、この記事を見てくださっている方はmetamask等のwalletを使ったことがあると思いますが、ウォレットでチェーンを追加する時にも必要になるあれですね。

BSCの場合の一例は以下にある通りです。

正直詳細の仕様等は把握していませんが、とりあえず適当なのを選ぶくらいでいいと思います。

筆者は今回は"https://bsc-dataseed.binance.org/"を使用します。

では、以下の内容をそのままコピペして、再度test.pyを実行してみます。

from web3 import Web3

# BSCのRPC
BSC_RPC = "https://bsc-dataseed.binance.org/"

def connectBSCChain(Web3) :
    web3_BSC = Web3(Web3.HTTPProvider(BSC_RPC))
    isConnected = web3_BSC.isConnected()
    if isConnected:
        print('---------- Connected to BSC ----------')
    return web3_BSC

# BSCチェーンへ接続したweb3インスタンスを作成
web3 = connectBSCChain(Web3)
(base) taka@Taka test % python3 test.py
---------- Connected to BSC ----------
(base) taka@Taka test %

Connected to BSCとでました!

文字を出しているだけに見えますが、裏ではRPCに基づいたweb3のインスタンスができているため、以降はweb3.hoge()といった形で色々操作ができるようになります。

では、次は本題のWalletの残高も確認していきます。

また、チェーンを変えたい場合はRPCを別のものに置き換えればOKです。(楽ちん!)

Walletの残高確認

Walletの残高確認ですが、筆者が触っている感じ、基軸通貨とその他の通貨で少し方法が異なります。
(もしかしたら一緒の形でもできるかもですが、一旦動く方でお伝えしようかなと...。)

基軸通貨

さて、ではまずは基軸通貨です。

今回はBSCなので、BNBですね。

なので、あるウォレットにあるBNBの残高を確認する....といったことをしてみます。

先にざっとコードを載せておきます。

(どうせ見せても害はないので、筆者のウォレットアドレスも置いておきます。初回のテストでう試すのにご利用ください...w)

from web3 import Web3

# BSCのRPC
BSC_RPC = "https://bsc-dataseed.binance.org/"

# 確認したい対象のウォレットアドレス
myWalletAddress = "0x83C989e4Af7ACa81066f4F7E3a29f190d88fB14a"

def connectBSCChain(Web3) :
    web3_BSC = Web3(Web3.HTTPProvider(BSC_RPC))
    isConnected = web3_BSC.isConnected()
    if isConnected:
        print('---------- Connected to BSC ----------')
    return web3_BSC

def getKeyCurrencyBalance(web3, address) :
    balance_wei = web3.eth.getBalance(address)
    return  web3.fromWei(balance_wei,'ether')

# BSCチェーンへ接続
web3 = connectBSCChain(Web3)

# ウォレットアドレスのBNB残高を表示
print(str(getKeyCurrencyBalance(web3, myWalletAddress)) + 'BNB')

さて、追加したのは以下のメソッドです。

def getKeyCurrencyBalance(web3, address) :
    balance_wei = web3.eth.getBalance(address)
    return  web3.fromWei(balance_wei,'ether')

また、メソッドの呼び出し側は見た目が良くなるように末尾にBNBを出しているだけなので、あまり重要ではありません。

上記を実行すると...

(base) taka@Taka test % python3 test.py
---------- Connected to BSC ----------
0.070198766450961167BNB

このような形で、今筆者のウォレット"0x83...."には0.07BNBあることがわかります。
(ちなみに、このウォレットアドレスは別に誰かに知られても大きな問題はありません...残高は監視されるかもしれませんが...ね!)

ウォレットの破壊的な操作(残高を送金したり、Defiの操作swap等...ウォレットの残高等の情報が変わるような操作)を執行する場合、シークレットキー(秘密鍵)が必要になります。

今回はあくまで残高を確認するだけなので、秘密鍵は必要ありません。

が、今後開発をしていく上で秘密鍵を使用する場合は絶対に漏らさないよう気をつけてください(特にプログラムにそのまま書き込んで使用して、githubへpush...等)。

"秘密鍵が漏れる=漏れた人の資産を第三者が好きにできる"です。

シードフレーズと秘密鍵は絶対誰にも教えない!これ鉄則!です!

さて、そして実際に筆者のメタマスクの残高を見ると...

メタマスク側では表示の都合上切り上げされていそうですが、残高取れていそうですね!

では簡単に解説を...

再度先程追加したメソッドを載せます。

def getKeyCurrencyBalance(web3, address) :
    balance_wei = web3.eth.getBalance(address)
    return  web3.fromWei(balance_wei,'ether')

ここに全てが詰まっているのですが、先程作成したインスタンスを第一引数に、そしてウォレットアドレスを第二引数に受け取るメソッドです。

web3.eth.getBalanceメソッドを使用すれば、そのweb3インスタンスの接続時RPCに対応したチェーンの基軸通貨の残高(balance)を取得できます。

web3.eth.getBalance(address)

ただし、気をつけなければいけないのが、上記だけだとwei建の値になってしまいます。

どういうことかというと...

以下のようにメソッドの途中でbalance_weiを出力すると...

...

def getKeyCurrencyBalance(web3, address) :
    balance_wei = web3.eth.getBalance(address)
    ## メソッドの中身でbalance_weiを確認
    print(balance_wei)
    return  web3.fromWei(balance_wei,'ether')
...

## 実行結果
(base) taka@Taka test % python3 test.py
---------- Connected to BSC ----------
70198766450961167
0.070198766450961167BNB

みていただければわかりますが、"70198766450961167"という値が出ています。

私たちが期待するBNBの単位とはどうも違いますね。

これは、イーサリアムチェーンでは全ての通貨が最小単位"wei"で表すことができるため、それでBalanceを取得していることにより、多く見えています。

そのため、実際にメタマスク等で表示される残高とは異なって見えるわけです。

そして、直後にある以下の部分でWeiから単位を戻しているわけですね。

web3.fromWei(balance_wei,'ether')

このfromWeiメソッドは第二引数に単位(ここではether)を指定しています。

これまたわかりづらいんですが、ここでのetherはイーサリアムとは関係なく、あくまで単位で、

'ether'は"1000000000000000000"を意味します。

そのため、上記のメソッドでは"balance_wei/1000000000000000000"をしている...ということですね。

なので、実際に先ほどのbalance_weiを計算してみると...

BNBの値と一致しそうですね。

ちなみに、単位はもっと色々あって、通貨によって'ether'でないものもあります。

さて、基本は以上です!

では、次に別の通貨の確認もしてみます。

その他の通貨

さて、それでは次に他の通貨も見てみましょう!

今回は一例としてBUSD(binanceが発行するステーブルコイン)を例に試してみようと思います。

こちらは少し複雑になります。

まず、特定の通貨の情報を出したい場合は、その通貨のアドレスとabiが必要になります。

(abiはある程度共通していそうな気もしていたり、その通貨のものでなくてもいい気がするのですが...ちょっと詳しくは調べきれていません)

アドレスの調べ方はまちまちですが、基本的にはその通貨のプロジェクトのホワイトリストや公式discord等で取得できるかと思います。

今回はBUSDで、通貨のアドレスは以下です。

# BUSDの通貨アドレス
0xe9e7CEA3DedcA5984780Bafc599bD69ADd087D56

そして、ABIというのはアプリケーションバイナリインタフェースの略で、プログラムをABIとして記載しておくことで、互換性のある別のプログラムで変更なしに使用することができるもの...です。

調べ方は、以下はBUSDの例ですが、BSCのエクスプローラーにて、Contractを開くと、下の方にContract ABIという項目があります。これをそのままコピーすればOKです。

こんな感じのババーっとあるめっちゃ長いものがABIです。

読み込むときは、以下のようにします。

# 実際の例
BUSD_abi = json.loads('[{"inputs":[],"payable":false,"stateMutability":"nonpayable","type":"constructor"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"owner","type":"address"},{"indexed":true,"internalType":"address","name":"spender","type":"address"},{"indexed":false,"internalType":"uint256","name":"value","type":"uint256"}],"name":"Approval","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"previousOwner","type":"address"},{"indexed":true,"internalType":"address","name":"newOwner","type":"address"}],"name":"OwnershipTransferred","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"from","type":"address"},{"indexed":true,"internalType":"address","name":"to","type":"address"},{"indexed":false,"internalType":"uint256","name":"value","type":"uint256"}],"name":"Transfer","type":"event"},{"constant":true,"inputs":[],"name":"_decimals","outputs":[{"internalType":"uint8","name":"","type":"uint8"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"_name","outputs":[{"internalType":"string","name":"","type":"string"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"_symbol","outputs":[{"internalType":"string","name":"","type":"string"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[{"internalType":"address","name":"owner","type":"address"},{"internalType":"address","name":"spender","type":"address"}],"name":"allowance","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"internalType":"address","name":"spender","type":"address"},{"internalType":"uint256","name":"amount","type":"uint256"}],"name":"approve","outputs":[{"internalType":"bool","name":"","type":"bool"}],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[{"internalType":"address","name":"account","type":"address"}],"name":"balanceOf","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"internalType":"uint256","name":"amount","type":"uint256"}],"name":"burn","outputs":[{"internalType":"bool","name":"","type":"bool"}],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"decimals","outputs":[{"internalType":"uint8","name":"","type":"uint8"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"internalType":"address","name":"spender","type":"address"},{"internalType":"uint256","name":"subtractedValue","type":"uint256"}],"name":"decreaseAllowance","outputs":[{"internalType":"bool","name":"","type":"bool"}],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"getOwner","outputs":[{"internalType":"address","name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"internalType":"address","name":"spender","type":"address"},{"internalType":"uint256","name":"addedValue","type":"uint256"}],"name":"increaseAllowance","outputs":[{"internalType":"bool","name":"","type":"bool"}],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"internalType":"uint256","name":"amount","type":"uint256"}],"name":"mint","outputs":[{"internalType":"bool","name":"","type":"bool"}],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"name","outputs":[{"internalType":"string","name":"","type":"string"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"owner","outputs":[{"internalType":"address","name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[],"name":"renounceOwnership","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"symbol","outputs":[{"internalType":"string","name":"","type":"string"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"totalSupply","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"internalType":"address","name":"recipient","type":"address"},{"internalType":"uint256","name":"amount","type":"uint256"}],"name":"transfer","outputs":[{"internalType":"bool","name":"","type":"bool"}],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"internalType":"address","name":"sender","type":"address"},{"internalType":"address","name":"recipient","type":"address"},{"internalType":"uint256","name":"amount","type":"uint256"}],"name":"transferFrom","outputs":[{"internalType":"bool","name":"","type":"bool"}],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"internalType":"address","name":"newOwner","type":"address"}],"name":"transferOwnership","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"}]')

# 以下の ' ' の間にコピペしたものをそのまま貼り付ける
BUSD_abi = json.loads('')

プログラムが長くなってしまうので、可能であれば別途別ファイルに分けた方が良いかもしれません。

上記のaddressとabiが準備できたら、以下のような形に実装を加えていきます。

from web3 import Web3
# 読み込み時jsonを使うため追加
import json

...

BUSD_address = "0xe9e7CEA3DedcA5984780Bafc599bD69ADd087D56"
BUSD_abi = json.loads('...ABIをここに貼る...')

...

def getSpecificCryptBalance(web3, myWalletAddress, specificAddress, abi):
    specificAddress = Web3.toChecksumAddress(specificAddress)
    contract = web3.eth.contract(address=specificAddress, abi=abi)
    return  web3.fromWei(contract.functions.balanceOf(myWalletAddress).call(), 'ether')

# BSCチェーンへ接続
web3 = connectBSCChain(Web3)
print(str(getKeyCurrencyBalance(web3, myWalletAddress)) + 'BNB')
print(str(getSpecificCryptBalance(web3, myWalletAddress, BUSD_address, BUSD_abi)))

ちょこちょこ省いていますが、ざっくり上記のような感じです。

新しく追加したメソッドは以下です。

def getSpecificCryptBalance(web3, myWalletAddress, specificAddress, abi):
    specificAddress = Web3.toChecksumAddress(specificAddress)
    contract = web3.eth.contract(address=specificAddress, abi=abi)
    return  web3.fromWei(contract.functions.balanceOf(myWalletAddress).call(), 'ether')

web3のインスタンス、ウォレットアドレス、そして、残高を確認したい通貨のアドレス、abiを渡します。

そして、アドレスのチェックサムを確認し、そのアドレスとabiを用いてBUSDのコントラクトインスタンスを作成、そのコントラクトから、balanceOfメソッドを用いて特定アドレスの残高を取得...といった流れです。

最初のtoChecksumAddressは以下に簡単な説明があります。

筆者もよくわかっていないけど、リンク先のgithubをみた感じ、16進パーサーの互換性とかで利点があるらしい・・・?(checksum自体は有効性の検証とかに使われるイメージですが、まぁ、そんなに詳細は知らなくても大丈夫な気がします)

そして、次のweb3.eth.contractメソッドで、ある通貨に対するabi記載の操作ができるコントラクトインスタンスを定義します。

abiには基本的に通貨に対して行える操作が定義されていて、(今回使用するbalanceOfメソッドなど...)今回、BUSDに対してそのabiの操作が行えるわけです。

そして、instanceからbalanceOfメソッドをcall()することで、wei建のBUSDの値が取得できる...といった流れです。

あとは先程同様、fromWeiで単位を揃えてあげれば...実際のBUSDの値が取得できます。

(単位はないけど、一番下の163がBUSDのbalance)

(base) taka@Taka test % python3 test.py
---------- Connected to BSC ----------
70198766450961167
0.070198766450961167BNB
163.809854199842376222

また、最後にオマケとして、以下のようなメソッドを定義すると通貨ごとのticker(ETHとか、BUSDとか)も取得できます。

やっていることはほぼ一緒でコントラクトインスタンスを作成し、symbol()メソッドを呼び出している形です。

def getSpecificCryptTicker(web3, specificAddress, abi):
    contract = web3.eth.contract(address=specificAddress, abi=abi)
    return contract.functions.symbol().call()

このsymbol()やbalanceof()はおそらくですが...ABIに定義がある通貨では共通して使える...と思います。

(筆者も曖昧な部分あるので、そこら辺は引き続き色々触ってしらべてみようかなと...!)

さいごに

最後に、色々調べていて、まだweb3の分野の資料はあまり日本語で出回っていませんね...

wei周りの知識とかは触ってみての憶測が中心です...

軽くみた感じ、USDCとかは'ether'ではなく、'gwei'だったり...と、実際に手元で桁を確認すればよしなに変換できるんですが、どっかからその単位の対応を持ってくる(USDCは'gwei' BUSDは'ether'みたいな)方法はいまだによくわかっておらず...

現状はよく使うものをあらかじめ手元で調べて、その対応表を事前に作っておけば一通りの通貨の残高表示とかはできそうな気がしています。

ただ、軽く試した感じ、実際にswapする際などはwei単位でやり取りするので、人が見なければ悩む必要もなさそうなきがします。

また、今回はBSCで試しましたが、冒頭に書いた通り、他のチェーンでも、EVMであれば同じ形で残高確認は可能でした。

唯一違うのは通貨ごとのaddressやabiを調べるexplorerがチェーンによって結構違うくらいかも。

最後に、もし他にわからないこと等あれば以下公式ドキュメントも参考にしてみてください...!(公式が一番!)

それでわ!

おすすめの記事