概要
web3.pyを利用して、Pythonからローカルのネットワークにデプロイしたコントラクトを実行する手順です。
本記事は下記の記事で利用した、スマートコントラクトやソースコードの一部を利用している続編となります。
本記事のリポジトリはこちらです。
事前準備
NFTをローカルのテストネットにデプロイ
実行する対象のコントラクトとして、下記の記事で作成したSampleNFT
のコントラクトを利用する。このコントラクトをローカルのテストネットにデプロイする。
テストネットの起動
はじめに、上記の記事の手順で環境構築とNFTの作成を行い、下記のコマンドでローカルにテストネットを起動する。今回の環境でテストネットは127.0.0.1:8545
のポートを利用する。
$ anvil -m $MNEMONIC
_ _
(_) | |
__ _ _ __ __ __ _ | |
/ _` | | '_ \ \ \ / / | | | |
| (_| | | | | | \ V / | | | |
\__,_| |_| |_| \_/ |_| |_|
0.1.0 (e0afc7c 2023-04-22T00:12:06.184818000Z)
https://github.com/foundry-rs/foundry
Available Accounts
==================
(0) "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266" (10000 ETH)
(1) "0x70997970C51812dc3A010C7d01b50e0d17dc79C8" (10000 ETH)
~ 省略 ~
(9) "0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC" (10000 ETH)
Private Keys
==================
(0) 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80
(1) 0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d
~ 省略 ~
(9) 0x2a871d0798f97d79848a013d4936a73bf4cc922c825d33c1cf7073dff6d409c6
Wallet
==================
Mnemonic: ${.envで$MNEMONICに指定したシークレットフレーズ}
Derivation path: m/44'/60'/0'/0/
Base Fee
==================
1000000000
Gas Limit
==================
30000000
Genesis Timestamp
==================
1682166468
Listening on 127.0.0.1:8545
テストネット起動後、コントラクトの実行の際にトランザクションに署名するアカウントの、PrivateKeyを控えておく。
上記の実行例の場合は、「Available Accounts」の一覧のうち、先頭の0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266
をコントラクトを実行するアカウントとする。
コントラクトのデプロイ
テストネット起動後、SampleNFT
をデプロイする。デプロイしたコントラクトアドレスも先ほどと同様に控えておく。
下記の実行例の場合は、0xbe9da202a5cb55afbcf3f45af9b4aec1cb1e7bf7
がコントラクトアドレスとなる。
$ source .env
$ forge script Deploy --broadcast --rpc-url $FOUNDRY
[⠆] Compiling...
No files changed, compilation skipped
Script ran successfully.
## Setting up (1) EVMs.
==========================
Chain 31337
Estimated gas price: 4.55551936 gwei
Estimated total gas used for script: 2000068
Estimated amount required: 0.00911134849531648 ETH
==========================
###
Finding wallets for all the necessary addresses...
##
Sending transactions [0 - 0].
⠁ [00:00:00] [#################################################] 1/1 txes (0.0s)
Transactions saved to: /Users/sey323/Workspace/programs/tmp/qiita-foundry-nft-sample/contract/broadcast/Deploy.s.sol/31337/run-latest.json
##
Waiting for receipts.
⠉ [00:00:00] [#############################################] 1/1 receipts (0.0s)
##### anvil-hardhat
✅ Hash: 0x46ada473f86ae6ba4ad43bc3de9f942963a04ad27a06273badebcf59b6894de8
Contract Address: 0xbe9da202a5cb55afbcf3f45af9b4aec1cb1e7bf7 # こちらを記録しておく
Block: 4
Paid: 0.005664249642788136 ETH (1538514 gas * 3.681636724 gwei)
Transactions saved to: /Users/sey323/Workspace/programs/tmp/qiita-foundry-nft-sample/contract/broadcast/Deploy.s.sol/31337/run-latest.json
==========================
ONCHAIN EXECUTION COMPLETE & SUCCESSFUL.
Total Paid: 0.005664249642788136 ETH (1538514 gas * avg 3.681636724 gwei)
Transactions saved to: /Users/sey323/Workspace/programs/tmp/qiita-foundry-nft-sample/contract/broadcast/Deploy.s.sol/31337/run-latest.json
下記の内容を確認して次の手順に進む
- テストネットが起動中であること
-
ネストネットが
127.0.0.1:8545
で起動していること - コントラクトを実行するアカウントのプライベートキーを控えていること
- コントラクトアドレスを控えていること
注意
anvil -m $MNEMONIC
コマンドで起動したテストネットを停止するとプライベートキーやデプロイしたスマートコントラクトの情報がリセットされる。
一度停止して再起動した場合は、再度プライベートキーの取得とコントラクトのデプロイを実行すること。
スマートコントラクトのABIを取得
web3.pyでスマートコントラクトを実行する場合、ABI形式のjsonファイルを取得する必要がある。
ABIとは「Contract Application Binary Interface」の略称で、Ethereum上で動くコントラクトに対するInterfaceの役割を持つ。ABIの仕様についてはSolidityのドキュメントを参照。
また、ABIについては下記の記事が非常にわかりやすかったので、こちらも合わせて参照。
ABIの取得はforgeコマンドを利用して行う。forgeコマンドではソースコードのbuildする際に、オプションを指定することで、ABI形式のjsonファイルを同時に生成することができる。
forge build --force --extra-output-files abi
ビルドの実行が完了後、デフォルトの設定の場合は./out/SampleNFT.sol/SampleNFT.abi.json
にabiファイルが出力される。
SampleNFT.abi.jsonの全体像
[
{
"inputs": [],
"stateMutability": "nonpayable",
"type": "constructor"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "owner",
"type": "address"
},
{
"indexed": true,
"internalType": "address",
"name": "approved",
"type": "address"
},
{
"indexed": true,
"internalType": "uint256",
"name": "tokenId",
"type": "uint256"
}
],
"name": "Approval",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "owner",
"type": "address"
},
{
"indexed": true,
"internalType": "address",
"name": "operator",
"type": "address"
},
{
"indexed": false,
"internalType": "bool",
"name": "approved",
"type": "bool"
}
],
"name": "ApprovalForAll",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "from",
"type": "address"
},
{
"indexed": true,
"internalType": "address",
"name": "to",
"type": "address"
},
{
"indexed": true,
"internalType": "uint256",
"name": "tokenId",
"type": "uint256"
}
],
"name": "Transfer",
"type": "event"
},
{
"inputs": [
{
"internalType": "address",
"name": "to",
"type": "address"
},
{
"internalType": "uint256",
"name": "tokenId",
"type": "uint256"
}
],
"name": "approve",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "owner",
"type": "address"
}
],
"name": "balanceOf",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "tokenId",
"type": "uint256"
}
],
"name": "getApproved",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "tokenId",
"type": "uint256"
}
],
"name": "getSentence",
"outputs": [
{
"internalType": "string",
"name": "",
"type": "string"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "owner",
"type": "address"
},
{
"internalType": "address",
"name": "operator",
"type": "address"
}
],
"name": "isApprovedForAll",
"outputs": [
{
"internalType": "bool",
"name": "",
"type": "bool"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "ownerAddress",
"type": "address"
},
{
"internalType": "string",
"name": "sentence",
"type": "string"
}
],
"name": "mintNft",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [],
"name": "name",
"outputs": [
{
"internalType": "string",
"name": "",
"type": "string"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "tokenId",
"type": "uint256"
}
],
"name": "ownerOf",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "from",
"type": "address"
},
{
"internalType": "address",
"name": "to",
"type": "address"
},
{
"internalType": "uint256",
"name": "tokenId",
"type": "uint256"
}
],
"name": "safeTransferFrom",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "from",
"type": "address"
},
{
"internalType": "address",
"name": "to",
"type": "address"
},
{
"internalType": "uint256",
"name": "tokenId",
"type": "uint256"
},
{
"internalType": "bytes",
"name": "data",
"type": "bytes"
}
],
"name": "safeTransferFrom",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "operator",
"type": "address"
},
{
"internalType": "bool",
"name": "approved",
"type": "bool"
}
],
"name": "setApprovalForAll",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "bytes4",
"name": "interfaceId",
"type": "bytes4"
}
],
"name": "supportsInterface",
"outputs": [
{
"internalType": "bool",
"name": "",
"type": "bool"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "symbol",
"outputs": [
{
"internalType": "string",
"name": "",
"type": "string"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "index",
"type": "uint256"
}
],
"name": "tokenByIndex",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "owner",
"type": "address"
},
{
"internalType": "uint256",
"name": "index",
"type": "uint256"
}
],
"name": "tokenOfOwnerByIndex",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "tokenId",
"type": "uint256"
}
],
"name": "tokenURI",
"outputs": [
{
"internalType": "string",
"name": "",
"type": "string"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "totalSupply",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "from",
"type": "address"
},
{
"internalType": "address",
"name": "to",
"type": "address"
},
{
"internalType": "uint256",
"name": "tokenId",
"type": "uint256"
}
],
"name": "transferFrom",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
}
]
実装
PythonからSmapleNFTのコントラクトに対してNFTの発行(mintNft
)と、発行したNFTの情報取得(getSentence
)を行うスクリプトを作成する。
mintNft
とgetSentence
をCLI上で実行した例はこちらを参照
必要ライブラリのインストール
web3.pyをインストールする。
pip install web3
web3.pyの詳細な利用方法などについては、下記の公式ドキュメントを参照。
SampleNFTを実行するクラスの作成
はじめにコントラクトを実行するラッパークラスを作成する。
今回実行するコントラクトの関数は、NFTをmintするnftMint
と、mintしたNFTに刻んだ文字列を取得するgetSentence
である。これらを実行するコードをそのまま書くとトランザクションの発行や署名処理などで冗長になってしまうため、SampleNFTContract
クラスでラップすることで呼び出し側の負荷を減らしてある。
import json
from web3 import Web3
from eth_account import Account
class WalletAccount:
def __init__(self, private_key: str) -> None:
_account = Account.from_key(private_key)
self.private_key = private_key
self.address = _account.address
print(f"アカウントのウォレットアドレス: {self.address}")
class SampleNFTContract:
def __init__(
self,
account: WalletAccount,
address: str,
abi_path: str,
http_provider: str = "http://127.0.0.1:8545",
):
self.contract_owner = account
self.network = Web3(Web3.HTTPProvider(http_provider))
if not self.network.is_connected():
print("Ethereum Networkとの接続に失敗しました。終了します。")
exit(-1)
self.contract = self.network.eth.contract(
address=Web3.to_checksum_address(address), abi=self._load_abi(abi_path)
)
name = self.contract.functions.name().call()
print(f"スマートコントラクトの初期化に成功しました. コントラクト名: {name}")
@staticmethod
def _load_abi(api_json_path: str):
with open(api_json_path, "r") as j:
return json.load(j)
def _execute(self, tx):
"""トランザクション実行の共通処理"""
signed_tx = self.network.eth.account.sign_transaction(
tx, self.contract_owner.private_key
)
# トランザクションの送信
tx_hash = self.network.eth.send_raw_transaction(signed_tx.rawTransaction)
return self.network.eth.wait_for_transaction_receipt(tx_hash)
def mint_nft(self, address: str, sentence: str):
"""NFTを発行する
Args:
address (str): 所有者のウォレットアドレス
sentence (str): 文章
"""
tx = self.contract.functions.mintNft(
Web3.to_checksum_address(address), sentence
).build_transaction(
{
"nonce": self.network.eth.get_transaction_count(
self.contract_owner.address,
),
"from": self.contract_owner.address,
}
)
return self._execute(tx)
def get_sentence(self, tokenId: int) -> str:
"""トークンIDからを文章を取得する
Args:
tokenId (int): 対象のトークンID
Returns:
str: NFTに記録された文字列
"""
return self.contract.functions.getSentence(tokenId).call()
コントラクトの実行にはトランザクション(今回の場合はNFTのmint)の実行に署名するウォレットアカウントが必要になる。ウォレットアカウントはWalletAccount
として抽象化し、SampleNFTContract
クラスに引数として与える。
個人の環境で実行する場合は、PRIVATE_KEY
とCONTRACT_ADDRESS
を、事前準備で取得したプライベートキーと、SampleNFTをデプロイした時に取得したコントラクトアドレスにそれぞれ変更する必要がある。
from contract import SampleNFTContract, WalletAccount
PRIVATE_KEY = "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266" # 個人の環境に合わせて修正の必要あり
CONTRACT_ADDRESS = "0xbe9da202a5cb55afbcf3f45af9b4aec1cb1e7bf7" # 個人の環境に合わせて修正の必要あり
ABI_PATH = "./out/SampleNFT.sol/SampleNFT.abi.json"
HTTP_PROVIDER = "http://127.0.0.1:8545"
account = WalletAccount(private_key=PRIVATE_KEY)
snc = SampleNFTContract(
account=account, # コントラクトの署名に利用するアカウント
address=CONTRACT_ADDRESS,
abi_path=ABI_PATH,
http_provider=HTTP_PROVIDER,
)
NFTのMint
NFTをmintする際はSampleNFTContract
クラスのmint_nft
を実行する。元のコントラクトの実行と同様に、第1引数にnftの所有者、第2引数にnftに刻む文字列を与える。
~~省略~~
print(f"トランザクションの詳細: {snc.mint_nft(account.address, 'mint by python')}")
NFTに刻んだ文章を取得
tokenIdに紐づくNFTに刻んだ文章を表示する。こちらも元のコントラクトと同様に、引数にNFTのトークンIDを指定する。
~~省略~~
print(f"NFTに記録された文字列: {snc.get_sentence(tokenId=0)}")
実行
作成したスクリプトを実行する。
$ python main.py
アカウントのウォレットアドレス: 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266
スマートコントラクトの初期化に成功しました. コントラクト名: SampleNFT
トランザクションの詳細: AttributeDict({'transactionHash': HexBytes('0xf40b83a9a710ca75604e46cdbbe63d8cdc8605582c7a9ca75ec3d5873cd75a58'), 'transactionIndex': 0, 'blockHash': HexBytes('0x345e4e93eb2dec0b1770fa2752038791df50dd59e0591601320cdcf25a23a8ab'), 'blockNumber': 11, 'from': '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266', 'to': '0xbe9da202a5cb55afbcf3f45af9b4aec1cb1e7bf7', 'cumulativeGasUsed': 169400, 'gasUsed': 169400, 'contractAddress': None, 'logs': [AttributeDict({'address': '0xbe9da202a5cb55afbcf3f45af9b4aec1cb1e7bf7', 'topics': [HexBytes('0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef'), HexBytes('0x0000000000000000000000000000000000000000000000000000000000000000'), HexBytes('0x00000000000000000000000044d2143174905b9a26b5dc2a4c67b651a9841e16'), HexBytes('0x0000000000000000000000000000000000000000000000000000000000000007')], 'data': HexBytes('0x'), 'blockHash': HexBytes('0x345e4e93eb2dec0b1770fa2752038791df50dd59e0591601320cdcf25a23a8ab'), 'blockNumber': 11, 'transactionHash': HexBytes('0xf40b83a9a710ca75604e46cdbbe63d8cdc8605582c7a9ca75ec3d5873cd75a58'), 'transactionIndex': 0, 'logIndex': 0, 'transactionLogIndex': '0x0', 'removed': False})], 'status': 1, 'logsBloom': HexBytes('0x00000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000001000000000000000000000000000000020000000000000000028000000000000000000000000000000000000200000000000020000000000000000000800000000000000000000000010000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000008000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000100000020000000000000000000000000000000000000000000000000000000000040000000'), 'type': 2, 'effectiveGasPrice': 1833397921})
NFTに記録された文字列: mint from python
トランザクションが発行され、NFTに記録した文字列「mint by python」がコンソールに表示されていることを確認できた。